6 pontos por GN⁺ 2025-06-21 | 1 comentários | Compartilhar no WhatsApp
  • 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 make no terminal, salve cada exemplo em um arquivo Makefile e execute o comando make
  • 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 hello não tem dependências e executa 2 comandos
  • Ao executar make hello, se o arquivo hello nã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

  1. Crie o arquivo blah.c (com o conteúdo int main() { return 0; })
  2. Escreva o seguinte Makefile
blah:  
	cc blah.c -o blah  
  • Ao executar make, se o alvo blah não existir, a compilação será executada e o arquivo blah será criado
  • Mesmo que blah.c mude, 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.c tiver sido alterado, o alvo blah será 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_file não é criado como arquivo real, o comando de some_file será 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, LDFLAGS etc.
  • 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

 
GN⁺ 2025-06-21
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

  • 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
    • Em sistemas ocupados ou ambientes com vários usuários, em vez de -j, dá para usar --load-average para controlar a carga do sistema durante o paralelismo (make -j10 --load-average=10)
    • A opção --shuffle, que embaralha aleatoriamente o agendamento dos alvos de build, é útil em CI para encontrar problemas de dependência no Makefile
    • Menciona 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 completo

    • Já viu com frequência problemas em máquinas DOS causados por make -j, então passou a enxergar esse comportamento como bug

    • Pergunta 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 .PHONY em um tutorial só porque não se usa seria uma desculpa fraca. A opinião é que o certo é ensinar a usar a ferramenta direito

    • Na equipe, houve discussão porque usavam Make como task runner e acrescentavam e mantinham .PHONY em todas as recipes
    • Recomenda o guia de estilo de Makefile de Clark Grubb (clarkgrubb.com/makefile-style-guide)
    • Compartilha experiências com estilos diferentes entre declarar .PHONY por recipe ou agrupar tudo de uma vez no topo do arquivo, e gostaria que isso fosse imposto por um linter
    • Depois de ler, achou um documento bom, mas discorda de alguns pontos
      • Aplicar -o pipefail cegamente é problemático; em pipelines com grep e similares, pode quebrar, então o ideal é aplicar conforme o caso
      • Marcar alvos que não são arquivos com .PHONY é mais rigoroso, mas quase sempre desnecessário e só deixa o Makefile mais verboso; melhor usar quando realmente precisar
      • Recipes que geram vários arquivos de saída antigamente usavam arquivo dummy, mas desde o GNU Make 4.3 há suporte oficial a group targets (veja aqui)
  • Afirmativa de que Make é uma ferramenta especializada em builds de grandes codebases em C

    • Alguém comentou que gosta de usar como job runner por projeto, mas que Make não é adequado como job runner e até coisas como condicionais ficam desajeitadas nessa estrutura
    • Também já viu tentativa fracassada de usá-lo como wrapper para ferramentas como Terraform
    • 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)

    • Na prática, como é quase impossível definir dependências de alvo estaticamente, adotou-se uma abordagem de análise dinâmica com ferramentas como 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)

    • tup é um sistema de build que detecta dependências automaticamente com base em acessos ao sistema de arquivos, podendo ser aplicado a qualquer compilador ou ferramenta
  • 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

    • Ao fazer parsing de opções em MAKEFLAGS, para lidar com opções longas ou opções curtas vazias, é preciso fazer assim
      ifneq (,$(findstring t,$(firstword -$(MAKEFLAGS))))
    • Se for necessário compatibilidade com a versão antiga do make fornecida por padrão no OS X, vários recursos estão ausentes ou se comportam de forma sutilmente diferente
    • Os outros problemas são em sua maioria typos ou violações de estilo consideradas menores, então foram omitidos
    • Como referência, load é mais portável que guile, e em ambientes de cross-compilation é preciso especificar corretamente o compilador
    • Recomenda muito ler Paul’s Rules of Makefiles (aqui), o manual do GNU make (aqui) e manuais relacionados
    • Também mantém um projeto simples de demonstração de Makefile (demo no GitHub)
  • Tem o hábito de sempre incluir um Makefile em cada repositório GitHub

    • Como é fácil esquecer os comandos toda vez, guardar isso no Makefile permite adicionar passos complexos com facilidade e, ao rodar apenas make, já executar imediatamente o comportamento esperado de cada projeto sem precisar lembrar de tudo