Automação de Build com Make

[413] Automação de Build com Make

Indentar o comando com espaço em vez de TAB gera um erro enigmático — é o primeiro tropeço de quase todo Makefile. Passado ele, vêm a regra de alvo, dependência e comando, a comparação de datas que recompila só o que mudou, o alvo phony e as variáveis que centralizam as flags do projeto.
Linguagem C

11 min de leitura

Lá na aula Compilação Separada: Cabeçalhos e Múltiplos Arquivos, quando dividimos nosso programa em múltiplos arquivos, você teve um vislumbre de um problema que só cresce: cada .c precisa ser compilado, e todos os objetos precisam ser ligados. Com dois arquivos, digitar os comandos à mão é tolerável. Com vinte, é um tormento — e recompilar tudo a cada pequena mudança é um desperdício de tempo. Hoje resolvemos isso com o Make, a ferramenta clássica que automatiza a compilação: você descreve como construir seu programa uma vez, e depois um único comando, make, cuida de tudo — recompilando apenas o que mudou.

O problema que o Make resolve

Imagine um projeto com main.c, mymath.c e mymath.h, como o da aula Compilação Separada: Cabeçalhos e Múltiplos Arquivos. Para construí-lo, você digita:

gcc -c mymath.c -o mymath.o
gcc -c main.c -o main.o
gcc mymath.o main.o -o programa

Três comandos. Se você mudar só o main.c, precisaria idealmente recompilar apenas ele e religar — mas na prática, para não errar, muitos acabam recompilando tudo. Num projeto real, com dezenas de arquivos, isso significa esperar minutos por uma compilação completa quando bastaria recompilar um arquivo. O Make elimina esse desperdício: ele conhece as dependências entre os arquivos e recompila apenas o que foi afetado por uma mudança. Você descreve a estrutura do projeto uma vez, num arquivo chamado Makefile, e o Make faz o trabalho inteligente.

Anatomia de uma regra

Um Makefile é composto de regras, e cada regra tem três partes: um alvo (o que se quer produzir), suas dependências (do que ele depende) e os comandos (como produzi-lo). A sintaxe:

alvo: dependências
    comando

Um detalhe absolutamente crítico, que causa incontáveis erros a iniciantes: a linha do comando deve começar com um caractere de tabulação (TAB), não com espaços. O Make é rigoroso quanto a isso — usar espaços em vez de TAB gera um erro enigmático. Guarde isso desde já: comandos são indentados com TAB.

Veja um Makefile completo para o nosso projeto de exemplo:

# Makefile para o projeto de exemplo

# regra principal: 'programa' depende dos dois objetos
programa: main.o mymath.o
    gcc main.o mymath.o -o programa

# main.o depende de main.c e do cabeçalho
main.o: main.c mymath.h
    gcc -c main.c -o main.o

# mymath.o depende de mymath.c e do cabeçalho
mymath.o: mymath.c mymath.h
    gcc -c mymath.c -o mymath.o

# regra utilitária para limpar os arquivos gerados
clean:
    rm -f programa main.o mymath.o

Cada regra declara o que produz e do que depende. A primeira diz: para construir programa, preciso de main.o e mymath.o, e o comando para produzi-lo é a ligação. As duas regras seguintes dizem como produzir cada objeto a partir do seu .c e do cabeçalho. Rodando make, a ferramenta lê essas regras e executa os comandos na ordem correta.

A mágica: recompilação inteligente por timestamps

Como o Make sabe o que recompilar? Ele compara as datas de modificação dos arquivos. A regra é simples e poderosa: se um alvo é mais antigo que qualquer uma de suas dependências, ele precisa ser reconstruído. Se main.c foi editado depois da última vez que main.o foi gerado, então main.o está "desatualizado" e o Make o recompila. Se nada mudou, o Make não faz nada e informa que tudo está atualizado.

Isso produz exatamente o comportamento que queremos. Ao rodar make pela primeira vez, ele compila tudo. Se você editar apenas main.c e rodar make de novo, ele recompila apenas main.o e religa o programa — mymath.o é reaproveitado, porque mymath.c não mudou. E as dependências se propagam: como declaramos que main.o depende de mymath.h, editar o cabeçalho faz o Make recompilar main.o também (afinal, uma mudança na interface pode afetar quem a usa). Essa propagação correta de dependências é o que torna o Make confiável — ele reconstrói tudo que foi afetado, e nada além disso.

