Introdução

Neste post, iremos ver algumas aplicações muito simples do comando make.

Make

O make é uma aplicação que permite automatizar a criação de código. É um programa já com alguma idade (foi criado em 1976), mas mantém-se ainda (muito) útil. Podem ver mais alguma informação na página da Wikipedia. Hoje em dia e no caso dos PCs, a versão mais comum do make é da GNU, que é o GNU make, ou gmake.

Makefiles

A informação para o make “fazer a sua magia” está nos ficheiros que contêm as regras (as instruções) para o make saber o que tem a fazer. Estes ficheiros, que normalmente têm o nome de Makefile definem dependências entre ficheiros. Se, para ter um programa chamado hello, eu preciso de compilar o ficheiro hello.c, isso significa que o hello depende de hello.c. Neste caso, e em linguagem make, o hello é o target e o hello.c é a dependency. As regras de uma Makefile destinam-se a definir os comandos que têm que ser executados para criar o target a partir das dependencies. Por exemplo, no caso anterior, a regra seria:

hello : hello.c
    gcc -o hello hello.c

Com esta regra, o make vai verificar se é preciso criar o target hello. O target terá que ser criado se:

  1. Não existir o ficheiro hello.
  2. Se o ficheiro hello.c for mais recente que o hello (significa que o hello.c já foi editado após se ter compilado o hello pela última vez)

Se se verificar qualquer uma destas condições, o target tem de ser criado de novo e o make lança os comandos respetivos.

Vamos supor que temos, numa determinada pasta, o ficheiro hello.c e a respetiva Makefile, como está acima. Lançando o comando make:

$ ls
hello.c  Makefile
$ make
gcc -o hello hello.c
$ ls
hello  hello.c  Makefile

Se, imediatamente a seguir, lançarmos novamente o comando make, ele vai verificar que não houve alteração do hello.c e que não é preciso fazer nada:

$ make
make: 'hello' is up to date.

Makefiles com múltiplos targets

Uma Makefile não precisa de ter apenas um target (raramente tem). Podemos criar Makefiles com vários targets. Para selecionarmos o target a criar, damos o nome desse target como argumento ao make. Por exemplo, podemos ter um target que apaga os ficheiros criados; neste caso, a Makefile ficaria:

hello : hello.c
    gcc -o hello hello.c

clean : 
    rm -f hello

Se quisermos apagar os ficheiros através do make, damos o comando make clean e o make fai executar as ações associadas ao target clean:

$ make clean
rm -f hello

A grande vantagem do make (e dos seus sucessores, como cmake e outros) está na gestão de projetos com um grande número de ficheiros. Consideremos o exemplo simples, de um programa (zcalc) que serve para fazer operações com números complexos e que utiliza um módulo com funções para trabalhar com números complexos. Este módulo é composto por dois ficheiros, complex.c e complex.h.

Uma Makefile simples para compilar este programa seria:

zcalc : zcalc.c complex.c
    gcc -o zcalc zcalc.c complex.c 

O ficheiro zcalc_simples.zip contém uma estrutura de ficheiros com o exemplo apresentado aqui.

Estruturando a Makefile

Embora seja possível utilizar a estrutura acima, o que encontramos muitas vezes são Makefiles como a seguinte:

zcalc : zcalc.o complex.o  
	gcc -o zcalc zcalc.o complex.o

zcalc.o : zcalc.c 
	gcc -c zcalc.c 

complex.o : complex.c complex.h 
	gcc -c complex.c 

Ou seja, em vez de se obter diretamente o resultado final através dos ficheiros source (.c), indica-se explicitamente os ficheiro objeto (.o) de que depende o programa principal e indicamos a forma de obter esses ficheiro objeto intermédios.1

Porquê complicar, então? Num programa do tamanho do zcalc não faz muita diferença (falamos de pouco mais de 100 linhas de código); compilar tudo demora menos de 1/10 de segundo. Por isso, não nos custa passar todo o código pelo compilador C. Só que, em geral, os programas são muito maiores que isso. Indo para o outro extremo: o kernel do Linux tem cerca de 8 milhões de linhas de código, o Firefox são 21 milhões de linhas de código e o Windows 11, 50 milhões2. Se alteramos alguma coisa num destes programas, não queremos recompilar todo o código. Apenas a parte que for necessária. Por isso, as Makefiles (ou os seus equivalentes) desdobram o programa em partes, para só ter de passar pelo compilador o novo código e tudo aquilo (e apenas aquilo) que depende do código alterado. Desta forma, numa Makefile como a que está acima, se se alterar o ficheiro zcalc.c, só há dois targets que têm de ser recriados: zcalc.o e zcalc. Não é necessário recriar o target complex.o e utiliza-se o que já existe. É fácil de imaginar a diferença que isto pode fazer num programa com 100 ou mais targets diferentes (especialmente se considerarmos que uma boa prática, quando se está a programar, é compilar frequentemente).

