Introduction à Make

(c) 2005 - Alexandre Brillant

Cette documentation ne peut être employée dans le cadre d'un cours ou d'une formation sans mon accord.

Table des matières

Introduction

Cet article est une introduction à l'usage de l'outil make. Cet utilitaire écrit en version GNU par Richard Stallman et Roland McGrath est associé à la plupart des développements (principalement c/c++). Par comparaison de date de création/mise à jour il évite de recompiler des sources inutilement. Mais son usage va bien au delà et vous pouvez vous en servir pour minimiser les commandes dans la plupart des projets. Ce document s'appuit sur des exemples triviaux en Java, LATEX et bash.

Règles

L'usage de Makefile consiste simplement à construire un fichier de même nom qui sera interprété à l'appel de la commande make. Cette commande peut prendre diverses arguments mais retenons pour l'instant qu'il s'agit souvent de cibles correspondant à une tâche à réaliser.

Règles courantes

Un Makefile est constitué de règles, ces règles suivent dans leur forme la plus simple la structure suivante :

cible: pré-requis
[tabulation]commandes
...

La cible constitue souvent un fichier à construire mais on peut la concevoir aussi comme un objectif (compiler/éffacer/générer...). Les pré-requis sont les cibles d'autres règles qui seront d'abord réalisés avant de réaliser la règle. La cible est suivie d'un ensemble de commandes toutes précédées par un caractère de tabulation.

all: pre1 pre2
echo ``ok pour all''

pre1:
echo ``ok pour pre1''

pre2:
echo ``ok pour pre2''

A l'exécution de ce ficher Makefile, la règle par défaut sera la première rencontrée (non précédée par un '.'). La règle 'all' est donc exécutée. Elle nécéssite la réalisation des pré-requis 'pre1' et 'pre2' qui sont associés aux règles 'pre1' et 'pre2'. Ces dernières affichent les messages ``ok pour pre1'' et ``ok pour pre2''.

Nous obtenons le résultat suivant par l'usage de la commande 'make' :

echo ``ok pour pre1''
ok pour pre1
echo ``ok pour pre2''
ok pour pre2
echo ``ok pour all''
ok pour all

Cet exemple permet d'entrevoir le graphe de résolution de la règle 'all'. Make agit en deux temps, tout d'abord, il gère les variables et les inclusions éventuels d'autres Makefiles puis il construit un graphe de dépendence qu'il parcourt afin de déterminer les règles à réaliser.

Les pré-requis ne sont pas toujours des cibles vers d'autres règles, ils peuvent être des fichiers qui sont liés à la construction du fichier cible (des en-têtes par exemple). Makefile évitera de reconstruire le fichier si la date de création/modification des fichies pré-requis est antérieur à celle du fichier cible.

data.txt: item1.dat item2.dat
cat item1.dat item2.dat > data.txt

Le cas ci-dessus construit le fichier data.txt en utilisant les fichiers item1.dat et item2.dat. En exécutant le Makefile, make vérifie si le fichier data.txt existe. Si il n'existe pas il utilise la commande 'cat' suivante qui concatène les fichiers item1.dat et item2.dat pour construire data.txt. Sinon, il vérifie si item1.dat ou item2.dat ne sont pas plus récents que le fichier data.txt et exécute si c'est la cas la commande suivante. Dans le cas contraire, la règle est considérée comme à jour et la commande 'cat' n'est pas exécutée.

Pour éviter de nommer tous les pré-requis de type fichier, il est possible de nommer des ensembles de fichiers par le caractére '*' (comme pour les commandes usuels du shell style 'ls').

dataFile=*.dat

data.txt: $(dataFile)
cat item1.dat item2.dat > data.txt

Dans cet exemple, on indique dans la variable dataFile que tous les fichiers d'extensions dat doivent être pris en compte. Ces fichiers appartiennent ensuite aux pré-requis de la cible data.txt. On aurait pu tout aussi bien insérer '*.dat' directement dans les pré-requis de la cible data.txt. A noter au passage, la syntaxe $(VAR) pour accéder au contenu d'une variable VAR à ne pas confondre avec $VAR qui représente le contenu d'une variable d'environnement sous certains shells comme 'bash'.

