Makefile en C : écrire son Makefile ligne par ligne

Un Makefile pour un projet C à trois fichiers, construit pas à pas : règles, CC et CFLAGS, $@ $< $^, .PHONY, clean, -MMD et l'erreur « missing separator ».

Par
Publié le

Ton projet a trois fichiers .c et un .h, et tu retapes à chaque modification gcc -Wall -Wextra main.c stats.c affichage.c -o notes. Ou tu as écrit ton premier Makefile et make te répond missing separator. On va construire ce fichier ligne par ligne, en regardant à chaque étape ce que make exécute vraiment.

En bref

Un Makefile est un fichier texte, nommé Makefile, qui décrit comment fabriquer chaque fichier du projet à partir des autres. Chaque règle donne une cible, ses dépendances, puis les commandes qui la produisent, et chaque ligne de commande commence obligatoirement par une tabulation (des espaces provoquent missing separator). make compare les dates de modification et ne relance que ce qui est plus ancien que ses dépendances. Pour un projet C, on écrit une règle qui compile chaque .c en .o, une règle d'édition de liens, et une cible clean déclarée .PHONY.

Qu'est-ce qu'un Makefile ?

Un fichier de règles que la commande make(1) lit dans le dossier courant. Voici la forme de base d'une règle :

Makefile
cible: dépendance1 dépendance2	commande qui fabrique la cible

La cible est en général un fichier à produire. Quand tu tapes make, il prend la première cible du fichier, vérifie ses dépendances, et lance la commande si la cible n'existe pas encore ou si une dépendance a été modifiée après elle. C'est tout le principe : des dates de fichiers comparées entre elles.

Le projet de cet article calcule la moyenne et la meilleure de quelques notes. stats.c contient moyenne et meilleure, affichage.c contient afficher, main.c appelle afficher, et les trois incluent le même en-tête :

notes.h
#ifndef NOTES_H#define NOTES_H
int moyenne(const int *notes, int n);int meilleure(const int *notes, int n);void afficher(const int *notes, int n);
#endif

Un premier Makefile en deux lignes

Makefile
notes: main.c stats.c affichage.c notes.h	gcc -Wall -Wextra main.c stats.c affichage.c -o notes

La cible est l'exécutable notes. Il dépend des trois sources et de l'en-tête, et la commande est celle que tu tapais à la main.

Terminal
$ makegcc -Wall -Wextra main.c stats.c affichage.c -o notes$ ./notesmoyenne : 13, meilleure : 18$ makemake: 'notes' is up to date.$ touch stats.c$ makegcc -Wall -Wextra main.c stats.c affichage.c -o notes

make affiche chaque commande avant de la lancer. Au deuxième appel, notes est plus récent que toutes ses dépendances, il n'y a rien à faire. touch change la date de stats.c comme si tu venais de l'enregistrer, et make recompile. Mais il recompile tout : les trois fichiers repassent dans gcc alors qu'un seul a changé. Sur trois petits fichiers, ça ne se voit pas ; sur trente, si.

missing separator : la tabulation obligatoire

make exige que chaque ligne de commande commence par un caractère de tabulation. Des espaces, même en nombre qui ressemble à une tabulation à l'écran, ne comptent pas. Ton éditeur en a peut-être inséré à ta place :

Terminal
$ makeMakefile:2: *** missing separator.  Stop.$ cat -A Makefilenotes: main.c stats.c affichage.c notes.h$    gcc -Wall -Wextra main.c stats.c affichage.c -o notes$

Makefile:2 désigne la ligne fautive. cat -A rend les caractères invisibles : une tabulation s'y affiche ^I, et ici il n'y en a pas, seulement quatre espaces. Dès que la ligne commence par au moins huit espaces, GNU Make devine l'erreur et le dit :

Terminal
$ makeMakefile:2: *** missing separator (did you mean TAB instead of 8 spaces?).  Stop.

