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.