La variable dataFile est associée à la valeur *.dat, cette valeur n'est interprétée que lors de la réalisation de la cible data.txt. Pour forcer cette interprétation lors de l'affectation d'une variable, il est possible d'utiliser la commande wildcard. Ce type de commande s'emploit grâce à la syntaxe $(commande paramètres...). Cette syntaxe se retrouve aussi dans le shell 'bash' (par exemple $(ls *.dat)).

dataFile=$(wildcard *.dat)

data.txt: $(dataFile)
cat $? > $@

info:
echo $(dataFile)

Nous avons modifier légèrement la règle data.txt pour que la commande 'cat' qui suit utilise tous les fichiers pré-requis grâce à la variable $?. La variable $@ représente le fichier cible (donc data.txt).

Si nous exécutons la règle info, les fichiers ``item1.dat item2.dat'' apparaissent bien. Supposons que les pré-requis n'existent pas encore, par ce qu'il s'agit de fichier produit par compilation, l'usage de la commande wildcard va retourner une liste vide, il est cependant possible de changer le résultat de wildcard par la commande patsubst.

JavaFile=$(patsubst %.java,%.class,$(wildcard *.java))
JavaClass=$(subst File.class,,$(JavaFile))

File.class: $(JavaClass)
javac -classpath . File.java

%:
javac $(patsubst %.class,%.java,$@)

Nous utilisons un exemple avec la compilation d'un code Java nommé File.java qui nécéssite toutes les classes du répértoire courant. La commande javac est associé à un fichier d'extension java et produit un fichier d'extension class.

Cet exemple utilise deux variables JavaFile et JavaClass. La première contiendra la substitution de tous les fichiers .java rencontrés dans le répértoire courant en .class. La deuxième appliquera une substitution remplaçant le fichier File.class par une chaîne vide. La règle File.class sera associée au début à des fichiers .class qui n'existent pas encore et qui représentent le résultat attendu de la compilation des fichiers .java du répértoire courant. La cible '%' va construire tous ces fichiers .class, en indiquant lors de la compilation par la commande javac que le résultat est un fichier .java grace à la substitution ``$(patsubst d'autres solutions pour compiler un programme.

Cible multiple

Lorsque vous avez besoin de générer plusieurs fichiers avec les mêmes pré-requis, vous pouvez factoriser vos règles de la manière qui suit:

all: file1.dat file2.dat

file1.dat file2.dat : data.txt
echo $@ $? > $@

Ce Makefile est en réalité traduit sous cette forme :

all: file1.dat file2.dat

file1.dat : data.txt
echo $@ $? > $@

file2.dat : data.txt
echo $@ $? > $@

La cible par défaut ``all'' est obligatoire pour réaliser la construction des cibles ``file1.dat'' et ``file2.dat'' sinon seule la première règle ``file1.dat'' serait réalisée par défaut. L'usage de la variable ``$@'' nous permet de connaître la cible concernée.

variable rôle
$@ La cible
$< Le premier pré-requis
$? Le nom de tous les pré-requis plus récent que la cible
$^ Le nom de tous les pre-requis séparés par un espace

Modèles de règles

Lorsque l'on traite toujours de la même manière un ensemble de fichiers, il peut être intéressant d'utiliser des règles plus génériques utilisant le motif '%' (représentant tous les éléments un peu comme ``.*'' dans une expression régulière).

Ces règles apparaissent sous ce format :

cible : pré-requis à changer : pré-requis final
commandes...

Ce type de règle est trés utile lorsque les fichiers associés aux pré-requis doivent eux-même être trouvés en fonction de la cible.

fic = f1.pdf f2.pdf

all: $(fic)

$(fic) : %.pdf : %.dvi
dvipdf $<

%.dvi : %.tex
latex $<

L'exemple ci-dessus sert à produire deux documents pdf ``f1.pdf'' et ``f2.pdf''. Les pré-requis sont des fichiers dvi qui sont déduit de la cible en remplaçant la portion ``.pdf'' par ``.dvi''.

