Aprenda Makefile com os melhores exemplos
(makefiletutorial.com)- Makefile é usado principalmente para automatizar a compilação de C/C++ em projetos grandes, reexecutando apenas as etapas de build necessárias com base em alterações nos arquivos
- Make compara os timestamps do sistema de arquivos de alvos e dependências, executando comandos somente quando o alvo não existe ou quando uma dependência é mais recente
- A regra básica é composta por
targets: prerequisitese comandos indentados com tab; indentação com espaços pode fazer omakefalhar - É possível estender o fluxo de build com variáveis, curingas, variáveis automáticas, regras de padrão, condicionais, funções, make recursivo,
include,.PHONYe.DELETE_ON_ERROR - O exemplo Cookbook transforma fontes C/C++ em
src/em artefatos embuild/e configura gerenciamento automático de dependências com-MMD -MPe-include
Quando um Makefile é necessário
- Makefile é usado em programas grandes para decidir quais arquivos precisam ser recompilados
- O caso central do tutorial é a compilação de arquivos C/C++
- Make também pode ser usado fora de compilação em tarefas que precisam executar uma sequência de comandos conforme arquivos são alterados
- Como outras ferramentas de build para C/C++, são apresentadas SCons, CMake, Bazel e Ninja
- Java conta com Ant, Maven e Gradle, enquanto linguagens como Go, Rust e TypeScript têm suas próprias ferramentas de build
- Linguagens interpretadas como Python, Ruby e JavaScript puro não precisam ser recompiladas quando arquivos mudam, portanto ferramentas como Makefile não são essenciais
Modelo básico de execução do Make
- Os exemplos funcionam criando um arquivo
Makefileem um terminal commakeinstalado e, em seguida, executandomakenesse diretório - A regra básica segue o formato abaixo
targets: prerequisitescommandiniciado por tab
- O alvo (target) geralmente é um nome de arquivo, e o comando é a etapa para criar esse alvo
- As dependências (prerequisites) são arquivos que precisam existir antes da execução do comando e também são chamadas de dependencies
- As linhas de comando em um Makefile devem obrigatoriamente ser indentadas com caractere de tab; usar espaços faz o
makefalhar
Comparação de tempo entre alvos e dependências
make helloexecuta o comando associado ao alvohello, mas não executa o comando se o arquivohellojá existir- Alvos e nomes de arquivos são diretamente relacionados; em geral, o comando de um alvo cria um arquivo com o mesmo nome do alvo
- Na regra
blah: blah.c, o comando de compilação só é executado quandoblahnão existe ou quandoblah.cé mais recente queblah - Make não rastreia diretamente o histórico de alterações; ele usa timestamps do sistema de arquivos como heurística
- Se um arquivo for modificado e depois tiver seu horário de modificação alterado para o passado, Make pode concluir incorretamente que esse arquivo não mudou
Alvo padrão e clean
- Se nenhum alvo for especificado em
make, o primeiro alvo do Makefile é executado como alvo padrão - O alvo
allé usado para criar vários alvos de uma vez; se ele for colocado como primeira regra, executar apenasmakejá faz o build de todos os alvos cleancostuma ser usado como alvo para apagar artefatos de build, mas não é uma palavra reservada especial do Make- Se
cleannão for o primeiro alvo nem uma dependência de outro alvo, ele precisa ser chamado explicitamente, como emmake clean, para ser executado - Se um arquivo chamado
cleanexistir de fato, o alvocleanpode não ser executado; isso é resolvido adiante com.PHONY
Variáveis e processamento de strings
- Variáveis do Make só podem conter strings, e o tutorial geralmente recomenda usar
:= - Referências a variáveis usam o formato
$(name)ou${name} - Aspas simples e duplas não têm significado especial no próprio Make; são apenas caracteres dentro da variável
- Há dois modos principais de definir variáveis
=: variável recursiva, que procura e expande variáveis quando é usada:=: variável simplesmente expandida, que expande apenas valores já definidos no momento da definição
?=define um valor apenas para variáveis que ainda não foram configuradas+=adiciona valor a uma variável- Variáveis não definidas são tratadas como string vazia
- Espaços no fim de uma linha não são removidos; para criar uma variável com um único espaço, pode-se usar uma técnica como
$(nullstring)
Curingas e variáveis automáticas
- Em Make,
*e%são ambos chamados de curingas, mas têm significados diferentes *procura nomes de arquivos correspondentes no sistema de arquivos; em vez de usá-lo diretamente na definição de variáveis, é mais seguro envolvê-lo com a função wildcard, como em$(wildcard *.o)- Se
*não corresponder a nenhum arquivo, ele pode permanecer como está quando não estiver dentro da funçãowildcard %é usado em regras e em certas funções, e a parte correspondente é chamada de stem- Variáveis automáticas usadas com frequência incluem:
$@: nome do alvo$?: dependências mais recentes que o alvo$^: todas as dependências$<: primeira dependência
Regras implícitas e regras de padrão
- Make tem regras implícitas para compilação em C, permitindo que o build avance mesmo sem escrever explicitamente o comando de compilação
- Regras implícitas representativas têm o formato abaixo
n.oé gerado a partir den.cno formato$(CC) -c $(CPPFLAGS) $(CFLAGS) $^ -o $@n.oé gerado a partir den.ccoun.cppno formato$(CXX) -c $(CPPFLAGS) $(CXXFLAGS) $^ -o $@né gerado a partir den.opor um comando de linker
- Variáveis importantes em regras implícitas são
CC,CXX,CFLAGS,CXXFLAGS,CPPFLAGSeLDFLAGS - Regras de padrão estático usam o formato
targets...: target-pattern: prereq-patterns ..., substituindo nos padrões de dependência o stem correspondente a%no alvo - Regras de padrão incluem
%no alvo, como em%.o: %.c, e podem ser usadas como regras implícitas criadas manualmente - Regras com dois-pontos duplo
::permitem definir várias regras para o mesmo alvo, mas são usadas raramente
Execução de comandos e tratamento de erros
- Colocar
@antes de um comando faz com que o próprio comando não seja exibido - Executar
make -sse comporta como se cada linha tivesse@no início - Cada linha de comando tem o efeito de ser executada em um novo shell, portanto um
cd ..em uma linha não afeta a linha seguinte - Para manter o mesmo estado do shell, é preciso escrever em uma única linha, como
cd ..; echo \pwd``, ou continuar a linha com barra invertida - O shell padrão é
/bin/sh, mas pode ser alterado, como emSHELL=/bin/bash - Para usar o
$de variáveis do shell dentro de um Makefile, use$$ - O tratamento de erros varia conforme o modo de execução
make -k: continua executando outras tarefas possíveis mesmo com erros-antes do comando: ignora erros desse comandomake -i: aplica a ignorância de erros a todos os comandos
- Se Make for interrompido com
ctrl+c, ele apaga o alvo recém-criado e mais recente
Make recursivo e variáveis de ambiente
- Ao chamar make novamente dentro de um Makefile, use a variável especial
$(MAKE)em vez demake $(MAKE)repassa flags do make e lida adequadamente com os efeitos dessas flags- Quando Make é iniciado, ele cria automaticamente variáveis do Make a partir das variáveis de ambiente configuradas no momento da execução
- A diretiva
exportdefine uma variável no ambiente dos comandos de shell de todas as recipes - Para usar uma variável também em makes subordinados, é necessário dar
exportnessa variável .EXPORT_ALL_VARIABLESpermite exportar todas as variáveis- Ao especificar vários alvos de uma vez, como
make clean run test, eles são executados na ordemclean,run,test
Condicionais e funções
- Condicionais são compostos por
ifeq,ifneq,ifdef,ifndef,elseeendif ifdefnão expande referências a variáveis; ele apenas verifica se a variável está definida- É possível usar
$(MAKEFLAGS)efindstringpara verificar se uma flag comomake -ifoi repassada - Funções do Make são usadas principalmente para processamento de texto e são chamadas no formato
$(fn, arguments)ou${fn, arguments} - As principais funções incluem:
subst: substituição de stringspatsubst: substituição de palavras que correspondem a um padrãoforeach: transformação de uma lista de palavras separadas por espaçosif: usa o segundo argumento se o primeiro não estiver vazio; caso contrário, usa o terceirocall: chama uma variável como se fosse uma função básica e usa parâmetros como$(1)e$(2)shell: chama o shell, mas transforma quebras de linha em espaçosfilter: seleciona da lista elementos que correspondem a um padrão específicofilter-out: seleciona elementos que não correspondem a um padrão específico
include, vpath, .PHONY, .DELETE_ON_ERROR
include filenames...faz com que um ou mais outros Makefiles sejam lidosincludeé útil ao ler Makefiles gerados por opções como-Mdogccvpath <pattern> <directories>especifica diretórios onde determinados arquivos de dependência devem ser procurados- A variável
VPATHpode especificar globalmente um comportamento semelhante de caminhos de busca - A barra invertida
\é usada para dividir comandos longos em várias linhas - Adicionar
.PHONYa um alvo faz com que Make não confunda esse alvo com um arquivo de mesmo nome .DELETE_ON_ERRORapaga o alvo da regra se, durante a execução da regra, um comando retornar um status de saída diferente de 0.DELETE_ON_ERRORnão se aplica apenas ao alvo imediatamente anterior, comoPHONY; ela se aplica a todos os alvos
Exemplo Makefile Cookbook
- O exemplo Cookbook fornece um template de Makefile adequado para projetos de porte médio
- Ao colocar arquivos C/C++ na pasta
src/, o Makefile determina automaticamente os alvos de compilação e as dependências - A configuração das principais variáveis é a seguinte
TARGET_EXEC := final_programBUILD_DIR := ./buildSRC_DIRS := ./src
SRCSusafinde$(shell ...)para localizar arquivos*.cpp,*.ce*.sOBJScria os caminhos dos artefatos de build adicionandoBUILD_DIRantes de cada arquivo-fonte e acrescentando.oDEPScria a lista de Makefiles de dependência trocando.opor.dINC_DIRSencontra todos os diretórios abaixo desrc/, eINC_FLAGSadiciona o prefixo-Ia cada diretórioCPPFLAGS := $(INC_FLAGS) -MMD -MPconfigura o GCC para criar arquivos de dependência.d- Fontes C e C++ criam arquivos
.oabaixo debuild/por regras de padrão separadas, e os diretórios necessários são criados commkdir -p $(dir $@) cleané declarado como.PHONYe apaga o diretório de build comrm -r $(BUILD_DIR)-include $(DEPS)inclui os arquivos.d, mas suprime erros que ocorrem quando os arquivos não existem, como no primeiro build
1 comentários
Opiniões no Hacker News
Há algumas flags pouco conhecidas, mas úteis, do
make. A sincronização de saída faz com que stdout/stderr só sejam exibidos depois que o target termina, reduzindo o problema de logs misturados e difíceis de ler em builds paralelosmake --output-sync=recurse -j10Em sistemas multiusuário ocupados, só o número de jobs com
-jpode não bastar, então também dá para limitar o paralelismo com base na média de carga:make -j10 --load-average=10--shuffle, que randomiza a ordem de agendamento dos targets, também é útil em CI para pegar dependências ausentes em Makefiles:make --shuffle # or --shuffle=seed/reverse-B, opção que força reconstruir tudo incondicionalmentemake -jdeixar a máquina em estado de DoS várias vezes, então considero quase um bugPor volta de 1985, vi alguém no Graphics Lab da Boston University usando Makefile para acionar a geração de um renderizador 3D para animação. Era alguém da área de Lisp, trabalhando com geração procedural inicial e sistemas de atores 3D, e o Makefile tinha umas 10 linhas, muito elegante
Usando apenas dependências simples por data de arquivo, ele gerava centenas de animações; o Lisp criava as formas 3D de cada quadro, e o Make gerava os frames
Era 1985, uma época em que quase nada do que hoje consideramos óbvio em 3D e animação existia, então impressionou todo mundo. Depois, pelo que me lembro, ele trabalhou no renderizador 3D de Iron Giant e teve um papel central também em Coraline. Era Brian Gardner
Make é uma daquelas ferramentas que fico muito feliz de ter aprendido no começo da carreira. Hoje não uso muito, mas ela me mostrou o poder que sistemas declarativos têm em comparação com sistemas imperativos
Percebi que essa ideia se estende naturalmente a outras tarefas. Olhando a imagem no topo deste site, parece que o autor sentiu algo parecido: dá para entender melhor receitas de cozinha se as enxergarmos de forma declarativa, como um Makefile, em vez de uma sequência de comandos em estilo script, como uma receita tradicional
Texto relacionado: https://blog.gpkb.org/posts/cooking-with-make/
Sempre escrevo receitas de modo que possam ser lidas como um Makefile e levo para a cozinha; fico curioso se alguém já as compôs ou exibiu assim. Acho que economizaria bastante tempo ao ler uma receita nova, sem precisar converter mentalmente o script em Makefile
Claro que o custo é ter de resolver o grafo de dependências por conta própria. Quando você só quer seguir uma sequência de passos já serializada, acaba assumindo mais trabalho
O texto diz que a maioria das receitas não é marcada como
.PHONYe usa isso como uma espécie de motivo para não tratar do assunto no tutorial, mas é uma desculpa fraca. Ferramentas devem ensinar o uso corretoNa minha equipe, como usamos make como executor de tarefas, levei uma chamada por adicionar e manter
.PHONYem todas as receitasO guia de estilo de Makefile do Clark Grubb é bom: https://clarkgrubb.com/makefile-style-guide
Fico curioso se alguém usa esse guia de estilo. Também queria saber se preferem marcar receitas phony no ponto da declaração ou manter uma lista enorme no topo do arquivo. Seria bom ter um linter que impusesse isso
Não gosto de usar
-o pipefailpor inércia. Pipefail é útil, mas quebra a coisa mais comum que as pessoas fazem em pipelines: filtrar saída comgrep. É melhor colocá-lo individualmente só nas receitas que precisamMarcar targets que não são arquivos como
.PHONYé estritamente correto, mas em geral não é necessário. Quando há muitos targets, isso acrescenta verbosidade desnecessária ao Makefile; acho melhor adicionar só quando for precisoEm receitas que produzem vários arquivos de saída, antes o padrão era usar arquivos dummy/arquivos de flag quando regras de padrão não se encaixavam, mas desde o GNU Make 4.3 há suporte nativo a targets agrupados. Também está no Ubuntu 22.04 LTS: https://www.gnu.org/software/make/manual/html_node/Multiple-...
Um ponto interessante recente é que o CMake concluiu que Makefiles são inadequados para projetos que usam módulos do C++20, e que o Ninja é a opção certa. https://cmake.org/cmake/help/latest/manual/cmake-cxxmodules....
Basicamente, parece considerar que é difícil demais, ou impossível, definir estaticamente as dependências dos targets. Hoje isso é tratado dinamicamente com ferramentas como
clang-scan-deps: https://llvm.org/devmtg/2019-04/slides/TechTalk-Lorenz-clang...Na verdade, como o Ninja exige que todas as dependências sejam definidas estaticamente, nesse aspecto ele tem menos recursos que o Make comum
Make tem seu lugar como ferramenta de build para grandes bases de código em C. Mas muitas vezes ele é tratado como uma espécie de “executor de tarefas por projeto” de uso geral, e para isso ele não se encaixa bem. Até condicionais simples são difíceis
Por exemplo, já vi várias tentativas bem-intencionadas de encapsular Terraform com Make, mas nenhuma terminou bem
Um excelente tool moderno para substituir as partes em que Makefiles ficam bagunçados é o just: https://github.com/casey/just
Como um conceito totalmente diferente, há também o Makedown, que apareceu no HN há 8 meses: https://news.ycombinator.com/item?id=41825344
Makefiles são ótimos, mas é melhor não se empolgar demais. Alguns anos atrás, tentei criar um framework GNU Make puro e percebi que estava basicamente reinventando o autoconf; só então entendi por que o GNU autotools foi criado
Makefiles são uma espécie de tarpit de Turing curiosamente parecido com Lisp. O GNU Make ainda tem recursos de metaprogramação, então é difícil resistir à vontade de metaprogramar algum sistema profano dentro de um Makefile. O fato de o GNU Make estar tão amplamente instalado aumenta ainda mais a tentação
Sou uma das pessoas que criou e mantém o Task, uma alternativa ao Make. Ele existe há mais de 8 anos e continua evoluindo, então, se você está procurando algo novo, vale a pena experimentar. Fiquem à vontade para fazer perguntas também
https://taskfile.dev/
https://github.com/go-task/task
Tenho curiosidade para saber se alguém já usou tup: https://gittup.org/tup/ex_dependencies.html
É um sistema de build que determina dependências automaticamente com base no acesso ao sistema de arquivos, então pode ser usado com qualquer compilador ou ferramenta