Pages

Sunday, June 14, 2009

Apresentando um Makefile em detalhes

Não tem muito tem e fiz uma introdução ao comando make. Como mencionado, a forma mais simples quanto possível de apresentar essa questão é utilizando um exemplo que seja tão simples quanto possível e que não seja simples demais para não ficar algo muito superficial. Por isso, escolhi o meu próprio Makefile, criado para gerar um dvi/pdf de um código LaTeX. Portanto, eu vou colocar esse código aqui conforme eu for citando, mas se quiser, baixe o mesmo para acompanhar localmente e fazer suas próprias anotações nele. Eu não vou modificar o Makefile hoje, mas eu posso vir a atualizar/modificar ele no futuro (até irei comentar sobre pontos falhos do mesmo durante a exposição).

Primeiro passo

O primeiro passo é definir o objetivo.

No nosso exemplo: Compilar um código LaTeX, considerando a possibilidade de existir uma bibliografia, um índice remissivo, nomenclatura e glossário. Isso é o mais completo que eu conheço. Além disso, o Makefile deve sempre gerar um PDF e gerar um DVI quando possível. Para gerar o PDF, eu quero que o usuário possa escolher entre gerar primeiro um DVI e depois converter para o PDF ou gerar direto o PDF. Eu também quero que o Makefile facilite o meu trabalho para visualizar o documento e que seja possível eliminar o resultado e o lixo intermediário gerado facilmente.

Esse era o meu objetivo nesse caso. E entenda que eu sei como fazer essas coisas no terminal (o make não faz mágica)...

Em outros casos, pode ser: gerar o programa helloworld a partir do código fonte main.c, incluindo uma forma de eliminar o resultado final e os arquivos intermediários.

Lembrando regras básicas de programação.

Recomendações que não são utilizadas por quem recomenda não são, necessariamente, recomendações ruins....

Comente sempre que achar necessidade e quando não achar também, seja sucinto, mas não omita informações. Entretanto, não se culpe se depois alguém disser que não entendeu o que você queria ter dito. Como senhores do programa, naquele momento parece muito claro o que estamos escrevendo. Depois de um certo tempo, isso tornar menos claro. Então, se possível, reveja suas anotações depois de um certo tempo.

O alinhamento do código é muito importante, mas não precisa ser de 8 espaços, pode ser 1 ou 2. Eu gosto de 2, mas nesse caso do meu Makefile eu utilizei apenas 1 espaço. Dizem por aí que o recomendado é 3.

Sempre que possível, faça a tarefa automaticamente. Não peça ao usuário informações que não sejam necessárias, automatize sempre que possível, sem perder a generalidade.

Por fim, porém não menos importante... escreva como usar o seu código. Isso é para você mesmo se lembrar depois...

O Makefile.

O Makefile começa com um cabeçalho.
Informe o que achar necessário.

# File: Makefile
# Author: J. F. Mitre <http://jfmitre.com>
# Created: Sex 29 Mai 2009 10:14:33 BRT
# Last Update: Qui 04 Jun 2009 21:30:21 BRT
# Notes: Arquivo Makefile para compilar códigos em LaTeX
# Execute "make help" para ajuda
###############################################################################


É educado guardar um CHANGELOG. Eu escrevi o programa em uma tacada só na quinta do dia 04. Na sexta anterior tinha sido o dia que eu decidi fazer o arquivo e escrever as idéias. Então meu CHANGELOG é pequeno. Ainda assim, eu não o divulguei...

Teria sido legal colocar uma nota sobre licença. Eu esqueci desse detalhe...

Definições de variáveis

A primeira parte do Makefile é composta por definições de variáveis.

# Programas Selecionados {{{
########################## PROGRAMAS SELECIONADOS #############################

# Comando que gera compila o código LaTeX. Escolha entre: latex ou pdflatex
TEX = latex
# Comando que visualiza o arquivo .dvi
DVIVIEW = kdvi
# Comando que visualiza o arquivo .pdf
PDFVIEW = evince
# Comando que converte o arquivo .dvi em .ps
DVIPS = dvips

