Noah Loomans

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 pouco main.c e compile foo novamente. Perceba os comandos executados pelo make.

Pergunta: Você rodou make foo 3 vezes, durante qual instância greeter.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ê executasse make 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 prefixo ft_, e pertencem a pasta src. Então nosso arquivo fonte main seria src/ft_main.c. Como podemos fazer arquivos objeto sem o prefixo ft_? Para que src/ft_main.c seja compilado para o arquivo objeto obj/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). Rodemos make 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.


  1. Uma pequena diferença, a segunda regra coincidirá qualquer arquivo .o, não somente main.o e greeter.o

  2. Perceba que a regra clean para os Makefiles que escrevemos na escola 42 funciona ligeiramente diferentemente.