Un makefile Kesako ?
Un makefile, c’est un fichier (makefile/Makefile) qui va regrouper des commandes qui seront exécutées lorsque l’on appellera la directive à laquelle elles dépendent. c’est très utilisé pour faire de la compilation de façon automatique.
+++ makefile +++ all: gcc -o exec main.c +++ +++
Ici un exemple de makefile, avec une commande gcc qui va lancer la compilation si la directive « all » est appelée :
$ make all
Dans un makefile une directive commence à la colonne 1 (au bord de la page) et se termine par « : », les commandes qui en dépendent se trouvent sur les lignes du dessous et sont indentées d’une tabulation (\t).
Pour appeler une directive d’un makefile il suffit de faire :
$ make directive
Mais par défaut si vous ne faites que make, ce sera la première directive du fichier qui sera appelé, pour cette raison dans bien des cas la directive « all » se trouve être la première du fichier.
Les dépendances
Si on appelle make plusieurs fois, à chaque appel il va recompiler l’exécutable, mais on peut lui rajouter des dépendances pour ne recompiler que ce qui à changé :
+++ makefile +++ exec: main.c gcc -o exec main.c +++ +++
Dans ce cas make va vérifier les dates avant de compiler, si la date de dernière modification de main.c est antérieur à la date de création de exec alors il ne recompilera pas, sinon (la date de modification de main.c est postérieure à la date de création de exec) il recompile. Ce qui signifie qu’il ne recompile que s’il y a eu une modification sur main.c depuis qu’on à créé exec.
Un second point important est que si vous mettez une dépendance sur une directive, makefile va chercher une directive portant ce nom, regardez l’exemple suivant puis j’explique :
+++ makefile +++ all: exec exec: main.c gcc -o exec main.c +++ +++
Ici quand je lance make, il cherche en lui-même une directive exec, s’il l’a trouve alors il l’exécute comme si on avait fait un make exec.
S’il ne trouve pas de directive portant le nom de la dépendance et qu’il ne trouve pas de fichier avec ce nom alors il vous dira un truc du genre : » make: *** No rule to make target `dependance’, needed by `directive’. Stop ».
Plusieurs fichiers
Dans un plus gros projet vous aurez plusieurs fichiers sources et c’est là que prend tout le sens du makefile, car recompiler les 642 fichiers sources du programme parce qu’on à changé un mot dans un printf c’est… pas rentable, dans cet exemple nous nous contenterons de 3 fichiers sources (mais c’est la même chose pour 642) :
$ tree . ├── fonction.c ├── fonction.h ├── main.c └── makefile 0 directories, 4 files
+++ makefile +++ all: exec exec: main.o fonction.o gcc -o exec main.o fonction.o main.o: main.c fonction.h gcc -c -o main.o main.c fonction.o: fonction.c fonction.h gcc -c -o fonction.o fonction.c +++ +++
Lors d’un appel à make, make cherche la directive exec, qui dépend elle même de main.o et fonction.o, et ainsi de suite. On va créer les deux *.o (code compilé mais non linké), puis on va les assembler et linker dans le exec, l’intérêt est que si vous changez un élément du main.c, on ne recompile que le main.c et le exec pas le fonction.o, donc imaginez le gain de temps sur un projet qui fait 642 fichiers.
Pour déterminer les dépendances que l’on doit ajouter à une directive il suffit de lire les includes (sans prendre en compte les librairies standards) et de rajouter le fichier source que l’on veut compiler.
Généralisation
C’est bien mais c’est super long à écrire et si on à 642 fichiers on va oublier un truc, donc maintenant je vais vous expliquer comment généraliser vos règles.
+++ makefile +++ all: exec exec: main.o fonction.o gcc -o $@ $^ %.o: %.c gcc -c -o $@ $< +++ +++
Ici c’est une copie du makefile précédent mais la génération des *.o ce fait de façon généralisé.
Pour les explications :
%.o: %.c # chaque .o dépend du fichier homonyme en .c # (main.o: main.c, fonciton.o: fonction.c, ...) $@ # nom de la directive (%.o) $< # premier élément de la liste des dépendances (%.c) $^ # liste des dépendances (main.o fonction.o)
Donc pour le main ça donne :
main.o: main.c gcc -c -o main.o main.c
Et là si vous avez bien suivi, vous aurez remarqué que le main.o ne dépend plus de fonction.h (ce qui est carrément pas cool), ce qui m’amène au point suivant.
Makedepend
Vous pouvez écrire plusieurs directives portant le même nom, avec des dépendances différentes, donc on peux rajouter les dépendances manquantes comme dans l’exemple suivant :
+++ makefile +++ all: exec exec: main.o fonction.o gcc -o $@ $^ %.o: %.c gcc -c -o $@ $< main.o: function.h fonction.o: fonction.h +++ +++
C’est génial, mais c’est toujours aussi long à écrire qu’au début… oui mais personne n’écrit les dépendances à la main, il existe un utilitaire qui le fait à notre place.
Makedepend, est une commande qui va permettre de générer les dépendances de façon automatique, en parsant vos fichiers sources et en interprétant les includes (cool non ?)
Pour l’installer :
$ sudo pat-get install xutils-dev
Et pour l’utiliser :
+++ makefile +++ all: makedepend exec exec: main.o fonction.o gcc -o $@ $^ %.o: %.c gcc -c -o $@ $< makedepend: makedepend -Y main.c fonction.c 2> /dev/null # +++ +++
Ici avant d’exécuter la directive exec, il va exécuter makedepend. La directive makedepend va être exécutée à chaque fois car elle n’a pas de dépendance, cette directive lance la commande makedpend. Cette commande va, en fin de fichier, créer un append en rajoutant les fichiers d’inclusion trouvés dans les fichiers sources :
Note : si je rajoute le # de fin c’est tout simplement car madepend ne va pas à la ligne quant il fait sont append et donc si votre fichier se termine juste après le /dev/null, la commande va vous dire : « /bin/sh: 1: cannot create /dev/null#: Permission denied »
+++ makefile +++ all: makedepend exec exec: main.o fonction.o gcc -o $@ $^ %.o: %.c gcc -c -o $@ $< makedepend: makedepend -Y main.c fonction.c 2>/dev/null ## DO NOT DELETE main.o: fonction.h fonction.o: fonction.h +++ +++
Pour comprendre un peu mieux ce qui ce passe :
-Y # c'est pour lui indiquer où chercher les fichiers d’inclusions # std (stdio.h, time.h, ...), mais comme on lui donne pas de # <path> ça empêche d'aller chercher les inclusions std (sinon # vous incluez trop de truc) main.c fonction.c # ce sont les fichiers sources à parser 2> /dev/null # c'est pour ne pas avoir le retour d'erreur
Pourquoi le retour d’erreur dans /dev/null ? tout simplement car comme vous lui demandez d’aller chercher les fichiers standards à une adresse que vous ne donnez pas, il vous dit qu’il n’est pas content, mais on s’en tamponne le coquillard des erreurs dans /dev/null, de plus si nous acceptons les erreurs il ne génère pas l’append en fin de fichier.
Pour afficher une information il suffit de faire un echo de celle-ci :
+++ makefile +++ all: echo test +++ +++
Une petite information : si vous faite un make, vous aurez comme résultat :
$ make echo test test
Pour éviter d’avoir l’affichage de la commande, soit, « echo test » il suffit de mettre un @ devant la ligne comme ceci :
+++ makefile +++ all: @echo test +++ +++
$ make all test
Variables
Avant de voir ce qui concerne les variables, je vous conseille, de regrouper vos sources dans un dossier src, ça va permettre d’éclaircir votre dossier de travail, personnellement mon dossier ressemble plus ou moins à ça :
$ tree . ├── makefile └── src ├── fonction.c ├── fonction.h └── main.c 1 directory, 4 files
Avant de vraiment voir les variables en makefile (oui ça existe), il est possible de créer des variables et de les utiliser :
+++ makefile +++ VAR=truc # création de la variable all: @echo $(VAR) # surtout ne pas oublier les parenthèses qui # sont indispensables +++ +++
Il faut savoir que si vous définissez une variable dans votre makefile et que vous définissez la même variable dans les arguments d’appel à make alors on prend en compte la variable passée en argument, de cette façon :
$ make truc
$ make VAR=test test
C’est le principe de overwrite, qui donne la priorité à la définition par argument de ligne de commande.
Il faut noter également que vous pouvez faire un append sur une variable par un simple « += »
VAR+= test
Cela va rajouter le mot test à la variable $(VAR) à la suite de ce qu’elle contenait déjà.
Maintenant que nous pouvons faire un makefile correcte, vous aurez remarqué que ce n’est pas pratique, nous devons mettre en dépendance de exec les *.o que nous avons besoin ainsi que dans makedepend les fichiers sources à parser, et bien en fait on est pas obligé, c’est ce que j’explique maintenant.
De même qu’il est possible de créer des variables, il est possible d’exécuter des commandes standards linux depuis un makefile comme ceci :
+++ makefile +++ all: $(shell find . -name *.c) +++ +++
En couplant ces deux données nous obtenons :
+++ makefile +++ SRC=$(shell find ./src -name *.c) +++ +++
Il existe aussi une instruction qui permet de changer l’extension du fichier par une autre :
OBJ=$(SRC:.c=.o)
Ce qui nous donne, pour un makefile complet et fonctionnel (en plus d’être portable d’un projet à un autre) :
+++ makefile +++ SRC=$(shell find ./src -name *.c) OBJ=$(SRC:.c=.o) all: makedepend exec exec: $(OBJ) gcc -o $@ $^ %.o: %.c gcc -c -o $@ $< makedepend: @makedepend -Y $(SRC) 2> /dev/null # +++ +++
Variables usuelles
Il y à cinq variables « standards », qui sont presque toujours définies dans un makefile, je vous conseille de faire de même :
CC=gcc # compilateur C CXX=g++ # compilateur C++ CCFLAGS= # options de compilation C CXXFLAGS= # options de compilation C++ LDFLAGS= # option du linker
Ce qui donne un makefile semblable à celui ci dessous. Comme on n’a que du C, je ne défini et utilise que les variables C, mais avec du C++, c’est identique.
+++ makefile +++ CC=gcc # compilateur C CCFLAGS= # options de compilation C LDFLAGS= # option du linker SRC=$(shell find ./src -name *.c) OBJ=$(SRC:.c=.o) all: makedepend exec exec: $(OBJ) $(CC) -o $@ $^ $(LDFLAGS) %.o: %.c $(CC) -c -o $@ $< $(CCFLAGS) makedepend: @makedepend -Y $(SRC) 2> /dev/null # +++ +++
Nettoyage
Il faut toujours avoir une commande de nettoyage pour remettre son arborescence au propre, il y à deux directives standards (communément utilisées), clean et mrproper. Clean permet de supprimer tous les fichiers *.o, et mrproper les *.o, l’exécutable, et tout ce qui peut être régénéré par une commande quelconque tel que la documentation (quand on travail avec doxygen), les sorties du programme (log, dump, …), très utile quand on veut déplacer un projet (beaucoup moins lourd).
+++ makefile +++ CC=gcc # compilateur C CCFLAGS= # options de compilation C LDFLAGS= # option du linker SRC=$(shell find ./src -name *.c) OBJ=$(SRC:.c=.o) all: makedepend exec exec: $(OBJ) $(CC) -o $@ $^ $(LDFLAGS) %.o: %.c $(CC) -c -o $@ $< $(CCFLAGS) makedepend: @makedepend -Y $(SRC) 2> /dev/null clean: rm -f $(shell find ./src -name *.o) mrproper: clean rm -f exec # +++ +++
Options de compilations
Avoir un mode de debug et un mode de release, si vous êtes comme moi, vous avez du code qui n’apparaît que quand on debug ou que quand on est en release, comme celui qui suit :
Si on compile avec le makefile précédent et qu’on exécute on obtient :
./exec debug
Et pour passer en mode release, il suffit de rajouter -D’MODE_RELEASE’ comme option de compilation sur chaque ligne gcc, mais si vous êtes comme moi, vous allez vite en avoir assez d’éditer le makefile pour changer les options de compilation, de plus si on change le mode de sortie on est obligé de faire un mrproper manuellement car makefile n’est pas capable de savoir que nous avons changé des options dans la compilation.
Et bien pour pallier ces problèmes, dans votre makefile, on va rajouter quelques lignes, pour la première chose :
+++ makefile +++ MODE=debug ifeq ($(MODE),release) CCFLAGS+= -D'MODE_RELEASE' else CCFLAGS+= -D'MODE_DEBUG' endif +++ +++
Donc pour définir le mode debug il suffira d’appeler :
$ make MODE=debug
ou
$ make
pour appeler le mode release il suffit donc :
$ make MODE=release
Maintenant que nous passons de release à debug simplement, automatisons le mrproper (oui on est obligé de quand même faire un mrproper sinon la moitié du code sera en debug et l’autre en release…) :
+++ makefile +++ # dans .mode nous ecrivont le dernier type de compilation LAST_MODE=$(shell [ -f .mode ] && cat .mode) # pour verifier que deux variables sont egales je n'ai pas trouvé # d'autre moyen que de faire appel à une commande shell mais bon # ça fonctionne ^^ EQ_MODE=$(shell [ \"$(LAST_MODE)\" = \"$(MODE)\" ] && echo true) ifneq ($(EQ_MODE),true) # on verifie que $(LAST_MODE) != $(MODE) DEPEND=mrproper # si c'est different alors on créait une # dependance mrproper endif all: mode $(DEPEND) makedepend exec # cette dependnace est rajouté # avant de demander la compilation on rajoute aussi une # dependance "mode" qui servira à ecrire le dernier type # de compilation mode: @echo $(MODE) > .mode +++ +++
Makefile complet
Voila un exemple complet de makefile fonctionnel
+++ makefile +++ CC=gcc # compilateur C CCFLAGS= # options de compilation C LDFLAGS= # option du linker MODE=debug LAST_MODE=$(shell [ -f .mode ] && cat .mode) EQ_MODE=$(shell [ \"$(LAST_MODE)\" = \"$(MODE)\" ] && echo true) ifeq ($(MODE),release) CCFLAGS+= -D'MODE_RELEASE' else CCFLAGS+= -D'MODE_DEBUG' endif ifneq ($(EQ_MODE),true) DEPEND=mrproper endif SRC=$(shell find ./src -name *.c) OBJ=$(SRC:.c=.o) all: mode $(DEPEND) makedepend exec exec: $(OBJ) $(CC) -o $@ $^ $(LDFLAGS) %.o: %.c $(CC) -c -o $@ $< $(CCFLAGS) mode: @echo $(MODE) > .mode makedepend: @makedepend -Y $(SRC) 2> /dev/null clean: rm -f $(shell find ./src -name *.o) mrproper: clean rm -f exec # +++ +++
Mes makefiles ressemblent à ça, bien qu’il soit plus long (j’ai plus d’options de compilation), mais avec ceci vous pouvez vous en sortir sans problème.
Bonjour
Votre explication sur les Makefiles est super claire. Bravo !
Je voulais entrer en relation avec vous pour de la technique..(« cambouis »)..
Je suis en train de développer du code microcontroleur. (plateforme mbed).
La plateforme mbed propose de compiler vos projets « en ligne ».
Je voudrais pour avoir un toolchain complet, quitter mbed, exporter mes projets
(GCC-ARM) et avoir toute la chaine dans visual studio code que j’aime bien.
De cette plateforme on peut builder, compiler et envoyer sur la flash du microcontroleur.
C’est à ce niveau que j’ai des petits soucis de mise en place..
est ce que je peux vous contacter pour avoir de l’aide ?
Merci par avance pour votre retour.
très cordialement
Philippe Durieux
bonjour, bien que je n’ai jamais utilisé Visual studio, je me tien à votre disposition pour toutes les question que vous pourriez poser (si tant est que j’ai la réponse).
Bonjour, merci pour votre travail. C’est bien fait. J’ai cependant une question. Je ne vois pas trop comment une moitié du code sera en debug et l’autre en release sans le mrproper si on passe d’un mode à l’autre. Merci d’avance.
Bonjour,
Ce qu’il va se passer c’est que le système compile chaque fichier *.c quand celui-ci change, même si vous changez de mode de compilation DEBUG / RELEASE, le fichier en lui même n’aura pas changé et donc il ne sera pas recompilé. C’est comme cela qu’on peut obtenir une moitié du code dans un mode et pas l’autre.