Prendre en main Make

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 : GNUMakeFile1, 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 = 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:

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 :

  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 :

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 :

  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:

.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 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:

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.


  1. Dans la version GNU de make, que vous devez certainement utiliser. 

  2. Mais il est possible d’utiliser $(eval override VAR = bonjour) pour forcer l’affectation.