Introduction à GNU Make

Lorsque je découvre un nouvel outil, j'ai envie d'écrire un pense-bête, un tutorial, quelque chose qui permet à la fois de partager la découverte et de ranger les informations utiles quelque part, au cas où. Ce pense-bête sur GNU Make ne fait pas exception à la règle : la première version doit dater de 2005, il disparaît à chaque nouvelle version de mon site, et réapparaît lorsque j'ai de nouveau besoin d'écrire un Makefile ;)

Généralités

GNU Make, ou tout simplement « Make », permet de scripter la compilation et le déploiement d'un projet, tout en économisant certaines opérations inutiles (comme la recompilation de fichiers sources inchangés). C'est généralement l'un des outils utilisés lors de l'installation sous Unix d'un programme à partir du code source.

Considérons par exemple la compilation d'un fichier C avec gcc :

gcc fichier.c -o executable -Wall -O2 -Wpedantic

Si le projet compte huit fichiers similaires, l'ensemble des instructions devient :

gcc fichier1.c -c -o objet1.o -Wall -O2 -Wpedantic
gcc fichier2.c -c -o objet2.o -Wall -O2 -Wpedantic
gcc fichier3.c -c -o objet3.o -Wall -O2 -Wpedantic
gcc fichier4.c -c -o objet4.o -Wall -O2 -Wpedantic
gcc fichier5.c -c -o objet5.o -Wall -O2 -Wpedantic
gcc fichier6.c -c -o objet6.o -Wall -O2 -Wpedantic
gcc fichier7.c -c -o objet7.o -Wall -O2 -Wpedantic
gcc fichier8.c -c -o objet8.o -Wall -O2 -Wpedantic
gcc objet1.o objet2.o objet3.o objet4.o objet5.o objet6.o objet7.o objet8.o -o executable

C'est généralement à partir de ce stade que l'on se met à utiliser Make ;) Pour ce faire, il suffit de créer un fichier appelé « Makefile » dans le répertoire où se trouvent nos fichiers sources. Un Makefile de base (ici pour un petit projet en C) ressemble à ceci :

CC=gcc -Wall -O2
SRC=$(wildcard *.c)
OBJ=$(SRC:.c=.o)
EXE=roger.bin

all: $(OBJ)
    $(CC) $(OBJ) -o $(EXE)
    @echo "Fini."

%.o: %.c
    @echo "Compiling $@..."
    $(CC) $(CFLAGS) $(LIBS) $^ -o $@

clean:
    rm -f *~

distclean: clean
    rm -f *.o $(EXE)

Cet exemple rassemble toutes les notions que nous allons aborder ici : en partant, il vous permettra de vérifier en un coup d'œil si vous avez compris ce qu'il y a à comprendre ;)

Commençons par les premières lignes, qui définissent des variables : Make utilise la même syntaxe que tout bon shell (bash, zsh, ...), c'est-à-dire VAR=valeur, où VAR est le nom de variable (généralement en majuscules) et où valeur est une chaîne de caractère ; la syntaxe $(VAR) permet quant à elle d'évaluer la variable ainsi définie.

Règles

Viennent ensuite les règles, qui constituent le cœur du Makefile. Chaque règle représente les opérations à exécuter lorsqu'un ensemble de fichiers (les « pré-requis » de la règle) ont été mis à jour depuis le dernier make. Une règle décrit par exemple comment compiler un fichier ou supprimer les fichiers temporaires. La syntaxe d'une règle est la suivante :

cible: pré-requis
    instructions

Un point de syntaxe important : les instructions sous une règle sont toutes indentées par une tabulation (cette indentation est importante pour Make). Vous pouvez utiliser le caractère d'échappement '' en fin de ligne pour continuer une instruction trop longue sur la ligne suivante.

Ainsi, dans le Makefile précédent, la règle « clean » supprime les fichiers temporaires (des éditeurs de texte comme vim ou emacs, qui sont préfixés par un tilde) du répertoire courant. Pour exécuter une règle donnée, il suffit de la passer en argument lors de l'appel à Make, par exemple ici : make clean. Lorsqu'aucune règle n'est précisée, la règle par défaut est « all » ; tout Makefile (principal) qui se respecte se doit donc de la définir.

Les pré-requis peuvent être des fichiers ou les cibles d'autres règles qui doivent être « construits » avant d'exécuter les instructions de la règle actuelle. Lors de l'évaluation d'une règle, Make inspecte ses pré-requis pour déterminer si elle a besoin d'être « reconstruite », c'est-à-dire si les instructions ont besoin d'être exécutées. Une règle a besoin d'être reconstruite si l'un des trois cas suivants se présente :

  • la règle n'a aucun pré-requis,
  • l'un des pré-requis est un fichier et a été mis à jour depuis la dernière exécution de make,
  • l'un des pré-requis est une règle qui a besoin d'être reconstruite.

