Make
Qu’est-ce que Make ?
Make fait partie des logiciels d’automatisation des tâches : il sert à créer des processus dans lesquels des actions s’enchaînent automatiquement.
À l’origine, Make est un outil conçu pour le développement de logiciels : il sert à automatiser la fabrication de programmes exécutables à partir de fichiers contenant du code.
Mais en réalité, Make peut être utilisé pour gérer toutes sortes de projets. En tant que langage, il permet de définir des processus de manière très simple (encore plus simple que les scripts shell). Et en tant qu’outil, il permet de gagner du temps, avec une gestion efficace des états des fichiers qui fait économiser du travail aussi bien à l’humain qu’à la machine.
Origine : gérer la fabrication de programmes informatiques
Make a été inventé pour automatiser la fabrication de programmes exécutables. Un programme exécutable est un fichier qui se fabrique en deux temps : d’abord, on écrit du code dans des fichiers texte ; puis on exécute un programme appelé compilateur qui transforme le code en un fichier exécutable – cette deuxième étape, c’est la compilation.
Make a été conçu en particulier pour déterminer quels fichiers doivent être recompilés lors de la fabrication d’un programme. Lorsqu’un fichier est modifié, il déclenche automatiquement les commandes appropriées. L’objectif était de ne pas tout recompiler si on ne modifiait qu’une petite partie du code d’un programme complexe, afin d’économiser des ressources et gagner du temps.
Pour faire cela, Make s’appuie sur deux choses : la date de dernière modification des fichiers ; et un arbre de dépendances, c’est-à-dire une représentation des relations entre des fichiers cible, qu’on veut fabriquer, et des fichiers source (les « dépendances »), qui sont nécessaires à la fabrication des fichiers cible. Évidemment, un fichier cible peut à son tour être fichier source pour fabriquer un autre fichier cible, et ainsi de suite.
Voici un exemple d’arbre de dépendances :
Si l’une des dépendances change, Make recompile le ou les fichiers cible correspondants. Dans l’exemple ci-dessus, si one.h
est modifié, Make recompile one.cpp
puis main.cpp
, mais pas two.cpp
.
Fonctionnement : les Makefiles
Le fonctionnement de Make repose sur des fichiers texte appelés Makefiles. Dans un Makefile, on rédige des instructions appelées règles (rules) ; chaque règle définit une recette (recipe) pour fabriquer une ou plusieurs cibles (targets) suivant des prérequis (prerequisites).
Les règles s’écrivent de la manière suivante :
cibles : prérequis
recette
Ce sont les relations entre cibles et prérequis qui constituent l’arbre de dépendances modélisé par Make.
Une recette est constituée d’une ou plusieurs commandes : des instructions textuelles qu’on exécuterait habituellement dans un terminal.
cibles : prérequis
commande
commande
commande …
Comme dans de nombreux langages informatiques, la syntaxe des Makefiles inclut la possibilité de créer des variables, de laisser des commentaires, de définir des fonctions qui incluent des étapes logiques, etc.
Pour exécuter les instructions contenues dans un Makefile, il faut ouvrir un terminal, se déplacer à l’emplacement du Makefile, puis exécuter la commande make
(ce qui déclenche les instructions qui sont contenues dans le Makefile).
Si vous souhaitez découvrir comment utiliser un terminal, consultez ma page Ligne de commande.
Utilisation en dehors du développement logiciel
Progressivement, d’autres usages de Make ont émergé. En effet, son fonctionnement a une portée plus générale que la seule fabrication de programmes, car il ne pose aucune restriction sur la nature de la cible, des prérequis ou de la recette. On peut utiliser Make pour automatiser le déclenchement de n’importe quel programme qui s’utilise habituellement à la ligne de commande.
Donc si Make est souvent présenté comme un outil d’automatisation pour la compilation de programmes, il serait en fait plus juste de le décrire comme un outil d’automatisation pour la ligne de commande. C’est un outil qui permet de définir des enchaînements de commandes textuelles ; dit de façon plus abstraite, Make permet de créer des processus, des séquences d’actions, où chaque action est une instruction envoyée à un ou plusieurs programmes à interface textuelle. Bref : Make permet de fabriquer des fabriques. (Clin d’œil à Antoine Fauchié qui a beaucoup travaillé sur ce concept de fabrique, voir son blog et sa thèse.)
J’ai partagé un modèle de Makefile qui automatise l’utilisation de Pandoc pour générer différents exports à partir d’une même source : Pandoc-SSP. Make a complètement remplacé mon usage des scripts shell pour automatiser le déclenchement de commandes Pandoc. J’utiliserai Pandoc comme exemple à plusieurs reprises sur cette page.
Make me sert donc à rédiger des recettes dans lesquelles je passe d’ingrédients de base (des fichiers) à un résultat final (d’autres fichiers). Mais comme me l’a fait remarquer David Larlet, on peut aussi l’utiliser pour des processus qui n’ont pas forcément de fichiers en entrée ou en sortie, comme la mise en place d’un serveur ou bien l’installation d’un logiciel. Make est un outil versatile.
Outils requis pour utiliser Make
Pour utiliser Make, vous avez besoin des choses suivantes :
- Un terminal
- Si vous n’êtes pas familier de ce type d’environnement, consultez ma page Ligne de commande.
- Le programme Make
-
Make existe dans plusieurs versions. Cette page est écrite en référence à GNU Make, la version la plus répandue. Sur Linux et macOS, Make est pré-installé ; pour vérifier de quelle version vous disposez, exécutez
make --version
dans un terminal. Sur Windows, vous pouvez installer Make par exemple en installant le gestionnaire de programmes Chocolatey, puis en utilisant Chocolatey pour installer Make en ouvrant un terminal (je recommande PowerShell) et en exécutant la commandechoco install make
. - Un éditeur de texte
- Pour rédiger des Makefiles. Je recommande souvent Notepad++ (Windows), BBEdit (macOS) et gedit (Linux), mais n’importe quel éditeur de texte fera l’affaire.
Prise en main : écrire un Makefile et exécuter Make
Make s’utilise en écrivant des Makefiles et en utilisant la commande make
dans un terminal. Par défaut, make
cherche un fichier appelé makefile
ou Makefile
situé dans le répertoire courant. On peut aussi nommer un Makefile par n’importe quel nom
et utiliser make
avec l’option --file=nom
ou -f nom
.
Règles : cibles, prérequis, recettes
Un Makefile contient une ou plusieurs règles.
La syntaxe générale d’une règle est la suivante :
cibles : prérequis
recette
Notez l’espacement avant le mot « recette » : dans une règle, la recette doit être indentée avec une tabulation (une seule tabulation, pas d’espaces).
(Si comme moi vous utilisez régulièrement YAML, un langage qui requiert d’indenter les lignes avec des espaces, préparez-vous à vous mélanger les pinceaux pendant quelques temps !)
Les cibles et les prérequis sont des noms de fichiers, séparés par des espaces. En pratique, il n’y a souvent qu’une cible par règle.
La recette est constituée d’une ou plusieurs commandes, qui sont une série d’étapes utilisées pour créer la cible. Make applique la recette :
- si les cibles n’existent pas ;
- si les cibles existent mais que les prérequis ont une date de dernière modification plus récente que les cibles.
Dans le Makefile suivant, la cible est un fichier PDF, doc.pdf
. Elle a un prérequis qui est un fichier texte rédigé en Markdown, doc.md
. La recette est constituée par une commande qui exécute le programme Pandoc.
doc.pdf : doc.md
pandoc doc.md -o doc.pdf
Si j’exécute make
:
- Si
doc.pdf
n’existe pas, ou qu’il existe mais quedoc.md
est plus récent (donc qu’il a été modifié depuis la dernière fabrication dedoc.pdf
), Make lance la commande Pandoc qui convertit le fichier Markdown en PDF. - Si
doc.pdf
existe et quedoc.md
n’est pas plus récent, rien ne se passe.
Cibles factices
Les cibles peuvent être des cibles « factices » (en anglais phony). Une cible factice ne correspond pas à un fichier réel : c’est une sorte de nom de code pour une série d’opérations qu’on veut exécuter. Les cibles factices doivent être déclarées comme prérequis de la cible spéciale .PHONY
.
Je reprends l’exemple précédent, avec cette fois deux fichiers Markdown, doc-fr.md
et doc-en.md
. Je définis une cible all
qui a deux prérequis, les cibles doc-fr.pdf
et doc-en.pdf
.
.PHONY: all
doc-fr.pdf : doc-fr.md
pandoc doc-fr.md -o doc-fr.pdf
doc-en.pdf : doc-en.md
pandoc doc-en.md -o doc-en.pdf
all : doc-fr.pdf doc-en.pdf
Si j’exécute make all
, Make déclenchera la fabrication des deux cibles correspondantes, à moins que les cibles existent déjà et qu’elles soient plus récentes que les prérequis.
Ordre d’exécution des règles
Si un Makefile contient plusieurs règles, par défaut make
fabrique la première cible qu’il trouve dans le fichier. Il est possible de définir manuellement la cible à exécuter par défaut grâce à la variable spéciale .DEFAULT_GOAL
(voir la section Variables spéciales plus bas).
Options d’exécution
La commande make
dispose de nombreuses options qui modifient son comportement. Utilisez make --option
pour appliquer l’option correspondante lors de l’utilisation de Make. Plusieurs options peuvent être utilisées simultanément. La plupart des options sont disponibles sous plusieurs noms (notamment pour des questions de compatibilité), dont des formes raccourcies avec un seul tiret et une seule lettre, pour aller plus vite lorsqu’on saisit les commandes manuellement.
Quelques exemples :
-n
(ou--just-print
,--dry-run
,--recon
) : affiche les commandes qui seraient exécutées, sans les exécuter. Ceci permet de vérifier ce que ferait un Makefile sans l’appliquer réellement.-j [jobs]
(ou--jobs[=jobs]
) : autorise Make à exécuter simultanément un certain nombre de commandes (jobs
). Si votre machine dispose d’un processeurs avec plusieurs cœurs, ceci permet de réduire considérablement le temps d’exécution du Makefile. Par exemple, avec un processeur à huit cœurs, vous pouvez utiliser l’option-j 8
pour que Make lance jusqu’à huit commandes en parallèle (une par cœur).
Dans un terminal, utilisez man make
pour afficher le manuel et découvrir les options. Appuyez sur la touche q pour quitter le manuel.
Supprimer l’écho
Par défaut, Make affiche dans le terminal chaque ligne d’une recette exécutée. On appelle cela « l’écho », par analogie avec la commande echo
qu’on utilise dans un terminal pour afficher des messages.
Il y a plusieurs manières de supprimer l’écho :
- On peut préfixer une ligne par une arobase
@
pour supprimer l’écho pour cette ligne.
.PHONY: all
doc-fr.pdf : doc-fr.md
@pandoc doc-fr.md -o doc-fr.pdf
doc-en.pdf : doc-en.md
@pandoc doc-en.md -o doc-en.pdf
all : doc-fr.pdf doc-en.pdf
Remarque : il existe d’autres préfixes qu’on peut ajouter à une ligne pour modifier le comportement de Make. Par exemple, préfixer une ligne par -
indique à Make qu’il ne doit pas s’interrompre en cas d’erreur, ce qui est pratique lorsqu’on dépanne un processus.
- On peut utiliser la cible spéciale
.SILENT
en indiquant des prérequis, ce qui supprimera l’écho pour la fabrication des prérequis correspondants.
.PHONY: all
.SILENT: doc-fr.pdf doc-en.pdf
doc-fr.pdf : doc-fr.md
pandoc doc-fr.md -o doc-fr.pdf
doc-en.pdf : doc-en.md
pandoc doc-en.md -o doc-en.pdf
all : doc-fr.pdf doc-en.pdf
# "make all" sera silencieux aussi !
Remarque : .PHONY
et .SILENT
sont deux exemples de cibles spéciales. Voir Special Targets dans le manuel GNU Make pour la liste complète.
- On peut utiliser l’option
-s
(ou--silent
,--quiet
) au moment d’exécuter la commandemake
. Toutes les recettes s’exécuteront alors silencieusement.
Variables
Make permet de créer des variables, c’est-à-dire d’affecter une valeur à un nom pour pouvoir ensuite utiliser le nom à la place de la valeur. L’intérêt premier des variables, c’est d’éviter d’écrire plusieurs fois la même chose, pour économiser du travail et réduire le risque de faire des erreurs.
Pour définir une variable :
Pour utiliser la valeur de cette variable :
Dans le Makefile ci-dessous, une longue liste de fichiers apparaît plusieurs fois :
edit : main.o kbd.o command.o display.o insert.o search.o files.o utils.o
cc -o edit main.o kbd.o command.o display.o insert.o search.o files.o utils.o
clean :
rm edit main.o kbd.o command.o display.o insert.o search.o files.o utils.o
On peut définir une variable dont la valeur est cette liste de fichiers, puis utiliser le nom de la variable partout où cette liste doit être utilisée :
objects = main.o kbd.o command.o display.o insert.o search.o files.o utils.o
edit : $(objects)
$(objects)
cc -o edit
clean :
$(objects) rm edit
Une variable peut être redéfinie, c’est-à-dire qu’on peut la définir à nouveau dans le même Makefile. La nouvelle valeur remplace alors la précédente.
message = Bonjour !
message = Bonsoir !
La valeur d’une variable peut elle-même contenir une ou plusieurs variables.
txt = doc1.txt doc2.txt
img = soleil.svg lune.svg
fichiers = $(txt) $(img)
# la valeur de "fichiers" est "doc1.txt doc2.txt soleil.svg lune.svg"
La plupart des langages informatiques reconnaissent plusieurs types de données, soit de manière implicite, soit de manière explicite grâce à l’ajout de caractères spéciaux. Exemple en JavaScript :
var = true // booléen (boolean)
var = 28 // nombre entier (integer)
var = "Bonjour !" // chaîne de caractères (string)
Make ne reconnaît qu’un type de données : la chaîne de caractères (en anglais : string). Par conséquent, il interprète les guillemets littéralement. Il est donc conseillé de n’utiliser les guillemets que lorsque la chaîne va être interprétée dans un autre environnement, par exemple lorsqu’on passe une variable au shell pour exécuter une commande.
Ainsi, plutôt que :
message = "Bonjour !"
test :
$(message) echo
On écrira :
message = Bonjour !
test :
"$(message)" echo
Expansion et opérateurs de définition
On parle d’expansion pour désigner le fait de remplacer une expression de la forme $(nom)
par la valeur de la variable correspondante.
Ce concept devient important lorsqu’on définit des variables qui contiennent d’autres variables, et plus encore lorsque des variables sont redéfinies.
En effet, Make permet de gérer de manière très fine l’ordre dans lequel se passe l’expansion. Ceci repose sur l’utilisation de différents opérateurs de définition (voir la section How Make reads a Makefile du manuel de GNU Make pour la liste des opérateurs et leurs effets). Or les différences de comportement entre ces opérateurs sont souvent source de confusion.
Les deux opérateurs qu’on rencontre le plus fréquemment sont =
et :=
.
- Quand on utilise
=
pour définir une variable, l’expansion des variables contenues dans la valeur se fait au moment où la variable est utilisée. Ceci permet de changer la valeur en cours de route, si on redéfinit les variables utilisées dans la valeur.
mot = bonjour
hello = $(mot) à vous !
# la valeur de "hello" est "bonjour à vous !"
# on redéfinit "mot" en changeant sa valeur
mot = bonsoir
# la valeur de "hello" est maintenant "bonsoir à vous !"
- Quand on utilise
:=
pour définir une variable, l’expansion des variables contenues dans la valeur se fait au moment où la variable est définie. Ceci permet de fixer la valeur une fois pour toutes. Exemple :
mot = bonjour
hello := $(mot) à vous !
# la valeur de "hello" est "bonjour à vous !"
# on redéfinit "mot"
mot = bonsoir
# la valeur de "hello" reste "bonjour à vous !"
# on redéfinit cette fois "hello", on répète la première définition
hello := $(mot) à vous !
# la valeur de "hello" est maintenant "bonsoir à vous !"
En résumé :
- Si on définit des variables qui contiennent des variables, et qu’on modifie ces définitions en cours de route, alors on peut alterner entre
=
et:=
suivant les besoins. - En revanche, lorsqu’une variable contient une valeur simple (on parle aussi de valeur littérale), c’est-à-dire qu’elle ne contient pas elle-même de variable, alors utiliser
=
ou:=
ne fait aucune différence, puisqu’il n’y a rien dans la valeur de la variable définie qui nécessite une expansion. - Et lorsque le Makefile ne contient que des variables définies une seule fois, alors utiliser
=
ou:=
ne fait aucune différence non plus, puisqu’il n’y a aucune redéfinition qui vient affecter l’expansion.
Variables spéciales
Make inclut quelques variables spéciales, dont le nom est réservé pour un comportement prédéfini.
Par exemple, lorsqu’on exécute make
sans argument, c’est la première règle contenue dans le Makefile qui est appliquée. On peut utiliser la variable spéciale .DEFAULT_GOAL
pour définir la règle à appliquer par défaut.
Avec le Makefile ci-dessous, exécuter make
fabriquera doc.html
au lieu de all
:
.PHONY: all
.DEFAULT_GOAL := doc.html
all : doc.pdf doc.html
doc.pdf : doc.md
pandoc doc.md -o doc.pdf
doc.html : doc.md
pandoc doc.md -o doc.html
Voir Special Variables dans le manuel GNU Make pour la liste complète.
Fonctions
Make inclut la possibilité d’utiliser des fonctions pour manipuler du texte. Il y a des fonctions prédéfinies qui correspondent aux opérations les plus courantes : trier, chercher, filtrer, tronquer, etc. Il est également possible de définir ses propres fonctions.
Une fonction s’utilise en faisant appel à son nom et en lui passant du texte en argument :
- La fonction
$(wildcard pattern)
cherche les noms de fichiers qui correspondent au motifpattern
. - La fonction de référence avec substitution
$(var:a=b)
remplace la chaîne de caractèresa
parb
dans la variablevar
.
Dans l’exemple ci-dessous, on combine ces deux fonctions pour créer une variable input
qui contient les noms de fichiers Markdown présents dans le répertoire où se situe le Makefile, puis une variable output
qui contient les mêmes noms en remplaçant l’extension .md
par .html
.
input = $(wildcard *.md)
output = $(input:.md=.html)
# si input a pour valeur par exemple "doc.md 2024-03-25.md"
# output aura pour valeur "doc.html 2024-03-25.html"
Une fonction peut avoir pour argument une valeur littérale, une variable ou une fonction. Plusieurs fonctions peuvent ainsi être imbriquées les unes dans les autres, comme des poupées russes :
$(fonction $(fonction $(fonction … )))
L’ordre d’exécution est simple à déterminer : c’est la fonction qui ne contient pas elle-même une autre fonction (la plus petite poupée russe) qui s’exécute en premier ; puis elle passe son résultat à la fonction qui la contient, et ainsi de suite jusqu’à la dernière fonction (la plus grande poupée russe), celle qui n’est pas elle-même contenue dans une autre fonction.
La fonction shell
est particulièrement utile. Comme son nom l’indique, elle permet de passer des commandes au shell, comme si on les exécutait dans un terminal.
Voir Fonctions dans le manuel GNU Make pour la liste complète des fonctions.
Règles implicites
Contrairement aux règles de base (aussi appelées règles explicites), les règles implicites ne définissent pas comment fabriquer une cible précise à partir de prérequis définis, mais comment fabriquer des types de cibles à partir de types de prérequis.
Une règle implicite ne définit pas de cibles. Lorsqu’on utilise une règle implicite, il faut donc par ailleurs définir explicitement les cibles à fabriquer.
Modèles (pattern rules)
Dans une règle implicite, au lieu d’indiquer explicitement le nom de cibles et de prérequis, on indique le modèle (en anglais pattern) que doivent suivre ces noms. Un modèle de nom contient le symbole pourcentage %
, qui constitue la part variable du modèle, avec éventuellement un préfixe et/ou un suffixe, qui constituent la part fixe du modèle.
Exemples :
%.txt
désigne tous les fichiers dont le nom finit par.txt
_%
désigne tous les fichiers dont le nom commence par_
_%.txt
désigne tous les fichiers dont le nom commence par_
et finit par.txt
Dans le Makefile ci-dessous, on génère automatiquement les noms des cibles en utilisant la fonction wildcard
et la fonction de référence avec substitution, et on crée une règle implicite qui définit comment fabriquer des fichiers HTML à partir de fichiers Markdown, quel que soit leur nom.
.PHONY: html
# liste des noms de fichiers Markdown présents
input = $(wildcard *.md)
# liste des noms de fichiers HTML à fabriquer
output = $(input:.md=.html)
# cible à fabriquer
html : $(output)
# règle implicite pour la fabrication
%.html : %.md
recette …
Variables automatiques
Comme pour les variables spéciales, les variables automatiques sont des variables dont le nom est réservé. En revanche, leur valeur n’est pas définie manuellement mais calculée automatiquement (d’où leur nom) en fonction du contexte.
$@ |
nom de la cible |
$< |
nom du premier prérequis |
$^ |
noms de tous les prérequis (séparés par des espaces) |
Les variables automatiques ne peuvent être utilisées que dans les recettes.
Les variables automatiques sont pensées pour être utilisées dans des règles implicites. Combiner les deux permet de réaliser facilement des traitements de fichiers par lots. À mes yeux, c’est l’une des fonctionnalités les plus utiles de Make.
On reprend l’exemple précédent de la règle qui définit comment fabriquer des fichiers HTML à partir de fichiers Markdown et on ajoute une recette qui consiste à exécuter Pandoc en lui donnant le nom des prérequis $^
comme entrée et le nom des cibles $@
comme sortie.
.PHONY: html
input = $(wildcard *.md)
output = $(input:.md=.html)
html : $(output)
%.html : %.md
$^ --output=$@ pandoc
Et voici une légère variation de la recette : on ajoute un prérequis, du coup on remplace $^
par $<
pour ne plus passer en entrée tous les prérequis mais uniquement le nom du fichier Markdown.
%.html : %.md styles.css
$< --css=styles.css --output=$@ pandoc
Voir Automatic variables dans le manuel GNU Make pour la liste complète des variables automatiques.