Salut, salut ! Aujourd’hui, nous allons parler d’un outil très connu des développeurs : Make ! Lorsque nous travaillons sur des projets de grande envergure, la gestion du code peut devenir assez difficile. Make est un outil qui nous aide à gérer nos projets plus facilement. Il est basé sur des principes et une syntaxe simples.
Introduction
Make présente un avantage majeur. Imaginons que vous trouviez un bug dans un fichier c et que vous souhaitiez le corriger. Pour obtenir un nouveau programme compilé, vous devez recompiler tous les fichiers, y compris les en-têtes et le code source, même si vous ne modifiez qu’un seul fichier. C’est là que Make entre en jeu : au lieu de compiler l’ensemble du code source, il ne construit que le code qui a été modifié.
Les Makefiles sont des fichiers utilisés par Make pour exécuter un ensemble d’actions, comme la compilation d’un projet, l’archivage de documents ou la création de la documentation. Dans cet article, je vais vous présenter brièvement le fonctionnement d’un Makefile en utilisant l’exemple de la compilation d’un projet C. Nous commencerons par présenter la syntaxe, puis nous verrons l’outil à travers un exemple.
La syntaxe d’un Makefile
Tout d’abord, Make ne fonctionne qu’avec des fichiers nommés Makefile ou makefile. Cependant, il est possible de créer un Makefile avec un autre nom. Par exemple, si nous avons un Makefile appelé “test”, la commande à utiliser pour demander à Make de traiter ce fichier est la suivante :
make -f test
Un Makefile se compose de sections pour les cibles, les dépendances et les règles. Les dépendances sont les éléments ou le code source nécessaires pour créer une cible. La cible est généralement un nom d’exécutable ou un fichier objet. Les règles sont les commandes nécessaires pour créer la cible.
cible: dépendances
règles
Il est possible de définir des variables dans votre fichier Makefile, par exemple le compilateur que vous souhaitez utiliser ou le chemin vers les sources :
CC=g++
Pour écrire des commentaires, il suffit de placer un # :
# Ceci est un commentaire
Il existe également certaines variables internes au Makefile, voici quelques exemples :
$@
représente la liste des cibles$ˆ
représente la liste des dépendances$<
représente le nom du fichier sans suffixe$*
représente la liste des dépendances$?
représente la liste des dépendances plus récentes que la cible
La commande make -C
permet à Make d’exécuter un autre Makefile situé dans un autre dossier. Par exemple, nous pouvons exécuter la cible “main” du Makefile situé dans le dossier “src” :
build: make -C src main
Enfin, parlons de la commande .PHONY
. Elle permet de définir une cible particulière. C’est simplement un nom de fichier fictif pour les commandes qui seront exécutées lorsque vous lancez une requête explicite. Cela évite les conflits avec un fichier du même nom et améliore les performances du Makefile.
Si nous écrivons une règle dont la commande ne crée pas de fichier cible, comme par exemple la commande make clean
qui permet de nettoyer un projet en supprimant tous les fichiers .o créés, nous ajoutons la commande .PHONY
pour que Make exécute toujours cette commande, qu’il existe ou non un fichier nommé clean dans le répertoire actuel :
.PHONY: clean
Grâce à cela, que nous ayons ou non un fichier nommé clean dans le répertoire actuel, la commande sera toujours exécutée.
Makefile par l’exemple
Maintenant que nous avons vu quelques règles de syntaxe, passons à un exemple concret. Voici l’arborescence d’un projet C :
CC=gcc
CFLAGS=-W -Wall -ansi -pedantic -std=c99 -g
INC=-I include/
SRC=src/
EXEC=main
all: $(EXEC)
main: $(SRC)main.c $(SRC)article.o
$(CC) $(INC) -o $(SRC)$@ $^ $(CFLAGS)
$(SRC)%.o: $(SRC)%.c
$(CC) $(INC) -o $@ -c $< $(CFLAGS)
clean:
rm -rf $(SRC)*.o
Nous déclarons une variable CFLAGS
qui contient toutes les options de compilation. La variable INC
est utilisée pour inclure les headers utilisés par nos fichiers .c. La variable SRC
contient le chemin vers les fichiers sources.
Maintenant, passons à la partie qui nous intéresse le plus : la compilation du binaire avec la commande main
:
main: $(SRC)main.c $(SRC)operation.o
$(CC) $(INC) -o $(SRC)$@ $^ $(CFLAGS)
Ici, notre cible est la création du fichier main. Pour cela, nous avons les dépendances main.c
et les fichiers .o, ici un fichier nommé operation.o. Pour compiler ce fichier .o, nous utilisons la règle générique suivante :
$(SRC)%.o: $(SRC)%.c
$(CC) $(INC) -o $@ -c $< $(CFLAGS)
Une fois la commande exécutée, nous obtenons la commande suivante :
gcc -I include/ -o operation.o -c operation.c -W -Wall...
De même pour la cible main
, les variables seraient remplacées par Make.
Wildcard et filter-out : ou comment compiler avec deux fichiers contenant un main
La fonction wildcard
permet de cibler un ensemble de fichiers .c et la fonction filter-out
permet de spécifier les éléments que nous souhaitons éviter de compiler. Imaginons que nous ayons ajouté deux fichiers test.c
et test2.c
et que chacun d’eux contienne un main. Cela poserait évidemment problème. Voici comment nous pourrions résoudre ce problème :
TESTS=test.c test2.c
SRC=$(filter-out $(TESTS),$(wildcard *.c))
La variable SRC
représente donc tous les fichiers .c, à l’exception de ceux contenus dans la variable TESTS
. Voilà, j’ai terminé ma présentation des fichiers Makefile. Bonne compilation à tous !