# }}}


Esse bloco acima define quais são os valores das variáveis TEX, DVIVIEW, PDFVIEW e DVIPS. São aplicativos do sistema que fazem aquilo o comentário indica. Esse são os comandos que serão utilizados pelo make ao executar um certa tarefa. Esses comandos foram colocados no primeiro bloco, porque podem ser modificados pelo usuário.

Veja como exemplo a variável TEX. Eu posso usar latex ou pdflatex. O primeiro comando gera um arquivo .dvi enquanto o segundo gera diretamente um arquivo .pdf. Eu também posso usar o visualizador que eu quiser para ver um certo formato de arquivo. Ao invés de kdvi, eu poderia especificar okular ou mesmo o evince (embora ele não inclua as imagens na visualização). E assim por diante. São variáveis para o usuário definir...

Que fique claro que um Makefile mais sofisticado teria colocar essas opções como argumentos, mas como eu disse, esse Makefile que eu escolhi é mais complexo que o básico e mais simples que o completo.

O bloco seguinte são as opções do programas

# Opções dos comandos {{{
############################ OPÇÕES DOS COMANDOS ##############################

# Opções do comando TEX especificado acima
OPTDVI = -halt-on-error
# Opções do comando dvips, que converte o arquivo .dvi para o .ps
OPTPS = -Z
# Opções do comando comando "pdflatex"
OPTPDF =
# Formato de conversão utilizado pelo ps2pdf (necessário especificar)
FORMATO = a4
# Estilos dos glossários, nomenclaturas, etc.
STYLENLS = nomencl.ist
STYLEGLS = nomencl.ist
# Opções do comando make
OPTMAKE = -s

# }}}

Se o usuário não conhece o comando, fica muito difícil dar palpites aqui. É necessário olhar o man de cada aplicativo para saber porque coloquei certas opções e para escolher outras. Afirmo que essas opções são agradáveis para 99 % dos usuários. Dois únicos cuidados são: definir o tamanho do papel para o ps2pdf e definir o estilo para o glossário e nomenclaturas. Note que eu usei o mesmo, mas há casos onde existe um estilo pré-definido e casos onde são diferentes os estilos.

Novamente, as opções padrões ficam no interior do Makefile, as alternativas deviam ser passadas por linha de comando, algo que eu não fiz intencionalmente... (quem sabe em uma versão futura)

Seguindo, encontramos mais variáveis.

# Outros programas {{{
############################# OUTROS PROGRAMAS ################################

# Comando que gera o índice, a nomenclatura, etc. Não existe outro atualmente
MAKEINDEX = makeindex
# Comando que gera a bibliografia, não existe outro atualmente
BIB = bibtex
# Conversão entre arquivo .ps e arquivo .pdf, existem outros, mas esse é o
# melhor para o GNU/Linux.
PSPDF = ps2pdf
# Eliminar arquivo. Não é razoável fazer de outra forma.
RM = rm -fv
# Comando make (até existem outros, mas não para esse Makefile).
MAKE = make

# }}}

Essas variáveis, são os outros comandos. Comandos no sistema que eu NÃO quero que sejam modificados. Quem gera o índice, e o makeindex, não existe outro. Quem gera a bibliografia é o bibtex, não existe outro.
Quem deleta arquivos é o rm. Bom, até existem outras formas de usá-lo, mas você quer mesmo modificar isso?

Da mesma forma, o make é o make. Até existem outros, como o colormake, mas fracamente, se é para usar um comando, use o mesmo que foi digitado, ou seja, se eu digito cmake, ele compilaria com o cmake, se eu digito make, ele compila com o make. Se entendeu a minha lógica, é melhor não modificar nesse comando até que consiga fazer o sincronismo entre o digitado pelo usuário e essa variável sem que o usuário deva especificar no interior.

Embora o autor (eu) não recomende modificá-las, essas variáveis são como quaisquer outras. Podem, sim, ser modificadas. O maior exemplo disso é o ps2pdf. Existem inúmeras formas de converter um arquivo .ps em um arquivo .pdf, mas apenas uma foi disponibilizada por mim, porque assim eu defini.

