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
1, 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.
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 :
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:
.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 :
.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
:
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
=
commeVAR = valeur
; - par expansion avec
:=
commeVAR := valeur
; - conditionnelle avec
?=
commeVAR ?= valeur
, ici l’affectation n’aura lieu seulement siVAR
n’a pas été définie auparavant; - par concaténation avec
+=
commeVAR += toto
.
Pour bien comprendre la différence entre les deux premières affectations, voici
un Makefile
d’exemple:
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 :
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
:
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’effet2 sur la valeur de VAR
.
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 :
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 :
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:
$(nom_fonction argument_1,argument_2)
Prenons l’exemple de la fonction subst
qui permet de substituer un motif par
un autre :
TEXT = hello world
NEW_TEXT = $(subst hello, bonjour, $(TEXT))
.PHONY: subst
subst:
@echo $(NEW_TEXT)
La fonction prend trois arguments :
- la chaine de caractère à rechercher;
- celle par laquelle la substituer;
- la chaine à modifier.
Remarquez que les macros peuvent être utilisé comme arguments. Voici le résultat de celle cible :
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 :
$(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 :
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 :
$(info message)
: affiche simplement message;$(warning message)
: affiche le message précédé du fichier et du numéro de ligne;$(error message)
: affiche le message avec les informations de fichier et de ligne et stoppe l’exécution de make.
Voici un exemple:
.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 :
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
:
# [...]
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:
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 siVAL
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:
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.
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:
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
.