Alvos que não são arquivos: .PHONY

Repare na regra clean do Makefile: ela não produz um arquivo chamado "clean" — é um comando utilitário que apaga os arquivos gerados, útil para forçar uma reconstrução do zero. Alvos assim, que representam ações em vez de arquivos, são chamados de phony (falsos). Você os roda explicitamente com make clean. É boa prática declará-los para o Make não confundi-los com arquivos reais:

.PHONY: clean

clean:
    rm -f programa main.o mymath.o

A declaração .PHONY: clean diz ao Make que clean é sempre uma ação a executar, nunca um arquivo a verificar. Sem ela, se por acaso existisse um arquivo chamado clean no diretório, o Make poderia se recusar a rodar a regra. É uma salvaguarda simples que se tornou convenção.

Variáveis: evitando repetição

Makefiles reais usam variáveis para evitar repetir valores como o nome do compilador e as flags. Isso torna o arquivo mais limpo e fácil de ajustar:

# variáveis no topo, fáceis de mudar
CC = gcc
CFLAGS = -Wall -Wextra -g

programa: main.o mymath.o
    $(CC) main.o mymath.o -o programa

main.o: main.c mymath.h
    $(CC) $(CFLAGS) -c main.c -o main.o

mymath.o: mymath.c mymath.h
    $(CC) $(CFLAGS) -c mymath.c -o mymath.o

.PHONY: clean
clean:
    rm -f programa main.o mymath.o

Definimos CC (o compilador) e CFLAGS (as flags) no topo, e as usamos com $(CC) e $(CFLAGS). Agora, para mudar as flags de compilação em todo o projeto — digamos, adicionar -fsanitize=address das aulas anteriores —, basta editar uma linha. Note que incluímos -Wall -Wextra -g como flags padrão: os avisos que recomendamos durante todo o curso e a informação de depuração das duas últimas aulas, agora automaticamente aplicados a cada compilação. Essa é a beleza do Make: boas práticas viram parte da infraestrutura, aplicadas sem esforço a cada build.

O valor real: consistência e velocidade

Além da conveniência óbvia, o Make traz dois benefícios profundos. Consistência: todos que trabalham no projeto constroem exatamente da mesma forma, com as mesmas flags — não há o risco de uma pessoa esquecer um -Wall ou compilar diferente. O Makefile documenta e garante o processo de build. Velocidade: em projetos grandes, a recompilação seletiva economiza enormemente — editar um arquivo e reconstruir leva segundos, não minutos. Esses ganhos são a razão de o Make, apesar de décadas de existência, continuar onipresente em projetos C. Ele é frequentemente a primeira coisa que você encontra ao clonar um projeto: um Makefile e a instrução "rode make".

O que vem a seguir

Hoje automatizamos a compilação com o Make: regras de alvo/dependências/comando, recompilação inteligente baseada em timestamps, alvos phony como clean, e variáveis para centralizar configurações — incorporando os avisos e a depuração das aulas anteriores diretamente ao processo de build. O Make é excelente, mas escrever Makefiles à mão para projetos muito grandes, com múltiplas plataformas e bibliotecas externas, torna-se complexo. Na próxima aula, conheceremos o CMake, uma ferramenta de nível mais alto que gera Makefiles (e outros sistemas de build) a partir de uma descrição mais simples e portável — o padrão moderno para projetos C e C++ de maior escala.

Fontes e leituras recomendadas

Exercícios

Exercício 1

Crie o projeto de exemplo da aula Compilação Separada: Cabeçalhos e Múltiplos Arquivos (main.c, mymath.c, mymath.h) e escreva um Makefile básico com regras para programa, main.o e mymath.o. Rode make e confirme que o programa é construído.

Ver resposta

✓ Resposta: Usando os arquivos da aula Compilação Separada: Cabeçalhos e Múltiplos Arquivos, um Makefile básico:

programa: main.o mymath.o
    gcc main.o mymath.o -o programa

main.o: main.c mymath.h
    gcc -c main.c -o main.o

mymath.o: mymath.c mymath.h
    gcc -c mymath.c -o mymath.o

