GNU Make : guide de survie pour vos Makefiles

Guillaume
2026-10-07

Aujourd’hui on retrouve Guillaume, notre ELEPHANTECH en IoT et systèmes embarqués, qui nous donne un petit guide de survie pour utiliser GNU Make. Que vous soyez néophytes ou utilisateurs réguliers, découvrez un article qui vous permettra d’approfondir vos connaissances des makefiles. 

Après avoir rappelé les bases, l’article approfondira certains concepts et listera une série de pièges et astuces pour aider au développement et à la maintenance des makefiles.

‍

Introduction à GNU Make

Make est un vieil outil à la longévité remarquable. Il a été écrit en 1978 et a vite été adopté et a même essaimé. Il existe aujourd'hui une collection de variantes pas toujours inter-compatibles, dont la plus utilisée, celle du projet GNU datant des années 80. Or, bien que très puissant, il souffre d'une syntaxe rebutante qui grève la maintenabilité des Makefiles. Il comporte également des subtilités qu'il vaut mieux connaître afin d'éviter de longues séances de debug.

Certains projets tels que CMake tentent d'y remédier en implémentant une surcouche à Make. Cependant, il est toujours utile de bien comprendre comment fonctionne la couche inférieure avant de commencer à utiliser de tels outils, ne serait-ce que pour mieux comprendre les messages d'erreurs générés. De plus, il reste toujours une très grande quantité de projet et d'outils qui fonctionnent (parfois uniquement) avec Make.

Ce guide, divisé en plusieurs parties, n'est donc pas un tutoriel pour débutant. En revanche, il s'adresse aux néophytes comme aux utilisateurs réguliers n'ayant pas approfondi leurs connaissances des Makefiles. Après avoir rappelé succinctement les bases, nous allons approfondir certains concepts. Sans oublier de lister une série de pièges et autres trucs et astuces en vue d'aider au développement et à la maintenance des Makefiles.

Rappel des bases

Le métier de Make :

Make est essentiellement un gestionnaire de dépendance et de construction.

Premièrement, il établit la liste optimisée des fichiers à construire en se basant sur les dates de modification. Il commence par établir un arbre des dépendances de chaque fichier cible demandé par l'utilisateur. À partir de ce graphe, il sélectionne :

  • tous les fichiers qui n'existent pas encore ;
  • tous les fichiers qui ont une dépendance à sa date de modification plus récente que le fichier lui-même ;
  • tous les fichiers qui dépendent directement ou indirectement d'un fichier déjà listé ci-dessus.

Il est nécessaire d'avoir cet arbre bien en tête pour comprendre le Makefile avant de le faire évoluer. Il est donc recommandé d'en poser le graphe sur papier.

Dans un second temps, il gère la construction de ces fichiers, possiblement de manière parallélisée en s'assurant que chacun soit bien reconstruit dans l'ordre imposé par le graphe de dépendance.

Les phases de travail :

Make travaille en deux phases distinctes, ce qui a un impact sur la manière dont peuvent être évaluées les variables :

  1. Phase I : Il lit les Makefiles, les variables et les règles pour construire l'arbre des dépendances (parsing) ;
  2. Phase II : Il détermine les cibles à reconstruire et les construit.

La syntaxe d'une cible

Un makefile est constitué de règles. La forme la plus simple de règle est la suivante :

1cible [cible ...]: [dépendance ...]
2	commande 1
3	[...]
4	commande n

Les cibles sont les fichiers construits par la règle. Les dépendances sont nécessaires à la construction des cibles. Ces fichiers peuvent être eux-mêmes générés par des règles. Les commandes forment la recette à suivre pour construire les cibles.

À noter que Make est agnostique au contenus des recettes de constructions et donc au type fichier construit. Ce qui en fait un outil très puissant, car capable de générer n'importe quel type de fichier : des logiciels, de la documentation, du changement de format de fichier, etc.

Type de dépendances

Make gère deux types de dépendances. Tout d'abord, les dépendances normales :

  1. le fichier cible dépend du contenu du fichier en dépendance ;
  2. le fichier cible doit être construit après la dépendance.

