Makefile

De Ensiwiki
Aller à : navigation, rechercher

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.

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
AttentionLe blanc avant gcc est une tabulation, et non 8 espaces !


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 :

  • calc a deux dépendances : calc.o et stack.o :
    • pour fabriquer calc.o, il faut calc.c :
      • dans notre cas, calc.c existe, c'est nous qui l'avons écrit ; make exécute donc gcc -c calc.c, et calc.o est créé ;
    • de même avec stack.o ;
  • maintenant, on peut fabriquer calc en lançant gcc -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 calc.c et gcc -o calc 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

Spécifique EnsimagCette partie décrit le langage compris de GNU make. C'est le make par défaut sous Linux (et souvent le seul) et celui installé à l'Ensimag. Sous d'autres systèmes, on retrouve d'autres implémentations, notamment BSD make sous BSD. Tous acceptent une syntaxe de base compatible, mais toutes les constructions avancées diffèrent (et pas qu'un peu !). GNU make étant devenu la variété la plus populaire aujourd'hui, les autres systèmes, s'ils n'ont pas GNU make par défaut, sont souvent équipés de celui-ci sous le nom 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.

Exemple 1

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.

Exemple 2

On utilise ici des dossier pour bien hiérarchiser un projet. On propose l'arborescence suivante :

.
|-- Makefile
|-- bin
|  `-- analyse
|-- doc
|-- inc
|  `-- complexity.h
|-- obj
|  |-- complexity.o
|  `-- main.o
`-- src
   |-- complexity.c
   `-- main.c

Ceci permet de séparer les fichiers source, headers, fichiers objets et exécutables. Le makefile suivant compilera le tout :

#Set up directories

OBJ_DIR = obj
SRC_DIR = src
INC_DIR = inc
BIN_DIR = bin  

# Define compilation flags  

CC = gcc 

analyse :  $(OBJ_DIR)/main.o  $(OBJ_DIR)/complexity.o
       $(CC) $(OBJ_DIR)/main.o $(OBJ_DIR)/complexity.o -o $(BIN_DIR)/analyse  

$(OBJ_DIR)/complexity.o : $(SRC_DIR)/complexity.c $(INC_DIR)/complexity.h
       $(CC) -g -c $(SRC_DIR)/complexity.c  -o $(OBJ_DIR)/complexity.o  

$(OBJ_DIR)/main.o : $(SRC_DIR)/main.c $(OBJ_DIR)/complexity.o
       $(CC) -g -c $(SRC_DIR)/main.c  -o $(OBJ_DIR)/main.o

clean:
       rm -rf $(OBJ_DIR)/*.o $(BIN_DIR)/*

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

Spécifique EnsimagSur telesun, la valeur par défaut de 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

Spécifique EnsimagRappel : GNU make est installé sur les machines de l'Ensimag, non 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.

D'autres sources de documentation