Apprendre les Makefiles avec les meilleurs exemples
(makefiletutorial.com)- Un Makefile sert surtout à automatiser la compilation C/C++ dans les grands projets en ne relançant que les étapes de build nécessaires selon les fichiers modifiés
- Make compare les horodatages du système de fichiers entre les cibles et leurs dépendances, et n’exécute une commande que si la cible n’existe pas ou si une dépendance est plus récente
- Une règle de base se compose de
targets: prerequisiteset de commandes indentées avec une tabulation ; une indentation avec des espaces peut faire échouermake - Le flux de build peut être étendu avec des variables, des wildcards, des variables automatiques, des règles de motif, des conditions, des fonctions, des appels récursifs à make,
include,.PHONYet.DELETE_ON_ERROR - Les exemples Cookbook construisent les sorties dans
build/à partir des sources C/C++ desrc/et mettent en place une gestion automatique des dépendances avec-MMD -MPet-include
Quand un Makefile est nécessaire
- Un Makefile sert à déterminer quels fichiers doivent être recompilés dans un grand programme
- Le cas central du tutoriel est la compilation de fichiers C/C++
- Make peut aussi être utilisé hors compilation pour les tâches qui doivent exécuter une série de commandes selon les modifications de fichiers
- Parmi les autres outils de build pour C/C++, le texte présente SCons, CMake, Bazel, Ninja
- Pour Java, on trouve Ant, Maven et Gradle ; des langages comme Go, Rust et TypeScript ont leurs propres outils de build
- Les langages interprétés comme Python, Ruby ou JavaScript pur n’ont pas besoin d’être recompilés quand un fichier change, donc un outil comme Makefile n’y est pas indispensable
Modèle d’exécution de base de Make
- Les exemples consistent à créer un fichier
Makefiledans un terminal oùmakeest installé, puis à exécutermakedans ce répertoire - Une règle de base suit la forme ci-dessous
targets: prerequisitescommandcommençant par une tabulation
- Une cible (target) est généralement un nom de fichier, et la commande correspond aux étapes nécessaires pour créer cette cible
- Les prérequis (prerequisites) sont les fichiers qui doivent exister avant l’exécution de la commande ; on les appelle aussi dependencies
- Les lignes de commande d’un Makefile doivent obligatoirement être indentées avec une tabulation ; utiliser des espaces fait échouer
make
Comparaison temporelle entre cibles et dépendances
make helloexécute la commande associée à la ciblehello, mais si le fichierhelloexiste déjà, la commande n’est pas exécutée- Les cibles sont directement liées aux noms de fichiers et, en général, les commandes d’une cible créent un fichier portant le même nom
- Dans la règle
blah: blah.c, la commande de compilation n’est exécutée que siblahn’existe pas ou siblah.cest plus récent queblah - Make ne suit pas directement l’historique des changements et utilise les horodatages du système de fichiers comme heuristique
- Si l’on modifie un fichier puis que l’on remet son horodatage dans le passé, Make peut conclure à tort que le fichier n’a pas changé
Cible par défaut et clean
- Si aucune cible n’est indiquée à
make, c’est la première cible du Makefile qui est exécutée par défaut - La cible
allsert à construire plusieurs cibles d’un coup ; si elle est placée en première règle, un simplemakelance le build complet cleanest couramment utilisée pour supprimer les artefacts de build, mais ce n’est pas un mot réservé spécial de Make- Si
cleann’est ni la première cible ni une dépendance d’une autre cible, elle doit être appelée explicitement, par exemple avecmake clean - Si un fichier nommé
cleanexiste réellement, la ciblecleanpeut ne pas s’exécuter ; ce problème est résolu plus loin avec.PHONY
Variables et traitement des chaînes
- Les variables Make ne peuvent contenir que des chaînes, et le tutoriel recommande en général d’utiliser
:= - Les références de variables utilisent la forme
$(name)ou${name} - Les apostrophes et les guillemets n’ont pas de signification particulière pour Make lui-même ; ce sont simplement des caractères de la variable
- Il existe principalement deux façons de définir une variable
=: variable récursive, développée au moment de l’utilisation:=: variable à expansion simple, qui ne développe au moment de la définition que les valeurs déjà définies
?=n’assigne une valeur que si la variable n’est pas encore définie+=ajoute une valeur à une variable- Une variable non définie est traitée comme une chaîne vide
- Les espaces en fin de ligne ne sont pas supprimés ; pour créer une variable contenant un seul espace, on peut utiliser une technique comme
$(nullstring)
Wildcards et variables automatiques
- Dans Make,
*et%sont tous deux appelés wildcards, mais ils n’ont pas la même signification *recherche les noms de fichiers correspondants dans le système de fichiers ; il est plus sûr de l’utiliser via la fonction wildcard, par exemple$(wildcard *.o), plutôt que directement dans une définition de variable- Si
*ne correspond à aucun fichier, il peut rester tel quel lorsqu’il n’est pas utilisé dans la fonctionwildcard %est utilisé dans les règles et certaines fonctions, et la partie correspondante est appelée stem- Les variables automatiques les plus courantes sont les suivantes
$@: nom de la cible$?: dépendances plus récentes que la cible$^: toutes les dépendances$<: première dépendance
Règles implicites et règles de motif
- Make dispose de règles implicites pour la compilation C, ce qui permet parfois de construire sans écrire explicitement la commande de compilation
- Quelques règles implicites représentatives ont la forme suivante
n.oest généré depuisn.cavec une commande du type$(CC) -c $(CPPFLAGS) $(CFLAGS) $^ -o $@n.oest généré depuisn.ccoun.cppavec une commande du type$(CXX) -c $(CPPFLAGS) $(CXXFLAGS) $^ -o $@nest généré à partir den.ovia une commande d’édition de liens
- Les variables importantes dans les règles implicites sont
CC,CXX,CFLAGS,CXXFLAGS,CPPFLAGSetLDFLAGS - Les règles de motif statiques suivent la forme
targets...: target-pattern: prereq-patterns ...et injectent dans le motif de dépendance le stem correspondant au%trouvé dans la cible - Les règles de motif incluent
%dans la cible, comme%.o: %.c, et peuvent être utilisées comme des règles implicites personnalisées - Les règles à double deux-points
::permettent de définir plusieurs règles pour une même cible, mais elles sont rarement utilisées
Exécution des commandes et gestion des erreurs
- Préfixer une commande avec
@empêche l’affichage de cette commande elle-même - Exécuter
make -srevient à se comporter comme si chaque ligne était préfixée par@ - Chaque ligne de commande est exécutée comme dans un nouveau shell, donc un
cd ..sur une ligne n’affecte pas la suivante - Pour conserver le même état de shell, il faut écrire sur une seule ligne, par exemple
cd ..; echo \pwd``, ou prolonger avec des antislashs - Le shell par défaut est
/bin/sh, mais on peut le changer avecSHELL=/bin/bash - Pour utiliser le
$d’une variable de shell dans un Makefile, il faut écrire$$ - La gestion des erreurs varie selon le mode d’exécution
make -k: continue les autres tâches possibles même en cas d’erreur-devant une commande : ignore les erreurs de cette commandemake -i: applique l’ignorance des erreurs à toutes les commandes
- Si l’on interrompt Make avec
ctrl+c, Make supprime la cible la plus récente qu’il vient de créer
Make récursif et variables d’environnement
- Lorsqu’on relance make depuis un Makefile, il faut utiliser la variable spéciale
$(MAKE)plutôt quemake $(MAKE)transmet les options de make et gère correctement leurs effets- Au démarrage de Make, les variables d’environnement définies au moment de l’exécution sont automatiquement converties en variables Make
- La directive
exportplace une variable dans l’environnement des commandes shell de toutes les recipes - Pour utiliser une variable dans un sous-make, il faut l’
export .EXPORT_ALL_VARIABLESpermet d’exporter toutes les variables- Si l’on indique plusieurs cibles d’un coup, comme
make clean run test, elles sont exécutées dans l’ordreclean,run,test
Conditions et fonctions
- Les conditions se composent de
ifeq,ifneq,ifdef,ifndef,elseetendif ifdefn’étend pas la référence de variable et vérifie uniquement si la variable est définie- On peut utiliser
$(MAKEFLAGS)etfindstringpour vérifier si une option commemake -ia été transmise - Les fonctions Make servent principalement au traitement de texte et s’appellent sous la forme
$(fn, arguments)ou${fn, arguments} - Les fonctions principales sont les suivantes
subst: remplacement de chaînepatsubst: remplacement de mots selon un motifforeach: transformation d’une liste de mots séparés par des espacesif: utilise le deuxième argument si le premier n’est pas vide, sinon le troisièmecall: appelle une variable comme une fonction avec des paramètres tels que$(1),$(2)shell: invoque le shell, mais remplace les sauts de ligne par des espacesfilter: sélectionne dans une liste les éléments correspondant à un motif donnéfilter-out: sélectionne les éléments ne correspondant pas à un motif donné
include, vpath, .PHONY, .DELETE_ON_ERROR
include filenames...permet de lire un ou plusieurs autres Makefilesincludeest utile pour lire des Makefiles générés par des options comme-Mdegccvpath <pattern> <directories>permet d’indiquer dans quels répertoires chercher certains fichiers de dépendance- La variable
VPATHpermet de définir globalement un comportement similaire de chemin de recherche - L’antislash
\sert à découper une commande longue sur plusieurs lignes - Ajouter une cible à
.PHONYévite que Make la confonde avec un fichier du même nom .DELETE_ON_ERRORsupprime la cible d’une règle si une commande de cette règle renvoie un code de sortie non nul- Contrairement à
PHONY,.DELETE_ON_ERRORne s’applique pas seulement à la cible juste avant, mais à toutes les cibles
Exemple du Makefile Cookbook
- L’exemple Cookbook fournit un modèle de Makefile adapté à un projet de taille intermédiaire
- Si l’on place des fichiers C/C++ dans le dossier
src/, le Makefile détermine automatiquement les cibles de compilation et les dépendances - La configuration principale des variables est la suivante
TARGET_EXEC := final_programBUILD_DIR := ./buildSRC_DIRS := ./src
SRCSutilisefindet$(shell ...)pour rechercher les fichiers*.cpp,*.cet*.sOBJSajouteBUILD_DIRdevant chaque fichier source puis le suffixe.opour construire les chemins des artefacts de buildDEPSremplace.opar.dafin de produire la liste des Makefiles de dépendancesINC_DIRSrecherche tous les répertoires soussrc/, etINC_FLAGSajoute le préfixe-Ià chacunCPPFLAGS := $(INC_FLAGS) -MMD -MPconfigure GCC pour générer les fichiers de dépendances.d- Les sources C et C++ utilisent chacune une règle de motif distincte pour produire des fichiers
.osousbuild/, et les répertoires nécessaires sont créés avecmkdir -p $(dir $@) cleanest déclarée en.PHONYet supprime le répertoire de build avecrm -r $(BUILD_DIR)-include $(DEPS)inclut les fichiers.dtout en supprimant les erreurs lorsqu’ils n’existent pas encore, par exemple lors du premier build
1 commentaires
Avis sur Hacker News
Il existe quelques options
makepeu connues mais utiles. La synchronisation de la sortie fait en sorte que stdout/stderr ne soient affichés qu’une fois la cible terminée, ce qui réduit le problème des logs entremêlés et difficiles à lire lors des builds parallèlesmake --output-sync=recurse -j10Sur un système multi-utilisateur chargé, le seul nombre de tâches
-jpeut ne pas suffire ; on peut donc aussi limiter le parallélisme en fonction de la charge moyenne :make -j10 --load-average=10--shuffle, qui randomise l’ordre de planification des cibles, est également utile en CI pour repérer les dépendances manquantes dans un Makefile :make --shuffle # or --shuffle=seed/reverse-B, une option qui force à tout reconstruire sans conditionmake -jmettre plusieurs fois des machines en état de DoS, donc je le considère presque comme un bugVers 1985, au Graphics Lab de Boston University, j’ai vu quelqu’un piloter avec un Makefile la génération d’un moteur de rendu 3D pour l’animation. C’était quelqu’un venant du monde Lisp, qui travaillait sur de la génération procédurale précoce et un système d’acteurs 3D ; le Makefile faisait une dizaine de lignes au total et était très élégant
Avec de simples dépendances sur les dates de fichiers, il générait des centaines d’animations ; Lisp créait la forme 3D de chaque image, puis Make générait les images
C’était en 1985, une époque où presque tout ce que l’on tient aujourd’hui pour acquis en 3D et en animation n’existait pas, donc cela avait impressionné tout le monde. Par la suite, il a utilisé le moteur de rendu 3D d’Iron Giant, et je crois qu’il a aussi joué un rôle clé sur Coraline. C’était Brian Gardner
Make est l’un de ces outils que je suis vraiment heureux d’avoir appris tôt dans ma carrière. Je ne l’utilise plus beaucoup aujourd’hui, mais il m’a montré la puissance des systèmes déclaratifs par rapport aux systèmes impératifs
J’ai compris que cette idée s’étendait naturellement à d’autres tâches. En regardant l’illustration en haut de ce site, l’auteur semble avoir eu un ressenti similaire : on comprend mieux une recette de cuisine en la voyant de manière déclarative, comme un Makefile, plutôt que comme une suite d’instructions scriptées à la manière d’une recette traditionnelle
Article lié : https://blog.gpkb.org/posts/cooking-with-make/
J’écris toujours les recettes de façon à pouvoir les lire comme des Makefile avant de les emporter en cuisine, et je me demande si quelqu’un les a déjà composées ou affichées de cette manière. Cela ferait gagner beaucoup de temps de ne pas avoir à convertir mentalement le script en Makefile à chaque nouvelle recette
Bien sûr, le prix à payer est qu’il faut résoudre soi-même le graphe de dépendances. Quand on veut simplement suivre une séquence d’étapes déjà sérialisée à l’avance, cela demande plus de travail
L’article dit que la plupart des recettes ne sont pas marquées comme
.PHONY, et s’en sert apparemment comme raison pour ne pas vraiment le traiter dans le tutoriel, mais c’est une excuse faible. Un outil devrait enseigner les bonnes pratiquesDans mon équipe, on utilise make comme exécuteur de tâches, et je me suis fait reprendre pour avoir ajouté et maintenu
.PHONYsur toutes les recettesLe guide de style Makefile de Clark Grubb est bon : https://clarkgrubb.com/makefile-style-guide
Je me demande si des gens utilisent ce guide de style. Je me demande aussi si, pour les recettes phony, vous les marquez à l’endroit de leur déclaration ou si vous gardez une énorme liste en haut du fichier. Ce serait bien d’avoir un linter pour imposer cela
Utiliser
-o pipefailpar inertie n’est pas une bonne idée. Pipefail est utile, mais il casse l’usage le plus courant que les gens font des pipelines : filtrer la sortie avecgrep. Mieux vaut l’ajouter individuellement seulement dans les recettes qui en ont besoinMarquer les cibles qui ne sont pas des fichiers avec
.PHONYest strictement correct, mais généralement pas nécessaire. Quand il y a beaucoup de cibles, cela ajoute une verbosité inutile au Makefile ; à mon avis, il vaut mieux ne l’ajouter que quand c’est nécessairePour les recettes qui produisent plusieurs fichiers de sortie, auparavant, quand les règles de motifs ne convenaient pas, les fichiers factices/fichiers indicateurs étaient la norme ; mais depuis GNU Make 4.3, les cibles groupées sont prises en charge nativement. C’est aussi inclus dans Ubuntu 22.04 LTS : https://www.gnu.org/software/make/manual/html_node/Multiple-...
Un point intéressant récemment : CMake a jugé que les Makefiles ne convenaient pas aux projets utilisant les modules C++20, et que Ninja était le bon choix. https://cmake.org/cmake/help/latest/manual/cmake-cxxmodules....
En gros, il semble considérer qu’il est trop difficile, voire impossible, de définir statiquement les dépendances des cibles. Désormais, cela se traite dynamiquement avec des outils comme
clang-scan-deps: https://llvm.org/devmtg/2019-04/slides/TechTalk-Lorenz-clang...En réalité, comme Ninja exige que toutes les dépendances soient définies statiquement, il a moins de fonctionnalités que Make classique sur ce point
Make a sa place comme outil de build pour de grands codebases en C. Mais on le traite souvent comme un « exécuteur de tâches » généraliste propre à chaque projet, et il n’est pas vraiment adapté à cela. Même de simples conditions y sont difficiles
Par exemple, j’ai vu plusieurs tentatives bien intentionnées d’envelopper Terraform avec Make, et ça ne s’est jamais bien terminé
just est un excellent outil moderne pour remplacer les parties où les Makefiles deviennent brouillons : https://github.com/casey/just
Dans un registre complètement différent, il y a aussi Makedown, évoqué sur HN il y a 8 mois : https://news.ycombinator.com/item?id=41825344
Les Makefiles sont formidables, mais il vaut mieux éviter de trop s’y plonger. Il y a quelques années, j’ai essayé de créer un framework GNU Make pur, avant de réaliser que j’étais en fait en train de réinventer autoconf ; c’est seulement à ce moment-là que j’ai compris pourquoi GNU autotools avait été créé
Les Makefiles sont une sorte de Turing tarpit étrangement lispien. GNU Make va même jusqu’à proposer des capacités de métaprogrammation, et il est difficile de résister à l’envie de métaprogrammer un système impie à l’intérieur d’un Makefile. Le fait que GNU Make soit aussi largement installé rend la tentation encore plus forte
Je suis l’une des personnes qui ont créé et maintiennent Task, une alternative à Make. Il existe depuis plus de 8 ans et continue d’évoluer, donc si vous cherchez quelque chose de nouveau, n’hésitez pas à l’essayer. Les questions sont aussi les bienvenues
https://taskfile.dev/
https://github.com/go-task/task
Je me demande si quelqu’un a déjà essayé tup : https://gittup.org/tup/ex_dependencies.html
C’est un système de build qui détermine automatiquement les dépendances à partir des accès au système de fichiers, ce qui permet de l’utiliser avec n’importe quel compilateur ou outil