xieme-art/content/articles/2023/fonctionnement_makefile/index.md

480 lines
14 KiB
Markdown

Title: Prendre en main Make
Category: sysadmin
Tags: make, Makefile, programmation
Date: 2023-08-02 12:25
status: published
`make` est un outil **d'aide à la compilation**. Il permet de construire des
fichiers cibles en fonction de dépendances. Il a été créé à la fin des années
1970 par Stuart Feldman afin de répondre à une problématique naissante : la
gestion de plus en plus compliquée des dépendances et les temps de compilation
de plus en plus élevés.
Dans cet article, nous allons voir ensemble comment utiliser cet outil, mais
avec un angle d'attaque éloigné de la compilation, du langage *C* et du
développement pour **nous concentrer sur son language**.
## Comment ça fonctionne?
`make` utilise un langage de programmation déclaratif pour déterminer les
différentes actions à effectuer.
Ces actions sont définies dans un fichier appelé `Makefile` et contenant les
différentes instructions servant pour la compilation mais aussi d'autres tâches
( installation / désinstallation, nettoyage, ... ).
`make` cherchera par défaut les noms de fichier suivants dans le dossier
courant : `GNUMakeFile`[^n_gnumakefile], `makefile`, `Makefile`. Il est bien
entendu possible de spécifier le fichier à utiliser avec l'argument `--file` ou
`-f`.
`make` se base sur les dates pour déterminer ce qui doit être (re)compilé. Si le
fichier source (la dépendance) est plus récent que le binaire (la cible), alors
les instructions permettant de construire la cible seront lancée.
[^n_gnumakefile]: Dans la version GNU de make, que vous devez certainement
utiliser.
## Le langage
Les fichiers *makefiles* contiennent donc les différentes instructions écrites
dans un langages spécifique. Nous allons en voir ici les concepts les plus
importants.
### Les cibles
Elles représentent les actions à effectuer comme par exemple la construction
d'un binaire à partir des sources. Elles sont suivies de leurs dépendances (sur
la même ligne), En dessous se trouve les instructions nécessaire à la
réalisation de notre action. Ces instructions doivent être indentée d'une
tabulation. Voici un exemple simple :
```make
build: text_1 text_2
cat text_1 text_2 > build
text_1:
echo "Hello" > text_1
text_2:
echo "World" > text_2
```
La cible `build` dépend des fichiers `text_1` et `text_2`, ces fichiers se
trouvent être aussi des cibles sans dépendances, elle permettent juste à créer
des fichiers textes à l'aide d'une simple commande `echo`.
Si l'un des deux fichiers utilisés en dépendances de `build` n'est pas présent,
alors `make` exécutera la cible permettant de le créer. Si *build* n'existe pas,
ou si sa date de création est plus ancienne que les dates de création de ses
dépendances (*text_1* et *text_2*), alors la cible sera exécutée.
Pour lancer une cible, il suffit d'exécuter `make` avec en paramètre la cible :
```
make build
echo "Hello" > text_1
echo "World" > text_2
cat text_1 text_2 > build
```
Ces nouveaux fichiers ont bien été créés dans notre répertoire contenant notre
`Makefile`:
```
.
├── build
├── Makefile
├── text_1
└── text_2
```
Une autre exécution de `make` nous indique que `build` est bien à jour :
```
make build
make: 'build' is up to date.
```
Supprimons maintenant `text_2` et relançons `make`, il va recréer ce dernier et
par conséquent le fichier *build* :
```
rm text_2
make build
echo "World" > text_2
cat text_1 text_2 > build
```
#### Cible `.PHONY`
Il y a des cibles que ne réalisent aucune "construction", celles qui nettoient
le dépôt ou affichent des informations. On placera celles-ci dans la cible
`.PHONY` comme par exemple:
```make
.PHONY: view clean
view:
[ -f build ] && cat build
clean:
rm build text_1 text_2
```
Voici ce qui se passe en exécutant la cible `clean` :
```
make clean
rm -f build text_1 text_2
```
La cible `.PHONY` permet d'éviter les conflits dans les noms de fichiers : que
se passerait-il si un fichier `clean` existait? La cible `clean` ne sera jamais
lancée étant donné qu'elle n'a aucune dépendance et donc considérée **toujours à
jour**.
#### Commande shell dans les "recettes"
Il est tout à fait possible d'utiliser des commandes *shell* comme instructions
dans nos cibles. Il est ainsi possible d'utiliser les commandes intégrée comme
`echo` ou des commandes externes comme `mkdir`, `touch` etc.
Il est aussi possible d'utiliser les condition comme vous pouvez le voir dans la
cible `clean` de l'exemple précédent.
Par défaut `make` affiche l'instruction sur la sortie standard avant de
l'exécuter (et afficher le résultat ce celle-ci). Il est possible de ne pas
l'afficher en ajoutant `@` devant :
```make
.PHONY: echo
echo:
echo "La commande sera affichée"
@echo "seul le résultat est affiché"
```
Et son exécution:
```
make echo
echo "La commande sera affichée"
La commande sera affichée
seul le résultat est affiché
```
Par défaut `make` utilise `/bin/sh -c` pour exécuter les commandes. Il est
possible de changer d'interpréteur grâce à la variable `SHELL` pour la commande
et `.SHELLFLAGS` pour les paramètres. Voici un exemple pour `bash`:
```make
SHELL = /bin/bash
.SHELLFLAGS = -c
```
### Les variables
Il est possible d'utiliser des variables dans `make` comme nous l'avons vu
ci-dessus. Il est d'usage de définir les variables en lettres capitales, mais rien
d'obligatoire. Il existe quatre type d'affectation:
* par référence via le signe `=` comme `VAR = valeur`;
* par expansion avec `:=` comme `VAR := valeur`;
* conditionnelle avec `?=` comme `VAR ?= valeur`, ici l'affectation n'aura lieu
seulement si `VAR` n'a pas été définie auparavant;
* par concaténation avec `+=` comme `VAR += toto`.
Pour bien comprendre la différence entre les deux premières affectations, voici
un `Makefile` d'exemple:
```make
VAR = "Hello"
REF = $(VAR)
EXP := $(VAR)
.PHONY: assign
assign:
@echo "initial value: $(VAR)"
$(eval VAR = Bonjour)
@echo "new value: $(VAR)"
@echo "assign by reference \`VAR_2 = value\`: $(REF)"
@echo "assign by expansion \`VAR_2 := value\`: $(EXP)"
```
Comme vous l'avez remarqué, ls variables s'utilise avec la notation `$(VAR)`, il
est aussi possible d'utiliser `${VAR}`.
L'exécution de note cible `assign` montre bien que la variable assignée avec `=`
prend en compte la modification de `VAR` via la fonction `eval` (nous parlerons
des fonctions plus tard), c'est donc une référence -- à la manière des pointeurs
-- vers celle-ci :
```text
make assign
initial value: Hello
new value: Bonjour
assign by reference `VAR_2 = value`: Bonjour
assign by expansion `VAR_2 := value`: Hello
```
#### Surcharge
Il est possible de surcharger toutes les variables présente dans notre
`makefile` lors de son exécution. Il suffira alors de les définit lors de la
commande `make` sous la forme `NOM=valeur` (ou `NOM="valeur avec espace"`).
Cette surcharge prendra le pas sur l'ensemble des affectation de notre
`Makefile` :
```text
make assign VAR="Guten tag"
initial value: Guten tag
new value: Guten tag
assign by reference `VAR_2 = value`: Guten tag
assign by expansion `VAR_2 := value`: Guten tag
```
En reprenant notre exemple nous voyons bien que même l'instruction d'affectation
`$(eval VAR = Bonjour)` n'a plus d'effet[^n_override] sur la valeur de `VAR`.
[^n_override]: Mais il est possible d'utiliser `$(eval override VAR = bonjour)`
pour forcer l'affectation.
#### Variables spécifiques
Il existe un ensemble de variables définie de base à utiliser dans les cibles,
en voici quelques une :
* `$@` contient le nom de la cible;
* `$<` contient le nom de la première dépendance;
* `$^` contient la liste de toutes les dépendances.
Reprenons notre exemple avec le `build` et ajoutons une cible comme
ci-dessous :
```make
build: text_1 text_2
cat text_1 text_2 > build
text_1:
echo "Hello" > text_1
text_2:
echo "World" > text_2
.PHONY: specific
specific: text_1 text_2 build
@echo "target.....$@"
@echo "First dep..$<"
@echo "All deps...$^"
```
L'exécution de notre cible `specific` entrainera la construction de `build`,
`text_1` et `text_2` ( si nécessaire ) et nous affichera les informations comme
ci-dessous :
```text
make specific
target.....specific
First dep..text_1
All deps...text_1 text_2 build
```
### Les fonctions
`make` dispose d'un ensemble de fonctions utilisables dans les variables, ou les
cibles. Il existe des fonctions pour manipuler du texte, des chemins, exécuter
des commandes shell etc. L'appel de se fait sous la forme suivante:
```make
$(nom_fonction argument_1,argument_2)
```
Prenons l'exemple de la fonction `subst` qui permet de substituer un motif par
un autre :
```make
TEXT = hello world
NEW_TEXT = $(subst hello, bonjour, $(TEXT))
.PHONY: subst
subst:
@echo $(NEW_TEXT)
```
La fonction prend trois arguments :
1. la chaine de caractère à rechercher;
2. celle par laquelle la substituer;
3. la chaine à modifier.
Remarquez que les macros peuvent être utilisé comme arguments. Voici le résultat
de celle cible :
```text
make subst :
bonjour world
```
#### Imbrication de fonctions
Il est aussi possible d'utiliser des fonctions comme arguments, voici l'exemple
de `patsubst` qui substitue des parties de chemin :
```make
$(patsub motif,remplacement, liste_fichiers)
```
Il est possible d'utiliser la fonction `wildcard` pour lister les fichiers en
fonction d'un motif donné en argument, voici un exemple :
```make
FILES = $(patsubst text_%,my_text_%, $(wildcard text*))
.PHONY: textfiles
textfiles: build
@echo "origin: $(wildcard text*)"
@echo "files: $(FILES)"
```
Remarquez l'utilisation de `%` comme caractère joker dans la syntaxe des
`Makefile`. Il est cependant nécessaire d'utiliser les caractères joker du shell
comme `*` ou `?` dans les dépendances des cibles (pour lister les fichiers) et
dans la fonction `wildcard`.
#### Les messages
`make` dispose de trois fonctions permettant d'afficher des informations à
l'utilisateur :
1. `$(info message)` : affiche simplement message;
2. `$(warning message)` : affiche le message précédé du fichier et du numéro de
ligne;
3. `$(error message)` : affiche le message avec les informations de fichier et
de ligne et **stoppe l'exécution de make**.
Voici un exemple:
```make
.PHONY: messages
messages:
$(info Message d'information)
$(warning Message d'alerte)
$(error Message d'erreur)
$(info Ce message ne s'affichera pas!)
```
Et le résultat :
```text
make messages
Message d'information
Makefile:54: Message d'alerte
Makefile:55: *** Message d'erreur. Stop.
```
#### Les fonctions liées aux variables
Il est possible d'affecter une fonction à une variable, elle devient alors **une
macro**. Les modes par référence et par expansion avec sont toujours
d'actualité. Voici un exemple de code à ajouter à la suite de notre fichier
`Makefile` :
```make
# [...]
EXP_FILES := $(wildcard text*)
REF_FILES = $(wildcard text*)
.PHONY: macro
macro: clean
@echo "EXP: $(EXP_FILES)"
@echo "REF: $(REF_FILES)"
```
Notre cible `macro` dépends de la cible `clean` qui supprime donc les fichiers
générés dont `text_1` et `text_2` récupérés par `wildcard`. Les deux actions de
notre cible affichent ensuite les variables, lançons d'abord notre cible `build`
afin de s'assurer de la présence des fichiers `text`, puis la cible `macro`.
Voici le résultat:
```text
make build
[...]
make macro
rm -f build text_1 text_2
EXP: text_1 text_2
REF:
```
Pour `EXP_FILES` le résultat de la fonction `wildcard` est affecté à note
variable. À ce moment les fichiers répondant au motif `text*` sont encore
présents.
Pour `REF_FILES` c'est la fonction elle même qui est affectée. Elle est donc
exécutée à chaque utilisation de la variable. Dans la commande `echo` les
fichiers répondant au motif `text*` n'existent plus, notre fonction ne renvoie
donc rien.
### Structures conditionnelles
Il est possible de rendre une partie du `Makefile` accessible en fonction de
condition. Quatre types de conditions sont disponibles:
* l'égalité via la commande `ifeq (<val_1>, <val_2>)` qui renvoie vrai si
`<val_1>` est égal à `<val_2>`. Les deux éléments peuvent être des macros ou
des chaines de caractères;
* la non égalité avec `ifneq (<val_1>,<val_2>)` qui renvoie vrai si les deux
éléments sont différents;
* la définition d'une macro avec `ifdef VAL` qui renvoi vrai si `VAL`
est définie ( une fois l'expansion effectuée );
* la non définition d'une macro avec `ifndef MACRO`.
Voici un exemple d'utilisation de ces structures:
```make
TEST = bonjour
DIST := $(shell lsb_release -i | awk -F ':\t' '{print $$2}')
ifeq ($(DIST), Arch)
MY_MESS := "By the Way"
endif
.PHONY: condition
condition:
ifndef MY_MESS
@echo "Vous n'utilisez pas ArchLinux mais $(DIST)"
else
@echo $(MY_MESS)
endif
ifneq ($(TEST), bonjour)
@echo 'TEST a été modifiée'
else
@echo "TEST n'a pas été modifiée"
endif
```
Ici nous utilisons `ifeq`, `ifneq` et `ifndef` que se soit dans ou même à
l'extérieur d'une cible. Nous avons aussi un exemple d'utilisation de la
fonction `shell` comme affectation de la variable `DIST`.
Dans les cibles, il est **impératif** de laisser une tabulation avant chaque
instruction dans les conditions sinon la cible ne fonctionnera pas.
```text
make condition
By the Way
TEST n'a pas été modifiée
```
Voici maintenant le résultat en modifiant la valeur de `TEST` lors de
l'exécution:
```text
make TEST=hallo DIST=Debian condition
Vous n'utilisez pas ArchLinux : Debian
TEST a été modifiée
```
## En conclusion
Nous avons vu dans cet article les notions les plus courantes de `make`, c'est
cependant un outil puissant dont le fonctionnement ne peut pas se résumer en un
article. Il se révèle utile dans bien des situations qui ne se limite pas qu'au
développement! Je compte bien vous proposer dans les semaines à venir des
**mises en applications** de ce que nous avons vu dans cet article pour divers
usages. Nous en profiterons alors pour approfondir ce que nous venons de voir
ici.
Tous les exemples présents dans cet articles sont disponibles dans
[ce `Makefile`]({static}./files/Makefile).