Aprenda Makefile com os Melhores Exemplos
(makefiletutorial.com)- Makefile é uma ferramenta que simplifica a automação de build em C/C++ e o gerenciamento de dependências
- Usa detecção de arquivos alterados com base em timestamps, executando a compilação apenas quando necessário
- Explica a estrutura central com exemplos, como regras (
rule), comandos (command) e dependências (prerequisite) - Também aborda de forma prática recursos avançados como variáveis automáticas, regras de padrão e expansão de variáveis
- Apresenta a importância de escalabilidade e manutenção com um template de Makefile para projetos de porte médio
Introdução ao guia tutorial de Makefile
- Makefile é uma ferramenta essencial para automação de build e gerenciamento de dependências em projetos
- Pode parecer complexo no primeiro contato por causa das várias regras implícitas e símbolos, mas este guia organiza os pontos principais com exemplos concisos e executáveis
- Cada seção permite compreensão por meio de exemplos práticos
Primeiros passos
Qual é o propósito do Makefile
- Makefile é usado para recompilar apenas as partes alteradas em programas grandes
- Além de C/C++, várias linguagens têm ferramentas de build próprias, mas o Make é usado de forma ampla em cenários gerais de build
- A lógica central é detectar arquivos alterados e executar apenas o que for necessário
Sistemas de build alternativos ao Make
- Ecossistema C/C++: há várias opções, como SCons, CMake, Bazel e Ninja
- Ecossistema Java: Ant, Maven, Gradle etc.
- Go, Rust, TypeScript etc. também oferecem ferramentas de build próprias
- Linguagens interpretadas como Python, Ruby e JavaScript não precisam de compilação, então geralmente há menos necessidade de gerenciamento separado com algo como Makefile
Versões e tipos de Make
- Existem várias implementações de Make, mas este guia é otimizado para GNU Make (usado principalmente em Linux e MacOS)
- Os exemplos são compatíveis com GNU Make 3 e 4
Como executar os exemplos
- Depois de instalar o
makeno terminal, salve cada exemplo em um arquivoMakefilee execute o comandomake - As linhas de comando dentro do Makefile devem obrigatoriamente ser indentadas com tab
Sintaxe básica de Makefile
Estrutura de uma regra (Rule)
-
alvo: dependência(s)- comando
- comando
-
Alvo: nome do arquivo gerado no build (normalmente um só)
-
Comando: script de shell que realmente será executado (começa com tab)
-
Dependência: lista de arquivos que precisam estar prontos antes de o alvo ser construído
A essência do Make
Exemplo Hello World
hello:
echo "Hello, World"
echo "This line will print if the file hello does not exist."
- O alvo
hellonão tem dependências e executa 2 comandos - Ao executar
make hello, se o arquivohellonão existir, os comandos serão executados. Se ele já existir, nada será executado - Em geral, escreve-se de forma que alvo = nome do arquivo
Exemplo básico de compilação de arquivo C
- Crie o arquivo
blah.c(com o conteúdoint main() { return 0; }) - Escreva o seguinte Makefile
blah:
cc blah.c -o blah
- Ao executar
make, se o alvoblahnão existir, a compilação será executada e o arquivoblahserá criado - Mesmo que
blah.cmude, não haverá recompilação automática → é preciso adicionar a dependência
Como adicionar dependência
blah: blah.c
cc blah.c -o blah
- Agora, se
blah.ctiver sido alterado, o alvoblahserá reconstruído - O critério para detectar alterações é o timestamp do arquivo
- Se o timestamp for manipulado manualmente, o comportamento pode não ser o esperado
Mais exemplos
Exemplo de alvos encadeados e dependências
blah: blah.o
cc blah.o -o blah
blah.o: blah.c
cc -c blah.c -o blah.o
blah.c:
echo "int main() { return 0; }" > blah.c
- As dependências são seguidas em estrutura de árvore, automatizando a geração em cada etapa
Exemplo de alvo que sempre executa
some_file: other_file
echo "This will always run, and runs second"
touch some_file
other_file:
echo "This will always run, and runs first"
- Como
other_filenão é criado como arquivo real, o comando desome_fileserá executado toda vez
Make clean
- O alvo
cleané frequentemente usado para remover artefatos de build - Não é uma palavra reservada especial do Make; precisa ser definido manualmente com comandos
- Se houver um arquivo chamado
clean, isso pode causar confusão, então recomenda-se usar.PHONY
Exemplo:
some_file:
touch some_file
clean:
rm -f some_file
Tratamento de variáveis
- Variáveis são sempre strings.
- Em geral, recomenda-se
:=, mas existem várias formas de atribuição, como=,?=e+= - Exemplo de uso:
files := file1 file2
some_file: $(files)
echo "Look at this variable: " $(files)
touch some_file
file1:
touch file1
file2:
touch file2
clean:
rm -f file1 file2 some_file
- Formas de referenciar variáveis:
$(variable)ou${variable} - Aspas dentro do Makefile não têm significado para o próprio Make (mas podem ser necessárias em comandos de shell)
Gerenciamento de alvos
Alvo all
- Para executar vários alvos de uma vez, atribui-se esse papel ao primeiro alvo (o padrão)
all: one two three
one:
touch one
two:
touch two
three:
touch three
clean:
rm -f one two three
Múltiplos alvos e variáveis automáticas
- É possível executar comandos individuais para vários alvos.
$@contém o nome do alvo atual
all: f1.o f2.o
f1.o f2.o:
echo $@
Variáveis automáticas e curingas
Curinga *
*procura nomes diretamente no sistema de arquivos- Recomenda-se usá-lo sempre envolvido pela função
wildcard
print: $(wildcard *.c)
ls -la $?
- Não use
*diretamente na definição de variável
thing_wrong := *.o
thing_right := $(wildcard *.o)
Curinga %
- É usado principalmente em pattern rules, permitindo extrair e expandir padrões específicos
Fancy Rules
Regras implícitas (Implicit)
- O Make traz embutidas várias regras padrão ocultas relacionadas a builds em C/C++
- Variáveis representativas:
CC,CXX,CFLAGS,CPPFLAGS,LDFLAGSetc. - Exemplo em C:
CC = gcc
CFLAGS = -g
blah: blah.o
blah.c:
echo "int main() { return 0; }" > blah.c
clean:
rm -f blah*
Static Pattern Rules
- Permitem escrever de forma concisa várias regras que seguem o mesmo padrão
objects = foo.o bar.o all.o
all: $(objects)
$(CC) $^ -o all
$(objects): %.o: %.c
$(CC) -c $^ -o $@
all.c:
echo "int main() { return 0; }" > all.c
%.c:
touch $@
clean:
rm -f *.c *.o all
Static Pattern Rules + função filter
- Com
filter, é possível selecionar apenas os alvos que correspondem a um padrão de extensão específico
obj_files = foo.result bar.o lose.o
src_files = foo.raw bar.c lose.c
all: $(obj_files)
.PHONY: all
$(filter %.o,$(obj_files)): %.o: %.c
echo "target: $@ prereq: $
1 comentários
Comentários do Hacker News
Em 1985, vi pessoalmente alguém no laboratório de Graphics da Boston University usando um Makefile para criar um renderizador 3D para animação. A pessoa era programadora Lisp e estava trabalhando em geração procedural inicial e em um sistema de atores 3D, além de ter feito um Makefile realmente elegante com cerca de 10 linhas. A estrutura gerava automaticamente centenas de animações apenas com dependências de data de arquivos. As formas 3D de cada frame eram criadas em Lisp, e o Make gerava os frames. Em 1985, ao contrário de hoje, quando 3D e animação parecem algo natural, todo mundo ficava maravilhado; lembro que essa pessoa depois foi Brian Gardner, responsável pelo renderizador 3D de Iron Giant e Coraline
Pergunta se seria esta pessoa mostrada em 3d-consultant.com/bio.html
Confirmação se estavam falando do filme Coraline
Apresentação de algumas flags úteis e pouco conhecidas ao usar Make
--output-sync=recurse -j10: faz com que stdout/stderr sejam agrupados e exibidos só quando o trabalho de cada alvo termina; sem isso, os logs se misturam e ficam difíceis de analisar-j, dá para usar--load-averagepara controlar a carga do sistema durante o paralelismo (make -j10 --load-average=10)--shuffle, que embaralha aleatoriamente o agendamento dos alvos de build, é útil em CI para encontrar problemas de dependência no MakefileMenciona a ideia de organizar oficialmente as várias opções do make e incluí-las no programa em formato de texto ou documentação, para facilitar o acesso
A opção que mais usa é a flag
-B, para forçar rebuild completoJá viu com frequência problemas em máquinas DOS causados por
make -j, então passou a enxergar esse comportamento como bugPergunta se, em sistemas ocupados ou com vários usuários, os problemas de paralelização não deveriam ser resolvidos pelo escalonador do sistema operacional
Recomenda não usar essas opções fora de projetos privados de uso próprio, porque apesar de úteis elas não são portáveis
Achar que dá para pular
.PHONYem um tutorial só porque não se usa seria uma desculpa fraca. A opinião é que o certo é ensinar a usar a ferramenta direito.PHONYem todas as recipes.PHONYpor recipe ou agrupar tudo de uma vez no topo do arquivo, e gostaria que isso fosse imposto por um linter-o pipefailcegamente é problemático; em pipelines comgrepe similares, pode quebrar, então o ideal é aplicar conforme o caso.PHONYé mais rigoroso, mas quase sempre desnecessário e só deixa o Makefile mais verboso; melhor usar quando realmente precisarAfirmativa de que Make é uma ferramenta especializada em builds de grandes codebases em C
A opinião é que Make, mais do que um job runner, é uma ferramenta shell genérica que transforma scripts shell lineares em dependências declarativas
A visão de Make como ferramenta de build só para codebases em C já não faria mais sentido. Menciona que, nos últimos 20 anos, foram criados sistemas de build mais robustos e claros. Defende uma atualização dessa visão
Pergunta o que seria um bom job runner. (Depois acrescenta um pedido de desculpas por ter confundido o significado de job runner.)
Recomenda just como ferramenta moderna para substituir a parte do Makefile que fica complexa
just é bom para substituir listas de scripts shell, mas não substitui a funcionalidade essencial do Make de “executar só as regras que precisam rodar de novo”
Outras alternativas mencionadas
Essas ferramentas alternativas se apresentam como substitutas do Make, mas a opinião é que são coisas totalmente diferentes e difíceis de comparar. O núcleo do Make está em gerar artefatos e evitar reconstruir o que já foi buildado. Já o just funciona como simples executor de comandos
A vantagem de usar Make como executor de comandos é a estabilidade de ser uma ferramenta padrão instalada em quase todo lugar. Mesmo que as alternativas sejam melhores, exigem instalação à parte e isso faz parecer desnecessário adotá-las
Usa Task em projetos simples de hobby em C, mas ainda não sabe dizer se ele serve bem para projetos grandes também (site oficial do Task)
Considera interessante que o CMake tenha decidido usar ninja por padrão ao concluir que Makefiles não são adequados para suporte a módulos C++20 (guia do CMake)
clang-scan-deps(slides técnicos)A opinião é que essa limitação na verdade é uma decisão do CMake ou um problema de falta de mantenedor para o gerador de Makefiles. O ninja também não oferece suporte direto a módulos C++ (issue relacionado), e inclusive tem menos funcionalidades que o Make, além de exigir que todas as dependências sejam declaradas estaticamente
Opinião de que a própria introdução de módulos já é algo complexo e confuso
Pergunta se alguém tem experiência com tup. (documentação oficial)
Apresenta-se como criador e maintainer principal do Task, ferramenta alternativa ao Make. Está desenvolvendo o projeto há mais de 8 anos e ele continua evoluindo
just também é recomendado como outra alternativa ao Make (GitHub do just)
Curiosamente, usa Task com frequência e até abriu esta issue hoje de manhã
Este tutorial tem alguns problemas perigosos e sutis
ifneq (,$(findstring t,$(firstword -$(MAKEFLAGS))))loadé mais portável que guile, e em ambientes de cross-compilation é preciso especificar corretamente o compiladorTem o hábito de sempre incluir um Makefile em cada repositório GitHub
make, já executar imediatamente o comportamento esperado de cada projeto sem precisar lembrar de tudo