Salut, salut !
Nous allons parler d’un outil très connu des développeurs make ! Quand on crée des projets de plus en plus conséquents, la production de code (compilation, éditions de liens) peut-être assez difficile à gérer « à la main » dès que le projet devient conséquent. Make est un outil qui va nous aider dans la gestion de projets. L’outil est assez facile à appréhender, il est basé sur une certain nombre de principes et de syntaxes simples.
Introduction
Make présente un très bon avantage. Supposez que vous trouviez un bogue dans un fichier c, et que vous deviez le corriger. Pour pouvoir obtenir un nouveau programme compilé, il faut recompiler tous les fichiers, les en-têtes et le code source, même si vous changez juste un fichier. C’est là, que make intervient au lieu de compiler la totalité du code source, il ne construit que le code source qui a subi des changements.
Les Makefiles sont justement des fichiers utilisés par le programme make pour exécuter un ensemble d’actions, comme la compilation d’un projet, l’archivage de documents, la création de la documentation… À travers mon article je vais tenter de vous faire une brève introduction au fonctionnement d’un makefile, notamment grâce à 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 qui se nomment Makefile ou makefile. Toutefois il est possible de créer un makefile avec un autre nom. Par exemple, si nous avons un makefile appelé test, alors la commande à utiliser pour donner l’ordre à make de traiter ce fichier est :
make -f test
Un makefile se compose d’une section cible, dépendances et règles. Les dépendances sont les éléments ou le code source nécessaires pour créer une cible ; la cible est le plus souvent un nom d’exécutable ou de 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 par exemple, le compilateur que l’on va choisir, le chemin vers les sources… Par exemple :
CC=g++
Pour écrire des commentaires, il suffit de placer un #.
# Ceci est un commentaire
Il existe quelques variables internes au makefile, en voici quelques-unes :
Nom | Action |
---|---|
$@ | 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 d’expliquer à make, d’aller exécuter un autre Makefile situé dans un autre dossier. Ici par exemple on exécute la cible main du Makefile situé dans le dossier src/
build: make -C src main
Enfin, on va parler de la commande .PHONY. Elle permet de définir une cible particulière. C’est juste un nom de fichier fictif pour les commandes qui seront exécutées quand vous lancez une requête explicite. Cela permet d’éviter des conflits avec un fichier du même nom et d’améliorer les performances du Makefile.
Si on écrit une règle donc la commande n’est pas censée créer un fichier cible, comme par exemple une fonction bien connut le make clean, qui permet de nettoyer un projet de tous les fichiers .o créés.
clean: rm *.o
Du fait que la commande rm ne crée pas de fichier nommé clean, ce fichier n’existera jamais. La commande rm sera donc toujours exécutée chaque fois qu’on lancera la commande :
make clean
Cela est dû au fait que make part du principe que le fichier clean est toujours nouveau. Mais si on créerait un fichier clean, dans le répertoire du makefile. Comme ce fichier n’exigera pas de dépendances. Make considérera le fichier comme étant à jour, la commande précédente ne s’éxecutera plus. C’est pour résoudre ce problème, que nous pouvons déclarer explicitement une cible comme « particulière » à l’aide de la commande .PHONY.
.PHONY : clean
Grâce à ceci, qu’il existe ou non un fichier nommé clean dans le répertoire actuel, la commande sera toujours exécuté.
Makefile par l’exemple
On a vu quelques règles de syntaxe, on va maintenant partir sur 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
- On déclare une variable CFLAGS qui contiendra toutes nos options de compilation.
- La variable INC, permettra d’inclure les headers utilisés par nos fichiers .c.
- La variable SRC, contient le chemin vers les fichiers sources.
Maintenant la partie qui nous intéresse le plus, la compilation du binaire, grâce à la commande main :
main: $(SRC)main.c $(SRC)operation.o $(CC) $(INC) -o $(SRC)$@ $^ $(CFLAGS)
Ici, notre cible c’est la création du fichier main, pour ce faire on a les dépendances main.c et les fichiers.o, ici un fichier appelé opération.o. Or pour compiler ce fichier .o on va utiliser la règle générique suivante :
$(SRC)%.o : $(SRC)%.c $(CC) $(INC) -o $@ -c $< $(CFLAGS)
Une fois la commande exécutée on obtiendrait 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 : où 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 décrire des éléments que l’on veut éviter de compiler. Imaginons, qu’on aurait ajouté deux fichiers test.c et test2.c et que chacun d’eux contiendrait un main, cela poserait évidemment problème, pour pallier à ça, voila comment on pourrait faire :
TESTS=test.c test2.c SRC=$(filter-out $(TESTS),$(wildcard *.c))
On pourrait traduire la variable SRC, par pour chaque .c sans le contenu de la variable TESTS.
Voilà, j’ai fini ma présentation des fichiers makefile, bonne compilation à tous.