Execução
Passada a parte de configuração, vem a parte de execução.

Os primeiros comandos são preparações, eventualmente necessárias.

# Indentificação do nome do arquivo
DOC := $(shell egrep -l '^[^%]*\\begin\{document\}' *.tex |rev|cut -b5- |rev)

# Verificando se existe bibliografia

BIBFILE := $(shell egrep -l '^[^%]*\\bibliography\{' $(DOC).tex)

# Verifica se é uma apresentação do prosper
PROSPER := $(shell egrep -l '^[^%]*\\documentclass\[.*\]\{prosper\}' *.tex)


O primeiro comando define qual é o arquivo principal dentro do diretório em que ele é executado.
É um comando no shell. Ele gera a variável DOC, essa variável é utilizada como $(DOC) (se fosse um shellscript, seria $DOC, mas não é um shellscript, é um Makefile). De forma que $(DOC).tex é o arquivo principal da estrutra LaTeX (que pode conter vários documentos independentes).

Nesse ponto, vemos a primeira falha do autor. Ele não deixou claro, mas se existirem dois arquivo .tex com nomes diferentes contem um \begin{document} NÃO COMENTADO no mesmo diretório, haverá erro de compilação.

Porque o comando seguinte, verifica o arquivo $(DOC).tex procurando pela existência do comando que define se existe ou não uma bibliografia.

O terceiro comando é um daqueles casos de automação úteis. Oras, se o documento .tex for um código do prosper, então o DVI não faz sentido... logo, eu posso identificar esses casos e resolver o problema dele gerando sempre um PDF no final.

Relembrando as ações básicas

Gerar um $(DOC).dvi, gerar um $(DOC).pdf, limpar os arquivos intermediários, eliminar tudo que foi gerado pelo Makefile, inclusive o produto final e mostrar o resultado, tanto do dvi, quanto do pdf.

A ordem dos blocos não importa, exceto pelo primeiro bloco.

Bloco $(DOC).dvi, o primeiro bloco
# Gerando o "arquivo" DVI ...
$(DOC).dvi:
# mas ele é esperto o bastando para saber que não gera-se DVI com o pdflatex.
# E também gerará o PDF caso seja o proper seja utilizado.
@if [ ! -z $(PROSPER) ]; then \
$(MAKE) dvi; \
$(DVIPS) $(OPTPS) $(DOC).dvi; \
$(PSPDF) -sPAPERSIZE=$(FORMATO) $(DOC).ps $(DOC).pdf; \
elif [ $(TEX) == latex ]; then \
$(MAKE) dvi; \
elif [ $(TEX) == pdflatex ]; then \
$(MAKE) $(DOC).pdf; \
fi
Em teoria, esse bloco gera um arquivo DVI, mas ele DEVE ser esperto o bastante para saber que não se gera um DVI quando utiliza-se o pdflatex, e o serviço NUNCA para no DVI quando se está utilizando o prosper.

Mas o primeiro detalhe é o nome do bloco: $(DOC).dvi. O nome do bloco é o nome do suposto produto final. Isso é MUITO importante, porque evita que você compile novamente aquilo que já foi compilado, ou seja, se ao digitar "make $(DOC).dvi" no terminal o make encontrar o arquivo $(DOC).dvi, então ele para ali mesmo.

O segundo detalhe é posição do bloco dentro do código. Ele é o primeiro bloco, logo, ao digitar "make" sem argumentos, será ele que será executado.

Todos os comandos que compõem esse bloco estão afastados de um tab da margem esquerda.

O algorítimo é outro assunto, mas para melhor entendimento do Makefile, eu vou passar por esse bloco mais detalhadamente.

