Tutorial Makefile 2.0 2021-08-15
O novo e melhorado tutorial Makefile, para pessoas iniciantes e intermediárias. Os tópicos incluem pattern matching, compilando de e para diretórios diferentes, dependências de cabeçalho, e variáveis.
Traduções: English | français | Português (Brasil)
Traduzido por: Vinicius Castilho
Presume-se conhecimento em:
- Proficiência básica em shell POSIX. Incluindo variáveis de ambiente.
- Argumentos básicos do GCC. Incluindo
-c
,-o
, e-W
. - Básico de C, saber a diferença entre um executável, arquivos objeto, arquivos de código fonte, e arquivos de cabeçalho.
O básico
É altamente recomendado para qualquer pessoa seguindo esse tutorial acompanhar passo a passo. Comece clonando este repositório:
git clone https://noahloomans.com/tutorials/makefile
Tendo um erro "certificate has expired"?
Os iMacs da 42 atualmente têm uma lista de autoridades certificadas obsoleta, causando um erro ao tentar clonar.
Você pode clonar a partir desse URL alternativo:
git clone https://gitlab.com/nloomans/noahloomans.com-tutorials-makefile
Esse repositório contém o seguinte Makefile
:
foo: main.o greeter.o
gcc -o foo main.o greeter.o
main.o: main.c
gcc -c -Wall -Wextra -Werror -o main.o main.c
greeter.o: greeter.c
gcc -c -Wall -Wextra -Werror -o greeter.o greeter.c
Esse Makefile consiste em 3 regras. Uma regra consiste de um alvo, qualquer quantidade de pré-requisitos, e finalmente uma receita. A sintaxe é a seguinte:
alvo: pré-requisitos
receita
Uma regra descreve como make
pode fazer algo. A primeira regra com o alvo
foo
descreve como nós podemos linkar e gerar nosso executável.
A segunda e terceira regra descreve como compilar os arquivos objeto individualmente.
Quando você roda make foo
, make buscará uma regra que pode compilar foo
, neste
caso a primeira regra. Antes de executar a receita, make primeiro descobrirá
se a receita precisa realmente ser executada, e se qualquer outra regra precisa
ser executada primeiro. Make primeiro verifica se todos pré-requisitos existem e
estão atualizados.
O pré-requisito main.o
não irá existir na primeira execução. Make buscará
nas regras uma que seja compatível com o alvo. Neste caso a regra main.o
.
Para essa regra, o mesmo processo começará novamente, como se você executasse
make main.o
. Make enxerga que main.c
existe e main.o
não. Em seguida
compilará main.o
executando a receita. No entanto, se main.o
já existe,
então make comparará a data da edição. Se main.c
é mais recente que main.o
,
make irá agir como se main.o
não existisse, e o recompilará.
Na primeira execução, a mesma coisa deve acontecer para greeter.o
.
Exercício: Compile o binário
foo
utilizando make. Depois o compile novamente. Agora, altere um poucomain.c
e compilefoo
novamente. Perceba os comandos executados pelo make.
Pergunta: Você rodou
make foo
3 vezes, durante qual instânciagreeter.o
foi compilado?
Variáveis e pattern matching
Escrevendo uma nova regra para cada arquivo objeto é incômodo e propenso a erros.
Pode-se escrever o Makefile
anterior de uma maneira muito mais limpa:
foo: main.o greeter.o
gcc -o $@ $^
%.o: %.c
gcc -c -Wall -Wextra -Werror -o $@ $^
Este Makefile funciona exatamente1 como o primeiro Makefile.
A regra %.o
irá lidar com a compilação de todos arquivos objeto. O %
é
um pattern match. Isto é o que o manual GNU Makefile diz sobre pattern matching,
em 10.5.4 How Patterns Match:
Um target pattern é composto de um ‘%’ entre um prefixo e um sufixo, um deles ou ambos podem estar vazios. O padrão somente corresponde ao nome do arquivo se o nome do arquivo começa com o prefixo e termina com o sufixo, sem sobrepor. O texto entre o prefixo e o sufixo é chamado stem. Assim, quando o padrão ‘%.o’ corresponde ao nome do arquivo test.o, a stem é ‘test’. Os pré-requisitos da pattern rule são transformados em nomes de arquivos reais, substituindo o ‘%’ pela stem. Assim, se no mesmo exemplo um dos pré-requisitos é escrito como ‘%.c’, é expandido para ‘test.c’.
Isto foi uma grande mancha de texto, vamos ver um exemplo. Quando make tenta
buscar por uma regra para compilar main.o
, ele irá corresponder ao padrão.
A stem, main
, é injetada em %.c
para se tornar main.c
. Efetivamente, make
entenderá que o alvo é main.o
e o pré-requisito é main.c
.
O mesmo acontece com greeter.o
, e qualquer outro arquivo que termine com .o
.
Perceba que %
não é uma declaração do pré-processador. Isso tudo acontece no tempo
de execução, e por isso também funciona em arquivos nunca referenciados no Makefile.
A nova versão do nosso Makefile também contém duas variáveis chamadas automatic variables. Nomeadas
$@
e $^
. Essas variáveis nos permitem escrever receitas genéricas que funcionam com
regras de pattern matching. $@
relaciona-se com o alvo e $^
relaciona-se
com todos pŕe-requisitos.
Pergunta: Há um arquivo adicional
stray.c
dentro do repo clonado. Utilizando o Makefile acima, o que deveria acontecer se você executassemake stray.o
?
Variáveis
Até agora nós escrevemos coisas como a lista de arquivos objeto e quais flags o gcc utiliza. Isso é bom para Makefiles pequenos, mas vamos limpá-lo um pouco:
NAME := foo
OBJFILES := main.o greeter.o
LDFLAGS ?=
CFLAGS ?= -Wall -Wextra -Werror
$(NAME): $(OBJFILES)
$(CC) $(LDFLAGS) -o $@ $^
%.o: %.c
$(CC) -c $(CFLAGS) -o $@ $^
Variáveis com :=
são sempre definidas incondicionalmente. Variáveis definidas
com ?=
são definidas somente se ainda não foram. Make herda variáveis das
variáveis de ambiente. Isso facilita para que possamos fácilmente executar make
com CFLAGS
customizadas, por exemplo, executando o seguinte shell:
# Compila o programa no modo de debug (-g) e não para de compilar nos
# avisos.
CFLAGS="-Wall -Wextra -g" make
A variável CC
é um pouco especial nesse quesito pois é [pré-definida] para cc
.
A ideia é simples, se você escrever um código em conformidade com as normas, por que
você deve especificar com qual compilador seu código será compilado? cc
é o compilador
padrão para C na maioria dos sistemas, podendo definir a variável CC
:
# Utilize o compilador clang e compile com a opção
# específica -fsanitize=address para detectar vazamento de memória.
export CC=clang
export CFLAGS="-Wall -Wextra -g -fsanitize=address"
export LDFLAGS="-g -fsanitize=address"
make
Nota: Você pode ter visto o operador
=
sendo utilizado nos Makefiles. Também é correto, mas funciona ligeiramente diferente de:=
. Para as diferenças, veja a documentação oficial.
Pastas
Com os Makefiles usados até o momento, todos arquivos de código fonte e arquivos objeto foram feitos na raiz do repositório. Podemos deixar nosso repositório muito mais organizado colocando tais arquivos em pastas diferentes.
Vamos rever nossa regra do arquivo objeto:
%.o: %.c
$(CC) -c $(CFLAGS) -o $@ $^
O pattern %
não funciona somente em sufixos, ele também funciona em prefixos. Podemos
usar isso para compilar nossos arquivos objeto para uma pasta diferente da
dos arquivos de código fonte.
# Compilar arquivos objeto para o diretório obj a partir do diretório
# src
obj/%.o: src/%.c
$(CC) -c $(CFLAGS) -o $@ $^
Perceba que o make não tem conhecimento de diretórios. O que ele faz é buscar por
“obj/” no ínicio e “.o” no fim, e substitui aqueles com “src/” no início e “.c”
no fim. Devido a S@
e $^
serem automatic variables equivalem a seguinte forma ao serem processadas:
cc -c -Wall -Wextra -Werror -o obj/example.o src/example.c
No entanto isso não funciona muito bem, pois o compilador não criará uma pasta obj
se ela ainda não existir. Nós podemos resolver isso executando mkdir -p
antes de
cc
, criando-se um diretório se ainda não existir. Para resolver qual diretório
criar, podemos usar a função $(dir ...)
obj/%.o: src/%.c
@mkdir -p $(dir $@)
$(CC) -c $(CFLAGS) -o $@ $^
O @
no início é apenas uma escolha estética. Ele faz com que as chamadas mkdir
não sejam mostradas. O output pode ser bem desordenado.
Agora para fazermos tudo isso funcionar, precisamos garantir que em qualquer lugar
que utilizarmos um arquivo objeto como pré-requisito nós o referenciaremos com seu
prefixo obj/
, do contrário make não será capaz de encontrá-los. Nosso Makefile
agora parece como esse:
NAME := foo
OBJFILES := obj/main.o obj/greeter.o
LDFLAGS ?=
CFLAGS ?= -Wall -Wextra -Werror
$(NAME): $(OBJFILES)
$(CC) $(LDFLAGS) -o $@ $^
obj/%.o: src/%.c
@mkdir -p $(dir $@)
$(CC) -c $(CFLAGS) -o $@ $^
Leitura adicional: Você pode evitar escrever o prefixo
obj/
toda hora usando: 6.3.1 Substitution References.
Pergunta: Imagine que todos arquivos
.c
têm um prefixoft_
, e pertencem a pastasrc
. Então nosso arquivo fonte main seriasrc/ft_main.c
. Como podemos fazer arquivos objeto sem o prefixoft_
? Para quesrc/ft_main.c
seja compilado para o arquivo objetoobj/main.o
?
Limpando tudo
Até agora, todas as regras do nosso Makefile fazem algo, definido pelo alvo.
Quase sempre é o que queremos, mas há algumas exceções.
Pegue o make clean
como exemplo. Na maioria dos Makefiles2, uma regra como essa
irá limpar todos arquivos criados pelo Makefile. Uma implementação simples seria:
NAME := foo
OBJFILES := obj/main.o obj/greeter.o
LDFLAGS ?=
CFLAGS ?= -Wall -Wextra -Werror
$(NAME): $(OBJFILES)
$(CC) $(LDFLAGS) -o $@ $^
obj/%.o: src/%.c
@mkdir -p $(dir $@)
$(CC) -c $(CFLAGS) -o $@ $^
clean:
rm -f $(NAME) $(OBJFILES)
O problema em implementar assim é que make irá pensar que nossa regra clean
tem
um arquivo alvo chamado clean
. Deixe-me mostrar porquê isso é um problema:
$ touch clean
$ make clean
make: 'clean' is up to date.
Make não quer executar a regra clean
, pois já existe um arquivo chamado clean
.
Para consertar isso, precisamos declarar o alvo clean como um phony. Make tem
um alvo mágico chamado .PHONY
, todos pré-requisitos desse alvo serão marcados
como phonies, e make não checará eles com mais nenhum arquivo com o mesmo nome. Ao invés
disso, make assumirá que phonies estão sempre desatualizados, e precisam
ser recompilados. Isto nos dá o seguinte Makefile
:
NAME := foo
OBJFILES := obj/main.o obj/greeter.o
LDFLAGS ?=
CFLAGS ?= -Wall -Wextra -Werror
$(NAME): $(OBJFILES)
$(CC) $(LDFLAGS) -o $@ $^
obj/%.o: src/%.c
@mkdir -p $(dir $@)
$(CC) -c $(CFLAGS) -o $@ $^
clean:
rm -f $(NAME) $(OBJFILES)
.PHONY: clean
Exercício: Imagine que nossa regra
.PHONY
foi definida como:.PHONY: clean $(NAME)
. Rodemosmake duas vezes
. O que deverá acontecer na segunda vez?
Toques finais
Existem mais duas coisas legais para se ter em nosso Makefile. Primeiro,
tentando recompilar nosso projeto usando make
não funcionará bem se você
tiver modificado o arquivo header, pois make o desconhece.
A maneira mais fácil é dizer ao make que todos arquivos objeto também dependem do arquivo header. Isso apareceria como:
NAME := foo
HEADERFILES := src/greeter.h
OBJFILES := obj/main.o obj/greeter.o
LDFLAGS ?=
CFLAGS ?= -Wall -Wextra -Werror
$(NAME): $(OBJFILES)
$(CC) $(LDFLAGS) -o $@ $^
obj/%.o: src/%.c $(HEADERFILES)
@mkdir -p $(dir $@)
$(CC) -c $(CFLAGS) -o $@ $<
Nós fizemos três mudanças. Primeiro adicionamos uma variável HEADERFILES
que
contém uma lista de arquivos header, então a adicionamos como um pré-requisito
para a regra obj/%.o
, e finalmente, trocamos a segunda $^
para $<
.
Nossa regra obj/%.o
agora tem múltiplos pré-requisitos: O arquivo correspondente
src/%.c
e todos arquivos header definidos em HEADERFILES
. Devido ao make checar
se qualquer pré-requisito é mais novo que o alvo, o alvo será recompilado
sempre que o arquivo header é atualizado. Essa checagem será executada para todas
regras de arquivos objeto, levando-os a serem recompilados.
A S^
e S<
automatic variables são diferentes de forma sútil mas importante:
$^
determina todos pré-requisitos, enquanto $<
apenas determina o primeiro.
Em nosso caso, isso significa que $<
ignora os arquivos header e apenas expande
para nosso arquivo de código fonte. Isso é exatamente o que queremos uma vez que
nosso compilador não deve saber o que são arquivos header diretamente.
Palavras Finais
Foi isso. Espero que o tutorial Makefile tenha sido útil! Muito obrigado Pepijn Holster por me ajudar a escrever isso, e obrigado a todxs que me ajudaram a concretizar essa leitura.
Quer traduzir esse tutorial? Aceitarei com muito prazer outras traduções!
Contate-me, em inglês, no email inbox@noahloomans.com, ou DM no Slack nloomans
se você
for um estudante que faz parte da 42 Network. Eu te ajudarei a começar.