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