Add Make article
This commit is contained in:
parent
8c01059550
commit
133536f665
2 changed files with 557 additions and 0 deletions
81
content/articles/2023/fonctionnement_makefile/files/Makefile
Normal file
81
content/articles/2023/fonctionnement_makefile/files/Makefile
Normal file
|
@ -0,0 +1,81 @@
|
|||
build: text_1 text_2
|
||||
cat text_1 text_2 > build
|
||||
|
||||
text_1:
|
||||
echo "Hello" > text_1
|
||||
|
||||
text_2:
|
||||
echo "World" > text_2
|
||||
|
||||
.PHONY: view clean
|
||||
view:
|
||||
$(shell [ -f build ] && cat build)
|
||||
|
||||
clean:
|
||||
rm -f build text_1 text_2
|
||||
|
||||
.PHONY: echo
|
||||
echo:
|
||||
echo "La commande sera affichée"
|
||||
@echo "seul le résuiltat est affiché"
|
||||
|
||||
VAR = "Hello"
|
||||
REF = $(VAR)
|
||||
EXP := $(VAR)
|
||||
.PHONY: assign
|
||||
assign:
|
||||
@echo "initial value: $(VAR)"
|
||||
$(eval override VAR = Bonjour)
|
||||
@echo "new value: $(VAR)"
|
||||
@echo "assign by reference \`VAR_2 = value\`: $(REF)"
|
||||
@echo "assign by expansion \`VAR_2 := value\`: $(EXP)"
|
||||
|
||||
.PHONY: specific
|
||||
specific: text_1 text_2 build
|
||||
@echo "target.....$@"
|
||||
@echo "First dep..$<"
|
||||
@echo "All deps...$^"
|
||||
|
||||
TEXT = hello world
|
||||
NEW_TEXT = $(subst hello, bonjour, $(TEXT))
|
||||
.PHONY: subst
|
||||
subst:
|
||||
@echo $(NEW_TEXT)
|
||||
|
||||
FILES = $(patsubst text_%,my_text_%, $(wildcard text*))
|
||||
.PHONY: textfiles
|
||||
textfiles: build
|
||||
@echo "origin: $(wildcard text*)"
|
||||
@echo "files: $(FILES)"
|
||||
|
||||
.PHONY: messages
|
||||
messages:
|
||||
$(info Message d'information)
|
||||
$(warning Message d'alerte)
|
||||
$(error Message d'erreur)
|
||||
$(info Ce message ne s'affichera pas!)
|
||||
|
||||
EXP_FILES := $(wildcard text*)
|
||||
REF_FILES = $(wildcard text*)
|
||||
.PHONY: macro
|
||||
macro: clean
|
||||
@echo "EXP: $(EXP_FILES)"
|
||||
@echo "REF: $(REF_FILES)"
|
||||
|
||||
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 : $(DIST)"
|
||||
else
|
||||
@echo $(MY_MESS)
|
||||
endif
|
||||
ifeq ($(TEST), bonjour)
|
||||
@echo "TEST n'a pas été modifiée"
|
||||
else
|
||||
@echo 'TEST a été modifiée'
|
||||
endif
|
476
content/articles/2023/fonctionnement_makefile/index.md
Normal file
476
content/articles/2023/fonctionnement_makefile/index.md
Normal file
|
@ -0,0 +1,476 @@
|
|||
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)"
|
||||
```
|
||||
|
||||
L'exécution de note cible `assign` montre bien que la variable assignée avec `=`
|
||||
prend en compte la modification de `VAR`, 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).
|
Loading…
Add table
Add a link
Reference in a new issue