A primeira linha verifica se a variável $(PROSPER) não está vazia. Por que ela não está vazia, deve-se criar o DVI, converter esse DVI para o formato PS e depois converter o PS para o PDF. Porque não converter direto do DVI para o PDF ? Porque fica muito melhor assim de acordo com minha opinião. Um Makefile mais completo devia dar margem a outras opiniões. Caso $(TEX) seja igual a "latex" e não seja um arquivo prosper, então, eu quero gerar um dvi. Se $(TEX) for igual a pdflatex, então eu quero gerar um pdf diretamente. Por que isso foi feito ? Caso eu digite make no terminal, não é esse o bloco que será executado ? Então, se eu tiver selecionado pdflatex, eu certamente quero um pdf, eu não quero o DVI (porque o pdflatex não gera dvi :-0 ), logo crie o pdf. Melhor que uma mensagem de "erro" antipática (criada porquem escreveu o Makefile) que não lhe permita usar o make sem argumento diretamente no terminal.

O arroba na frente do if significa que eu não quero que o comando ecoe pelo terminal. Por padrão, o comando make funciona como se estivesse em permanente modo de debug. Mostrando quais são os comando que serão executados e executando-os. Eu não quero que ele escreva o comando na tela ? Então coloco o arroba na frente. O arroba na frente do if serve para todo o grupo de "uma única linha" do if.

Digo uma única linha, porque a barra invertida no final de cada linha significa que o comando continua na linha seguinte. O motivo pelo qual fiz isso no arquivo, é porque é mais fácil lidar com erros de formatos quando se utiliza a barra invertida (que deve ser o último caractere de uma certa linha).

Note os comandos no interior das condições.
    $(MAKE) dvi; \
ou então,
    $(MAKE) $(DOC).pdf; \
Esse comandos estão invocando o próprio make, que invoca um bloco dentro do Makefile. Um dos blocos chama-se "dvi" e o outro "$(DOC).pdf". Por similaridade com o "$(DOC).dvi", acredito que todos imagem o que seja o "$(DOC).pdf". Por sua vez, o bloco "dvi". É quem faz o serviço sujo. É ele quem de fato realiza o trabalho de compilar os código fonte gerando o resultado. Eu poderia dispensar esse bloco adicional, contudo, o código fica mais legível com ele, que faz um papel de subrotina ou função (como preferir).

Antes olhar o bloco que faz o serviço sujo. Vamos observar o bloco $(DOC).pdf

Bloco $(DOC).pdf
# Gera o arquivo PDF...
$(DOC).pdf:
# de um arquivo DVI, convertendo-o para PS e em seguida para PDF caso
# o comando padrão seja o latex ou diretamente, caso o comando padrão seja
# o pdflatex.
# Também diminuimos o esforço caso o prosper esteja sendo usado.
@if [ ! -z $(PROSPER) ]; then \
$(MAKE) $(DOC).dvi; \
elif [ $(TEX) = latex ]; then \
$(MAKE) $(DOC).dvi; \
$(DVIPS) $(OPTPS) $(DOC).dvi; \
$(PSPDF) -sPAPERSIZE=$(FORMATO) $(DOC).ps $(DOC).pdf; \
elif [ $(TEX) = pdflatex ]; then \
$(MAKE) pdflatex; \
fi
Primeiro, ele verifica se é um arquivo prosper e caso seja, passa ao bloco $(DOC).dvi o dever de cuidar desse caso. Se $(TEX) for igual a "latex", então primeiro deve-se gerar um DVI e sem seguida converter para ps e por fim, converter para pdf. Nesse ponto nota-se claramente a vantagem de ter o bloco com o nome $(DOC).dvi. Caso o DVI exista, ele não será recriado. Caso $(TEX) seja pdflatex, então chame o comando "$(MAKE) pdflatex", ou seja, o originalíssimo nome de bloco igual a "pdflatex" para a criar um pdf com o "pdflatex".

Bloco dvi

Aquele que faz o serviço sujo. Ele é idêntico ao bloco "pdflatex". O único motivo para os dois blocos existirem é porque assim eu defini. Até as opções, que são diferentes variáveis para cada comando, não justifica tal escolha. Primeiro porque eu poderia usar um if para gerar diferentes opções para cada comando, segundo porque as opções do comando latex são iguais as do comando pdflatex.