C'est comme-ci nous avions écrit les deux règles suivantes.

f1.pdf : f1.dvi

f2.pdf : f2.dvi
...

Exécution conditionnelle

On aimerait parfois pouvoir choisir une commande en fonction du contexte.

OS=Linux
JC=javac
JLIB=

all: clean
ifeq ($(OS),Linux)
$(JC) -classpath linux.jar *.java
else
$(JC) *.java
endif
echo $(OS)

clean:
ifneq ($(OS),Solaris)
rm *.class
else
/bin/rm *.class
endif

Le Makefile ci-dessus n'a évidemment pas grand intérêt, il montre simplement l'usage de deux conditions sur la valeur de la variable OS. Dans le premier cas si Linux est indiqué, on ajoute pour la compilation la librairie linux.jar. Dans la deuxième régle, si la variable OS n'a pas pour valeur Linux, on appelle la commande 'rm' par son path complet.

On peut appeler la commande make en changeant la valeur de la variable OS par exemple :

make OS=`uname`

La commande 'uname' nous donne le nom de l'OS courant (nous aurions pu aussi affecté le résultat de cette commande dans le Makefile par OS=$(shell uname). Le tableau ci-dessous nous résume la plupart des possibilités de tests.

test rôle
ifeq (arg1,arg2) Egalité de arg1 et arg2
ifneq (arg1,arg2) Inégalité de arg1 et arg2
ifdef variable Existence de variable
ifndef variable Inexistence de variable

Variable

Il existe plusieurs formes d'affectation de variables. Le premier type le plus courant '=' autorise une affectation par référence ( l'association avec une autre variable fonctionne un peu comme un pointeur). Le deuxième type ':=' représente un mode de fonctionnement usuel dans le développement. Le troisième type '+=' est une concaténation. Enfin le dernier type '?=' sert pour les variables non définis uniquement.

B:=test

A:=$(B)
B:=$(C)
C:=test
D?=test1

A1=$(B1)
B1=test2
B1+=b

all:
echo $(A) / $(B) / $(D)

all2:
echo $(A1) / $(B1)

Les cas ci-dessus illustrent les différents types d'affectation. Ainsi la cible 'all' affichera 'test / / test1', tandis que la cible 'all2' affichera 'test2b / test2b'. Le cas des variables B et A1 illustrent les différences des opérateurs '=' et ':='. La variable B étant affectée par valeur au moment ou make rencontre l'affectation, la variable n'ayant pas de valeur, B reçoit donc une valeur vide. A contrario, la variable B1 sera associée à la variable A1, lorsque make rencontrera une affectation '=', il remontrera toutes les dépendances de variables pour remettre à jour tous les valeurs.

Make autorise aussi la construction de nouvelle variable, par exemple nous pouvons écrire

A=test
$(A)=ok
echo $test

Remarque : Attention à ne pas confondre les variables d'environnement $VAR et les variables du Makefile $(VAR).

Usage de makefiles externes

include

Il est possible d'insérer plusieurs Makefile par la commande include. Cette commande peut se trouver n'importe où dans le Makefile à condition de ne pas être préfixée par une tabulation. Lorsque make s'exécute, il commence par résoudre ces commandes avant de construire le graphe de dépendances des cibles et pré-requis. La variable d'environnement MAKEFILES peut aussi être utilisée pour intégrer une liste de Makefile, toutefois sont usage est déconseillé pour le Makefile principale puisqu'elle oblige l'utilisateur à modifier son environnement.

>export MAKEFILES=rule.mk
>make

Contenu Makefile :

all: all2
@echo ok1

Contenu rule.mk :

all2:
@echo ok2

A l'exécution, les messages ok1 et ok2 seront affichés. Si nous supprimons la référence au fichier rule.mk dans la variable MAKEFILES, nous obtiendrons le message d'erreur suivant :

make: *** Pas de règle pour fabriquer la cible `all2', nécessaire pour `all'. Arrêt.

