La commande make

Christophe Rippert

2004

Make

Principes

Make est un utilitaire permettant d’automatiser la compilation de logiciels. Il permet de ne recompiler que les modules ayant été modifiés depuis la dernière compilation. Il se base sur un fichier décrivant les relations entre les modules et les actions à effectuer : le Makefile. Ce fichier doit s’appeler exactement Makefile pour que make puisse le trouver automatiquement. Si on lui donne un autre nom, il faut lancer make avec l’option -f :

make -f MonMakefile

Format d’un Makefile

Un Makefile est un fichier texte dans lequel on trouve principalement des déclarations de constantes et des règles. Les déclarations de constantes facilitent la lecture et la modification du Makefile. Les règles décrivent le travail à effectuer par make. Elles doivent être écrites selon une syntaxe très stricte.

Syntaxe des règles

Cas général

Une règle doit respecter la syntaxe suivante :

<cibles>: <dépendances> <retour à la ligne>
<tabulation> <commande> <retour à la ligne>
<tabulation> <commande> <retour à la ligne>
...
Les cibles

sont les fichiers qui seront générés par les actions.

Note : on ne traitera ici que des Makefiles où la liste des cibles de chaque règle ne comporte qu’un seul fichier.

Les dépendances
sont les fichiers nécessaires à la génération des cibles.
Les actions
sont les commandes à exécuter pour générer les cibles à partir des dépendances.

Exemple :

prog.o: proc.c
    gcc -c -o proc.o proc.c

Erreurs fréquentes

cible: dep1 dep2 dep3 dep4 dep5 dep6 \
dep7 dep8 dep9
    command

Fonctionnement

Lorsqu’on lance make sans argument, il prend la cible de la première règle comme cible principale. On peut aussi préciser la cible principale explicitement : make prog.o. On détaille le fonctionnement de make sur un exemple :

# Règle 1
prog: prog.o module.o
    gcc -o prog prog.o module.o

# Règle 2
prog.o: prog.c
    gcc -c prog.c

# Règle 3
module.o: module.c
    gcc -c module.c

L’utilisateur a écrit un programme en C composé de deux fichiers, prog.c et module.c. Il lance make pour compiler son programme. La cible principale est celle de la première règle, à savoir prog.

  1. Make vérifie si le fichier prog existe, ce n’est pas le cas.
  2. Il cherche les fichiers prog.o et module.o, qui n’existent pas non plus.
  3. Il cherche une règle lui permettant de construire prog.o et la trouve (règle 2). Il cherche le fichier prog.c et le trouve. Il exécute l’action gcc -c prog.c correspondant à la règle 2. Le fichier prog.o existe donc maintenant.
  4. Make cherche une règle lui permettant de construire module.o et la trouve (règle 3). Il cherche le fichier module.c et le trouve. Il exécute l’action gcc -c module.c correspondant à la règle 3. Le fichier module.o existe donc maintenant.
  5. Puisque les fichiers prog.o et module.o existent, make peut appliquer l’action gcc -o prog prog.o module.o correspondant à la règle 1. Le fichier prog existe donc maintenant : la cible principale a été générée, make s’arrète avec succés.

L’utilisateur relance immédiatement make.

  1. Make vérifie si le fichier prog existe et c’est le cas.
  2. Il cherche alors une règle permettant de construire prog.o et trouve la règle 2. Il cherche alors une règle permettant de construire prog.c qui est la seule dépendance de prog.o et n’en trouve pas. Il considère donc que prog.c est à jour.
  3. Il compare alors la date de dernière modification de prog.o avec celle de prog.c. Puisque prog.o est plus récent que prog.c, il considère que prog.o est à jour.
  4. Il fait de même avec module.o et module.c, d’après la règle 3. Puisque prog.o et module.o sont à jour, make considère que prog l’est aussi.
  5. Il s’arrète donc en affichant un message du type «Target prog is up to date.» et aucune action n’est exécutée.

L’utilisateur modifie le fichier prog.c et relance make. Make remonte jusqu’à prog.c comme précédement, mais cette fois-ci, prog.o est plus ancien que prog.c qui vient juste d’être modifié. Make exécute donc la commande gcc -c -o prog.o prog.c pour mettre à jour prog.o. Le fichier module.c n’ayant pas été modifié, make considère module.o à jour. Mais comme prog.o vient d’être regénéré, il est donc plus récent que prog, qui n’est donc plus à jour. Donc make exécute la commande gcc -o prog prog.o module.o pour mettre à jour prog et s’arrète avec succés.

On voit donc que Make travaille sur le graphe des dépendances entre les fichiers, chaque noeud étant étiquetté avec sa date de dernière modification. On notera que ce graphe des dépendances ne doit pas contenir de cycle (eg : si A dépend de B qui dépend de C qui dépend de A).

Constantes

Make permet d’utiliser des constantes dans un Makefile afin d’en faciliter la lecture et la mise à jour. Ainsi, on pourra écrire :

CC = gcc

prog.o: prog.c
    $(CC) -c prog.c

module.o: module.c
    $(CC) -c module.c