Rodando make, a ferramenta compila main.o e mymath.o e depois liga ambos em programa. (Lembre-se: os comandos devem ser indentados com TAB, não espaços.)

Exercício 2

Com o projeto do exercício 1 já compilado, edite apenas o main.c (mude uma mensagem, por exemplo) e rode make de novo. Observe quais arquivos são recompilados. Por que mymath.o não é recompilado?

Ver resposta

✓ Resposta: Ao editar apenas main.c e rodar make novamente, apenas main.o é recompilado, seguido da religação de programa. O mymath.o não é recompilado porque mymath.c e mymath.h não foram modificados — a data de modificação de mymath.o continua mais recente que a de suas dependências, então o Make o considera atualizado e o reaproveita. Isso demonstra a recompilação seletiva: só o que foi afetado pela mudança é reconstruído.

Exercício 3

Adicione ao seu Makefile uma regra clean (declarada como .PHONY) que apague o executável e os arquivos .o. Teste rodando make clean e confirme que os arquivos gerados foram removidos.

Ver resposta

✓ Resposta:

.PHONY: clean
clean:
    rm -f programa main.o mymath.o

Rodando make clean, o comando rm -f apaga o executável e os dois objetos. A declaração .PHONY: clean garante que a regra sempre execute a ação, mesmo que existisse um arquivo chamado "clean" no diretório. Depois de make clean, um novo make reconstrói tudo do zero.

Exercício 4

Modifique o Makefile para usar variáveis CC e CFLAGS, incluindo -Wall -Wextra -g nas flags. Explique a vantagem de centralizar essas configurações em variáveis.

Ver resposta

✓ Resposta:

CC = gcc
CFLAGS = -Wall -Wextra -g

programa: main.o mymath.o
    $(CC) main.o mymath.o -o programa

main.o: main.c mymath.h
    $(CC) $(CFLAGS) -c main.c -o main.o

mymath.o: mymath.c mymath.h
    $(CC) $(CFLAGS) -c mymath.c -o mymath.o

A vantagem de centralizar em variáveis é que qualquer mudança de configuração (trocar o compilador, adicionar ou remover flags, ligar sanitizers) se faz num único lugar, no topo do arquivo, e se aplica automaticamente a todas as regras que usam $(CC) e $(CFLAGS). Sem variáveis, você teria que editar cada regra individualmente, com risco de esquecer alguma e criar inconsistências (um arquivo compilado com flags diferentes dos outros). As variáveis tornam o Makefile mais fácil de manter e garantem que todos os arquivos sejam construídos da mesma forma.

Exercício 5

Explique, com suas palavras, como o Make decide se um alvo precisa ser reconstruído. O que acontece quando você edita o mymath.h (o cabeçalho compartilhado), e por quê?

Ver resposta

✓ Resposta: O Make decide se um alvo precisa ser reconstruído comparando as datas de modificação (timestamps) do alvo e de suas dependências. A regra: se o alvo não existe, ou se é mais antigo que qualquer uma de suas dependências, ele está desatualizado e precisa ser reconstruído; caso contrário, o Make o considera atualizado e não faz nada. Quando você edita o mymath.h (o cabeçalho compartilhado), sua data de modificação passa a ser mais recente. Como declaramos que tanto main.o quanto mymath.o dependem de mymath.h, ambos os objetos ficam mais antigos que essa dependência recém-modificada — então o Make recompila os dois, e depois religa o programa. Isso é o comportamento correto e desejável: uma mudança no cabeçalho (a interface compartilhada) pode afetar qualquer arquivo que o inclui, então todos precisam ser recompilados para refletir a alteração. É por isso que declarar as dependências de cabeçalho nas regras é importante — sem isso, o Make não saberia que uma mudança no .h deve propagar a recompilação, e você poderia acabar com objetos inconsistentes.

Comentários

Mais em Linguagem C

O Pré-processador: #define, Macros e Inclusões
O Pré-processador: #define, Macros e Inclusões

O quadrado de 2 + 3 dá 11 quando a macro esquece os parênteses — o…

Ponteiros para Função: Passando Comportamento como Argumento
Ponteiros para Função: Passando Comportamento como Argumento

O nome de uma função, escrito sem os parênteses, é um endereço — e dessa…

Threads com pthreads
Threads com pthreads

contador++ não é uma operação só: são ler, somar e escrever — e duas threads…