Pour empêcher ce message d'erreur concernant la cible 'all2', il suffit d'inclure une règle générique ``ramasse-miette'' qui traitement n'importe quel cible non déclaré dans le Makefile :

all: all2
@echo ok1

%:

A l'exécution le message ok1 apparaîtra. La cible 'all2' est traitée par la cible %.

'make' vour empêchera de changer une règle en affichant un message d'erreur du type :

Makefile:7: attention : écrasement des commandes pour la cible `all2'
NewRule.mk:2: attention : anciennes commandes ignorées pour la cible `all2'

Si vous voulez qu'une règle puisse être définie dans un autre Makefile il vous suffit d'intégrer à votre Makefile ces deux règles :

%: toujours 
@$(MAKE) -f rule.mk $@

toujours: ;

La première ligne contient une ligne qui traite toutes les cibles n'appartenant pas au Makefile. Le pré-requis toujours est ``toujours'' à  résoudre ce qui empêche une absence d'exécution pour une cible existente. Le makefile rule.mk est ensuite appelé en complément avec la cible du Makefile représentée par la variable $@

Appel d'autres Makefiles

Un Makefile peut fait appel à d'autres makefiles. La première méthode est simple et consiste à ajouter une ou plusieurs commandes appelant la commande 'make' dans le bon répertoire.

all: courant
(cd a;$(MAKE) all)
(cd b;$(MAKE) all)
(cd c;$(MAKE) all)

courant:
echo "Traiter courant"

Ce makefile utilise la variable MAKE contenant la commande 'make' courante, lorsque la règle 'all' doit être réalisée soit par appel du Makefile sans paramètre (la première par défaut est donc utilisée), soit par ajout du paramètre 'all', les Makefile des sous-répertoires a, b, c sont appelés les uns à la suite des autres. Les Makefiles des répertoires a, b, c affiche simplement un message indiquant leur exécution.

(cd a;make all)
make[1]: Entre dans le répertoire `/home/brillant/articles/makefile/test/repertoire1/a'
echo "all pour a"
all pour a
make[1]: Quitte le répertoire `/home/brillant/articles/makefile/test/repertoire1/a'
(cd b;make all)
make[1]: Entre dans le répertoire `/home/brillant/articles/makefile/test/repertoire1/b'
echo "All pour b"
All pour b
make[1]: Quitte le répertoire `/home/brillant/articles/makefile/test/repertoire1/b'
(cd c;make all)
make[1]: Entre dans le répertoire `/home/brillant/articles/makefile/test/repertoire1/c'
echo "All pour c"

All pour c
make[1]: Quitte le répertoire `/home/brillant/articles/makefile/test/repertoire1/c'

La trace d'exécution ci-dessus représente bien l'ordre des commandes. A remarquer l'usage d'un sous-process par l'usage de parenthèses pour exécuter les commandes 'cd' et 'make'. En cas d'erreur dans un des Makefile, un message d'erreur de ce style sera affiché