Ceci permettra de changer facilement de compilateur C sans avoir à modifier les règles. Par convention, les constantes s’écrivent en majuscules. Comme on le voit, un nom de constante doit être entouré de $() lorsqu’on veut lire sa valeur.

Cibles particulières

Il est parfois utile de définir des cibles qui ne sont pas des fichiers. Par exemple, pour supprimer les fichiers intermédiaires lors de la compilation précédente, on pourrait écrire :

clean:
    rm -f *.o

Make accepte les règles sans dépendance, dans ce cas le fichier est considéré à jour s’il existe. Ce Makefile simpliste fonctionnera correctement la plupart du temps. Il existe cependant un cas dans lequel il ne se comportera pas comme prévu : s’il existe dans le répertoire courant un fichier ou un répertoire de nom clean, make affichera le message «Target clean is up to date» et ne fera rien. C’est assez logique puisque comme on l’a dit, un fichier cible sans dépendance est considéré comme étant à jour s’il existe. Pour éviter cette erreur, il suffit de rajouter la directive .PHONY: clean au sommet du Makefile. Ainsi, make sait que clean n’est pas un fichier mais une cible particulière (phony target). Cette directive a de plus un effet de bord intéressant. Par défaut, make s’arrète dès qu’une erreur est détectée. Ainsi, avec le Makefile :

clean:
    rm -f *.bak rm -f *.o

s’il n’existe pas de fichier .bak dans le répertoire courant, rm va renvoyer un code d’erreur et make s’arrêtera sans avoir effacé les fichiers .o. Avec la directive .PHONY, make exécute toutes les commandes de la règle correspondante et ignore les erreurs.

Règles implicites

Make est capable de générer certains fichiers même si on ne lui donne pas de règle appropriée. En effet, certaines commandes sont assez standard, comme la compilation d’un fichier .c en .o par exemple. Ainsi, dans l’exemple ci-dessus, si make n’avait pas trouvé de règle pour construitre prog.o à partir de prog.c, il aurait tout de même été capable de le faire. Cependant, l’utilisation de ces règles implicites induit des difficultés supplémentaires lors de la rédaction du Makefile. On désactivera donc cette fonction de make en utilisant la directive .SUFFIXES:.

Raccourcis

Make permet d’utiliser des raccourcis pour éviter d’avoir à taper des longues listes de fichiers. On présente ici les trois raccourcis les plus utiles :

$@
représente la liste des cibles.
$^
représente la liste des dépendances.
$<
représente la première dépendance (ie : la plus à gauche dans la liste).

Exemple :

prog: prog.o module.o
    gcc -o $@ $^

fct.o: fct.c fct.h
    gcc -c -o $@ $<

Ces raccourcis sont surtout utiles lorsqu’on utilise des règles génériques.

Règles génériques

Dans l’exemple ci-dessus, on écrit deux règles très similaires pour générer prog.o et module.o :

prog.o: prog.c
    gcc -c -o prog.o prog.c
          
module.o: module.c
    gcc -c -o module.o module.c

Pour éviter d’écrire ces deux règles, on peut définir une règle générique :

%.o: %.c
    gcc -c -o $@ $^

Cette règle peut être lue comme «chaque fichier .o dépend du fichier .c de même nom et peut être généré en utilisant la commande gcc -c -o $@ $^». On remarque que l’on est ici obligé d’utiliser les raccourcis puisque les noms des fichiers sont variables.

Appel récursif à make

Il est possible d’utiliser make dans une commande. On parle alors d’appel récursif. Par exemple :

clean:
    rm -f *.o
          
real-clean:
    make clean
    rm -f prog

Si l’on veut exécuter cette commande dans un autre répertoire, il suffit d’utiliser l’option -C <repertoire>.

clean:
    make -C subdir/ clean
    rm -f *.o

Lorsqu’il exécutera la commande associée à la cible clean, make entrera dans le répertoire subdir, lancera la commande make clean qui utilisera le Makefile présent dans le répertoire subdir, puis une fois cette commande terminée, reviendra dans le répertoire de départ et exécutera rm -f *.o. Cette option est utile pour éviter d’avoir à écrire de très gros Makefiles. En général, on place un Makefile par répertoire, dans lequel on écrit les règles correspondant aux fichiers présents dans ce répertoire.

Exemple complet

Makefile

# Ces cibles ne sont pas des vrais fichiers
.PHONY: clean install real-clean

# On désactive toutes les règles implicites
.SUFFIXES:

# Déclarations de constantes
CC = gcc
CFLAGS = -O4 -W -Wall
LD = gcc
LDFLAGS = -s

# $@ == prog.exe et $^ == main.o fctg1.o fct2.o
prog.exe: main.o fct1.o fct2.o
    $(LD) $(LDFLAGS) -o $@ $^

# Règle générique : $< == le fichier .c compilé
%.o: %.c
    $(CC) $(CFLAGS) -c -o $@ $<

clean:
    rm -f *.o

# Appel récursif à make avec ce Makefile
real-clean:
    make clean
    rm -f bin/prog.exe

# Appel récursif à make avec un Makefile dans un autre répertoire
install:
    mv prog.exe bin/
    make -C bin/ install

bin/Makefile

.PHONY: install
          
.SUFFIXES:

install:
    chmod 755 prog.exe

Références