Um Makefile mínimo

Outro dia eu precisei criar um Makefile. Eu resolvi programar em C usando só um editor de texto e a linha de comando, mas a última vez que eu fiz isso eu estava na faculdade. Depois de quebrar a cabeça um pouco eu cheguei nisto aqui:1

TARGET   ?= program
BUILDDIR ?= build
SRCDIR   := src
SRC      := $(wildcard $(SRCDIR)**/*.c)
OBJ      := $(subst $(SRCDIR),$(BUILDDIR),$(SRC:.c=.o))
DEPS     := $(OBJ:.o=.d)
DEBUG    ?= -g
CPPFLAGS ?= -MMD -MP -I$(SRCDIR) -I vendor/lua/include
CFLAGS   ?= $(DEBUG) -std=c99 -W -Wall -Wextra
LDFLAGS  ?= -L vendor/lua/lib
LDLIBS   ?= -llua
MKDIR    ?= mkdir -p

$(BUILDDIR)/$(TARGET): $(OBJ)
	$(CC) $(LDFLAGS) $(OBJ) -o $@ $(LDLIBS)

$(BUILDDIR)/%.o: $(SRCDIR)/%.c
	$(MKDIR) $(dir $@)
	$(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@

-include $(DEPS)

.PHONY: clean

clean:
	$(RM) -r $(BUILDDIR)

Minha lista de desejos era:

  1. botar o código-fonte em src e os resultados da compilação em build;
  2. não precisar mexer no Makefile toda vez que eu adicionasse um arquivo fonte novo…
  3. …incluindo aí os cabeçalhos.

E como funciona?

A variável SRC recebe o nome de todos os arquivos-fonte em C. Eu uso a função wildcard do make pra identificá-los dentro do diretório SRCDIR.2 A variável OBJ recebe o nome dos arquivos-objeto correspondentes. A expressão substitui SRCDIR por BUILDDIR e .c por .o no nome dos arquivos-fonte. Isso resolve meus desejos № 1 e № 2.

Já as flags -MMD e -MP instruem o preprocessador a escrever um arquivo .d pra cada arquivo .o gerado. O d é de dependência e o conteúdo do arquivo são regras no formato Makefile indicando todos os cabeçalhos que foram lidos pelo preprocessador para gerar o objeto. A variável DEPS contém o nome desses arquivos .d. Por fim, a instrução -include $(DEPS) junta-os ao Makefile, estruturando o grafo de dependências do programa. Resolvido meu desejo № 3.

As duas regras que aparecem depois são adaptações das regras implícitas do make, mas que levam em conta minha estrutura de diretórios.

Com esse Makefile pronto, eu posso apenas escrever meu código-fonte em paz.

Referências

Antigamente, eu gostava de ler a documentação completa das ferramentas. Como hoje eu já não tenho tanto tempo livre, busquei ajuda nestes dois posts:

  1. Como dá pra ver, eu estava usando Lua como biblioteca nesse projeto. 

  2. Observe que não há uma barra, “/”, entre a variável $(SRCDIR) e o início do glob, **/*.c