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 (, )` qui renvoie vrai si `` est égal à ``. Les deux éléments peuvent être des macros ou des chaines de caractères; * la non égalité avec `ifneq (,)` 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).