Introduction aux Makefiles¶
Les Makefiles sont des fichiers utilisés par le programme make(1) afin d’automatiser un ensemble d’actions permettant la génération de fichiers, la plupart du temps résultant d’une compilation.
Un Makefile est composé d’un ensemble de règles de la forme:
target [target ...]: [component ...] [command] ... [command]
Chaque règle commence par une ligne de dépendance qui définit une ou plusieurs cibles (target
) suivies par le caractère :
et éventuellement une liste de composants (components
) dont dépend la cible. Une cible ou un composant peut être un fichier ou un simple label.
Il est important de se rendre compte que l’espacement derrière les command
doit impérativement commencer par une tabulation.
Ça ne peut pas commencer par des espaces.
Il ne faut pas non plus confondre la touche tabulation du clavier
qui est souvent interprétée par les éditeurs de texte
par une indentation et le caractère de tabulation
(souvent écrit \t
comme en C ou en bash) qui sont souvent affichés
avec 2, 3, 4 ou 8 espacements en fonction des préférences de l’utilisateur.
On parle bien ici du caractère de tabulation.
Heureusement, bien que beaucoup de gens configurent
leur éditeur de texte pour indenter avec des espaces,
la plupart des bons éditeurs reconnaissent que c’est
un Makefile et indentent avec des tabulations.
Le fichier suivant reprend un exemple de règle où la cible et le composant sont des fichiers.
text.txt: name.txt echo "Salut, " > text.txt cat name.txt >> text.txt
Lorsque make
est exécuté en utilisant ce Makefile, on obtient:
$ make make: *** No rule to make target `name.txt', needed by `text.txt'. Stop.
Comme text.txt
dépend de name.txt
, il faut que ce dernier soit défini comme cible dans le Makefile ou existe en tant que fichier. Si nous créons le fichier name.txt
contenant Tintin
et que make
est ré-exécuté, on obtient la sortie suivante :
$ make echo "Salut, " > text.txt cat name.txt >> text.txt $ cat text.txt Salut, Tintin
Lorsqu’une dépendance change, make
le détecte et ré-exécute les commandes associées à la cible. Dans le cas suivant, le fichier name.txt
est modifié, ce qui force une nouvelle génération du fichier text.txt
.
$ make make: `text.txt' is up to date. $ echo Milou > name.txt $ make echo "Salut, " > text.txt cat name.txt >> text.txt $ cat text.txt Salut, Milou
Comme spécifié précédemment, les Makefiles sont principalement utilisés pour automatiser la compilation de projets. Si un projet dépend d’un fichier source test.c
, le Makefile permettant d’automatiser sa compilation peut s’écrire de la façon suivante:
test: test.c gcc -o test test.c
Ce Makefile permettra de générer un binaire test
à chaque fois que le fichier source aura changé.
Les variables¶
Il est possible d’utiliser des variables dans un fichier Makefile. Celles-ci sont généralement définies au début du fichier, une par ligne comme :
CC = GCC OPT = -ansi VARIABLE_AU_NOM_TRES_LONG = 1
Notez que les noms sont écrits en majuscule par convention. Leur appel est semblable à celui en script shell (bash) excepté les parenthèses après le symbole $.
On écrit par exemple $(CC)
, $(CFLAGS)
, $(VARIABLE_AU_NOM_TRES_LONG)
. Make autorise de remplacer les parenthèses par des accolades mais cette pratique est moins répandue.
CC = GCC CFLAGS = -ansi build: $(CC) $(CFLAGS) foo.c -o foo
Vous aurez compris qu’ici, la cible build
effectue la commande gcc -ansi foo.c -o foo
.
Il est très intéressant de savoir que toutes les variables d’environnement présentes lors de l’appel au Makefile sont également disponibles avec la même notation.
Vous pouvez donc très bien utiliser la variable $(HOME)
indiquant le répertoire attribué à l’utilisateur sans la définir.
Il existe six différentes manières d’assigner une valeur à une variable. Nous ne nous intéresserons qu’à quatre d’entre elles.
- La première permet de lier la variable à une valeur (ici value). Mais celle-ci ne sera évaluée qu’à son appel.
- La seconde permet de déclarer une variable et de l’évaluer directement en même temps.
- La troisième permet d’assigner une valeur à la variable uniquement si celle-ci n’en a pas encore.
- La quatrième permet d’ajouter une valeur à une autre déjà déclarée.
Une description détaillée de ces méthodes d’assignation et des deux autres restantes se trouve à l’adresse suivante http://www.gnu.org/software/make/manual/make.html#Setting
Les conditions¶
Les variables ne servent pas uniquement à éviter la redondance d’écriture dans votre fichier. On peut aussi les utiliser pour réaliser des opérations conditionnelles comme :
DEBUG = 1 build: ifeq ($(DEBUG), 1) gcc -Wall -Werror -o foo foo.c else gcc -o foo foo.c endif
Ici ifeq
permet de tester un « si égal ». Il existe aussi l’opération opposée ifneq
pour « si non-égal ». Remarquez que les conditions ne doivent pas être tabulées au risque d’obtenir une erreur
de syntaxe incompréhensible. Les conditions peuvent avoir différentes syntaxes. Vous pouvez les trouver sur cette page http://www.gnu.org/software/make/manual/make.html#Conditional-Syntax
Avec les sections précédentes et la suivante nous allons pouvoir nous aventurer dans la création de Makefiles plus complexes. On peut vouloir effectuer des compilations différentes suivant l’environnement de l’utilisateur comme son OS, son matériel ou juste son nom. Encore une fois Make nous gâte en nous offrant la possibilité d’exécuter des commandes shell dans nos Makefiles. Imaginez avoir besoin d’options de compilation supplémentaires à cause de votre OS que seul vous avez besoin. Vous pouvez effectuer une compilation conditionnelle sur votre nom.
USER := $(shell whoami) build: ifeq ($(USER), sfeldman) gcc -I($HOME)/local/include -o foo foo.c else gcc -o foo foo.c endif
Ici $(shell whoami)
est un appel à la fonction shell (de Make) qui nous permet d’assigner à la variable USER
, en évaluant immédiatement l’appel, le résultat de la commande shell (bash) whoami
renvoyant le
nom de l’utilisateur actuel. Cela ne fonctionnera que si la commande whoami
est disponible dans le shell évidemment.
La cible .PHONY¶
Make compare les dates de modification des fichiers produits avec les dates de leur(s) source(s) pour savoir si celles-ci ont été modifiées depuis leur dernière compilation. Cela lui permet de ne pas devoir recompiler des fichiers qui n’auraient pas changé d’un appel à
l’autre. Malheureusement ce comportement qui peut sembler avantageux amène aussi des problèmes, en l’occurrence pour des règles ne produisant aucun fichier.
Une solution pour pallier le problème consiste à indiquer que la règle ne crée rien. Pour faire cela il existe une cible spéciale .PHONY
permettant de définir
quelles règles doivent toujours être exécutées à nouveau. Ainsi une règle .PHONY
ne rencontrera jamais le problème d’être déjà à jour.
Une bonne pratique est de déclarer dans .PHONY
toutes les règles de nettoyage de votre projet.
build: gcc -o foo foo.c .PHONY: clean clean: rm -f *.o
Cela est aussi pratique pour forcer une nouvelle compilation.
build: gcc -o foo foo.c .PHONY: clean rebuild clean: rm -f *.o foo rebuild: clean build
Compléments¶
Afin de rendre vos Makefiles plus lisibles, vous pouvez y insérer des commentaires en plaçant un croisillon en début de ligne. Cette syntaxe est semblable au script shell.
# Commentaire sur # plusieurs lignes build: gcc -o foo foo.c # commentaire en fin de ligne
Corriger les erreurs de vos Makefiles peut sembler difficile lorsque vous êtes baignés dans un flux d’instructions. Vous pouvez néanmoins régler leur verbosité. Il est possible de rendre silencieuse une commande en plaçant une arobase devant. Ceci indique juste à Make de ne pas imprimer la ligne de commande. La sortie standard de cette commande restera visible.
build: @echo "Building foo" @gcc -o foo foo.c
Pour plus d’informations en français sur l’écriture ou utilisation des Makefiles voir [DeveloppezMake].
Documentation complète en anglais sur le site officiel [GNUMake].