O ficheiro zcalc_struct.zip contém os ficheiros com o exemplo agora apresentado.

Como o Make adivinha o que tem de fazer

Até agora, indicámos explicitamente ao make quais os comandos a executar. No entanto, o make tem a capacidade de “adivinhar” o que há para ser feito a partir do que se chama regras implícitas. Há muita coisa a dizer sobre as regras implícitas do make, mas iremos focar apenas o essencial.

Consideremos o exemplo desta Makefile:

CC=gcc
CFLAGS=-Wall -Wextra -pedantic

zcalc : zcalc.o complex.o  
	gcc -o zcalc zcalc.o complex.o

clean:
	rm -f *.o

cleanall: clean
	rm -f zcalc

Esta estrutura de ficheiro é disponibilizada em zcalc_implicit.zip.

Em relação ao exemplo anterior, vemos algumas diferenças:

  • apareceram umas linhas com = no começo da Makefile;
  • as regras para criar os ficheiros .o a partir dos ficheiros .c desapareceram;
  • há algumas regras (cleane cleanall) que servem, não para criar, mas para apagar ficheiros (fazer “limpezas”).

No entanto, embora faltem as regras para gerar os ficheiros objeto, esta Makefile continua a funcionar:

$ make
gcc -Wall -Wextra -pedantic   -c -o zcalc.o zcalc.c
gcc -Wall -Wextra -pedantic   -c -o complex.o complex.c
gcc -o zcalc zcalc.o complex.o

Como foi isto possível? Adivinhou o que era para fazer?

O ficheiro Makefile consegue resolver e determinar o que tem para fazer, neste caso, usando a primeira regra implícita do make:

Compiling C programs

n.o is made automatically from n.c with a recipe of the form       
‘$(CC) $(CPPFLAGS) $(CFLAGS) -c’.

De acordo com esta regra e se nada for dito em contrário, um ficheiro .o é gerado a partir de um ficheiro .c que tenha o mesmo nome base. Neste caso, zcalc.o é criado a partir de zcalc.c e complex.o a partir de complex.c. Isto permite ao make saber como criar as dependências zcalc.o e complex.o para as quais não há regra explícita.

A receita acima também ajuda a explicar o significado das duas primeiras linhas da makefile: GCC é a variável que guarda o nome do compilador de C (neste caso, gcc) e CFLAGS são as flags3 a dar ao compilador. Neste caso (info retirada do man gcc):

       -Wall
           This enables all the warnings about constructions that some users
           consider questionable, and that are easy to avoid (or modify to
           prevent the warning), even in conjunction with macros.  This also
           enables some language-specific warnings described in C++ Dialect
           Options and Objective-C and Objective-C++ Dialect Options.
       -Wextra
           This enables some extra warning flags that are not enabled by
           -Wall. (This option used to be called -W.  The older name is still
           supported, but the newer name is more descriptive.)
       -pedantic
           Issue all the warnings demanded by strict ISO C and ISO C++; reject
           all programs that use forbidden extensions, and some other programs
           that do not follow ISO C and ISO C++.  For ISO C, follows the
           version of the ISO C standard specified by any -std option used.

A compilação de cada um dos ficheiros .c para gerar o .o é assim feita com o comando:

$ gcc -Wall -Wextra -pedantic   -c -o <ficheiro>.o <ficheiro>.c 

em que <ficheiro> é o nome base do ficheiro. Reparem que os comandos até -o resultam diretamente da regra enunciada acima.

Conclusão

Vimos aqui, brevemente, quais as capacidades do make. O make permite automatizar a compilação de programas e retirar do programador a necessidade de saber quais os comandos a correr para atualizar o código. Vimos também que a escrita das Makefiles pode ser feita usando regras explícitas ou implícitas.

Há muito mais de que se poderia falar sobre o comando make e Makefiles. O que está aqui é apenas uma breve apresentação. Para ir mais longe, há muita informação disponivel, como o manual do make da GNU.

Recursos

Os ficheiros seguintes contêm, em forma compactada, pastas com os exemplos referidos acima:

  1. Na realidade, em qualquer das alternativas, os ficheiros objeto são sempre criados. A diferença é que, na primeira opção (gcc -o zcalc zcalc.c complex.c), eles são criados como ficheiros temporários que são apagados assim que são utilizados e não ficam em disco. 

  2. Embora neste último número esteja incluída muita coisa e muitos programas diferentes. Mas dá uma ideia… 

  3. espero que compreendam que não dá para traduzir…