Entendendo o Make
August 1, 2026 · – visitantes
make aparece em praticamente todo projeto embarcado e de sistemas que já toquei — receitas do Yocto, builds de kernel, U-Boot, pequenos repositórios de firmware. Também é uma daquelas ferramentas que as pessoas usam por anos por imitação, copiando um Makefile do último projeto sem nunca chegar a fixar de fato o modelo por trás dele. Aqui está esse modelo, exposto diretamente.
O que é o Make?
Make é uma ferramenta de automação de build: ele lê um arquivo (um Makefile) que descreve um conjunto de alvos (targets), os pré-requisitos dos quais cada alvo depende, e a receita (comandos de shell) necessária para produzir esse alvo a partir de seus pré-requisitos. Com essa descrição em mãos, o make descobre quais alvos estão desatualizados e executa apenas as receitas necessárias para atualizá-los — nada mais.
main: main.o utils.o
gcc -o main main.o utils.o
main.o: main.c
gcc -c main.c
Ao rodar make, ele constrói main.o apenas se main.c for mais recente que main.o (ou se main.o ainda não existir), e então vincula main apenas se main.o ou utils.o tiver mudado. Nada é reconstruído desnecessariamente.
Para quem é?
O Make foi escrito por Stuart Feldman no Bell Labs em abril de 1976. Segundo o próprio relato dele, o estopim foi um colega — Steve Johnson, autor do yacc — que perdeu uma manhã inteira depurando um programa que, na verdade, estava correto; o binário simplesmente não havia sido religado após uma mudança no código-fonte. Feldman recebeu o ACM Software System Award de 2003 por causa disso. A ferramenta que surgiu daquela tarde — rastrear dependências uma vez, deixar a máquina descobrir o que precisa ser refeito — se tornou uma das ideias mais copiadas em ferramentas de software. O GNU Make, a versão que quase todo mundo usa hoje, é uma reimplementação livre do Projeto GNU (1) (5) (6).
Na prática, é para qualquer pessoa que esteja montando um artefato não trivial — um binário compilado, um documento, um conjunto de arquivos gerados — a partir de peças menores, em que reconstruir tudo do zero a cada mudança seria desperdício ou lento. Isso cobre projetos em C/C++, builds de kernel e bootloader, documentos em LaTeX, e bastante automação improvisada que não tem nada a ver com compilar código.
Quando você deveria recorrer a ele?
O Make se justifica quando três coisas são verdadeiras ao mesmo tempo:
- Existe um grafo de dependências real — algumas saídas dependem de outras, e reconstruir não é simplesmente "regerar tudo".
- As reconstruções são caras o suficiente para que pular as desnecessárias importe (uma compilação de dois segundos não precisa disso; um build de kernel de vinte minutos precisa).
- As regras são razoavelmente estáveis — o modelo de timestamps de arquivo do Make não lida com pipelines extremamente dinâmicos com a mesma elegância que um executor de tarefas com invalidação explícita conseguiria.
Para um punhado de arquivos sem interdependências, um script de shell costuma ser mais simples e mais honesto. O Make começa a valer a pena quando "isso realmente precisava ser reconstruído?" se torna uma pergunta que vale a pena fazer automaticamente em vez de manualmente.
Onde ele se encaixa na toolchain?
O Make geralmente fica uma camada acima do compilador e uma camada abaixo do que quer que um humano execute. Em um projeto C típico: arquivos-fonte → (compilador, acionado pelo Make) → arquivos-objeto → (linker, acionado pelo Make) → binário final. O próprio Make não compila nem vincula nada — ele invoca as ferramentas que fazem isso, como comandos de shell em cada receita.
Ele também tende a ficar abaixo de ferramentas de configuração de build de nível mais alto. CMake, Autotools e o BitBake do Yocto, por exemplo, não substituem o Make; eles geram Makefiles (ou algo equivalente que dirigem) como seu backend real de execução. Então até projetos que nunca mostram um Makefile diretamente muitas vezes estão rodando um em algum ponto do pipeline.
Por que ele funciona do jeito que funciona?
A decisão central de design — comparar os horários de modificação de arquivos para decidir o que está desatualizado — é o que torna o Make rápido em árvores grandes: verificar um timestamp é barato, então reconstruir um projeto enorme depois de uma mudança de uma linha pode pular quase tudo e ainda assim estar correto, desde que o grafo de dependências fornecido a ele seja preciso.
Essa última cláusula é a origem de quase toda reclamação do tipo "o Make está quebrado" que as pessoas encontram, e vale a pena parar para pensar nisso: o Make só é tão correto quanto o grafo sobre o qual foi informado. Se um arquivo .c faz #include de um header que não está listado como pré-requisito do arquivo .o correspondente, editar esse header não vai disparar uma reconstrução — não porque o Make está confuso, mas porque nunca ninguém disse a ele que essa dependência existia. É por isso que Makefiles do mundo real quase sempre vêm acompanhados de uma etapa de geração de dependências pelo compilador (-MMD -MP com GCC/Clang) em vez de listas de headers escritas à mão — o compilador já conhece o verdadeiro grafo de includes, então ele é a única fonte confiável para isso (4).
Como ele realmente funciona?
Alvos, pré-requisitos, receitas. A unidade básica é:
target: prerequisite1 prerequisite2
recipe line 1
recipe line 2
As linhas de receita precisam ser indentadas com um tab, não espaços — uma famosa verruga histórica que ainda hoje pega as pessoas de surpresa.
Variáveis automáticas reduzem a repetição:
%.o: %.c
gcc -c $< -o $@
$@ é o alvo, $< é o primeiro pré-requisito — essa única regra de padrão consegue construir todo .o a partir do .c correspondente sem uma linha por arquivo.
Alvos .PHONY marcam nomes que não correspondem a arquivos reais — clean, all, test — para que o Make não se confunda caso exista, por acaso, um arquivo com esse nome, e sempre execute a receita em vez de verificar timestamps: (2)
.PHONY: clean
clean:
rm -f *.o main
Variáveis evitam repetir flags em todo lugar e facilitam o reaproveitamento entre projetos:
CC = gcc
CFLAGS = -Wall -O2
main.o: main.c
$(CC) $(CFLAGS) -c main.c
Geração automática de dependências fecha a lacuna descrita acima:
CFLAGS = -Wall -O2 -MMD -MP
-include $(wildcard *.d)
%.o: %.c
$(CC) $(CFLAGS) -c $<
-MMD faz o compilador emitir um arquivo .d por objeto, listando cada header que realmente foi incluído naquela compilação. O -include incorpora esses arquivos de volta ao Makefile, para que o grafo de dependências permaneça preciso à medida que os headers mudam — sem que ninguém precise mantê-lo manualmente (3).
Uma reflexão final
A maior parte do que parece uma peculiaridade do Make — as comparações de timestamp, a insistência em tabs, o jeito como uma dependência ausente falha silenciosamente em vez de ruidosamente — decorre de uma única decisão de design tomada em 1976: manter o modelo simples e a verificação barata, e confiar que o Makefile descreve a realidade com precisão. Uma vez adotada essa lente, a maioria dos momentos de "o Make está agindo de forma estranha" acaba se revelando como "o grafo que eu dei a ele estava incompleto" — o que é algo muito mais fácil de corrigir.
Leituras Relacionadas
- Git: Apagando Branches, Fazendo Merge e Desfazendo um Push Errado — os comandos de branch e push do dia a dia
Referências
- Feldman, S. I. (1979). Make — A Program for Maintaining Computer Programs. Software: Practice and Experience, 9(4), 255–265. — o próprio artigo de Feldman apresentando o Make, incluindo a motivação original.
- GNU Make Manual — a referência completa e oficial para a sintaxe e a semântica do GNU Make.
- GNU Make Manual: Automatic Prerequisites — o padrão
-MMD/-includenas palavras dos próprios mantenedores. - GCC: Preprocessor Options (
-Mfamily) — o que-MMDe-MPfazem por baixo dos panos. - Make (software) — Wikipedia — histórico geral e linhagem das implementações do Make (BSD Make, GNU Make, NMAKE, etc.).
- Stuart Feldman — Wikipedia — histórico do trabalho de Feldman no Bell Labs e do ACM Software System Award de 2003.
Comentários