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 :

Le programme « main.cpp » dépend de quatre fichiers, dont deux dépendent eux-mêmes d’autres fichiers. Diagramme inspiré de makefiletutorial.com

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 commande choco 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 que doc.md est plus récent (donc qu’il a été modifié depuis la dernière fabrication de doc.pdf), Make lance la commande Pandoc qui convertit le fichier Markdown en PDF.
  • Si doc.pdf existe et que doc.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 commande make. 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)
   cc -o edit $(objects)

clean :
   rm edit $(objects)

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 :
   echo $(message)

On écrira :

message = Bonjour !
test :
   echo "$(message)"

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 motif pattern.
  • La fonction de référence avec substitution $(var:a=b) remplace la chaîne de caractères a par b dans la variable var.

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.

Exemples de variables automatiques
$@ 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
   pandoc $^ --output=$@

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
   pandoc $< --css=styles.css --output=$@

Voir Automatic variables dans le manuel GNU Make pour la liste complète des variables automatiques.