Makefile
De Ensiwiki.
La compilation d'un projet est souvent une tâche complexe : on doit par exemple transformer (compiler) des fichiers source en fichiers objet, puis faire l'édition de liens pour générer un fichier exécutable.
Considérons par exemple un projet « simple », disons une calculatrice postfixée. Le projet comporte 2 fichiers : calc.c (qui contient le code de main() et l'analyseur syntaxique) et stack.c, qui définit des primitives de gestion d'une pile.
Pour compiler le projet « à la main », avec GCC, vous feriez :
$ gcc -c calc.c $ gcc -c stack.c $ gcc -o calc calc.o stack.o
Cela a plusieurs désavantages : tout d'abord c'est long à taper, et donc on peut par exemple faire des fautes de frappe malencontreuses (du type gcc -o calc.c calc.o stack.o, qui écrase le fichier source à cause d'une auto-complétion malheureuse). Ensuite, cela recompile tous les éléments du projet à chaque fois (rageant quand le projet dépasse les 4-5 fichiers et qu'on en édite qu'un à la fois).
Une première solution est d'utiliser un script de compilation. Cela résout le premier problème, mais pas le second. Avec l'utilitaire make et l'écriture d'un fichier Makefile, on peut trouver une meilleure solution.
Sommaire |
Concepts
Dépendances
Ici, les fichiers calc.c, stack.c, calc.o, stack.o et calc sont liés entre eux par des relations de dépendance :
calc.c ---> calc.o ---
\
v
calc
^
/
stack.c ---> stack.o --
Fichiers Fichiers Exécutable
source objets
Règles de transformation
En fait, à chaque dépendance, on peut associer une série de commandes pour passer de l'avant à l'après. Ce sont les règles de transformation.
Une règle de transformation décrit comment obtenir une cible à partir d'un ou plusieurs fichiers dont elle dépend. Par exemple, pour fabriquer stack.o à partir de stack.c, il faut exécuter gcc -c stack.c. Et pour construire calc il faut faire gcc -o calc calc.o stack.o.
make et makefiles
Le makefile n'est ni plus ni moins qu'une description des relations de dépendance et des règles de transformation, dans un format compris de l'outil make.
Un premier exemple
Reprenons notre exemple de calculatrice. Créons un fichier nommé Makefile (attention à la majuscule) dans le répertoire du projet, contenant les lignes suivantes :
calc: calc.o stack.o
gcc -o calc calc.o stack.o
calc.o: calc.c
gcc -c calc.c
stack.o: stack.c
gcc -c stack.c
Ici, on a 3 règles qui sont de la forme :
cible: dépendances
commande
Remarquez que par rapport à la théorie, ici, on mélange la description des dépendances et de la règle de transformation (commande). En réalité, vous pouvez spécifier plusieurs lignes de dépendances mais un seul bloc de commandes pour une cible donnée. Par exemple :
calc.o: stack.h
calc.o: calc.c
gcc -c calc.c
Ce qui est équivalent à :
calc.o: calc.c stack.h
gcc -c calc.c
L'outil make
L'utilitaire make est capable de lire ce fichier Makefile. Si on tape make calc par exemple, il va essayer de « fabriquer » (make) le fichier nommé calc. Sur notre exemple :
-
calca deux dépendances :calc.oetstack.o:- pour fabriquer
calc.o, il fautcalc.c:- dans notre cas,
calc.cexiste, c'est nous qui l'avons écrit ; make exécute doncgcc -c calc.c, etcalc.oest créé ;
- dans notre cas,
- de même avec
stack.o;
- pour fabriquer
- maintenant, on peut fabriquer
calcen lançantgcc -o calc calc.o stack.o, et le projet est ainsi compilé.
En fait, c'est plus subtil que ça, car la notion de « fichier à jour » intervient. Si vous venez de lancer la commande précédente (make calc), et que vous réessayez la même, make ne fera rien : en effet, aucun fichier n'ayant été modifié, il n'y a pas besoin de tout recompiler.
Maintenant, si on modifie un peu l'interface utilisateur, par exemple, en rajoutant une aide, on ne va toucher qu'à calc.c. Au moment de la compilation, stack.o ne sera pas régénéré, car il est à jour, c'est à dire que sa date de modification est postérieure à la date de modification de stack.c. make calc exécutera uniquement gcc -c main.c et gcc -o stack stack.o calc.o.
Avec ce que nous venons de voir, vous pouvez déjà écrire des makefiles rudimentaires qui vous permettront d'automatiser la compilation de votre projet. La suite de cet article explique des fonctionnalités plus avancées de l'outil.
Makefiles complexes
gmake. Voyez #BSD make pour plus d'informations.
Variables simples
On peut utiliser des « variables » pour représenter des données répétées plusieurs fois (ou juste pour la clarté, pour séparer la configuration des règles elles-mêmes).
Une variable est définie par une ligne de la forme :
nom = valeur
Et on peut y faire référence en écrivant $(nom) (ou ${nom}, $(nom) est l'usage avec GNU make) virtuellement n'importe où dans le makefile (dans les cibles, les dépendances ou les commandes). Il faut écrire $$ pour obtenir un simple dollar.
Par exemple :
PROG = calc
OBJS = calc.o stack.o
CC = gcc
CFLAGS = -Wall -ansi -pedantic -g
$(PROG): $(OBJS)
$(CC) $(CFLAGS) -o $@ $(OBJS)
Une fonctionnalité très intéressante de make est que les variables peuvent être affectées depuis la ligne de commande :
$ make CFLAGS='-O2 -fomit-frame-pointer' calc
Si make détecte que la variable a déjà une valeur donnée par la ligne de commandes, l'affectation dans le makefile même sera ignorée.
Règles implicites
En vérité, le makefile que nous avons écrit en exemple dans la première partie est inutilement long. Il suffirait d'écrire :
calc: calc.o stack.o
gcc -o calc calc.o stack.o
Les règles de transformation des fichiers C en fichiers objets n'ont pas besoin d'être explicitées car make possède des règles implicites qui lui permettent de deviner comment fabriquer les fichiers .o ; dans notre cas, make trouve un fichier de même nom de base, avec un suffixe .c, il applique donc une règle implicite permettant de le compiler en fichier .o !
Ce n'est pas le cas pour toutes les extensions, cependant, et vous pourriez avoir envie d'ajouter vos propres règles implicites, pour gagner du temps. Plus formellement, une règle implicite décrit comment convertir un fichier avec une extension donnée (p.ex. .ancien) en un autre, avec une autre extension (p.ex. .nouveau). Une règle implicite peut donc s'appliquer à plusieurs fichiers.
Pour écrire une telle règle, il faut d'abord ajouter les suffixes comme « dépendances » de la cible spéciale .SUFFIXES. La règle implicite elle-même s'écrit ensuite presque comme une règle normale :
- sa cible prend la forme
.ancien.nouveau; - elle n'a pas de dépendances ;
- et on utilise les variables spéciales (dites automatiques)
$@et$<pour désigner respectivement le nouveau et l'ancien fichiers qui auront été reconnus grâce à leur extension.
Comme ceci :
.SUFFIXES: .nouveau .ancien
.ancien.nouveau:
commandes
Un exemple, pour convertir un fichier LaTeX en PDF :
.SUFFIXES: .pdf .tex
.tex.pdf:
pdflatex $<
Un point important à noter est que l'usage d'une règle implicite n'empêche pas l'ajout de dépendances supplémentaires, par exemple, la seule ligne suivante suffit à dire à make de recompiler foo.o si foo.h ou <code>bar.h est modifié :
foo.o: foo.h bar.h
Règles implicites et variables prédéfinies
Il existe un certain nombre de variables prédéfinies par le système. Entre autres, CC, qui correspond au compilateur C, CFLAGS qui spécifie les options à utiliser pour compiler du C, et LDFLAGS qui contient les options à passer au compilateur C pour l'édition de liens. Ces variables sont notamment utilisées par les règles implicites. Par exemple, la règle qui compile les fichiers C ressemble à :
.c.o:
$(CC) $(CFLAGS) -c $<
Il n'y a en général pas besoin de définir ces variables dans le corps du makefile, sauf si vous souhaitez leur affecter une valeur particulière par défaut. Il est conseillé de positionner CFLAGS à une valeur raisonnable pour le développement (voyez GCC).
CC est cc, qui appelle GCC 3.x ; pour avoir GCC 4.x, il faut effectivement affecter CC = gcc au début de votre makefile.
Il vous est également vivement conseillé de suivre ces conventions lorsque vous écrivez vos propres règles. Si vous avez besoin d'invoquer le compilateur C, utilisez $(CC) ; ne codez pas le nom du compilateur en dur, et à moins que vous ayez besoin d'un second compilateur C différent du premier, n'utilisez pas une autre variable.
Règles génériques
Les règles génériques sont une extension des règles implicites, spécifique à GNU make. Elles permettent de spécifier des motifs plus complexes que simplement examiner le suffixe. Une règle générique prend la forme d'une règle normale dont les noms de fichiers contiennent le symbole %. Celui-ci désigne la partie variable du nom et doit correspondre dans toutes ses occurrences. En pratique, nous réécririons la règle ci-dessus comme ceci :
%.pdf: %.tex
pdflatex $<
Les règles génériques sont plus souples que les règles implicites et il est par exemple possible d'écrire :
%.o: %.c %.h
$(CC) $(CFLAGS) -c $<
Vous reconnaissez $<, qui a un rôle analogue à celui qu'elle avait dans les règles implicites : pour une règle générique, c'est la première dépendance spécifiée (%.c). Dans GNU make, il existe aussi d'autres variables automatiques semblables à $<. Citons notamment $+ qui signifie « l'ensemble des sources » (vous en trouverez d'autres dans la section Variables automatiques du manuel de GNU Make).
La règle d'édition de liens pourrait donc s'écrire (avec les variables précédemment vues) :
$(PROG): $(OBJS)
$(CC) -o $@ $+
Règles factices (phony), ne créant pas de fichier
Il est très fréquent d'avoir des règles ne servant pas à créer des fichiers, mais à effectuer des tâches comme nettoyer le répertoire (supprimer les fichiers objets et exécutables) ou installer le projet une fois compilé.
Pourquoi ne pas faire un de scripts shell, vous demandez-vous sans doute. Peut-être parce que cela permet de centraliser et de réutiliser les variables servant à la configuration du projet. Mais aussi parce que c'est l'usage.
Sous GNU make, ces règles sont des phony, et on peut les déclarer comme telles ; ainsi, make ne vérifiera pas si un fichier du nom de la cible existe et est à jour, les commandes seront toujours exécutées.
Il faut pour cela spécifier la cible factice comme « dépendance » de la cible .PHONY :
.PHONY: clean
clean:
rm -f $(PROG) $(OBJS)
Traditionnellement, .PHONY n'existait pas, et, à la place, on faisait dépendre les règles factices telles que clean ci-dessus d'une cible inexistante, sans dépendances, souvent appelée FORCE :
clean: FORCE rm -f $(PROG) $(OBJS) FORCE:
Générer des Makefiles
L'écriture de makefiles peut être rébarbative, surtout si on veut lister toutes les dépendances des fichiers .c sur les .h (pour éviter d'avoir à faire make clean à chaque fois qu'on modifie un fichier d'en-tête). Pour se rendre la vie plus facile, on peut utiliser gcc :
$ gcc -MM *.c
La commande ci-dessus générera une liste des .c avec leurs dépendances sur les .h (en analysant les #include), au format make, prêt à être copié/collé dans le Makefile. Ces règles et les règles implicites permettent de compiler les .c en .o. Ensuite, il n'y a plus qu'à rajouter la ou les règles globales qui construisent l'éxécutable à partir des fichiers objets, et le tour est joué.
Il faut quand même se rappeler de regénérer la liste des dépendances si on rajoute des #include ou des fichiers. Une solution à ce problème est d'avoir une cible qui construit ces dépendances (typiquement appelée depend), et d'inclure le fichier de dépendances généré :
MKDEP = gcc -MM -o .depend
SRCS = foo.c bar.c baz.c
HDRS = foo.h bar.h
.PHONY: depend
depend: .depend
.depend: $(SRCS) $(HDRS)
$(MKDEP) $(CFLAGS) $(SRCS)
-include .depend
Il suffira alors de faire make depend pour régénérer la liste des dépendances.
-include utilisé ici, et son cousin include (sans le -) sont des directives spécifiques à GNU make et permettent d'inclure d'autres makefiles, à la manière d'#include en C.
Automatisation de la génération de makefiles
Pour les projets plus conséquents, notamment avec des dépendances sur des bibliothèques externes, des outils de génération et de configuration automatiques tels que les autotools sont souvent utilisés.
Les autotools sont un ensemble d'outils (automake, autoconf, ...) qui produisent un script appelé configure, extrêmement portable, et qui autodétecte l'environnement de l'utilisateur, vérifie la présence des bibliothèques, choisit où installer le programme, et génère un makefile en conséquence (d'où les incantations classiques d'installation de programmes Unix à partir des sources : ./configure && make && make install).
BSD make
BSD make diffère de GNU make en pratiquement tout sinon la syntaxe de base. Nous résumons dans cette partie les points communs et les différences :
- Variables
Sous BSD make, il est traditionnel d'utiliser ${var} pour les variables plutôt que $(var). De plus, les variables automatiques sont généralement référencées par leur nom long (spécifique à BSD make) :
-
${.IMPSRC}pour$<; -
${.TARGET}pour$@; -
${.ALLSRC}pour$>(sous BSD make) qui correspond à$+sous GNU make !
- Règles implicites et génériques
Les règles implicites sont pleinement supportées mais les règles génériques ne sont pas reconnues.
- Règles factices
Les règles factices sont supportées de la même manière que sous GNU make.

