Pied à l'étrier pour les Makefile avec GNU Make (gmake)
Posté le 02/08/2017 00:59
Sous les OS descendant d'Unix tels que Linux, OS X, Solaris, et autres, pour construire un projet, nous utilisons ce que nous appelons des Makefile.
L'idée derrière eux, c'est de faire un livre de cuisine : on décrit comment, à partir d'ingrédients, on arrive à un résultat, pour que lorsque l'utilisateur demande le résultat, l'utilitaire de construction, ici GNU Make, sache ce qu'il doit faire pour parvenir à ce résultat, et de quels ingrédients il a besoin.
Le principe dans un cas préçis : construction d'un add-in
Commençons par décrire le principe dans un cas bien précis, celui qui nous intéresse sur ce site : la construction d'un add-in. L'add-in est au format G1A, un format propriétaire de CASIO, pour le construire, nous allons avoir besoin d'un fichier binaire contenant les données à mettre dans l'add-in, et un utilitaire,
g1a-wrapper, que vous avez normalement installé avec
le reste.
Sauf que nous avons des fichiers C et des fichiers assembleurs (source). Comment produire ce fichier binaire à partir de fichiers ? Nous savons que nous pouvons compiler les fichiers C et assembleurs (source) en fichiers objets à l'aide du compilateur C (
sh3eb-elf-gcc) et de l'assembleur (
sh3eb-elf-as). Ces fichiers objets contiennent du binaire avec symboles (au format ELF), et nous voulons les assembler en un fichier binaire pour la mise au format de l'add-in.
Malheureusement, cela ne se fait pas en une passe : il faut d'abord produire un fichier ELF à partir de tous les objets, puis "retirer" les métadonnées dans le fichier ELF (d'où la production d'un fichier intermédiaire "addin.elf").
Nous avons toutes nos règles, et l'enchaînement est bon : nous savons comment faire des fichiers objet à partir des fichiers C et assembleur, le fichier ELF de ces fichiers objet, le fichier binaire de ce fichier ELF, puis l'add-in à partir de ce fichier binaire. Maintenant, il va falloir mettre ça en forme !
Comment on écrit une règle (recette) ?
Chaque règle contient trois éléments : élément à construire, dépendances, commandes liées à la règle. Dans un Makefile, cela se présente de la façon qui suit :
fichier_à_produire: ce_fichier_utile_là ce_fichier_aussi
commande --produit-un-fichier-avec-le-nom="fichier_à_produire" \
ce_fichier_utile_là ce_fichier_aussi
À noter que GNU Make n'aime pas vraiment les fichiers dont le nom contient des espaces, donc je vous conseille vraiment d'éviter ça.
Commençons par l'add-in final : le fichier à produire (résultat), c'est le G1A, ce dont nous avons besoin (ingrédients), c'est le fichier binaire, et le contenu de la recette, c'est l'utilisation de
g1a-wrapper ! Voilà ce que ça donne :
addin.g1a: addin.bin
g1a-wrapper -o addin.g1a -n ADDIN --version="12.34.0000" \
-i chemin/vers/icone.bmp --date="2017.1231.2359"
Au passage, je vous déconseille fortement d'utiliser les quatre derniers chiffres de la version (ils ont une signification spéciale).
Y a plein de trucs qui se répètent, y a pas moyen de faire des variables ?
Si, il est possible d'utiliser des variables ! Il faut les définir en dehors du contenu des règles. Pour cela, faites simplement :
NOM_VARIABLE = contenu de la variable
Ici, des variables utiles à définir pour notre add-in seraient par exemple :
NOM = ADDIN
VERSION = 12.34
ICONE = chemin/vers/icone.bmp
DATE = 2017.1231.2359
Pour les utiliser, il faudra néanmoins utiliser des parenthèses, par exemple,
$(NOM), et
pas $NOM.
Au-delà de ces variables définies par l'utilisateur, il y a des variables spéciales définies par GNU Make, dites "automatiques". Elles sont définies pour les instructions dans les règles. Parmi
d'autres, nous avons :
-
$@ : le nom de la cible (résultat), par exemple
addin.g1a ;
-
$^ : le nom des dépendances (ingrédients), par exemple
addin.bin ;
-
$< : le nom de la première dépendance.
Nous pouvons donc améliorer notre règle pour qu'elle devienne :
$(NOM).g1a: $(NOM).bin
g1a-wrapper -o $@ -n $(NOM) --version="$(VERSION).0000" \
-i $(ICONE) --date="$(DATE)"
Ainsi, vous aurez moins d'emplacements à changer à chaque fois.
Et si je veux faire des trucs un peu plus compliqués, y a des fonctions ?
Y en a pas mal avec GNU Make. On citera par exemple
$(wildcard pattern), qui permet d'évaluer une wildcard (chemins avec des étoiles '*' et autres métacaractères) pour récupérer plusieurs fichiers. Par exemple, on peut faire
$(wildcard source/*.c) pour récupérer tous les fichiers C dans le dossier
source/.
Pour le reste, ceci est un tutoriel rapide et n'a pas vocation à devenir une référence complète : vous saurez chercher par vous-mêmes
Y a des trucs cool à faire avec les dépendances ?
L'idée avec les dépendances, c'est que si un fichier n'existe pas dedans, ça va faire une erreur, et si un fichier a été mis à jour depuis la dernière construction, ça va refaire le fichier source, et tout ce qui dépend de ce fichier source (donc seulement les fichiers objets concernés, et l'ELF/binaire/add-in).
Donc il est possible de faire une règle du type
truc.o: truc.c machin.h pour que si
machin.h est mis à jour, ça considère que ça doive reconstruire
truc.o (ce qui est une bonne chose, puisque des structures ou autres choses prototypes ont peut-être été mis à jour !).
Conclusion
L'usage des commandes de compilation, de production des fichiers objet, du fichier binaire, et de l'add-in en lui-même, c'est
sur le tutoriel d'installation du cross-compilateur, étapes 7 et 8.
gint requiert des commandes propres, et c'est probablement renseigné sur le topic de celui-ci.
À partir d'ici, vous avez les armes de base pour décider comment vous allez organiser votre projet. Vous devriez avoir ce qu'il faut pour avoir tout dans le même dossier, mais peut-être voudrez-vous avoir un dossier
src/ ou
source/ pour séparer les sources du reste ? Peut-être avec un dossier
obj/ ou
objets/ pour séparer les objets des sources, histoire que ça fasse moins cracra ? De là, vous avez le contrôle et la documentation, je vous fais confiance
Citer : Posté le 02/08/2017 12:13 | #
Merci pour le tutoriel
J'ai deux trois questions qui vont venir au fur et à mesure. J'en ai déjà 2:
- Pour ce qui est de la manipulation des variables, si je veux lister tout les .o en fonction des .c, est-ce que je peux faire quelque chose de ce style:
OBJ= $(SRC:.c=.o,scr=build)
- Pour les dépendances, si j'ai par exemple un projet avec un main.c, main.h et const.h qui est inclut dans main.h et qui stocke des variables importantes, est-ce que si je modifie le fichier const.h et que mon Makefile a une instruction de ce type:
blabla
le fichier const.h est compté dans les dépendances ? (bon en fait en écrivant ça j'ai une petite idée mais histoire d'être sûr)
Ajouté le 02/08/2017 à 12:20 :
Ah et pour la date, c'est du troll ou c'est vraiment le format genre mmhh:ddmm:yyyy ?
Citer : Posté le 02/08/2017 12:31 | #
Pour ce qui est de la manipulation des variables, si je veux lister tout les .o en fonction des .c, est-ce que je peux faire quelque chose de ce style:
OBJ= $(SRC:.c=.o,src=build)
Pas directement. Je te conseille de faire ça :
SRC = $(SRC:src/%=%)
OBJ = $(SRC:%.c=build/%.o)
Ici, SRC contiendra par exemple truc.c machin.c, et OBJ contiendra build/truc.o build/machin.o.
Pour les dépendances, si j'ai par exemple un projet avec un main.c, main.h et const.h qui est inclut dans main.h et qui stocke des variables importantes, est-ce que si je modifie le fichier const.h et que mon Makefile a une instruction de ce type :
blabla
le fichier const.h est compté dans les dépendances ? (bon en fait en écrivant ça j'ai une petite idée mais histoire d'être sûr)
GNU Make n'explore pas directement les sources C pour voir les fichiers qu'ils incluent. Y a moyen de faire ça avec GCC, mais je conseille pas à votre niveau : contentez-vous de faire en sorte que n'importe quel fichier header (.h) modifié fasse recompiler tous les fichiers C.
Ah et pour la date, c'est du troll ou c'est vraiment le format genre mmhh:ddmm:yyyy ?
J'ai fait ça de tête, et j'aurais pas dû. Le format c'est YYYY.MMDD.HHmm. J'ai corrigé le tutoriel.
Mon blog ⋅ Mes autres projets
Citer : Posté le 02/08/2017 14:33 | #
Merci de ton aide
Bon voilà donc le fichier auquel j'ai abouti:
VERSION= 1.0
ICONE= moisland.bmp
DATE= 02.08.2017
HDR = $(wildcard src/*.h)
HDR = $(HDR:src/%=%)
SRC = $(wildcard src/*.c)
SRC = $(SRC:src/%=%)
OBJC = $(SRC:%.c=%.o)
IMG = $(wildcard assets/image-*.bmp)
IMG = $(IMG:assets/%=%)
OBJI = $(IMG:%.bmp=%.bmp.o)
FNT = $(wildcard assets/font-*.bmp)
FNT = $(FNT:assets/%=%)
OBJF = $(FNT:%.bmp=%.bmp.o)
$(NOM).g1a: build/$(NOM).bin
g1a-wrapper $< -o $@ -i $(ICONE) --version="$(VERSION).0000"
build/$(NOM).bin: build/$(NOM).elf
sh3eb-elf-objcopy -R .comment -R .bss -O binary $< $@
build/$(NOM).elf: build/$(OBJC) build/$(OBJI) build/$(OBJF)
sh3eb-elf-gcc $^ -o $@ `fxsdk --cflags --libs`
build/%.o: src/%.c src/$(HDR)
sh3eb-elf-gcc -c $^ -o $@ `fxsdk --cflags`
build/%.bmp.o: assets/%.bmp
ifeq ($(findstring font, %),)
fxconv -image $< -o $@ -n assets_%
else
fxconv -font $< -o $@ -n assets_%
endif
.PHONY: clean mrproper
clean:
rm -rf build/*.o
rm -rf build/*.elf
rm -rf build/*.bin
mrproper: clean
rm -rf $(NOM).g1a
Quand je lance la commande make, j'ai une erreur: "Makefile:9: *** La variable récursive « SRC » se référence elle-même (à la fin). Arrêt."
Si je comprend bien ça vient de la modification de la variable à partir d'elle même mais bon ...
Sinon dans l'ensemble, cela vous semble-t-il correct ?
J'ai notamment un doute sur la variable %, il faut mettre $(%) ?
Citer : Posté le 02/08/2017 14:59 | #
La version doit vraiment être au format MM.mm.0000, donc il faut faire 01.00 au lieu de 1.0. N'oublies pas d'utiliser ta variable DATE avec g1a-wrapper !
Pour ton erreur, en fait, il y a différents types d'affectations, j'en ai pas parlé et je me suis dit que GNU Make comprendrait, mais apparemment non.
= c'est une affectation avec référence, donc par exemple, VERSION = $(TRUC) ça lira TRUC au moment de l'évaluation de VERSION, donc plus tard, i.e. si tu changes TRUC après, ça prendra en compte. Mais du coup, ici, il vaut mieux utiliser :=, qui évalue les variables référencées à l'affectation et non plus tard.
Pour les variables automatiques, tu peux faire $<caractère>, par exemple $@.
Mon blog ⋅ Mes autres projets
Citer : Posté le 02/08/2017 17:09 | #
Merci Cake pour ce tutoriel, même si mes makefiles sont (déjà) faits, c'est toujours bien d'avoir quelques explications complémentaires à côté
Citer : Posté le 02/08/2017 19:17 | #
Bon du coup j'ai presque fini, simplement il me reste une erreur:
g1a-wrapper: warning: version string "01.00.0000" does not have expected format 'MM.mm.pppp'
g1a-wrapper: warning: date string "2017.0101.0000" does not have expected format 'yyyy.MMdd.hhmm'
La j'avoue que je vois pas trop où il veut en venir
Citer : Posté le 02/08/2017 19:21 | #
Ce n'est pas une erreur, c'est juste Lephenixnoir qui a la flemme de corriger. Je lui ai report ça il y a quelques mois, et c'est toujours là apparemment. Mais ne t'inquiètes pas, le g1a est bien produit !
Mon blog ⋅ Mes autres projets
Citer : Posté le 02/08/2017 19:22 | #
Dac impec alors
Merci
Ajouté le 03/08/2017 à 23:45 :
Rebonjour,
Alors voilà un nouveau problème que je comprend pas trop sur un Makefile:
VERSION= 01.00
ICONE= MainIcon.bmp
DATE= 2017.0101.0000
HDR = $(wildcard src/*.h)
HDR := $(HDR:src/%=%)
SRC = $(wildcard src/*.c*)
SRC := $(SRC:src/%=%)
OBJC := $(SRC:%=%.o)
IMG = $(wildcard assets/image_*.bmp)
IMG := $(IMG:assets/%=%)
OBJI := $(IMG:%.bmp=%.bmp.o)
FNT = $(wildcard assets/font_*.bmp)
FNT := $(FNT:assets/%=%)
OBJF := $(FNT:%.bmp=%.bmp.o)
$(NOM).g1a: build/$(NOM).bin
g1a-wrapper $< -o $@ -i $(ICONE) --version='$(VERSION).0000' --date='$(DATE)'
build/$(NOM).bin: build/$(NOM).elf
sh3eb-elf-objcopy -R .comment -R .bss -O binary $< $@
build/$(NOM).elf: build/$(OBJC) $(if $(OBJI), build/$(OBJI)) $(if $(OBJF), build/$(OBJF))
sh3eb-elf-gcc $^ -o $@ `fxsdk --cflags --libs`
build/%.c.o: src/%.c $(if $(HDR), src/$(HDR))
sh3eb-elf-gcc -c $< -o $@ `fxsdk --cflags`
build/%.cpp.o: src/%.cpp $(if $(HDR), src/$(HDR))
sh3eb-elf-gcc -c $< -o $@ `fxsdk --cflags`
build/%.bmp.o: assets/%.bmp
ifeq ($(findstring font, $<),)
fxconv -image $< -o $@ -n $(<:assets/%.bmp=assets_%)
else
fxconv -font $< -o $@ -n $(<:assets/%.bmp=assets_%)
endif
.PHONY: clean mrproper
clean:
rm -rf build/*.o
rm -rf build/*.elf
rm -rf build/*.bin
mrproper: clean
rm -rf $(NOM).g1a
En fait ce qui m'espante quand je le lance c'est qu'il me donne l'erreur suivante:
alors que le fichier est bel et bien présent au milieu des autres fichiers sources, et que la même commande dans la console directement marche... (sh3eb-elf-gcc -c src/syscall.c -o build/syscall.c.o fxsdk --cflags)
Citer : Posté le 04/08/2017 00:37 | #
Les règles avec patterns ('%') ne matchent que si les dépendances existent.
Essaies de faire $(info $(if $(HDR), src/$(HDR))) juste avant la règle pour build/%.c.o, et regardes si les fichiers existent bien (si ta variable est bonne).
J'en profite pour dire que gint n'offre pas le support pour le C++, donc inutile d'ajouter les fichiers .cpp pour le moment.
Mon blog ⋅ Mes autres projets
Citer : Posté le 04/08/2017 10:15 | #
Tu peux parfaitement compiler du C++ avec gint, à condition de faire mon boulot à ma place (c'est-à-dire ajouter les extern "C" {} où il faut dans les headers). Il n'y a certes pas de STL, pas de string, pas même de new/new[]/delete/delete[], mais on peut compiler du C++ normalement.
Après, Smash' m'avait parlé de problèmes sérieux sur ces points, donc je déconseille de le faire. Mais si vous êtes forts et que vous voulez creuser un peu, c'est parfaitement possible.
Citer : Posté le 04/08/2017 11:59 | #
Bon en effet il y a un bien un problème à cet endroit là. En fait le truc c'est que mettre le chemin avec src/ en faisant src/$(HDR) ne s'applique qu'au premier header et non pas aux autres Mais bon la je sais pas comment faire pour éviter ça.
Citer : Posté le 04/08/2017 12:05 | #
Citer : Posté le 04/08/2017 12:07 | #
Ou tout simplement $(HDR:%=src/%). Les substitutions, c'est joli. (ou plus indirectement, patsubst, mais c'est substentiellement la même chose)
Mon blog ⋅ Mes autres projets
Citer : Posté le 12/08/2017 23:06 | #
CMake
Citer : Posté le 12/08/2017 23:07 | #
-20 VDD
Citer : Posté le 17/08/2017 23:43 | #
Mais pourquoi ? Cmake est super bien
Citer : Posté le 17/08/2017 23:45 | #
N'hésites pas à en faire un tutoriel dans ce cas ! De préférence persistent, après, c'est toi qui vois
Mon blog ⋅ Mes autres projets
Citer : Posté le 17/08/2017 23:49 | #
Flemme de réfléchir, alors je cite.
« cmake (written in C++) - so huge and bloated, compilation takes longer than compiling GCC (!). It’s not even possible to create freestanding Makefiles, since the generated Makefiles call back into the cmake binary itself. »
Citer : Posté le 19/08/2017 20:52 | #
@Cake: déso, pas déso, j'ai trop d'boulot
@breizh: Cmake met moins de temps pour compiler que GCC ( Et encore moins que cette fils de ****rie de VS Studio de sa maman la s***** ) et y a pas besoin de créer une makefile custom vu que Cake s'en charge. C'est comme dire qu'un char d'assaut n'a pas de mitraillette Et il permet d'avoir une compilation multiplateforme optimale et le plus important, IL AFFICHE LA PROGRESSION DE LA COMPILATION *0*
Citer : Posté le 19/08/2017 22:59 | #
Nan mais Intel, t'as pas du comprendre ce que ça implique x)
« It’s not even possible to create freestanding Makefiles, since the generated Makefiles call back into the cmake binary itself »
En gros tu peux pas partager ton projet à quelqu'un qui n'a pas cmake. C'est dommage…
Après, le multiplateforme osef, le fxsdk est dispo uniquement sous Gnunux.
Et la progression, vu que ça compile rarement plus de 10 secondes pour un addin, voilà quoi.
On en revient au même point : pourquoi une usine à gaz pour un truc trivial ? Après, chacun a sa propre réponse, mais moi je suis plutôt du style à utiliser l'outil le plus adapté