Sans oublier les dépendance de temporalité (order-only), qui signifie seulement qu'une cible doit être construite après une autre (mais que son contenu n'en dépend pas). C'est donc une version allégée du type ci-dessus puisqu'elle en retire la première composante.

Elle est marquée par un | dans la règle et s'applique à toutes les dépendances qui suivent :

cible: composant1 | composant2

Ici, composant1 est défini en tant que dépendance normale alors que composant2 est une dépendance order-only.

Règles implicites

Make dispose d'un catalogue de règles prédéfinies (parfois appelées built-in rules) notamment pour compiler des fichiers sources en C ou C++. Ceci vous évite d'avoir à les écrire explicitement dans votre Makefile. Cependant, il est plus compliqué pour les mainteneurs de comprendre l'arbre de dépendance en lisant le makefile.

Personnellement, je préfère les définir moi-même explicitement. Cela me permet d'améliorer la maintenabilité et d'être plus serein sur la stabilité de ces règles et la reproductibilité des fichiers cibles.

Ces règles sont de type pattern rules. Elles peuvent être redéfinies dans le Makefile. Dans ce cas, si la recette est vide, cela équivaut à les désactiver.

Pattern rules

Les règles avec un % dans le nom de la cible sont de type pattern rule, le nom de la cible étant le motif (pattern). Elles s'appliquent à toutes les cibles dont le nom correspond au motif. Ceci est très utile pour ne pas avoir à définir une règle par cible (une règle par fichier source) ou une macro pour ce faire.

1%.c: %.o
2	$(CC) ...

Dans le motif, il est possible d'ajouter des caractères autres que l'extension de fichier, un path, etc. Ces règles permettent notamment aux développeurs de Make de définir les règles implicites.

Précisions sur les variables et fonctions

Dans Make, la nature des entités nommées peut paraître déroutante. De plus, la terminologie varie entre les variantes de Make ainsi que dans les articles qui en traitent. Essayons de clarifier tout cela ci-dessous.

1BIN_NAME=program.elf

Ceci est une variable. Cependant, c'est parfois nommée une macro. Cela provient probablement du fait que Make soit un langage interprété ou les variables sont expansées à l'image des macros en C. Cette terminologie semble d'ailleurs plutôt utilisée dans les vieilles variantes de Make. Là où cela se complique, c'est lors de l'utilisation des macros :

1define MACRO_NAME
2...
3endef

Ceci est tantôt appelé une fonction ou une macro. Nous utiliserons macro afin de les différencier de ce qui suit :

$(warning ceci est un warning)

Ici, la fonction built-in warning est appelée pour afficher une chaine d'informations à destination de l'utilisateur. Elles sont parfois appelées directives... C'est clair comme de l'eau de roche, non ?

Dans cet article, nous nous en tiendrons respectivement à la terminologie suivante : variables, macros, fonctions. Voyons-les maintenant en détails.

Variable

Une variable ne peut contenir qu'une chaine de caractères. Aucune opération arithmétique n'est possible. Pour ce faire, il faut passer par une commande shell.

Make reconnait les espaces et les retours à la ligne comme des séparateurs de valeurs. Une variable peut donc contenir plusieurs valeurs (par exemple une liste de fichier à compiler).

C_SRCS=foo.c bar.c

Dans Make, les variables sont expansées récursivement. La récursivité est le fait que Make va expanser la variable autant de fois que nécessaire tant que sa valeur contient une référence à une autre variable. Cela s'arrête lorsque la chaine de caractère est simple, sans référence.

Il y a deux types de variables, mais leurs noms sont trompeurs, car elles sont toutes les deux expansées récursivement :

  • Variable à expansion récursive : C'est le type standard. L'expansion à lieu lors de l'utilisation de la variable, c'est à dire au dernier moment. Sa valeur peut donc changer au long du parsing. L'opérateur d'assignation est =.
1un=aaa
2deux=$(un)
3un=bbb
4$(info $(deux))

affiche : bbb

  • Variable à expansion simple : L'expansion est bien récursive, mais elle a lieu au moment de la définition de la variable. Ensuite, sa valeur est fixe, c'est une constante. L'opérateurd'assignation est :=.