echo "Traiter courant"
Traiter courant
(cd a;make all)
make[1]: Entre dans le répertoire `/home/brillant/articles/makefile/test/repertoire1/a'
make[1]: *** Pas de règle pour fabriquer la cible `all2', nécessaire pour `all'. Arrêt.
make[1]: Quitte le répertoire `/home/brillant/articles/makefile/test/repertoire1/a'
make: *** [all] Erreur 2

Dans cette derniète trace, le Makefile du sous-répertoire a a été corrompu par l'ajout d'un pré-requis ne correspondant à aucun fichier et aucune cible d'une autre règle.

Il existe une manière plus commode d'écrire ce Makefile

REP=a b c

.PHONY=rep $(REP)

rep: $(REP)

$(REP): courant
$(MAKE) -C $@

courant:
echo "Traiter courant"

Cet exemple l'usage d'un cible particulière PHONY que nous verrons dans la suite et l'usage de cibles multiples. En créant une cible de type 'a b c:' par la variable REP, make créé en réalité trois cibles 'a:', 'b:', 'c:' qui sont appelés les unes à la suite des autres grâce à la règle ``rep''.

Répertoires

Il n'est généralement pas bon de mélanger les fichiers sources et les fichiers binaires. Make utilise la variable VPATH pour trouver les fichiers cibles ou pré-requis.

OUTPUT=../classes
VPATH=$(OUTPUT)
FILE=$(wildcard *.java)
CLASS=$(patsubst %.java,%.class,$(FILE))

all: $(CLASS)

$(CLASS) : %.class : %.java
javac $< -d $(OUTPUT)

Cet exemple permet de compiler les fichiers du répertoires courant en stockant les fichiers résultats (.class) dans le répertoire ../classes (variable OUTPUT). Une règle générique $(CLASS) sert à produire tous les fichiers résultats en changeant les fichiers binaires attendus par leurs fichiers sources respectifs. La commande qui suit compile le fichier. A noter au passage l'usage de la variable $< (à ne pas confondre avec $?) qui contient les fichiers pré-requis finaux (obtenu en remplaçant les fichiers d'extension .class en fichier .java. Ce cas qui ne représente pas la complexité des Makefile est déjà tout à fait exploitable dans vos projets Java.

La variable VPATH s'applique à toutes les règles, il est cependant possible par la directive vpath (en minuscule) de modifier les répertoires de recherche des fichiers sur une cible.

OUTPUT=../classes
FILE=$(wildcard *.java)
CLASS=$(patsubst %.java,%.class,$(FILE))

vpath %.class ../classes

all: $(CLASS)

$(CLASS) : %.class : %.java
javac $< -d $(OUTPUT)

Nous reprenons le Makefile précédent en ajoutant la directive vpath %.class ../classes indiquant que les fichiers .class (et seulement eux) peuvent être recherchés dans le répertoire ../classes.

Si vos fichiers de pré-requis n'appartiennent pas au répertoire courant la variable $permet d'obtenir la liste des pré-requis avec le path complet.

OUTPUT=../classes
FILE=$(wildcard *.java rep/*.java)
CLASS=$(patsubst %.java,%.class,$(FILE))

vpath %.class ../classes
vpath %.java rep

all: $(CLASS)

$(CLASS) : %.class : %.java
@echo Compiling $^
@javac $< -d $(OUTPUT)

Cet exemple utilise des fichiers sources dans le répertoire courant et dans le répertoire ``rep''. La variable FILE est initialisée en prenant en compte grace à la commande wildcard tous les fichiers d'extensions java dans le répertoire courant et le répertoire ``rep''. Nous indiquons à 'make' de rechercher les fichier java aussi dans le répertoire rep grace à la directive ``vpath %.java rep''. A noter qu'il n'est pas nécessaire d'indiquer le répertoire courant car il est utilisé par défaut.

Il arrive parfois que l'on retrouve un répertoire pour une cible sans pré-requis. Ce répertoire sera traité comme un fichier et empêchera l'exécution de la cible. Pour régler ce problème on utilise un cible particulière PHONY.

.PHONY:classes

classes:
javac *.java -d classes

L'exemple ci-dessus contient deux cibles. La première qui n'est exploitée que par 'make' en interne indique que la cible classes doit toujours être réalisée. La commande 'make classes' effectuera toujours la commande javac. Si on enlève la règle commençant par .PHONY, la répertoire classes sera considéré comme toujours à jour et la règle classes ne sera jamais réalisée. A noter que vous pouvez mettre plusieurs cibles comme pré-requis de PHONY.

Conclusion

Make reste incontournable pour vos projets. Il vous reste à en approfondir l'usage en fonction de votre contexte (développement c,c++,java...) car il est trés rare que make ne soit pas trés pratique. Si vous voulez être portable, attention à l'usage de commandes shell, intégrer plutôt des blocs conditionnels avec une variable contenant le système courant.

Liens

Site Officiel :
http://www.gnu.org/software/make/make.html
Manuel version 3.7 :
http://www.gnu.org/manual/make-3.79.1/make.html
Source d'info :
http://www.paulandlesley.org/gmake/

Copyright

(c) 2005 - Alexandre Brillant

Cette documentation ne peut être employée dans le cadre d'un cours ou d'une formation sans mon accord.