Apprendre les Makefile avec les meilleurs exemples
(makefiletutorial.com)- Makefile est un outil qui simplifie l’automatisation des builds C/C++ et la gestion des dépendances
- Il détecte les fichiers modifiés à l’aide des horodatages et n’exécute la compilation que lorsque c’est nécessaire
- Il explique avec des exemples la structure essentielle : règles (rules), commandes (commands) et dépendances (prerequisites)
- Il aborde aussi de manière pratique des fonctions avancées comme les variables automatiques, les pattern rules et l’expansion de variables
- Il présente l’importance de l’extensibilité et de la maintenabilité à travers un modèle de Makefile prêt pour des projets de taille intermédiaire
Présentation du guide tutoriel Makefile
- Makefile est un outil central pour l’automatisation des builds et la gestion des dépendances d’un projet
- À cause de nombreuses règles implicites et symboles peu visibles, il peut sembler complexe au premier abord, mais ce guide organise les points essentiels de manière concise avec des exemples directement exécutables
- Chaque section permet une compréhension fondée sur la pratique grâce à des exemples d’exécution
Bien démarrer
Pourquoi Makefile existe
- Un Makefile sert à recompiler uniquement les parties modifiées dans les grands programmes
- Au-delà du C/C++, il existe des outils de build dédiés à de nombreux langages, mais Make reste utile dans des scénarios de build généraux
- La logique clé consiste à détecter les fichiers modifiés pour n’exécuter que les tâches nécessaires
Systèmes de build alternatifs à Make
- Écosystème C/C++ : SCons, CMake, Bazel, Ninja et d’autres options
- Écosystème Java : Ant, Maven, Gradle
- Go, Rust, TypeScript, etc. fournissent aussi leurs propres outils de build
- Les langages interprétés comme Python, Ruby ou JavaScript n’ont pas besoin de compilation, donc la nécessité d’une gestion séparée via un Makefile est plus faible
Versions et variantes de Make
- Il existe plusieurs implémentations de Make, mais ce guide est optimisé pour GNU Make (principalement utilisé sur Linux et MacOS)
- Les exemples sont compatibles avec GNU Make 3 et 4
Comment exécuter les exemples
- Après avoir installé
makedans le terminal, enregistrez chaque exemple dans un fichierMakefile, puis exécutez la commandemake - Dans un Makefile, les lignes de commande doivent impérativement être indentées avec un caractère de tabulation
Syntaxe de base d’un Makefile
Structure d’une règle (Rule)
-
cible: dépendance(s)- commande
- commande
-
Cible : nom du fichier produit par le build (généralement un seul)
-
Commande : script shell réellement exécuté (commençant par une tabulation)
-
Dépendance : liste des fichiers qui doivent impérativement être prêts avant que la cible soit construite
L’essence de Make
Exemple Hello World
hello:
echo "Hello, World"
echo "This line will print if the file hello does not exist."
- La cible
hellon’a pas de dépendance et exécute 2 commandes - Lors de l’exécution de
make hello, si le fichierhellon’existe pas, les commandes sont exécutées. Si le fichier existe déjà, elles ne le sont pas - En général, on écrit les Makefile de sorte que la cible corresponde au nom du fichier
Exemple de base de compilation d’un fichier C
- Créez un fichier
blah.c(avec le contenuint main() { return 0; }) - Écrivez le Makefile suivant
blah:
cc blah.c -o blah
- Lors de l’exécution de
make, si la cibleblahn’existe pas, la compilation s’exécute et crée le fichierblah - Même si
blah.cest modifié ensuite, il n’y a pas de recompilation automatique → il faut ajouter une dépendance
Ajouter une dépendance
blah: blah.c
cc blah.c -o blah
- Désormais, si
blah.ca été modifié, la cibleblahest reconstruite - La détection des changements repose sur l’horodatage des fichiers
- Si les horodatages sont modifiés manuellement, le comportement peut ne plus correspondre à l’intention
Exemples supplémentaires
Exemple de cibles et dépendances chaînées
blah: blah.o
cc blah.o -o blah
blah.o: blah.c
cc -c blah.c -o blah.o
blah.c:
echo "int main() { return 0; }" > blah.c
- Le processus de génération à chaque étape est automatisé en suivant les dépendances sous forme d’arborescence
Exemple de cible toujours exécutée
some_file: other_file
echo "This will always run, and runs second"
touch some_file
other_file:
echo "This will always run, and runs first"
- Comme
other_filen’est pas réellement créé en tant que fichier, la commande desome_files’exécute à chaque fois
Make clean
- La cible
cleanest souvent utilisée pour supprimer les artefacts de build - Ce n’est pas un mot réservé spécial dans Make : il faut la définir explicitement comme commande
- Si un fichier s’appelle
clean, cela peut créer une ambiguïté ; il est donc recommandé d’utiliser.PHONY
Exemple :
some_file:
touch some_file
clean:
rm -f some_file
Gestion des variables
- Une variable est toujours une chaîne de caractères.
- On recommande généralement
:=, mais il existe plusieurs formes d’affectation comme=,?=et+= - Exemple d’utilisation :
files := file1 file2
some_file: $(files)
echo "Look at this variable: " $(files)
touch some_file
file1:
touch file1
file2:
touch file2
clean:
rm -f file1 file2 some_file
- Référencer une variable :
$(variable)ou${variable} - Dans un Makefile, les guillemets n’ont pas de signification pour Make lui-même (mais ils peuvent être nécessaires dans les commandes shell)
Gestion des cibles
La cible all
- Pour exécuter plusieurs cibles d’un coup, on donne ce rôle à la première cible (celle par défaut)
all: one two three
one:
touch one
two:
touch two
three:
touch three
clean:
rm -f one two three
Cibles multiples et variables automatiques
- Il est possible d’exécuter des commandes distinctes pour plusieurs cibles.
$@contient le nom de la cible courante
all: f1.o f2.o
f1.o f2.o:
echo $@
Variables automatiques et wildcards
Wildcard *
*parcourt directement les noms présents dans le système de fichiers- Il est recommandé de l’utiliser encapsulé dans la fonction
wildcard
print: $(wildcard *.c)
ls -la $?
- N’utilisez pas
*directement dans une définition de variable
thing_wrong := *.o
thing_right := $(wildcard *.o)
Wildcard %
- Il est surtout utilisé dans les pattern rules, où il permet d’extraire et d’étendre un motif donné
Fancy Rules
Règles implicites (Implicit)
- Make intègre plusieurs règles par défaut cachées liées au build C/C++
- Variables représentatives :
CC,CXX,CFLAGS,CPPFLAGS,LDFLAGS, etc. - Exemple C :
CC = gcc
CFLAGS = -g
blah: blah.o
blah.c:
echo "int main() { return 0; }" > blah.c
clean:
rm -f blah*
Static Pattern Rules
- Elles permettent d’écrire de façon concise plusieurs règles suivant le même motif
objects = foo.o bar.o all.o
all: $(objects)
$(CC) $^ -o all
$(objects): %.o: %.c
$(CC) -c $^ -o $@
all.c:
echo "int main() { return 0; }" > all.c
%.c:
touch $@
clean:
rm -f *.c *.o all
Static Pattern Rules + fonction filter
- En utilisant
filter, on peut sélectionner uniquement les éléments correspondant à un motif d’extension donné
obj_files = foo.result bar.o lose.o
src_files = foo.raw bar.c lose.c
all: $(obj_files)
.PHONY: all
$(filter %.o,$(obj_files)): %.o: %.c
echo "target: $@ prereq: $
1 commentaires
Commentaires Hacker News
Quelqu’un raconte avoir vu en 1985, au laboratoire Graphics de Boston University, une personne créer un moteur de rendu 3D pour l’animation avec un Makefile. Cette personne était programmeuse Lisp, travaillait sur de la génération procédurale précoce et un système d’acteurs 3D, et avait écrit un Makefile d’une élégance remarquable d’environ dix lignes. La structure permettait de générer automatiquement des centaines d’animations à partir de simples dépendances sur les dates des fichiers. Les formes 3D de chaque image étaient produites en Lisp, puis Make générait les images. À l’époque, en 1985, contrairement à aujourd’hui où la 3D et l’animation semblent aller de soi, tout le monde trouvait cela stupéfiant, et l’auteur se souvient qu’il s’agissait de Brian Gardner, qui a ensuite travaillé sur le moteur de rendu 3D de Iron Giant et Coraline
Quelqu’un se demande s’il s’agit de la personne présentée ici : 3d-consultant.com/bio.html
Quelqu’un demande confirmation qu’il s’agit bien du film Coraline
Présentation de quelques indicateurs utiles et peu connus de Make
--output-sync=recurse -j10: cela permet de regrouper stdout/stderr jusqu’à la fin du travail de chaque cible avant affichage ; sinon, les journaux se mélangent et deviennent difficiles à analyser--load-averageplutôt que-jpour réguler la charge système lors du parallélisme (make -j10 --load-average=10)--shuffle, qui mélange aléatoirement l’ordonnancement des cibles de build, est utile pour détecter les problèmes de dépendances dans un Makefile en environnement CIQuelqu’un évoque l’idée de regrouper officiellement les différentes options de make et de les inclure dans un programme sous forme de texte ou de documentation pour en améliorer l’accessibilité
Une personne explique que l’option qu’elle utilise souvent est le drapeau
-B, pour forcer une reconstruction complèteQuelqu’un dit avoir souvent vu
make -jprovoquer des problèmes sur des machines DOS, au point de considérer cela comme un bugQuelqu’un demande si, sur un système chargé ou dans un environnement multi-utilisateur, les problèmes de parallélisation ne devraient pas être gérés par l’ordonnanceur de l’OS
Ces indicateurs sont utiles, mais ils ne sont pas portables ; il est donc recommandé de ne pas les utiliser en dehors de projets privés destinés à soi-même
L’idée selon laquelle un tutoriel pourrait faire l’impasse sur
.PHONYest jugée peu convaincante sous prétexte de ne pas l’utiliser. Il vaudrait mieux apprendre à utiliser correctement l’outil.PHONYpour toutes les recettes.PHONYrecette par recette ou tout regrouper une fois en haut du fichier, avec le souhait qu’un linter puisse l’imposer-o pipefailpose problème ; cela peut casser des usages commegrepdans un pipeline, donc mieux vaut l’appliquer selon le contexte.PHONYest rigoureux, mais presque toujours inutile et alourdit le Makefile ; il vaudrait mieux ne le faire qu’en cas de besoinSelon certains, Make est un outil spécialisé dans la compilation de grandes bases de code C
Un autre estime que Make n’est pas tant un task runner qu’un outil shell générique permettant de transformer des scripts shell linéaires en dépendances déclaratives
Un autre considère que voir Make uniquement comme un outil de build pour bases de code C n’est plus juste. Il rappelle que, ces vingt dernières années, des systèmes de build plus robustes et plus explicites ont été développés, et qu’il serait temps d’actualiser cette vision
Question sur ce qui ferait un bon task runner. (Avec ensuite des excuses pour avoir confondu ce que signifiait task runner)
just est recommandé comme outil moderne pour remplacer les parties des Makefiles qui deviennent complexes
justest bien pour remplacer une liste de scripts shell, mais ne remplace pas la fonction essentielle de Make : n’exécuter que les règles qui doivent l’être à nouveauAutres alternatives mentionnées
Ces outils alternatifs se présentent comme des remplaçants de Make, mais quelqu’un estime qu’ils sont en réalité très différents et difficiles à comparer. Le cœur de Make, c’est la génération d’artefacts et le fait de ne pas reconstruire ce qui l’a déjà été.
just, lui, sert simplement d’exécuteur de commandesL’avantage de Make lorsqu’on l’utilise comme exécuteur de commandes, c’est la stabilité d’un outil standard installé presque partout. Même si les alternatives sont mieux conçues, elles demandent une installation supplémentaire, ce qui réduit leur intérêt
Quelqu’un explique qu’il utilise bien Task pour de petits projets personnels en C, mais ne sait pas encore s’il convient aussi à de grands projets (site officiel de Task)
Le fait que CMake ait récemment choisi ninja par défaut en considérant que les Makefiles ne convenaient pas au support des modules C++20 est jugé intéressant (guide CMake)
clang-scan-deps(slides techniques)Quelqu’un pense que cette limitation relève surtout d’une décision côté CMake ou du manque de contributeurs pour le générateur Makefile. ninja non plus ne prend pas directement en charge les modules C++ (issue liée), et il est même plus limité que Make car il impose de déclarer statiquement toutes les dépendances
Quelqu’un trouve que l’introduction des modules est en elle-même complexe et déroutante
Quelqu’un demande s’il y a des retours d’expérience avec tup. (documentation officielle)
Une personne se présente comme le créateur et mainteneur principal de l’outil alternatif à Make nommé Task. Le projet est développé depuis plus de huit ans et continue d’évoluer
justest lui aussi recommandé comme autre alternative à Make (GitHub de just)Coïncidence amusante : quelqu’un dit utiliser souvent Task et avoir même ouvert une issue ce matin
Ce tutoriel comporterait plusieurs problèmes subtils et potentiellement dangereux
ifneq (,$(findstring t,$(firstword -$(MAKEFLAGS))))loadest plus portable que guile, et qu’en environnement de compilation croisée il faut spécifier correctement le compilateurQuelqu’un explique avoir l’habitude d’inclure un Makefile dans chaque dépôt GitHub
makepour lancer immédiatement le comportement attendu du projet, sans avoir à tout mémoriser