E porque defini dois blocos ? Porque eu estava com pressa para assistir uma defesa de tese nesse dia e não percebi o que tinha escrito até que era tarde demais. Aí tive pouco tempo para modificar.

Reconheço e reafirmo a utilidade de existir o bloco dvi e o bloco pdflatex, assim como devia existir o bloco pdf. porque a intuição humana fará o usuário digitar "make dvi" ou "make pdflatex" ou "make pdf" e assim é muito bom sincronizar o código a intuição humana. Mas definitivamente, não precisa ser dois blocos grandes e idênticos.

Bom, o bloco dvi é dado abaixo
# Gerando o arquivo DVI com o latex
dvi:
# Compliação inicial do arquivo tex
$(TEX) $(OPTDVI) $(DOC).tex
# Se existe bibliografia ...
@if [ ! -z $(BIBFILE) ]; then\
$(BIB) $(DOC).aux; \
$(TEX) $(OPTDVI) $(DOC).tex; \
$(BIB) $(DOC).aux; \
$(TEX) $(OPTDVI) $(DOC).tex; \
fi
# Se existe glossário, nomenclatura ou indice...
@if [ -f "$(DOC).nlo" ]; then \
$(MAKEINDEX) $(DOC).nlo $(STYLENLS) -o $(DOC).nls; \
$(TEX) $(OPTDVI) $(DOC).tex; \
fi
@if [ -f "$(DOC).glo" ]; then \
$(MAKEINDEX) $(DOC).glo -s $(STYLEGLS) -o $(DOC).gls; \
$(TEX) $(OPTDVI) $(DOC).tex; \
fi
@if [ -f "$(DOC).nlo" ]; then \
$(MAKEINDEX) $(DOC).nlo $(STYLENLS) -o $(DOC).nls; \
$(TEX) $(OPTDVI) $(DOC).tex; \
fi
@if [ -f "$(DOC).glo" ]; then \
$(MAKEINDEX) $(DOC).glo -s $(STYLEGLS) -o $(DOC).gls; \
$(TEX) $(OPTDVI) $(DOC).tex; \
fi
@if [ -f "$(DOC).idx" ]; then \
$(MAKEINDEX) $(DOC).idx; \
$(TEX) $(OPTDVI) $(DOC).tex; \
$(MAKEINDEX) $(DOC).idx; \
$(TEX) $(OPTDVI) $(DOC).tex; \
fi
# Uma última vez apenas para confirmar que tudo está ok...
$(TEX) $(OPTDVI) $(DOC).tex
Observe que ele não passa de um bloco de comandos em lista para os quais são efetuadas operações adicionais caso exista bibliografia, glossário, nomenclatura e índice. Nada diferente do que faria se estivesse escrevendo um shellscript, ou seja, nada diferente do que faria se estivesse digitando tudo no terminal. Todos os comandos são repetidos pelo menos duas vezes para assegurar o sucesso de qualquer projeto, não importa o tamanho. Mas esse tipo de dica é latex/terminal/linux não Makefile. Caso venha a compilar o código fonte em c, teriamos algo como "$(GCC) $(GCC_FLAGS) $(CODE)" e seria apenas uma vez.

Blocos clean e cleanall

... ou melhor dizendo limpando o diretório. Os dois blocos são reponsáveis por eliminar os arquivos não mais desejados. O bloco clean limpa os arquivos intermediários (e como tem arquivos intermediários nesse caso). o bloco cleanall, elimina todos os arquivos, inclusive os resultados, ou seja, o pdf e o dvi gerado.
# Limpando o básico dos arquivos gerados pelo compilador
clean:
@$(RM) *.aux *.log
@$(RM) *.toc *.lot *.lof
@$(RM) *.ttt *.fff *.blg *.out
@$(RM) *.ind *.ilg *.idx *.aux *.glo
@$(RM) *.gls *.abx *.nlo *.syx *.nls
@$(RM) *.ps
@$(RM) *.tex.backup
@$(RM) *.bib.backup *.bib.bak
@$(RM) *.tex.bak
@$(RM) *.bbl
@$(RM) *.*~
@$(RM) Makefile~

