Makefiles

Le stream de l’ami Imil de samedi dernier était dédié au bon vieux make, et c’était l’occasion de revoir un peu les bases des Makefiles. Rien de bien compliqué, mais ça fait du bien et j’ai quand même appris quelques trucs au passage ! Voici quelques notes perso, qui pourront peut-être servir à d’autres qu’à moi.

Les bases

Un fichier makefile se compose principalement de deux types de blocs :

Attention : C’est bien une tabulation, pas des espaces !

Par exemple:

CC = gcc

monprog: main.o bidule.o
	${CC} -o monprog main.o bidule.o

main.o: main.c
	${CC} -c main.c

bidule.o: bidule.c
	${CC} -c bidule.c

On voit bien la structure des “recettes” : pour fabriquer la cible main.o, par exemple, il faut main.c et on applique les lignes de commandes dans l’ordre. Ici, c’est facile, il n’y en a qu’une ( gcc -c main.c ).

Au passage, on peut voir l’utilisation d’une variable. Ici, des accolades sont utilisées. Je ne le savais pas, mais on peut également utiliser des parenthèses ! D’ailleurs, si l’on n’en met pas, le $ ne portera que sur le premier caractère … :-(

Invocation de make

Pour exécuter l’une des recettes, on invoque make avec pour argument la cible que l’on souhaite construire. Par exemple, pour fabriquer main.o :

make main.o

Si nécessaire, les recettes des dépendances seront également exécutées :

$make monprog
 cc -c bidule.c
 cc -c main.c
 cc -o monprog main.o bidule.o
$

C’est quand même un peu l’objectif du machin !

Pour savoir si une recette doit être exécutée ou pas, make se base sur l’heure de modification des fichiers.

Par défaut, make lance la première cible du fichier Makefile lorsqu’il est exécuté sans argument. Il est donc préférable de placer la recette générant l’exécutable en premier.

Faire des règles génériques

On n’a pas besoin d’écrire des règles pour absolument tous les fichiers … Surtout lorsque ces règles sont absolument les mêmes. On a donc la possibilité de créer des règles génériques.

CC = gcc
OBJ = main.o bidule.o fonk.o

all: projet

projet: $(OBJ)
	$(CC) -o projet $(OBJ)

%.o: %.c
	$(CC) -c $<

La dernière règle est ici une règle générique. Elle sera appliquée pour la fabrication de tout fichier en .o

Autre syntaxe pour faire une règle générique :

.c.o:
	$(CC) -c $<

Ce qui peut se traduire par “Pour transformer un .c en .o” … Et du coup, l’ordre est inversé : la dépendance d’abord, la cible après !

Comme on peut le voir ci-dessus, il existe des variables particulières, qui permettent décrire les règles générique de façon … Ben générique justement. Il en existe (au moins) cinq, mais les trois premières me semblent les plus utiles :

Tiens, tant qu’on y est, il est également possible de rendre silencieuse une ligne de commande, et de ne pas l’afficher. Pour cela, il suffit de préfixer la ligne par @. Par exemple :

CC = gcc
OBJ = main.o bidule.o fonk.o

all: projet

projet: $(OBJ)
	@$(CC) -o projet $(OBJ)

%.o: %.c
	@$(CC) -c $<

Les règles implicites

L’outil make applique, en plus des règles de compilation décrites dans le Makefile, un ensemble de règles par défaut. Pour la version gnu de make, on peut les trouver ici :

Catalog of Built-In Rules

Si l’on veut pouvoir profiter de ces règles, il faut mieux respecter de bonnes habitudes pour le choix du nom de certaines variables. Pour le C, on utilisera :

Il y en a bien plus : pour le C++, Lex, Yacc, Fortran, etc. On peut en retrouver la liste ici :

Implicit Variables

La cible .PHONY

La cible .PHONY est une cible particulière, qui permet à make de savoir que certaines règles n’aboutissent pas à la création d’un fichier, et que donc il ne faut pas se baser sur la date de modification des dépendances pour ces règles là. On l’utilisera, par exemple, pour une cible “clean” qui permet de nettoyer les fichier temporaires, ou une cible “mrproper” qui fait réellement le ménage.

build:
	gcc -o foo foo.c

.PHONY: clean

clean:
	rm -f *.o

Quelques petites choses utiles

L’affectation avec ?=

Si l’on affecte une variable en utilisant l’opérateur =, l’ancien contenu de cette variable est effacé. Avec ?=, on peut n’affecter une valeur que si ladite variable n’a pas déjà été déclaré précédemment. Par exemple :

CC ?= gcc

test: main.c
	${CC} -o test main.c

C’est utile, par exemple, lorsque ce Makefile peut être appelé depuis un autre Makefile.

Surchage d’une variable par la ligne de commande

Lorsqu’une variable est définie dans le Makefile, on peut la surcharger en la précisant dans la ligne de commande. Par exemple :

CC = gcc

test: main.c
	${CC} -o test main.c
$make CC=echo
echo -o test main.c
-o test main.c

Comme on peut le voir, la variable ${CC} est imposée par la ligne de commande. Contrairement à ce que je pensais, qu’elle soit affectée par = ou par ?= ne change rien : La ligne de commande a priorité ( et dans le fond, c’est tout à fait logique ! )

Les conditions

On peut aussi faire des conditions … Pratique !

DEBUG = 1

build:
	ifeq ($(DEBUG), 1)
	gcc -Wall -Werror -o foo foo.c
	else
	gcc -o foo foo.c 
	endif

Wildcards

Pour éviter de longues listes de fichier à taper à la main, et surtout à entretenir et mettre à jour :

SRC= $(wildcard *.c)
OBJ= $(SRC:.c=.o)

A la mode bsd … Ouah !

Franchement, là c’est la classe ! Bon, c’est pas ultra portable je pense, mais ça vaut le coup d’œil :

PROG = fin

.include <bsd.prog.mk>

Plus concis, ça va quand même être dur à faire !!!