Accès rapide :
Un petit projet de départ
Un premier Makefile
Factoriser avec des variables
Variables automatiques et règles génériques
Séparer les fichiers générés
Suivre automatiquement les fichiers d'entête
Cibles factices
Lier une librairie
Un Makefile plus complet
Dans les chapitres précédents, nous avons utilisé directement la commande gcc. Cette approche est parfaite pour
comprendre le processus de construction, mais elle devient vite pénible dès qu'un projet contient plusieurs fichiers sources.
L'outil make permet de décrire les fichiers à produire, les fichiers dont ils dépendent et les commandes nécessaires
pour les construire.
Un Makefile n'est pas un script exécuté aveuglément du début à la fin. C'est un ensemble de règles. Pour chaque cible,
make regarde les dates de modification des fichiers : si une dépendance est plus récente que la cible, la commande
associée est relancée. C'est ce mécanisme qui évite de recompiler tout le projet à chaque modification.
Considérons un projet volontairement simple. Il contient un fichier d'entête, deux fichiers d'implémentation et un programme principal. La structure reste modeste, mais elle suffit à montrer l'intérêt d'un Makefile.
Nous allons utiliser l'arborescence suivante pour les exemples de ce chapitre.
SampleProject/
|-- Makefile
|-- include/
| `-- calculator.h
`-- src/
|-- calculator.c
`-- main.c
Le fichier d'entête expose simplement les fonctions que les autres fichiers pourront utiliser.
1 2 3 4 5 6 7 |
#ifndef CALCULATOR_H #define CALCULATOR_H int add( int first, int second ); int sub( int first, int second ); |
Le fichier d'implémentation contient le code effectif des fonctions déclarées dans l'entête.
1 2 3 4 5 6 7 8 9 |
#include "calculator.h" int add( int first, int second ) { return first + second; } int sub( int first, int second ) { return first - second; } |
Le programme principal inclut l'entête et appelle les fonctions de calcul.
1 2 3 4 5 6 7 8 9 10 11 12 |
#include <stdio.h> #include <stdlib.h> #include "calculator.h" int main() { printf( "add( 40, 2 ) == %d\n", add( 40, 2 ) ); printf( "sub( 40, 2 ) == %d\n", sub( 40, 2 ) ); return EXIT_SUCCESS; } |
Le Makefile le plus simple consiste à écrire explicitement toutes les étapes. On compile chaque fichier .c en fichier
.o, puis on lance l'édition des liens pour produire l'exécutable final.
Voici une première version explicite du fichier Makefile.
app: main.o calculator.o gcc -o app main.o calculator.o main.o: src/main.c include/calculator.h gcc -Wall -Wextra -Iinclude -c src/main.c -o main.o calculator.o: src/calculator.c include/calculator.h gcc -Wall -Wextra -Iinclude -c src/calculator.c -o calculator.o clean: rm -f app main.o calculator.o
Chaque règle commence par une cible, suivie de deux points puis de ses dépendances. Les lignes suivantes sont les commandes à exécuter. Attention à un détail important : les commandes doivent commencer par une vraie tabulation. Des espaces ne produiront pas le même résultat.
L'exécution suivante montre que make ne relance pas les commandes inutiles quand la cible est déjà à jour.
$> make gcc -Wall -Wextra -Iinclude -c src/main.c -o main.o gcc -Wall -Wextra -Iinclude -c src/calculator.c -o calculator.o gcc -o app main.o calculator.o $> ./app add( 40, 2 ) == 42 sub( 40, 2 ) == 38 $> make make: 'app' is up to date. $>
Le premier Makefile fonctionne, mais il répète beaucoup d'informations : le compilateur, les options de compilation, les dossiers d'entêtes, le nom de l'exécutable. Les variables permettent de centraliser ces choix.
La version suivante introduit les variables classiques d'un Makefile C.
CC = gcc CFLAGS = -Wall -Wextra -Wconversion -Werror -std=c17 -Iinclude LDFLAGS = LDLIBS = TARGET = app OBJECTS = main.o calculator.o $(TARGET): $(OBJECTS) $(CC) $(LDFLAGS) -o $(TARGET) $(OBJECTS) $(LDLIBS) main.o: src/main.c include/calculator.h $(CC) $(CFLAGS) -c src/main.c -o main.o calculator.o: src/calculator.c include/calculator.h $(CC) $(CFLAGS) -c src/calculator.c -o calculator.o clean: rm -f $(TARGET) $(OBJECTS)
Par convention, on utilise souvent CC pour le compilateur C, CFLAGS pour les options de compilation,
LDFLAGS pour les options destinées à l'éditeur de liens et LDLIBS pour les librairies à lier. Ce ne sont
pas des obligations, mais ces noms sont suffisamment classiques pour faciliter la lecture.
make propose aussi des variables automatiques. Elles évitent d'écrire plusieurs fois la même information dans une
règle. Les plus utiles au début sont les suivantes.
| Variable | Signification |
|---|---|
$@ |
nom de la cible en cours de construction ; |
$< |
première dépendance de la règle ; |
$^ |
liste de toutes les dépendances, sans doublon. |
Avec ces variables et une règle générique, on peut déjà obtenir un Makefile plus propre.
Voici la même construction, cette fois avec une règle générique et des variables automatiques.
CC = gcc CFLAGS = -Wall -Wextra -Wconversion -Werror -std=c17 -Iinclude TARGET = app OBJECTS = main.o calculator.o $(TARGET): $(OBJECTS) $(CC) -o $@ $^ %.o: src/%.c include/calculator.h $(CC) $(CFLAGS) -c $< -o $@ clean: rm -f $(TARGET) $(OBJECTS)
La règle %.o: src/%.c signifie que, pour produire un fichier objet, make peut utiliser le fichier source
qui porte le même nom dans le dossier src. Le symbole % représente la partie variable du nom.
Dans un projet réel, il est préférable de ne pas mélanger les sources et les fichiers générés. Le dossier build peut
recevoir les fichiers objets et l'exécutable final.
Le Makefile suivant place les fichiers produits dans un dossier dédié.
CC = gcc CFLAGS = -Wall -Wextra -Wconversion -Werror -std=c17 -Iinclude TARGET = build/app SOURCES = src/main.c src/calculator.c OBJECTS = $(SOURCES:src/%.c=build/%.o) $(TARGET): $(OBJECTS) $(CC) -o $@ $^ build/%.o: src/%.c | build $(CC) $(CFLAGS) -c $< -o $@ build: mkdir -p build clean: rm -rf build
La dépendance placée après le caractère | est une dépendance d'ordre. Ici, elle indique que le dossier build
doit exister avant de compiler, mais qu'une modification de la date du dossier ne doit pas déclencher une recompilation complète.
Le Makefile précédent est encore imparfait : il ne sait pas automatiquement quels fichiers .h sont inclus par chaque
fichier source. On peut demander à gcc de produire ces dépendances avec les options -MMD et -MP.
Cette version demande à gcc de générer et recharger les dépendances d'entêtes.
CC = gcc CFLAGS = -Wall -Wextra -Wconversion -Werror -std=c17 -Iinclude -MMD -MP TARGET = build/app SOURCES = src/main.c src/calculator.c OBJECTS = $(SOURCES:src/%.c=build/%.o) DEPS = $(OBJECTS:.o=.d) $(TARGET): $(OBJECTS) $(CC) -o $@ $^ build/%.o: src/%.c | build $(CC) $(CFLAGS) -c $< -o $@ build: mkdir -p build clean: rm -rf build -include $(DEPS)
Les fichiers .d sont générés à côté des fichiers .o. La ligne -include $(DEPS) les recharge
dans le Makefile. Le tiret placé devant include évite une erreur au tout premier lancement, quand ces fichiers n'existent
pas encore.
Certaines cibles ne correspondent pas à un fichier réel. C'est le cas de clean, all ou install.
Pour éviter toute ambiguïté si un fichier porte le même nom, on les déclare avec .PHONY.
L'extrait suivant montre quelques cibles factices fréquentes.
.PHONY: all clean run install PREFIX ?= /usr/local BINDIR = $(PREFIX)/bin all: $(TARGET) run: $(TARGET) ./$(TARGET) install: $(TARGET) install -d $(BINDIR) install -m 755 $(TARGET) $(BINDIR)/app clean: rm -rf build
L'opérateur ?= affecte une valeur seulement si la variable n'est pas déjà définie. On peut donc lancer
make install PREFIX=/tmp/demo pour changer le répertoire d'installation sans modifier le fichier.
Dès qu'un programme utilise une librairie externe, il faut souvent fournir des options d'inclusion, des options de recherche de librairies et le nom des librairies à lier. L'exemple suivant utilise la librairie mathématique standard.
Le Makefile suivant ajoute la librairie mathématique pendant l'édition des liens.
CC = gcc CFLAGS = -Wall -Wextra -std=c17 LDLIBS = -lm TARGET = app OBJECTS = main.o $(TARGET): $(OBJECTS) $(CC) -o $@ $^ $(LDLIBS) main.o: main.c $(CC) $(CFLAGS) -c $< -o $@
Avec gcc, l'ordre des arguments pendant l'édition des liens a son importance : les librairies sont généralement
placées après les fichiers objets qui les utilisent. C'est pour cela que $(LDLIBS) apparaît en fin de ligne.
Voici une version récapitulative raisonnable pour un petit projet. Elle reste simple, mais elle intègre les points importants : séparation des fichiers générés, dépendances automatiques, cibles factices, exécution et nettoyage.
Pour terminer, voici une version récapitulative que vous pouvez adapter à vos petits projets.
CC = gcc CFLAGS = -Wall -Wextra -Wconversion -Werror -std=c17 -Iinclude -MMD -MP LDFLAGS = LDLIBS = TARGET = build/app SOURCES = src/main.c src/calculator.c OBJECTS = $(SOURCES:src/%.c=build/%.o) DEPS = $(OBJECTS:.o=.d) .PHONY: all clean run all: $(TARGET) $(TARGET): $(OBJECTS) $(CC) $(LDFLAGS) -o $@ $^ $(LDLIBS) build/%.o: src/%.c | build $(CC) $(CFLAGS) -c $< -o $@ build: mkdir -p build run: $(TARGET) ./$(TARGET) clean: rm -rf build -include $(DEPS)
Ce Makefile n'est pas une vérité absolue. Chaque équipe adapte ses règles à ses conventions, à sa plateforme et à ses outils. L'objectif est surtout d'éviter les lignes de compilation tapées à la main, de reconstruire uniquement ce qui est nécessaire et de rendre le projet reproductible.
Améliorations / Corrections
Vous avez des améliorations (ou des corrections) à proposer pour ce document : je vous remerçie par avance de m'en faire part, cela m'aide à améliorer le site.
Emplacement :
Description des améliorations :