1un=aaa
2deux:=$(un) # Notez l'opérateur
3un=bbb
4$(info $(deux))

affiche : aaa. Elle prend bien la valeur actuelle lors de la définition.

Cette différence pouvant s'avérer critique, notamment si l'on utilise le traitement parallèle et/ou les fichiers temporaires, la lecture de la documentation officielle à ce sujet est vivement recommandée !

Variables raccourcis

Make aide à l'écriture des recettes Makefile en définissant automatiquement des variables utilisables dans les commandes. Celles-ci sont pratiques, mais participent grandement à l'illisibilité des scripts pour les novices. En voici quelques-uns :

$@

C'est la cible de la règle. Cette variable est particulièrement utilisée car elle raccourcit grandement les commandes dans la recette.

$^

La liste de toutes les dépendances de la règle.

$<

La première dépendance listée dans la règle.

Fonction

Make fournit pléthore de fonctions permettant de transformer des chaînes de caractères, notamment :

  • Les paths (retirer des portions de path, en ajouter...) ;
  • Les noms de fichier (récupérer le nom, l'extension, changer l'extension...).

Elles sont très utilisées pour traiter les noms, chemins et extensions de fichiers. D'autres fonctions sont plus complexes et permettent des actions spécifiques. Par exemple :

if

Faire des branchements conditionnels.

call

Expanser une macro.

eval

Interpréter une chaine comme du code Make.

shell

Exécuter des commandes shell en dehorsd'une recette.

info, warning, error

Trace de debug.

L'utilisation de ces fonctions permet beaucoup de souplesse.

Macro

Les macros sont techniquement des variables multi-lignes, mais passons. Elles sont extrêmement puissantes, car on peut les expanser (entendre appeler, telles des fonctions) grâce à la fonction call().

1define ASSIGN
2# A simple macro with two args
3$1=$2
4endef

define et endef délimitent une macro. Les arguments passés à call se retrouvent dans les variables $1 $2 etc. Voici donc un exemple pour générer deux règles debug.bin et release.bin générant un .bin depuis un .elf à partir d'une liste de variantes cibles :

1VARIANTES=debug release
2define GEN_VARIANT
3# Rule to build bin from elf; $1 is the variant name
4$1.bin: $1.elf
5	objdump -o binary $$@ $$<
6endef
7$(foreach variante,$(VARIANTES),$(eval$(call GEN_VARIANT,$(variante))))

Ici, la fonction foreach permet d'itérer sur chaque valeur contenue dans une variable. Ensuite la fonction eval permet de parser le retour de la fonction call comme code Make. La fonction call permet d'appeler la macro avec un ou plusieurs paramètres.

À noter l'utilisation du caractère d'échappement $ devant les variables $@ et $<. Sans lui, elles seraient expansées de suite et seraient donc vides car elles sont en dehors d'une règle. En effet, ce code ne devient une règle qu'après, lors du parsing par eval. Dans un souci d'optimisation de temps d'exécution, les variables globales peuvent également être échappées afin de n'être expansées que lors de l'utilisation de la règle, et non lors du parsing.

Il est possible de mettre n'importe quel code dans la macro. Par exemple, il est possible de déclarer des variables et plusieurs règles dans la même macro. Dans notre exemple, nous aurions également généré la règle pour la construction de l'elf.

Appel à une fonction

L'appel à une fonction se fait ainsi :

$(function_name param1,param2,paramN)

Attention cependant à ne pas mettre d'espace après la virgule entre les arguments ! Sinon, celui-ci se retrouve concaténé à l'argument, ce qui peut avoir son importance pour des chemins de fichier notamment.

Les appels aux fonctions peuvent être faits n'importe où, dans ou en-dehors d'une recette.

‍

Un grand merci à Guillaume pour son article très complet autour de l’utilisation de GNU Make. Dans la même catégorie, vous pouvez retrouvez l'article d'Olivier : Le C++ n'a pas dit son dernier mot

Sources :

Partager l’article
Systèmes embarqués
Guillaume
2026-10-07