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. Chacune des commandes (command) est simplement une ligne de commande shell. Les commandes seront exécutées dans l’ordre de la cible du Makefile. Les commandes peuvent donc être n’importe quelle ligne de commande shell, mais les Makefiles sont en général utilisés pour automatiser la compilation de projets informatiques, et dans ce cas les lignes de commandes s’occuperont de la compilation (par exemple avec la commande `gcc).

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

Pour exécuter les commandes fournies dans un Makefile, il suffit d’appeler la commande shell make dans le dossier où se situe le Makefile. 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

Si les fichiers de dépendance n’ont pas été modifiés, une prochaine exécution de la commande make ne fera rien, comme montré ci-dessous. Puisque les Makefiles sont en général utiliser pour compiler des projets, cela permet de ne pas recompiler un projet qui n’aurait pas été modifié.

$ make
make: `text.txt' is up to date.

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.

$ 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 cibles (targets)

Comme indiqué ci-dessus, une règle d’un Makefile commence par une cible ou target. Cette cible peut indiquer le nom du fichier qui sera créé par la règle, ou simplement un nom simple pour la règle.

Soit un fichier Makefile contenant 2 règles:

target1:
    echo "Target 1"

target2:
    echo "Target 2"

En utilisant la commande make sans préciser de cible, c’est la première cible du Makefile qui est exécutée:

$ make
echo "Target 1"
Target 1

Il est également possible de préciser quelle cible exécuter, en donnant la cible en argument lorsqu’on appelle make:

$ make target1
echo "Target 1"
Target 1
$ make target2
echo "Target 2"
Target 2

Les variables

Les fichiers Makefile permettent d’utiliser des variables, qui permettent de stocker potentiellement n’importe quelle valeur. Ces variables peuvent être de deux types différents:

  • Les variables personnalisées, définies par l’utilisateur, et qui peuvent prendre n’importe quelle valeur.
  • Les variables automatiques, qui sont des raccourcis pour des valeurs déjà présentes dans le fichier.

Les deux types de variable seront présentés ci-après.

Variables personnalisées

Les variables personnalisées permettent d’associer un nom à potentiellement n’importe quelle valeur. Elles permettent de faciliter l’évolution du fichier, car si une valeur doit changer, on peut se contenter de modifier la variable associée, au lieu de devoir modifier toutes les règles. 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 https://www.gnu.org/software/make/manual/make.html#Setting

Variables automatiques

Les variables automatiques sont des raccourcis, propres à la syntaxe des fichiers Makefile, qui permettent d’exprimer succinctement des valeurs déjà présentes dans le fichier. Elles sont utilisées dans les commandes formant les différentes règles. Elles sont en général formées de deux caractères spéciaux, le premier étant toujours $. Les plus utilisées seront présentées dans cette section.

La variable $@ référence le nom de la cible. Par exemple, pour compiler un exécutable prog, on peut utiliser la règle suivante:

prog: src.c
  gcc -o $@ src.c

La variable $< référence le nom de la première dépendance. Par exemple, pour compiler un exécutable prog, on peut utiliser la règle suivante:

prog: src.c
  gcc -o prog $<

La variable $^ référence la liste des dépendances. Par exemple, pour compiler un exécutable prog basé sur deux fichiers objets, on peut utiliser la règle suivante:

prog: src_1.o src_2.o
  gcc -o prog $^

D’autres variables existent, mais sont moins utilisées en pratique. Plus d’informations sont disponibles à l’adresse suivante: https://www.gnu.org/software/make/manual/html_node/Automatic-Variables.html.

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 https://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

Cette section propose quelques compléments, utiles pour la création de fichiers Makefile plus complexes.

Règles d’inférence

Il est possible de définir des règles génériques, qui fonctionneront pour tous les fichiers qui correspondent à un pattern. Le pattern est alors exprimé avec le caractère %. Par exemple, pour compiler tous les fichiers sources, possédant l’extension .c, en fichiers objets correspondant, on peut utiliser la règle suivante:

%.o: %.c
    gcc -o $@ -c $^

Remarquez que cette règle utilise les variables automatiques, décrites plus haut.

Commentaires

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

Commandes silencieuses

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].