Voici un exemple de Makefile reposant sur les règles que nous venons de décrire :

CC=gcc -Wall -O2
SRC=main.c module1.c module2.c
OBJ=main.o module1.o module2.o
EXE=roger.bin

all: $(OBJ)
    $(CC) $(OBJ) -o $(EXE)

main.o: main.c
    $(CC) main.c \
        -o main.o
module1.o: module1.c
    $(CC) module1.c \
        -o module1.o
module2.o: module2.c
    $(CC) module2.c \
        -o module2.o

clean:
    rm -f *~

distclean: clean
    rm -f *.o $(EXE)

Ce Makefile a un peu bourrin présente déjà l'intérêt que les fichiers objet (extension .o) ne seront recompilés que si les fichiers sources correspondants ont été modifiés. Nous verrons par la suite comment définir des règles génériques qui nous éviteront d'écrire une règle par fichier.

Variables et procédures

Nous avons vu que Make permettait de manipuler des variables de même que dans un script shell. La syntaxe pour la procédure est également similaire au shell : $(cmd param1, param2, ...) (mais notez les virgules entre les paramètres) où cmd est une commande interne à Make ; cette syntaxe ne permet pas d'appeler des commandes système. Aussi, souvenez-vous que les valeurs des variables sont évaluées comme des chaînes de caractères : dans les trois définitions suivantes,

SRC=*.c
LS1=`ls`
LS2=$(ls)

