From 133536f665f79c7ab7100b89c5e41498d8c72062 Mon Sep 17 00:00:00 2001 From: Yorick Barbanneau Date: Wed, 2 Aug 2023 13:15:46 +0200 Subject: [PATCH] Add Make article --- .../fonctionnement_makefile/files/Makefile | 81 +++ .../2023/fonctionnement_makefile/index.md | 476 ++++++++++++++++++ 2 files changed, 557 insertions(+) create mode 100644 content/articles/2023/fonctionnement_makefile/files/Makefile create mode 100644 content/articles/2023/fonctionnement_makefile/index.md diff --git a/content/articles/2023/fonctionnement_makefile/files/Makefile b/content/articles/2023/fonctionnement_makefile/files/Makefile new file mode 100644 index 0000000..415fbc5 --- /dev/null +++ b/content/articles/2023/fonctionnement_makefile/files/Makefile @@ -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 diff --git a/content/articles/2023/fonctionnement_makefile/index.md b/content/articles/2023/fonctionnement_makefile/index.md new file mode 100644 index 0000000..3bc57e5 --- /dev/null +++ b/content/articles/2023/fonctionnement_makefile/index.md @@ -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 (, )` 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).