J’aime les Makefiles
(switowski.com)- Même si de nouveaux outils de build continuent d’apparaître, les Makefiles restent utiles comme outil de base léger pour regrouper les tâches répétitives d’un projet
- Grâce à des commandes conventionnelles comme
make,make build,make install,make dev, on peut rapidement essayer de builder, installer ou lancer le serveur de développement d’un projet inconnu - Que la stack repose sur Jekyll, Hugo, 11ty, un framework web Python, Docker ou gulp, le Makefile joue le rôle d’interface d’exécution commune
- Sur des projets personnels, quelques cibles simples comme
dev,build,deploysuffisent souvent, plutôt que des conditions ou des flags - GNU Make est très répandu et, même s’il n’est pas disponible, on peut exécuter directement les commandes shell du Makefile, ce qui en fait une solution peu contraignante même dans des environnements limités
Regrouper des commandes différentes selon les projets derrière un point d’entrée unique
- Un Makefile sert à envelopper des projets dont les méthodes de build, d’exécution et de déploiement diffèrent derrière un jeu de commandes familier
- Lorsqu’un projet inconnu contient un
Makefile, on peut en général commencer par essayer les commandes suivantesmakemake buildmake install
- Quand on rouvre un ancien projet personnel, on peut aussi le configurer pour que le simple lancement de
make devexécute d’un coup les étapes de build nécessaires et le serveur de développement - Même si la stack technique varie — Jekyll, Hugo, 11ty, ou plusieurs frameworks web Python — on peut conserver le même point d’entrée
- Avec Docker ou gulp également, le Makefile ne remplace pas ces outils mais fonctionne comme une couche d’orchestration
- Exemple :
make buildconstruit les images Docker nécessaires en passant des arguments supplémentaires propres au projet
- Exemple :
Une utilisation simple dans les projets personnels
- L’usage du Makefile se concentre davantage sur des ensembles de commandes shell que sur des fonctionnalités complexes
- Les fonctionnalités comme les conditions ou les flags ne sont pas utilisées
- Techniquement, l’unité de travail de make est la cible (target), mais en pratique elle est traitée comme une tâche
- On pourrait aussi le faire avec des fonctions dans un script bash, mais le Makefile est jugé plus simple et plus rapide
- Dans un projet personnel, les tâches les plus fréquentes sont généralement au nombre de 2 ou 3
dev: démarrer le serveur de développementbuild: builder le projet si nécessairedeploy: déployer ou publier le projetwatch: tâche supplémentaire servant à relancer automatiquement le build quand les fichiers source changent
- Le Makefile du blog actuel ne contient qu’une seule cible,
dev- Sous
dev:, il exécutenpm run dev
- Sous
- Dans des projets personnels plus complexes, les tâches suivantes sont regroupées dans le Makefile
dev: lancer le serveur de développement avecbundle exec jekyll servebuild: builder les assets avecnpm run gulp buildwatch: surveiller un dossier spécifique avecnpm run gulp watchdeploy: après avoir buildé Jekyll en production, exécutermake encrypt, puisnetlify deploy --prodencrypt: chiffrer les fichiers HTML du dossier_siteavecnpx staticrypt
Limites et avantages à connaître
- Dans ces exemples, les phony targets ont été omises, mais si des fichiers portent des noms comme
dev,build,watch,deploy,encrypt, le Makefile peut ne pas se comporter comme prévu- Les lecteurs de Hacker News recommandent d’ajouter des phony targets dans ce cas
- GNU Make est le logiciel qui exécute les makefiles ; il est probablement déjà installé sous Linux et il est si courant sur MacBook qu’on ne se souvient même plus d’avoir dû l’installer séparément
- Make ne demande pas beaucoup de dépendances supplémentaires, ce qui peut le rendre utile dans des environnements limités où l’installation de paquets est difficile ou impossible pour des raisons de sécurité
- Même sans Make, on peut copier et exécuter directement dans le shell les commandes présentes dans le Makefile
- À l’inverse, si gulp n’est pas installé sur le serveur, il est difficile de copier-coller du code JavaScript tel quel dans le terminal pour l’exécuter
- Même si l’on découvre un nouvel outil de build meilleur et plus rapide, Make peut continuer à servir de couche de commandes commune pour gérer des outils et configurations hétérogènes
2 commentaires
Si votre
makefilene définit pas de dépendances, le remplacer parjustfileoffre une meilleure ergonomie.Avis de Hacker News
Il n’y a pas lieu de se décourager en lisant, dans ce fil, qu’on « utilise mal make ». L’une des raisons pour lesquelles Make est un bon outil, c’est qu’on peut commencer plus simplement qu’il n’y paraît
Ne pas utiliser
.PHONYpeut poser problème, mais sur un petit projet, c’est peut-être un piège dans lequel vous ne tomberez que dans un an, et même alors, il y a de bonnes chances qu’une heure à vous gratter la tête suffise à le régler. Dans 99 % des cas, vous pouvez vous lancer sans vous soucier de la « bonne manière » de faire, et Make ne demande que le niveau de complexité nécessaire pour ne pas s’écroulerMake prend des fichiers d’entrée et des dépendances pour produire exactement un fichier de sortie. Les règles qui ne produisent pas de sortie, comme
install,allouclean, ainsi que toutes les cibles de l’article, doivent dépendre d’une cible magique spéciale comme.PHONY, mais.PHONYn’existait pas dans POSIX 2017 (IEEE Std 1003.1-2017, https://pubs.opengroup.org/onlinepubs/9699919799/utilities/m...) et n’a été intégré que dans la norme actuelle (IEEE Std 1003.1-2024, https://pubs.opengroup.org/onlinepubs/9799919799/utilities/m...). Si vous voulez créer plusieurs fichiers, comme des fichiers objet, des modules ou des en-têtes précompilés, vous devez bricoler vous-même des contournements fragiles. Tous les Make ne sont pas GNU Make, et des systèmes de type Unix comme BSD ou Solaris/Illumos existent encoreCela ne veut pas dire que Make est inutile. Il l’est pour des projets suffisamment complexes, mais pas encore au point de nécessiter un « meilleur » système de build. Le problème, c’est que ces projets gagnent en code et deviennent plus complexes, jusqu’à finir avec des scripts/programmes qui génèrent le Makefile, ou une partie de celui-ci, créant ainsi un méta-système de build improvisé
Le plus gros problème n’est pas de l’utiliser, mais de le proposer comme solution pour « tout le monde ». Et dès qu’un répertoire ou un fichier comme
buildoudevapparaît à la racine du projet, ce Makefile cesse de fonctionnerAujourd’hui je passe plutôt à pnpm, mais je ne trouve toujours pas cela si complexe. Il faut certes connaître un peu la syntaxe, mais c’est moins difficile que de faire fonctionner correctement d’autres outils dès le départ
.PHONYdont les gens se préoccupent tantC’est simple, lisible, modifiable, composable, et déjà installé partout. Il fait ce qui est écrit en surface, et guère plus
Au passage, quand je dois gérer des systèmes de build à la mode ou cauchemardesques dans d’autres projets, je les enveloppe aussi avec make
Tout le monde doit commencer quelque part, et une fois que ça tourne, les améliorations progressives s’ajoutent facilement
$@. Le Makefile dans son ensemble peut produire un nombre arbitraire de sorties, puisque les règles peuvent dépendre d’autres règlesIl en découle une règle empirique propre pour les cibles phony. Si une recette ne touche pas uniquement
$@, alors$@doit être marqué comme phony. Gérer les cibles phony sous forme de liste rend les choses beaucoup plus simplesphonies :=phonies += somethingsomething:./do-somethingphonies += something-elsesomething-else: something./do-something-else# touches $@ and thus does not need to be phonycreate-file:./generate-some-output > $@.PHONY: $(phonies)Makefile est une technologie médiocre. Simplement, elle est un peu moins mauvaise que la plupart des autres systèmes de build que nous avons créés, ce qui la rend utile dans un sens masochiste.
Les systèmes de build sont généralement soit trop rudimentaires et deviennent chaotiques dès qu’on construit autre chose qu’un jouet, soit trop complexes avec trop de prérequis, de bureaucratie, de synchronisation et de boilerplate, soit dépourvus de bibliothèque standard, si bien que les mêmes patterns de build se dispersent en 10 000 implémentations incompatibles, soit trop contraignants et obligent à migrer quand les besoins augmentent, soit remplis de magie, soit dotés d’une syntaxe obscure et incohérente.
Plus de la moitié des logiciels que j’utilise reposent seulement sur un Makefile ordinaire et parfois un script configure. Sans autotools, on lance éventuellement
./configure, puismake,make install, et ça marche. Ce n’est absolument pas une configuration jouet : c’est composé de petits programmes qui font bien une seule chose.Make et son mode de fonctionnement sont faciles à comprendre, du moins pour moi. Je pense que les mauvaises expériences des gens viennent souvent davantage de mauvaises configurations de build dans des projets complexes, ou d’un manque de connaissances nécessaires, que de Make lui-même.
L’absence de bibliothèque standard vise justement l’indépendance vis-à-vis des outils, ce qui permet de l’appliquer largement à divers outils, langages et usages. Je vois ça comme une fonctionnalité, pas comme un défaut. La couche simple, c’est le suivi des dépendances ; les cibles et dépendances sont également explicites, donc difficile d’y voir quelque chose de magique. La syntaxe, elle, pourrait clairement être meilleure.
Les seuls systèmes de build généralistes couvrant plusieurs langages sont
makeou ceux qui ciblentmake, comme CMake. La raison pour laquelle ce n’est pas terrible, c’est quemaken’est pas terrible.makeest vraiment difficile à bien utiliser, il y a des problèmes comme le make récursif et non récursif, beaucoup de variantes incompatibles comme Unix/POSIX make, BSD make, GNU make, Windowsnmake, et c’est assez laid.Malgré tout, un
makebien utilisé est plutôt bon. C’est une vraie chance d’avoirmakecomme plus petit dénominateur commun.C’est assez sidérant que Make ne gère même pas proprement les sous-répertoires. Je me demande s’il existe mieux que Make. Il y a bien Ninja, mais il n’a pas été conçu pour être écrit directement par des humains.
À quel point, selon toi ? 20 %, 40 %, peut-être 70 %, non ?
J’aime vraiment Make. Ces 20 dernières années, j’en ai écrit au moins un pour chaque tâche ou projet auquel j’ai touché.
Rien d’intelligent : juste un assemblage de bouts avec quelques variables. Des choses comme
make run,make test,make lint.make recentlance le lint puis exécute le script modifié le plus récemment. On pourrait le faire en Bash ou avec un autre shell, mais on se ferait alors aspirer dans le monde des développeurs, avec plus de complexité et aucune valeur ajoutée. Make n’est qu’un langage dédié à un domaine qui dit : « ce fichier se fabrique à partir de ces fichiers, et à ce moment-là on exécute une ou deux commandes ». C’est ce qui le rend extrêmement puissant.Il y a des décennies, j’ai créé un framework de test en Java, où l’on pouvait spécifier les tests et les classes dépendantes avec une syntaxe proche de make. Il y avait un ensemble de classes de test définissant un « lot de tests de référence », puis d’autres couches de classes de test qui en dépendaient et ne s’exécutaient que si l’étape précédente réussissait.
Je ne comprends pas pourquoi aujourd’hui on rend tout si compliqué. J’ai toujours conseillé de s’en tenir aux outils Unix standards et à leur approche, c’est-à-dire à des méthodes éprouvées par le temps, sauf dans les situations où c’est absolument impossible. Le temps est limité, donc je veux le consacrer à ce qui est au cœur de la résolution de problèmes — conception de systèmes/programmes, modélisation, structure, patterns, etc. — le reste est secondaire.
Savoir que
make testfonctionne, quels que soient la stack, le langage ou le dépôt, est un vrai soulagement.Le Makefile de mon dernier projet avait quatre commandes principales :
build,test,frontend,deploy. build et test appelaient maven, frontend appelait npm, et deploy appelait docker et aws.Tous ces outils ont leur propre suivi d’état, leur cache et leur traitement incrémental, mais ils ne signalent pas ce qu’ils ont fait. On ne peut donc pas écrire de règles atomiques du type « déployer uniquement si le build a été mis à jour », parce que maven/cargo/dotnet/npm/go n’exposent pas cette information.
L’auteur n’utilise même pas le suivi des dépendances basé sur mtime. Les cibles devraient aussi être
.PHONY, mais elles ne sont pas marquées comme telles. Cela aurait pu être remplacé par un script shell qui lit$1, le fait correspondre et décide quoi fairehttps://just.systems/
.PHONY: $(MAKECMDGOALS)./build.sh devest certes moins explicite quemake dev. Même s’il n’y a aucune étape non phony, une autre raison d’utiliser Make est qu’on peut en ajouter plus tard si nécessaire. Bien sûr, je suis d’accord pour dire que l’auteur devrait marquer{dev,build,deploy}comme phonySon Makefile pourrait être amélioré techniquement, mais le plus important est que l’expérience de “prise en main” soit cohérente quand on arrive dans un projet
Les gens veulent qu’ils fassent beaucoup d’autres choses — langage de script, suivi des dépendances, etc. — et finissent par introduire des outils boursouflés. Nouveau ne veut pas forcément dire meilleur. Autoconf et automake ne sont pas make
Techniquement, toutes ces cibles make cherchent un fichier portant le même nom que la cible. Chaque cible devrait en réalité être définie comme
.PHONYAvant, j’utilisais tout le temps ce genre de Makefile, mais ces dernières années je suis passé à just et aux justfile. Ce comportement y est celui par défaut et, globalement, c’est plus facile à utiliser. Les paramètres et autres choses du genre sont aussi plus simples
https://github.com/casey/just
:, pour qu’on puisse les distinguer rien qu’à leur nomBien sûr, il est désormais trop tard
Il serait peut-être temps d’avoir une sorte de nouvelle distribution de coreutils regroupant des outils nouveaux et utiles comme just, ripgrep, et qui s’installe partout où l’on veut les utiliser
C’est amusant de voir à quel point make déclenche des débats aussi virulents. Cela ressemble aux anciennes querelles presque religieuses entre vi et emacs
Je suis entièrement d’accord avec l’article original, et je trouve particulièrement malin d’envelopper même des outils de build plus sophistiqués dans un Makefile de premier niveau. L’avantage, c’est la standardisation. Pas besoin de se souvenir de quoi que ce soit : si c’est un projet que j’ai écrit, je sais qu’il suffit de taper
makepour que ça marchePar exemple, si quelqu’un qui fait du C veut compiler un projet Rust, il n’a pas besoin de chercher comment utiliser cargo : il lui suffit de taper
makeougmake. J’essaie de respecter POSIX sans utiliser de fonctionnalités propres à GNU, même s’il est vrai que quasiment 100 % des make réellement utilisés sont des gmakeProposer d’utiliser l’intemporel
makecomme une sorte de pilote de système de build de premier niveau fonctionnera probablement encore dans 250 anscargo buildMake lui-même est excellent, mais dans la plupart des projets C sur lesquels j’ai travaillé, c’était une combinaison de cmake/autotools et d’installation globale de paquets, et il fallait souvent aller vérifier
Je suis d’accord avec l’état d’esprit ici, mais je suis en train de réécrire beaucoup de choses en Justfile
https://github.com/casey/just
Cela permet d’éviter beaucoup des bizarreries propres aux Makefile
Oui, Make est excellent. Je l’utilise pour énormément de choses. C’est une bonne façon d’automatiser des tâches
Par exemple, pour mon site personnel, un Makefile appelle des scripts bash pour régénérer les pages web mises à jour, fait un git push vers le serveur, puis un hook git sur le serveur appelle Make pour déployer. En revanche, je ne veux pas mettre dans le dépôt Git de gros blobs binaires qui changent souvent, comme les PDF de supports de cours. Pas de souci : le Makefile a une cible
uploadsqui envoie uniquement les PDF modifiés vers le serveur, et cette cible est une dépendance de la cibledeployqui fait le git push, donc je n’ai pas à m’en préoccuperDe même, les PDF mis à jour pour les supports de cours sont automatiquement placés dans l’arborescence source du site par un autre Makefile. Avec ce Makefile, je peux générer depuis les sources LaTeX le PDF que j’utilise, ou une version alternative pour les étudiants à partir des mêmes sources, par exemple sans les corrigés des exercices, puis la publier automatiquement dans la version locale du site et la mettre en ligne avec le site mis à jour quand je le souhaite
C’est une sorte de chaîne infinie de Makefile. J’aime les Makefile
Dans ce cas, ce dont l’auteur a vraiment besoin, ce n’est pas make, mais just
https://just.systems/man/en/
En tant qu’exécuteur de tâches, make n’est pas mauvais, mais aujourd’hui, comme le disent d’autres commentaires, il existe de meilleures alternatives comme just
En tant que système de build, make convient jusqu’à ce qu’on se heurte à ses limites. make/Makefile n’est pas standardisé, d’où l’existence d’automake. On n’écrit donc pas un Makefile directement : on utilise un template qui génère le makefile réel. Si vous possédez toute la chaîne d’outils, ce n’est pas un problème, mais comme ce n’est généralement pas le cas, on procède ainsi pour garantir un Makefile portable
make ne fait aucune forme de résolution de dépendances et suppose que ce dont il a besoin se trouve déjà là. C’est pour cela que les scripts configure sont apparus ; et comme les scripts configure ne sont pas non plus un standard, on génère le script configure avec autoconf/autoreconf avant d’exécuter les cibles make
make et les outils voisins comme automake/autoconf/autoreconf utilisent le mtime pour déterminer si les entrées sont obsolètes. Même après avoir exécuté autoconf/autoreconf/automake/configure, on peut se retrouver dans une situation où les entrées restent définitivement considérées comme obsolètes et où rien ne peut être construit. À noter que beaucoup de systèmes de build peuvent très bien utiliser le mtime, à condition que le suivi des dépendances soit correct
Dans l’ensemble, le défaut de conception fondamental de make est qu’il suit la philosophie Unix, à savoir « faire une seule chose et la faire bien ». Cette chose, c’est « reconstruire la cible si les entrées sont obsolètes », mais c’est un outil très limité ; les systèmes de build modernes doivent donc empiler beaucoup de choses par-dessus pour le rendre utile au-delà d’un simple exécuteur de tâches
makene sait pas résoudre les dépendances ?dependency:...target: dependency...make configure. C’est ce qui configure ce dontmake [build]a besoinC’est pour ça qu’on finit par utiliser des touchfiles ; c’est un peu sale, mais ça fonctionne toujours mieux que beaucoup d’autres choses. Je pense notamment au caching de docker build