# Limpando tudo, inclusive os arquivos finais.
cleanall:
$(MAKE) clean
@$(RM) *.dvi *.pdf
Detalhe interessante fica por conta de como o cleanall foi criado. Ele usa o bloco clean e depois elimina o que sobrou, que são os resultados. Essa é a forma elegante de aproveitar os comandos de um bloco em outro bloco.

Blocos de visualização

Quatro blocos adicionais foram criados para permitir visualizar o resultado: show e shownew, showpdf e shownewpdf.
show:
@if [ ! -z $(PROSPER) ]; then \
$(MAKE) $(DOC).dvi; \
$(PDFVIEW) $(DOC).pdf; \
elif [ $(TEX) == latex ]; then \
$(MAKE) $(DOC).dvi; \
$(DVIVIEW) $(DOC).dvi; \
elif [ $(TEX) == pdflatex ]; then \
$(MAKE) $(DOC).pdf; \
$(PDFVIEW) $(DOC).pdf; \
fi

# Idem ao "show", a diferença é que aqui
# é feito uma nova compilação, de um jeito ou de outro
shownew:
$(MAKE) cleanall
@if [ ! -z $(PROSPER) ]; then \
$(MAKE) $(DOC).dvi; \
$(PDFVIEW) $(DOC).pdf; \
elif [ $(TEX) == latex ]; then \
$(MAKE) $(DOC).dvi; \
$(DVIVIEW) $(DOC).dvi; \
elif [ $(TEX) == pdflatex ]; then \
$(MAKE) $(DOC).pdf; \
$(PDFVIEW) $(DOC).pdf; \
fi

# Idem ao "show", mas para arquivos PDF apenas
showpdf:
$(MAKE) $(DOC).pdf
$(PDFVIEW) $(DOC).pdf

# Idem ao "shownew", mas para arquivos PDF apenas
shownewpdf:
$(MAKE) cleanall
$(MAKE) $(DOC).pdf
$(PDFVIEW) $(DOC).pdf

# }}}
Nesse ponto, espero que tenham compreendido o suficiente do código para compreender o que foi feito. Pois o objetivo é visualizar o arquivo. Se for um DVI abrir o visualizador de DVI, se for um PDF abrir um visualizador de pdf, caso não exista o arquivo, crie-o.

Os blocos com "new" no nome, são assim definidas porque eliminam o pdf/dvi que eventualmente exista, compila novamente e exibe o novo arquivo.

Os blocos com "pdf" no nome, força que seja visualizado o pdf, não importa qual o processo que o gera. Os demais vão exibir o DVI sempre que esse for válido/existir ou o PDF, em contrário.

Finalizando

O último bloco (que eu não vou transcrever para esse ponto) é o bloco help, que permite que o usuário digite "make help" para descobrir quais são os comandos diponíveis.

Conclusão
Esse texto ficou longo demais. Então vou ser breve.

Falta discutir alguns pontos e abordar um Makefile para programação em si (ou seja, para arquivo C/C++/Fortran). Assim como algumas normas de etiqueta para esses tipos de Makefile. Entenda: existem nomes de variáveis que são reconhecidos por qualquer programador no mundo, como GCC_FLAGS, porque usá-las ou não usá-las. E finalmente discutir porque usar o Makefile e não um shellscript. Existem fatores que induzem a usar o Makefile, mas se é apenas para você, porque usar o make ? Mas não prometo a terceira parte para logo...

Uma quarta parte agendada para daqui a muito (muito mesmo) tempo, seria como lidar de forma correta com as opções dentro do Makefile. Esse é o ponto mais delicado e complexo e daria um ar profissional ao Makefile (como os dos grandes programas). Mas é também o que dá mais trabalho, portanto, exige planejamento e detalhamento ... e tempo para escrever.

No comments:

Post a Comment