ELEPHANT technologies est une ESN experte et reconnue sur ses 4 expertises : électronique, embarqué, software et web.

 

Aujourd’hui on retrouve Guillaume, notre elephantech en systèmes embarqués qui nous propose 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.
 

Petit guide de survie pour GNU Make

Partie I

2021-11-17

 

Introduction

 

Make est un vieil outil à la longévité remarquable car il a été écrit en 1978. Il a vite été adopté et a même essaimé car il y a 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 connaitre, afin d'éviter de longue séances de debug.

 

Certain projet tel que CMake tente 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 tel 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 n'est pas un tutoriel pour débutant. En revanche, il s'adresse aux néophytes comme aux utilisateurs réguliers n'ayant pas approfondis leurs connaisances des Makefiles. Après avoir rappelé succintement les bases, il approfondie certains concepts. Puis il liste une série de pièges et autre trucs et astuces en vue d'aider au développement et à la maintenance des Makefiles.

 

Ce guide est divisé en plusieurs parties

 

Rappel des bases

  • Le métier de make

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

1. 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 graph, il sélectionne :

         + tous les fichiers qui n'existent pas encore.

          + tous les fichiers dont une dépendance a 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 necéssaire 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 graph sur papier.

2. Il gère la construction de ces fichiers, possiblement de manière parallélisée :

Il s'assure que chacun des fichiers soit reconstruit dans l'ordre imposé par le graph de dépendance.

  • Les phases de travail 

Make travail 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 :

cible [cible ...]: [dépendance ...]
   commande 1
       [...]
   commande n

Les cibles sont les fichiers construits par la règle. Les dépendances sont necéssaires à 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.

  • Type de dépendances

Make gère deux types de dépendance:

  • Dépendance normale 
  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.
  • Dépendance de temporalité (order-only) :

Cette dépendance 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 à toute les dépendances qui suivent :

 

cible : composant1 | composant2

Ici, composant1 est défini en tant que dépendance normael 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.

 

Note

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.

 

Note

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.

%.c: %.o
   $(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 tous cela ci-dessous.

 

BIN_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 veilles variantes de Make.

Là où cela se complique, c'est lors de l'utilisation des macros:

 

define MACRO_NAME
    ...
endef

 

Ceci est tantôt appelée 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'information à 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ère. 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 necéssaire 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 est trompeur car elles sont toutes les deux expansées récursivement :

 

  • Variable à expansion récursive :

C'est le type standard. L'expansion à lieux 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 =.

un=aaa

deux=$(un)

un=bbb

$(info $(deux))

affiche : bbb

 

  • Variable à expansion simple :

L'expansion est bien récursive mais elle à lieu au moment de la définition de la variable.

Ensuite, sa valeur est fixe, c'est une constante.

L'opérateur d'assignation est :=.


  1. deux:=$(un) # Notez l'opérateur
    un=bbb

    $(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 temporaire, la lecture de la documentation officielle à ce sujet est vivement recommandée : https://www.gnu.org/software/make/manual/html_node/Flavors.html.

 

  • Variables raccourcies

Make aide à l'écriture des recettes Makefile en définissant automatiquement des variables utilisable dans les commandes. Celles-ci sont pratique mais participe grandement à l'illisibilité des scripts pour les novices. En voici quelque uns :

$@:

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

$^:

La liste de toute 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 chaines 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 dehors d'une recette.

info, warning, error :

Trace de debug.

Leurs utilisation 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, tel des fonctions) avec des arguments grâce à la fonction call.

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

define et endef délimitent une macro. Les arguments passés à call se retrouve dans les variables $1 $2 etc.

 

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 :

 

VARIANTES=debug release

define GEN_VARIANT
# Rule to build bin from elf; $1 is the variant name
$1.bin: $1.elf
    objdump -o binary $$@ $$<
endef

$(foreach variante,$(VARIANTES),$(eval $(call GEN_VARIANT,$(variante))))

 

Ici, la fonction foreach permet d'itérer sur chaque valeur contenu 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ètre.

 

À noter l'utilisation du caractère d'échappement $ devant les variables $@ et $<. Sans lui, elles serait expansées de suite et seraient donc vides car elle 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 soucis d'optimisation de temps d'exécution, les varaibales globales peuvent également être échapé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 example 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)

 

Warning

 

Il ne faut 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 fait 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, on le retrouve très prochainement pour la prochaine partie avec une liste non exhaustive des pièges à éviter et des astuces utiles à connaître.


Découvrez également nos articles en C++ et sur l’utilisation de CMake juste ici :

 

C++ : https://www.elephant-technologies.fr/actualite/45
CMake : https://www.elephant-technologies.fr/actualite/19