Make et Makefile : petit résume rapide
(Vos commentaires sont les bienvenus : jean-claude.iehl at
univ-lyon1.fr)
Make est outil très général permettant, entre autre, d'automatiser
la compilation d'un projet.
Supposons que le projet soit constitué des
sources C suivants : main.c, structure.c, et operation.c . Il est
possible de compiler ce projet de trois manières différentes, les
parties suivantes précisent ces différentes étapes.
1. une seule commande
gcc -o prog -Wall main.c structure.c operation.c
(rappel des options de gcc : man gcc, info gcc)
2. une commande pour chaque fichier source
et une autre pour créer l'éxécutable :
gcc -c -Wall main.c
gcc -c -Wall structure.c
gcc -c -Wall operation.c
Les commandes précédentes compilent
chaque fichier source et créent les fichiers objets correspondants
qui s'appellent : main.o, structure.o et operation.o. Un fichier
objet est le résultat de la compilation d'un fichier source et
contient les instructions machines associées à chaque fonction du
fichier source, ainsi que la liste des fonctions appelées qui ne se
trouvent pas dans le fichier source (par exemple, les fonctions des
librairies standards printf, scanf, malloc, etc. et les fonctions
que vous avez écrites mais qui se trouvent dans les autres fichiers
sources (voir l'utilisation du mot clé extern)).
gcc -o prog main.o structure.o operation.o
Cette dernière commande crée l'éxécutable prog en
assemblant (cette partie de la compilation s'appelle l'édition de
liens) les ensembles d'instructions associés à chaque fichier objet.
C'est à ce moment que le compilateur vérifie qu'il connait
l'ensemble d'instructions associé à chaque fonction.
Le cycle de travail sur le projet nécessitera
généralement plusieurs corrections d'erreurs. Il faudra donc, après
chaque correction, recompiler le fichier source modifé (après avoir
enregistré les modifications ...). La compilation de ce fichier
source produira un fichier objet et il faudra bien sur recréer
l'éxécutable.
Prenons un exemple :
supposons que l'on ait modifié main.c, la commande suivante permet
de le recompiler (et de créer une nouvelle version de main.o) :
gcc -c -Wall main.c
il faut encore reconstruire l'éxécutable, afin de
tenir compte des modifications apportées à main.c (et donc main.o).
Par contre, les objets structure.o et operation.o n'ont pas été
modifiés, il n'est donc pas nécessaire de les recompiler.
gcc -o prog main.o structure.o operation.o
On peut maintenant vérifier le bon fonctionnement
du programme.
Un projet "moyen" comporte rapidement une dizaine
de fichiers sources, et il devient vite pénible de taper toutes les
commandes nécessaires à la compilation du projet. De plus, le risque
d'erreur ou d'oubli augmente très rapidement, d'ou l'utilité, voire
la nécessité, d'un outil permettant d'automatiser la construction du
projet.
3. Make
L'utilisation de make est relativement simple,
une fois que l'on a compris que son rôle est de produire
automatiquement la séquence de commandes permettant de construire un
projet. Pour créer l'exécutable, la première fois, il faut que make
génère la séquence de commandes décrite dans la partie
2 :
gcc -c -Wall main.c
gcc -c -Wall structure.c
gcc -c -Wall operation.c
gcc -o prog main.o structure.o operation.o
De la même manière, après la modification de
main.c, make doit générer la séquence de commandes suivante :
gcc -c -Wall main.c
gcc -o prog main.o structure.o operation.o
Le raisonnement qui nous a permis d'écrire cette
séquence de commande est basé sur les dépendances (ou les relations)
entre ces fichiers :
main.c produit main.o
et
main.o, structure.o et operation.o permettent de construire le
projet prog.
Il suffit de décrire ces relations à make, ainsi
que les commandes associées pour qu'il puisse produire la séquence
de commandes correcte. Ces relations sont décrites dans un fichier
texte, nommé Makefile ou makefile. Ce fichier est constistué des
descriptions des relations entre les fichiers sources et les
fichiers objets ainsi que des relations entre les fichiers objets et
le projet.
Une relation s'écrit de la manière suivante dans
le fichier makefile :
produit : source
commande
Cette règle indique plusieurs choses à make.
Premièrement, que produit est crée à partir de source
et que c'est commande qui permet de le faire. On peut donc
décrire la relation entre main.c et main.o, qui est produit par la
compilation de main.c :
main.o : main.c
gcc -c -Wall main.c
De même, la relation entre les fichiers objets et
le projet s'écrit :
prog: main.o structure.o operation.o
gcc -o prog main.o structure.o
operation.o
Pour achever la création du makefile, il ne reste
plus qu'à écrire les règles pour structure et operation :
structure.o : structure.c
gcc -c -Wall structure.c
operation.o : operation.c
gcc -c -Wall operation.c
Un dernier détail, comme le makefile est composé
de plusieurs relations, il faut indiquer à make laquelle construire
en priorité : par convention, c'est tout simplement la première. Il
suffit donc d'écrire la règle décrivant la construction du projet au
début du fichier makefile. Le makefile complet ressemblera donc à :
prog: main.o structure.o operation.o
gcc -o prog main.o structure.o
operation.o
main.o : main.c
gcc -c -Wall main.c
structure.o : structure.c
gcc -c -Wall structure.c
operation.o : operation.c
gcc -c -Wall operation.c
Lors d'une modification, make se base sur la date
de modification des fichiers pour déterminer les mises à jours à
effectuer. Dans le scénario précédent, le projet complet est compilé
et éxécuté. Lors de ce premier appel à make, ni les fichiers objets,
ni l'éxécutable n'existent, ils sont donc créés en utilisant la
commande associée à leur règle. Ensuite, main.c est modifié, main.c
devient donc plus récent que main.o et prog. Make analyse les régles
définies dans le makefile et vérifie les dates des fichiers. Comme
main.c est plus récent que main.o, ce dernier ne peut être le
produit de la version actuelle de main.c, il faut donc le
recompiler, la commande associée à la règle main.o sera donc
éxécutée (gcc -c -Wall main.c). Après la compilation, la
date de main.o est plus récente que celle de prog, il faudra donc
récréer prog avec la commande correspondante (gcc -o prog main.o
structure.o operation.o).
3. Quelques idées d'utilisation
Make ne pose aucune contrainte sur la
compilation. Il ne sait pas qu'il est en train de compiler un
projet, vous pouvez donc en profiter pour lui faire faire autre
chose, comme effacer les fichiers temporaires, créer une archive de
votre projet, ou définir plusieurs versions du même projet ou de
plusieurs projets dans le même makefile.
Les sections suivantes présentent quelques règles
à ajouter au makefile. Pour indiquer à make que l'on veut éxécuter
une règle particulière il suffit de lui indiquer : make regle,
ce qui éxécutera les commandes associées à la règle regle.
3.1 Effacer les fichiers temporaires
Lors de modifications importantes d'un projet, on
a souvent besoin de recompiler la totalité des sources, ou de
nettoyer le répertoire de travail. Il est toujours possible de taper
une commande rm dans un terminal, mais on peut aussi le faire faire
à make. Comment écrire cette règle ? Quelle relation existe-t-il
entre un fichier et un fichier effacé ? La réponse est simple :
aucune. Mais on peut quand même écrire une règle qui éxécute
toujours sa commande, il suffit de ne pas indiquer de source et de
donner à la règle un nom qui n'est pas un fichier.
Pour effacer les fichiers objets du projet, il
suffit de d'écrire la règle suivante qui s'appelle clean par
convention :
clean :
rm -f prog *.o
L'option -f (force) indique juste à rm de ne pas
signaler les erreurs éventuelles (plus de détail sur les options de
rm, man
rm). Le *.o est un joker qui indique à rm d'effacer
tous les fichiers se terminant par .o, c'est à dire les fichiers
objets.
make clean éxécutera donc la commande rm
-f prog *.o associée à la règle clean.
3.2 Archiver le projet
Les règles sans sources (voir ci-dessus)
permettent d'éxécuter une commande quelconque, on peut en profiter
pour archiver le projet, par exemple. On peut compléter le makefile
par la règle :
zip:
tar -zcvf prog.tar.gz main.c structure.c
operation.c Makefile
La commande tar permet de créer une archive
compressée, il suffit d'indiquer le nom de l'archive à créer et la
liste des fichiers à archiver (plus de détails sur l'utilisation de
tar, man
tar). Vous pouvez aussi envoyer l'archive par
mail (man mail) ou la copier sur disquette.
make zip éxécutera donc la commande tar
-zcvf prog.tar.gz main.c structure.c operation.c Makefile
associée à la règle zip.
3.3 Plusieurs versions du projet
Dans le TP3,
il était demandé de fournir plusieurs versions du même programme. On
peut créer plusieurs projets séparés, ou tout simplement décrire les
différentes versions dans le même makefile. Le TP3 était composé des sources C
suivants : blob.c tga.c tri_blob.c. Voici le fichier Makefile fourni
avec le sujet :
blob: blob.o tga.o tri_blob.o
gcc -o blob blob.o tga.o tri_blob.o -lm
blob.o: blob.c
gcc -Wall -c blob.c
tga.o: tga.c
gcc -Wall -c tga.c
tri_blob.o: tri_blob.c
gcc -Wall -c tri_blob.c
tarball:
tar -zcvf blob.tar.gz blob.c tga.c tri_blob.c
blob.h tga.h Makefile
clean:
rm *.o blob
Ce Makefile crée un éxécutable nommé blob à
partir de blob.o, tga.o et de tri_blob.o. Le travail demandé
consistait principalement a écrire de nouvelles fonctions de tri.
Supposons que le tri par insertion se trouve dans le source
tri_insertion.c, et que le tri par tas se trouve dans tri_tas.c. On
veut créer une version de blob, blob_insertion, compilée avec le tri
par insertion et une autre compilée avec le tri par tas, blob_tas.
Les deux nouvelles règles :
blob_insertion: blob.o tga.o tri_insertion.o
gcc -o blob_insertion blob.o tga.o tri_insertion.o -lm
tri_insertion.o: tri_insertion.c
gcc -Wall -c tri_insertion.c
permettent de créer l'éxécutable blob_insertion qui est bien blob
compilé avec la fonction de tri par insertion. De même, il suffit de
rajouter les règles suivantes pour la version tri par tas :
blob_tas: blob.o tga.o tri_tas.o
gcc -o blob_tas blob.o tga.o tri_tas.o -lm
tri_tas.o: tri_tas.c
gcc -Wall -c tri_tas.c
Au final, le Makefile ressemblera à :
blob: blob.o tga.o tri_blob.o
gcc -o blob blob.o tga.o tri_blob.o -lm
blob_insertion: blob.o tga.o tri_insertion.o
gcc -o blob_insertion blob.o tga.o tri_insertion.o -lm
blob_tas: blob.o tga.o tri_tas.o
gcc -o blob_tas blob.o tga.o tri_tas.o -lm
tri_tas.o: tri_tas.c
gcc -Wall -c tri_tas.c
tri_insertion.o: tri_insertion.c
gcc -Wall -c tri_insertion.c
blob.o: blob.c
gcc -Wall -c blob.c
tga.o: tga.c
gcc -Wall -c tga.c
tri_blob.o: tri_blob.c
gcc -Wall -c tri_blob.c
tarball:
tar -zcvf blob.tar.gz blob.c tga.c tri_blob.c blob.h tga.h Makefile
clean:
rm *.o blob
Les trois premières règles (blob, blob_insertion,
blob_tas) correspondent aux trois versions du projet et les règles
suivantes décrivent la compilation des différents sources des
projets.
4. Makefile avancé
La rédaction d'un makefile est relativement
délicate, il est courant d'oublier de rajouter un objet dans les
sources d'une règle ou dans la liste des objets à compiler pour
créer l'éxécutable. De même, l'écriture des règles de compilation
est particulièrement longue. Heureusement make dispose de
fonctionnalités supplémentaires permettant d'automatiser une partie
de la rédaction. Les deux sections suivantes présentent
l'utilisation des variables afin de simplifier l'écriture des
makefile.
4.1 variables
Des variables existent dans make, ce sont
simplement des chaînes de caractères. On peut par exemple définir
une variable indiquant quel compilateur utiliser, quelles options de
compilation utiliser ou encore une liste de fichiers.
Pour définir une variable, il suffit de lui
donner un nom et une valeur. Un nom de variable est une suite de
caractères ne contenant pas `:', `#' et `='. Une valeur est une
chaîne de caractères quelconque. Pour substituer une variable à sa
valeur, il suffit d'écrire $nomvariable. Quelques exemples :
CC= gcc
blob: blob.o tga.o tri_blob.o
$(CC) -o blob blob.o tga.o tri_blob.o -lm
...
ou
CC= gcc
objets= blob.o tga.o tri_blob.o
blob: $(objets)
$(CC) -o blob $(objets) -lm
...
Les parenthèses autour des noms de variables ne
sont pas obligatoires mais permettent d'éviter certains problèmes
lors de la substitution de la variable par sa valeur (c'est un
simple remplacement de caractères).
4.2 variables automatiques et règles
implicites
Il existe un autre type de variables : les
variables automatiques, leurs noms sont imposés et make défini leurs
valeurs. Voici les plus utilisées :
- $@ : produit (ou but) de la règle
- $< : nom de la première dépendance (ou source)
- $? : toutes les dépendances plus récentes que le but
- $^ : toutes les dépendances
- $+ : idem mais chaque dépendance apparait autant de
fois qu'elle est cité et l'ordre d'apparition est conservé.
La variable $@ représente le produit
(ou le but) d'une règle, en reprenant l'exemple précédent :
CC= gcc
objets= blob.o tga.o tri_blob.o
blob: $(objets)
$(CC) -o $@ $(objets) -lm
La variable $^ représente toutes les
dépendances d'une règle, par exemple tous les objets lors de la
création du projet :
CC= gcc
objets= blob.o tga.o tri_blob.o
blob: $(objets)
$(CC) -o $@ $^ -lm
La variable $< représente la
première dépendance, on peut l'utiliser pour compiler un source C :
CC= gcc -Wall
blob.o: blob.c
$(CC) -o $@ -c $<
L'utilisation des variables automatiques est
particulièrement puissante lors de la définition de règles
implicites qui s'appliquent à plusieurs fichiers. Par exemple, il
possible, avec une seule règle implicite, de compiler tous les
sources C :
%.o: %.c
$(CC) -o $@ -c $<
Le but est défini par un pattern, ou un
joker : %.o . Cette règle indique que chaque fichier .o
nécessaire à la création du projet est obtenu à partir du fichier .c
correspondant et que la commande $(CC) -o $@ -c $<
permet de créer le fichier objet à partir du fichier source. Les
variables automatiques permettent de retrouver le nom du fichier
concerné par la règle.
En résumé, il est donc possible de compiler un projet quelconque
avec un Makefile de quelques lignes, par exemple blob :
CC= gcc #
compilateur
CFLAGS= -Wall # options de compilation pour les sources
C
objets= blob.o tga.o tri_blob.o
blob: $(objets)
$(CC) -o $@ $^ -lm
%.o: %.c
$(CC) $(CFLAGS) -o $@ -c $<
4.3 Génération automatique des noms de
fichiers
Il est possible d'utiliser des substitutions dans
les noms de fichiers, par exemple pour générer automatiquement les
noms des fichiers objets à partir des noms des fichiers sources, en
remplaçant l'extension .c par .o :
SRC= main.c struct.c operation.c
# nommage automatique des fichiers objets d'apres les noms des
sources C
OBJ= $(SRC:.c=.o)
dans cet exemple $(OBJ)
contiendra main.o struct.o
operation.o
exemple du makefile précedent avec nommage automatique des objets a
partir des fichiers sources .c :
CC=
gcc # compilateur
CFLAGS= -Wall # options de compilation pour les sources C
sources= blob.c tga.c tri_blob.c
objets= $(sources:.c=.o)
blob: $(objets)
$(CC) -o $@ $^ -lm
%.o: %.c
$(CC) $(CFLAGS) -o $@ -c $<
5. Génération automatique des dépendences
Lorsque un projet prends un petit peu
d'importance, de nombreux headers (.h) sont utilisés. Lorsque l'on
modifie un header, il est nécessaire de recompiler tous les sources
qui l'utilisent. Il est évidement possible d'ajouter les fichiers .h
comme dépendance des sources, mais c'est particulièrement
fastidieux. Le compilateur est le mieux placé pour savoir quels sont
les headers utilisés par chaque source et il est même possible de
récupérer ces dépendances au format des régles de make. Il suffit de
stocker cette nouvelle règle dans un fichier (.d par convention) et
de l'inclure dans le makefile. Pour vous convaincre, essayer sur un
de vos sources :
gcc -o source.d -MM source.c
cat source.d
regardez les options -MM dans le manuel
de gcc
(la suite, bientôt ...)
exemple de makefile complet :
3 projets différents sont décrits,
archivage de l'ensemble,
numérotation automatique du projet,
génération automatique de changelog
...
# correction TP5 systemes d'exploitation 2003 : partage de
fichiers
CFLAGS= -g -Wall -D_REENTRANT
LDFLAGS=
LIB= -lpthread
CC= gcc $(CFLAGS)
LD= gcc
# serveur d'annonce
ASRC=annonce.c \
annonce_sha1_f.c \
mtfifo.c \
critique.c \
socket.c \
fichier.c \
util.c \
debit.c \
version.c \
sha1.c \
sha1_f.c
# nommage automatique des fichiers objets d'apres les noms des
sources C
AOBJ= $(ASRC:.c=.o)
# nommage automatique des fichiers de dependance d'apres les noms
des sources C
ADEP= $(ASRC:.c=.d)
# serveur de fichiers
FSRC=serveur_fichier.c \
serveur_fichier_sha1.c \
serveur_fichier_sha1_f.c \
mtfifo.c \
critique.c \
socket.c \
fichier.c \
util.c \
debit.c \
version.c \
sha1.c \
sha1_f.c
# nommage automatique des fichiers objets d'apres les noms
des sources C
FOBJ= $(FSRC:.c=.o)
# nommage automatique des fichiers de dependance d'apres
les noms des sources C
FDEP= $(FSRC:.c=.d)
#client
CSRC=client_sha1.c \
client_sha1_f.c \
mtfifo.c \
critique.c \
socket.c \
fichier.c \
util.c \
debit.c \
version.c \
sha1.c \
sha1_f.c
# nommage automatique des fichiers objets d'apres les noms
des sources C
COBJ= $(CSRC:.c=.o)
# nommage automatique des fichiers de dependance d'apres
les noms des sources C
CDEP= $(CSRC:.c=.d)
# ensemble des fichiers de dependance
DEP= $(ADEP) $(FDEP) $(CDEP)
# numero de version auto
include build
#
BIN=annonce \
serveur_fichier \
client
#
VER=annonce.version \
serveur_fichier.version \
client.version
.PHONY: all
all: $(BIN) $(VER) changelog
# numerotation des compilations completes
build:
echo BUILD= 1 > build
rebuild: build
@echo BUILD= `expr $(BUILD) + 1` > build
# genere un fichier C avec une chaine de caracteres decrivant la
version du projet
version.c: build
@echo "char version_id[]= \"build
$(BUILD)\";" > $@
# conserve les commentaires sur les versions du projet
changelog: build
@echo -- changelog build $(BUILD)
@echo -e --\\n$(USER)@`hostname -s` --
build $(BUILD) $(HOSTTYPE) -- `date` > $@.tmp
@if test -f $@; then cat $@ >> $@.tmp; fi
@mv $@.tmp $@
annonce.version: annonce
@echo $< -- $(USER) build
$(BUILD) -- `date` > $@
annonce: $(AOBJ)
@echo -- build $(BUILD)
$(LD) $(LDFLAGS) -o $@ $+ $(LIB)
#
serveur_fichier.version: serveur_fichier
@echo $< -- $(USER) build
$(BUILD) -- `date` > $@
serveur_fichier: $(FOBJ)
@echo -- build $(BUILD)
$(LD) $(LDFLAGS) -o $@ $+ $(LIB)
#
client.version: client
@echo $< -- $(USER) build
$(BUILD) -- `date` > $@
client: $(COBJ)
@echo -- build $(BUILD)
$(LD) $(LDFLAGS) -o $@ $+ $(LIB)
#
%.o: %.c
$(CC) -o $@ -c $<
%.d: %.c
$(CC) -MM -MD -o $@ $<
.PHONY: clean
clean: rebuild
rm -f $(BIN) $(OBJ) $(DEP)
*.i *.s
.PHONY: tarball
tarball:
@echo -- build $(BUILD)
-tar -zcf annonce_b$(BUILD).tar.gz Makefile
build changelog *.[ch]
# inclusion des dependances
-include $(DEP)
6. Conclusion
Il existe de nombreuses fonctionnalités, lisez le
manuel : info make