Automatizando pipelines de bioinformática (parte III): organizando pipelines com Makefiles

Frederico Schmitt Kremer
omixdata
Published in
4 min readOct 5, 2019

Olá pessoal! Hoje damos continuidade à nossa série automatizando pipeline sde bioinformática, Nos posts anteriores mostrei como é possível automatizar tarefas utilizando scripts de shell, e também ilustrei esta utilização implementando uma pipeline de anotação funcional para genomas microbianos. A pipeline criada integra difentes etapas (ex: criação de banco de dados do BLAST, predição de genes, BLAST), sendo todas realizadas por um único script, e por conta disso, é impossível re-executar uma etapa especifca sem re-executar as demais. É possível, é claro, separar cada etapa em um script específico, mas quanto mais script temos, mais dificil é de organizarmos a nossa pipeline.

Para resolver este problema, muitos cientistas e dados e bioinformatas passaram a organizar as suas pipelines de análise com Makefiles, que fornecem uma interface simples para execução de tarefas, inclusive com suporte à paralelização :)

O que são é um Makefile?

Os arquivos Makefile são utilizados como entrada pelo programa make, um gerenciador de processo de compilação desenvolvido originalmente para sistemas UNIX, ainda nos anos 70, e posteriormente portado e reimplementado para outros sistemas operacionais. Este utilitário foi escrito para gerenciar a compilação de programas escritos em linguagens com C, onde é necessário compilar muitas vezes uma centena de arquivos de código fonte separadamente e em uma ordem correta devido à dependência entre cada etapa.

Para resolver este problema, o programa utiliza uma estrutura de grafo direcionado acíclico (directed acyclic graph, ou DAG), onde as estapas que não possuem dependência são realizadas primeiro, para então serem realizadas aquelas cujos requisitos foram satisfeitos. Este processo é repetido até todas as etapas serem finalizadas, ou um erro ocorrer. O grafo é considerado acíclico pois cada etapa é executado apenas uma vez a partir de um mesmo caminho, não existindo “dependência cíclica” (ex: tarefa “A” depender de “B” e “B” depender de “A”).

Ao digitarmos o comando make em um diretório, o programa automaticamente buscará um arquivo com nome Makefile. Caso queiramos executar um arquivo cujo nome é diferente (ex: Makefile.linux), é necessário utilizar a opção -f.

$ ls
Makefile Makefile.linux
# executar o arquivo "Makefile"
$ make
# executar o arquivo "Makefile.linux"
$ make -f Makefile.linux

Estrutura de um Makefile

Podemos dividir a estrutura de um Makefile em 3 componentes principais: variáveis, regras e alvos.

Variáveis (variables)

As variáveis de um Makefile são geralmente declaradas no início do arquivo, e da mesma forma como os scripts de shell não precisam ter um tipo definido. As variáveis podem ser declaradas com := ou apenas =. Exemplo:

PROJECT_DIR := /home/bioinfo 

Um ponto interessante sobre estas variáveis é que podemos alterar o seu valor padrão através de argumentos passados por linha de comando. Caso queiramos alterar o valor da variável PROJECT_DIR para /home/ubuntu utilizamos os seguinte argumento na linha de comando:

$ make PROJECT_DIR=/home/ubuntu

Regras (rules)

As regras em um Makefile definem as tarefas que serão executadas. Para criarmos uma regra, definimos um nome e adicionamos : ao final da linha. As linhas seguintes que forem identadas (com tab) incluem os comandos de shell que devem ser executados pela regra. Ao executarmos um Makefile, podemos especificar quais regras queremos executar, o definir uma regra padrão através do nome

message:
echo "Hello, world!"

Neste caso, definimos uma regra, message , que executa o comando echo "Hello, world". Ao executarmos o comando make message, a seguinte mensagem será mostrada na tela:

$ make message
echo "Hello, world!"
Hello, world!

Note que tanto o comando quanto o seu resultado são mostrados. Para que apenas o resultado do comando seja apresentada, colocamos um arroba (“@”) no começo da linha:

message:
@echo "Hello, world!

Sendo assim, o resultado passa a ser:

$ make message
Hello, world!

Podemos também definir a mensagem a ser mostrada como uma variável. Para acessarmos o valor dela dentro da regra, usamos a estrutura $(...).

message = "Hello, world!"
message:
@echo $(message)

Agora, podemos alterar o valor da mensagem quando chamarmos o comando make:

$ make
Hello, world
$ make message="1 2 3 4"
1 2 3 4

Alvos (targets)

Uma regra pode ter um ou mais alvos, e estes podem ser arquivos ou diretórios que devem existir para a regra ser executada, ou regras que devem ser executadas previamente. É a partir dos alvos que o programa make define a estrutura de DAG para cada pipeline.

Os alvos de uma regra são definidos na mesma linha que define seu nome. Para exemplificar isso, vamos considerar três regras: print_hello, print_world e print_hello_world. Para este exemplo, as duas primeiras serão alvos da terceira, e o arquivo Makefile terá a seguinte estrutura:

print_hello:
echo "Hello"
print_world:
echo "World!"
print_hello_world: print_hello print_world

Ao executarmos o comando make print_hello_world temos a mensagem:

$ make print_hello_world
Hello
World!

Organizando a pipeline de anotação funcional com Makefile

Agora, vamos re-organizar o código de nossa pipeline para anotação funcional de genomas microbianos criada no post anterior e separar cada etapa em um shell script diferente. Para organizar a execução dos scripts, vamos criar um Makefile e definir uma regra para cada etapa, e também uma regra que execute toda a pipeline de uma só vez :)

Estrutura do projeto

A estrutura do projeto será igual à estrutura usada no projeto anterior, com execeção do Makefile e da pasta scripts/.

scripts/
format_blastdb.sh
run_prodigal.sh
split_genes.sh
run_blast.sh
run_quickgo.sh
create_report.sh
genome/
genome.fasta
blast_db/
prodigal_out/
emboss_out/
blast_out/
blast_ids/
quickgo/
Makefile

O Makefile

Scripts

O nosso script pipeline.sh será dividido nos seguintes scripts:

scripts/format_blast.sh

scripts/run_prodigal.sh

scripts/split_genes.sh

scripts/run_blast.sh

scripts/run_quickgo.sh

scripts/create_report.sh

Executando a pipeline

Agora, basta executar a pipeline com o comando make all 😃

--

--