480 lines
14 KiB
Markdown
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).
|