les valeurs respectives de ces trois variables seront "*.c", "ls" et "" (la chaîne vide, à moins que vous n'ayez défini une variable nommée ls auparavant). Ce comportement peut porter à confusion dans le cas de SRC : une règle comme

all:
    gcc $(SRC) -o kron.bin

aura bien le comportement attendu, car Make enverra au terminal l'instruction gcc *.c -o kron.bin (le terminal remplacera ensuite "*.c" par la liste des fichiers C du répertoire). Toutefois, pour Make SRC n'est que la chaîne de caractères "*.c" : pour lui indiquer d'évaluer cette chaîne, il faut utiliser la commande wildcard :

SRC=$(wildcard *.c)

all:
    gcc $(SRC) -o kron.bin

Dans ce cas, si le répertoire courant contient deux fichiers C main.c et annexe.c, Make enverra au terminal l'instruction gcc main.c annexe.c -o kron.bin. Faire évaluer la liste des fichiers par Make permet de l'utiliser ensuite comme pré-requis, par exemple :

SRC=$(wildcard *.c)

all: $(SRC)
    gcc $(SRC) -o kron.bin

Ici, un appel à make ne recompilera kron.bin que si l'un des fichiers C a été modifié.

Un autre commande utile est patsubst : $(patsubst motif, rempl, str) remplace les occurrences de motif par rempl dans la chaîne de caractères str, où motif et rempl sont des expressions rationnelles. En bref, la syntaxe des expressions rationnelles dans Make est la même que dans un shell (comme bash ou zsh), si ce n'est que le caractère wildcard * est remplacé par % pour éviter les confusions. Ainsi,

SRC=$(wildcard *.c)
OBJ=$(patsubst %.c, %.o, $(SRC))

a le comportement attendu. Cela dit, comme vous avez pu le voir dans le Makefile d'introduction ci-avant, il existe une syntaxe simplifiée pour remplacer seulement les extensions de fichiers :

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

Note en passant : si vous ne voulez pas que les instructions du terminal (comme rm, echo, ...) soient recopiées sur la sortie standard, vous pouvez précéder les précéder par un arobase '@' pour rendre la commande silencieuse.

Règles implicites

Il existe deux types de règles dans GNU Make : les règles « explicites » et « implicites ». Jusqu'à présent nous n'avons vu que des règles explicites : les cible et pré-requis sont des chaînes de caractères (écrite directement ou issues de l'évaluation de variables) et Make n'a pas à se creuser la tête : il lui suffit de les évaluer. Avec les règles implicites au contraire, les ennuis commencent (pour lui) et nous apportent le niveau de généralité dans nous avons besoins pour compiler tous nos fichiers sources (tous ? non ! les irréductibles problèmes de templates en C++ entraîneront l'émergence d'outils encore plus compliqués comme CMake, mais c'est une autre histoire…)

Une règle implicite permet de donner à Make des instructions du type « si tu as besoin d'un pré-requis de la forme X, tu peux utiliser cette règle pour le construire à partir de pré-requis de la forme Y ». Plus concrètement, toujours en C : « si tu as besoin de construire un fichier xxx.o, voici comment le construire à partir du fichier xxx.c ». En pratique, cela donne :

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

On retrouve le pourcent % utilisé comme wildcard dans les expressions rationnelles de Make : il représente ici le motif commun aux noms de fichier de la cible (%.o) et du pré-requis (%.c). Ce que nous avons vu précédemment pour les règles explicites s'applique aussi aux règles implicites ; ainsi,

aa_%.o: bb_%.c bb_%.h global.h
    $(CC) $< -o $@

permet de construire des fichiers objets avec le préfixe « aa_ » à partir des fichiers C et H qui partagent le même motif, mais préfixé par « bb_ » ; aussi, si le fichier global.h est modifié, il faudra reconstruire tous les objets produits par cette règle.

Vous aurez remarqué les deux variables $< et $@ dont je n'ai pas encore parlé : lorsqu'une règle implicite est exécutée, ses différents paramètres (la valeur du motif %, la liste des pré-requis, etc.) sont stockés dans des variables aux noms barbares, que voici :

  • $@ : cible de la règle
  • $< : premier pré-requis
  • $? : noms de tous les pré-requis plus récents que la cible
  • $^ : noms de tous les pré-requis
  • $* : nom du fichier sans suffixe (pour les règles génériques)

Encore une fois, lorsqu'il s'agit juste de remplacer une extension de fichier, Make propose une syntaxe simplifiée. Par l'exemple : les deux règles suivantes sont équivalentes :

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

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

Appels de sous-Makefiles

Lorsque la taille de votre projet vous pousse à l'organiser en répertoires et sous-répertoires, vous aurez sans doute envie de définir des Makefiles spécifiques à chacun. Ce cas de figure dépasse largement le cadre de ce billet, et vous aurez alors sans doute intérêt à utiliser des outils comme CMake pour générer vos Makefiles automatiquement. Nous allons nous contenter de voir comment appeler des sous-Makefiles depuis un Makefile parent.

Pour ce faire, on passe par le shell en utilisant des parentèses pour isoler le contexte, c'est-à-dire que tout ce qui se passe entre les parenthèses (changement de répertoire, modification des variables d'environnement, etc.) reste entre les parenthèses ;) Au passage, une autre variable utile : MAKE contient l'appel actuel à Make (par exemple make clean).

CC=gcc -Wall
SRC=$(wildcard *.c)
OBJ=$(SRC:.c=.o)

all: modules $(OBJ)
    $(CC) $(OBJ) module1/final.o module2/final.o

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

modules:
    (cd module1; $(MAKE))
    (cd module2; $(MAKE))

Dupont et Dupond ? Non, VPATH et vpath

Une dernière curiosité : VPATH et vpath permettent d'indiquer à Make d'autres répertoires où aller chercher les fichiers dont il a besoin (comme pré-requis de règles implicites). VPATH est une variable globale utilisée dans n'importe quelle situation, tandis que vpath permet de préciser quels types de fichiers aller chercher dans quels répertoires, suivant la syntaxe vpath motif répertoires (les répertoires sont séparés par : lorsqu'il y en a plusieurs), par exemple :

vpath %.sh /usr/bin:/home/kron/bin:./scripts

Attention : VPATH ne modifie pas le répertoire courant, et n'a par exemple aucun effet sur le résultat de $(wildcard *.c). Voici un dernier exemple : supposons que l'on dispose d'un script Python rst2html.py dans le répertoire courant, qui lit sur l'entrée standard un fichier au format RST (comme le billet que je suis actuellement en train d'écrire) et écrit en sortie une traduction au format HTML. Si tous nos fichiers .rst sont stockés dans un sous-répertoire billets/, on peut utiliser VPATH comme suit :

VPATH=billets/

all: make.html

%.html: %.rst
    cat $< | python rst2html.py > $@
$ find
.
./Makefile
./billets
./billets/make.rst
$ make
cat billets/make.rst | python rst2html.py > make.html

Conclusion

Ce billet touche à sa fin. Peut-être vous aura-t-il permis de mieux comprendre cet outil que certains utilisent tous les jours ; ou peut-être que, comme moi depuis des années, vous vous en foutez pas mal et vous vous êtes contentés de recopier le Makefile générique que j'ai donné en introduction ;)

Si vous avez vraiment aimé cette expérience et que vous ne demandez qu'à l'approfondir, je vous recommande cette page de manuel en français, en plus de la documentation officielle.

Discussion

Feel free to post a comment by e-mail using the form below. Your e-mail address will not be disclosed.

📝 You can use Markdown with $\LaTeX$ formulas in your comment.

By clicking the button below, you agree to the publication of your comment on this page.

Opens your e-mail client.