La correction est de remplacer ces espaces par une vraie tabulation, et de régler ton éditeur pour qu'il n'en convertisse plus dans les Makefile. Le bon fichier s'affiche ainsi avec cat -A :

Terminal
$ cat -A Makefilenotes: main.c stats.c affichage.c notes.h$^Igcc -Wall -Wextra main.c stats.c affichage.c -o notes$

Compiler chaque fichier séparément

Pour ne recompiler que ce qui a changé, on passe par les fichiers objets. gcc -c stats.c produit stats.o, le code machine de ce seul fichier, et une dernière commande relie les .o en un exécutable (l'édition de liens). Chaque .o devient une cible que le Makefile sait fabriquer. Voici le Makefile complet de cette étape :

Makefile
CC     = gccCFLAGS = -Wall -Wextra -gOBJ    = main.o stats.o affichage.o
notes: $(OBJ)	$(CC) $^ -o $@
%.o: %.c notes.h	$(CC) $(CFLAGS) -c $< -o $@
clean:	rm -f notes $(OBJ)
.PHONY: clean

Les trois premières lignes sont des variables, qu'on lit avec $(NOM). CC (le compilateur) et CFLAGS (ses options) sont les noms conventionnels, ceux que les règles intégrées de make utilisent aussi : changer d'options revient à modifier une ligne.

La ligne 8 est une règle à motif : %.o: %.c notes.h se lit « n'importe quel .o se fabrique à partir du .c de même nom, et dépend aussi de notes.h ». Une seule règle sert pour les trois fichiers. Les commandes y utilisent les variables automatiques, que make remplit pour chaque cible :

VariableContenuPour stats.o
$@la ciblestats.o
$<la première dépendancestats.c
$^toutes les dépendances, sans doublonstats.c notes.h

Dans la règle de notes, $^ vaut donc main.o stats.o affichage.o et $@ vaut notes. Le résultat :

Terminal
$ makegcc -Wall -Wextra -g -c main.c -o main.ogcc -Wall -Wextra -g -c stats.c -o stats.ogcc -Wall -Wextra -g -c affichage.c -o affichage.ogcc main.o stats.o affichage.o -o notes$ makemake: 'notes' is up to date.$ touch stats.c$ makegcc -Wall -Wextra -g -c stats.c -o stats.ogcc main.o stats.o affichage.o -o notes

Après touch stats.c, une seule compilation et l'édition de liens. main.o et affichage.o sont réutilisés tels quels :

  1. stats.cmodifié
  2. stats.oseul objet recompilé
  3. notesmain.o et affichage.o réutilisés
Ce que make refait après touch stats.c

À quoi sert .PHONY ?

La cible clean ne produit aucun fichier, elle sert à lancer une commande avec make clean. Le problème arrive le jour où un fichier nommé clean existe dans le dossier. Sans la ligne .PHONY, make le voit, constate que cette « cible » n'a aucune dépendance plus récente, et ne fait rien :

Terminal
$ touch clean$ make cleanmake: 'clean' is up to date.

Avec .PHONY: clean, make sait que clean ne désigne aucun fichier et exécute la commande à chaque fois :

Terminal
$ touch clean$ make cleanrm -f notes main.o stats.o affichage.o

Déclare .PHONY toutes les cibles qui ne produisent pas de fichier à leur nom : clean, all, test.

Recompiler quand un .h change : -MMD et -MP

Dans le Makefile précédent, notes.h est écrit à la main dans la règle à motif. Retire-le de la règle, et make ne sait plus que les .o dépendent de l'en-tête :

Terminal
$ touch notes.h$ makemake: 'notes' is up to date.

Tu changes un prototype ou une structure dans notes.h, et les anciens .o restent en place, compilés avec l'ancienne version. Le programme peut alors se comporter de façon incohérente sans aucun message. Tenir la liste des en-têtes à la main ne passe pas à l'échelle ; GCC sait la produire. L'option -MMD lui fait écrire, en compilant main.c, un fichier main.d qui liste les en-têtes de ton projet que main.c inclut. -MP y ajoute une règle vide par en-tête, pour que make ne s'arrête pas en erreur le jour où tu supprimes un .h.

Makefile
CC       = gccCFLAGS   = -Wall -Wextra -gCPPFLAGS = -MMD -MPOBJ      = main.o stats.o affichage.o
notes: $(OBJ)	$(CC) $^ -o $@
%.o: %.c	$(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@
clean:	rm -f notes $(OBJ) $(OBJ:.o=.d)
.PHONY: clean
-include $(OBJ:.o=.d)

CPPFLAGS est le nom d'usage pour les options du préprocesseur. $(OBJ:.o=.d) remplace .o par .d dans la liste, et -include lit ces fichiers sans erreur s'ils n'existent pas encore, ce qui est le cas avant la première compilation.

Terminal
$ makegcc -MMD -MP -Wall -Wextra -g -c main.c -o main.ogcc -MMD -MP -Wall -Wextra -g -c stats.c -o stats.ogcc -MMD -MP -Wall -Wextra -g -c affichage.c -o affichage.ogcc main.o stats.o affichage.o -o notes$ cat main.dmain.o: main.c notes.hnotes.h:$ touch notes.h$ makegcc -MMD -MP -Wall -Wextra -g -c main.c -o main.ogcc -MMD -MP -Wall -Wextra -g -c stats.c -o stats.ogcc -MMD -MP -Wall -Wextra -g -c affichage.c -o affichage.ogcc main.o stats.o affichage.o -o notes$ make cleanrm -f notes main.o stats.o affichage.o main.d stats.d affichage.d

main.d contient exactement la règle qu'on aurait écrite à la main, plus la règle vide notes.h: ajoutée par -MP. Les trois sources incluent notes.h, donc les trois sont recompilées. Ajoute un fichier ou un #include, la liste suit toute seule. Ce Makefile suffit pour la plupart des petits projets en C : pour un nouveau fichier, tu ajoutes son .o à OBJ, et c'est tout.

Reste un point que make ne règle pas : un en-tête doit supporter d'être inclus plusieurs fois dans le même fichier, d'où le #ifndef NOTES_H en tête de notes.h.

C

header_guard

Protège counter.h avec une garde d'inclusion (les tests l'incluent deux fois dans le même fichier source), puis implémente les trois fonctions qu'il déclare, sans variable globale ni static : deux compteurs doivent rester indépendants.

Ouvrir l'exercice
Headers and the preprocessor
3 exercices

Questions fréquentes

Faut-il l'appeler Makefile ou makefile ?

Les deux marchent. GNU Make cherche dans l'ordre GNUmakefile, makefile, puis Makefile, et son manuel recommande Makefile, qui apparaît en tête d'un listing de dossier, près du README. Pour un autre nom, passe-le avec make -f.

Que veut dire « make: 'notes' is up to date » ?

Que la cible existe et qu'elle est plus récente que toutes ses dépendances : il n'y a rien à refaire. Si tu viens de modifier un fichier et que tu vois ce message, il manque probablement une dépendance dans tes règles, souvent un .h.

Comment voir ce que make va faire sans rien lancer ?

make -n affiche les commandes qu'il exécuterait, sans les exécuter. Pratique pour vérifier une règle à motif ou des variables avant de lancer une compilation.

Comment compiler plus vite sur une machine à plusieurs cœurs ?

make -j4 lance jusqu'à quatre commandes en même temps. Les fichiers objets sont indépendants les uns des autres, donc ils se compilent en parallèle, et make attend qu'ils soient tous prêts avant l'édition de liens.

Pourquoi make lance-t-il la première règle et pas clean ?

Sans argument, make construit la première cible du fichier, qu'on appelle le but par défaut. Place donc la règle de l'exécutable en premier, et les cibles utilitaires comme clean après.

Sources

À toi d'écrire le code.