Skyduino:~#
Articles
Corrigé, programmation, tutoriel

[Tuto] Makefile

Bonjour tout le monde !

S’il y a bien une chose que je fais h24 depuis plusieurs jours semaines c’est de compiler des programmes …
A tel point que ça en devient mon activité principale avec le debuggage …
(c’est en partie pour ça que je n’ai pas pu faire d’article depuis une grosse semaine)

Aujourd’hui je vais vous parler d’un truc super pratique pour compiler sans se prendre la tête : les makefiles !
J’en utilise à toute les sauces, que ce soit pour mes projets arduino, avr, arm, desktop, … vous allez comprendre pourquoi 😉

Qu’est ce qu’un makefile ?

Question bête mais légitime, pourquoi faire un makefile ?

Quand vous compilez un programme vous faites surement un truc dans ce genre :
gcc -Wall -o trucmachin trucmachin.c

Le tout avec une série de rm trucmachin, ./trucmachin et autres commandes en tout genre … pas très pratique pour compiler encore et encore à chaque modification !

C’est là que les makefile entre en jeux.
Pour faire simple, un makefile est une sorte de script shell intelligent (que les puristes m’excusent, je sais ce n’est pas du tout du shell) qui permet de compiler, nettoyer, etc tout son projet en une commande !

Traditionnellement avec un makefile on compile, fait le ménage et gère l’installation / archivage d’un programme (ou de plusieurs, je préfère appeler ça un « projet »).
Le tout avec une seule commande : make (et ses déclinaisons make clean, make install, …)

Quel intérêt à faire des makefile ?

Notre but en tant que bon informaticien c’est de travailler vite, efficacement et surtout … sans trop se fatiguer.
(c’est bien connu, l’informaticien est par définition un fainéant qui fera tout pour ne pas se casser la tête)

Imaginons un projet constitué des deux fichiers : main.c et lib.c
Si on ne modifie pas un fichier source, inutile de le compiler à chaque fois.
Ainsi on gagne du temps et le temps c’est de l’argent !

C’est le but des makefile, compiler uniquement ce qui est nécessaire pour gagner du temps.
Le tout en gérant les dépendances, les imbrications de makefile et les variables de compilation.

Le tout sans se prendre la tête 🙂

Comment écrire un makefile ?

La syntaxe de base est très simple :
cible: dépendances
(tabulation) commande(s) shell
(tabulation) commande(s) shell
(...)
(entré)

Les points clés à respecter :
– Chaque ligne de commandes DOIT être précédée d’une tabulation.
– La 1er « cible » (= règle) est exécutée par défaut si on ne précise pas de cible à la commande make

Par convention la 1er règle de compilation se doit d’être « all ».
Ainsi si une personne tente de compiler votre projet avec « make » ou « make all » (grands classiques) cela donnera le même résultat.

Reprenons l’exemple de tout à l’heure, en version makefile cela donnerait :
all: trucmachin

trucmachin: main.o lib.o
(tabulation) gcc -o trucmachin main.o lib.o

main.o: main.c
(tabulation) gcc -Wall -c -o main.o main.c

lib.o: lib.c
(tabulation) gcc -Wall -c -o lib.o lib.c

Il suffit maintenant de faire « make » pour compiler le code.
Le makefile va alors résoudre les dépendances (ici il va vérifier la présence des fichiers main.o et lib.o et les compiler si nécessaire) puis faire les commandes liées à la cible choisie.

Dans le cas où il n’y aurait aucun fichier .o voila comment le makefile procède :
1 – la cible all demande de faire la cible trucmachin avant
2 – la cible trucmachin demande de faire les cibles main.o et lib.o avant
3 – la cible main.o requière la présence du fichier main.c
4 – le fichier existe, mais le .o correspondant n’est pas à jour (il n’existe pas)
5 – les commandes de la cible main.o sont exécutées (on suppose que tout se passe bien lors de la compilation et que main.o est bien généré)
6/7/8 – même principe que pour main.o mais avec lib.o
9 – les deux cibles main.o et lib.o sont maintenant à jour, les commandes de la cible trucmachin sont exécutées (ici c’est une étape de linkage)
10 – la cible trucmachin est à jour, la cible all est à jour (pas de commandes), fin du makefile

Bon pour être franc ce makefile d’exemple est totalement inutile, en plus d’être mal fait 😉

Pourquoi ?
– le makefile ne gère pas le nettoyage des fichiers du projet (make clean)
– il ne permet pas de changer quoi que soit dans les options de compilation ou même de choisir le compilateur à utiliser
– il y a toujours les mêmes commandes pour différents fichiers, règle n°1 du développeur : « le copier coller, tu éviteras »

Les astuces pour faire de « beaux » makefile, simples et puissants

Astuce 1, suivre la structure classique :
– all : compile le projet
– clean : supprime les fichiers intermédiaire mais pas l’exécutable final
– install : installe le programme, ou plus généralement fait une archive des fichiers binaires / ressources
– help : affiche les cibles du makefile (c’est toujours pratique de savoir qui fait quoi)
– mrproper : fait un clean puis supprime l’exécutable final (c’est l’effet Monsieur Propre, on se retrouve avec un projet comme après le téléchargement des sources)

Astuce 2, utiliser les variables conventionnelles de compilations :
Dans le monde du makefile il existe plein de variables aux noms et aux utilisations bien définies :
– CC pour fixer le chemin pointant vers le compilateur C à utiliser
– CXX pour fixer le chemin pointant vers le compilateur C++ à utiliser
– CFLAGS ou CCFLAGS (j’ai vu les deux versions) pour spécifier des arguments supplémentaires lors de la compilation
– CXXFLAGS, même chose que CFLAGS mais pour le C++
– LFLAGS ou LDFLAGS (idem) pour spécifier des arguments supplémentaire lors de l’édition de liens
– EXEC pour fixer le nom de l’exécutable final
– OBJ pour fixer la liste des fichiers objets (.o)
– SRC pour fixer la liste des fichiers sources
et plein d’autre aux utilités divers et variées suivant le langage …

Note: vous n’êtes pas obligé d’utiliser ces variables c’est juste une convention, mais bon c’est mieux de faire avec.

Pour déclarer une variable :
nom=valeur

Pour ajouter un élément à une variable (une variable n’est rien d’autre qu’une série de chaine de caractères séparées par des espaces) :
nom+=valeur

Pour récupérer la valeur d’une variable :
$(nom)

Remarque: par défaut une variable inexistante est vide = 1 espace

Reprenons le makefile d’exemple :
CC=gcc
CFLAGS=-Wall
EXEC=trucmachin

all: trucmachin

trucmachin: main.o lib.o
(tabulation) $(CC) $(LDFLAGS) -o $(EXEC) main.o lib.o

main.o: main.c
(tabulation) $(CC) $(CFLAGS) -c -o main.o main.c

lib.o: lib.c
(tabulation) $(CC) $(CFLAGS) -c -o lib.o lib.c

install:
(tabulation) @echo "Pas de procédure d'installation nécessaire."

help:
(tabulation) @echo "Cibles disponible : all, install, clean, mrproper"

clean:
(tabulation) rm main.o lib.o

mrproper: clean
(tabulation) rm $(EXEC)

C’est mieux mais c’est pas encore ça !
Il reste encore des lignes qui font doublon et ça c’est pas propre du tout !

Les variables internes à make

Make dispose de plusieurs variables automatiques très pratique :
$@ Le nom de la cible en cours
$< Le nom de la première dépendance
$^ La liste des dépendances
$? La liste des dépendances plus récentes que la cible
$* Le nom du fichier sans suffixe

On peut donc d’ores et déjà améliorer notre makefile :
CC=gcc
CFLAGS=-Wall
EXEC=trucmachin

all: trucmachin

trucmachin: main.o lib.o
(tabulation) $(CC) -o $(EXEC) $^ $(LDFLAGS)

main.o: main.c
(tabulation) $(CC) $(CFLAGS) -c -o $@ $^

lib.o: lib.c
(tabulation) $(CC) $(CFLAGS) -c -o $@ $^

install:
(tabulation) @echo "Pas de procédure d'installation nécessaire."

help:
(tabulation) @echo "Cibles disponible : all, install, clean, mrproper"

clean:
(tabulation) rm main.o lib.o

mrproper: clean
(tabulation) rm $(EXEC)

Mais c’est toujours pas génial car il reste des lignes qui cette fois sont totalement identiques !

Remarque: en mettant un @ devant une commande celle ci devient silencieuse, en gros seul le résultat de la commande est affiché mais pas la commande en elle même.

Les cibles génériques, aussi appelés règles génériques

Reprenons ce morceau de l’exemple :
lib.o: lib.c
(tabulation) $(CC) $(CFLAGS) -c -o $@ $^

Pour faire lib.o il nous faut lib.c, de même pour faire main.o il nous faut main.c.
Vous voyez où se trouve le point commun ?

Pour tous .o il nous faut le .c correspondant, en makefile cela ce traduit par :
%.o: %.c
(tabulation) $(CC) $(CFLAGS) -c -o $@ $^

Pratique n’est ce pas ?
Oui mais … pas totalement !

Imaginons que nous ayons un fichier lib.h (chose classique pour une librairie d’avoir son fichier header).
Si on modifie lib.h il faut recompiler lib.c puisque que ce fichier en dépend !
Oui mais dans notre cible ne dépend que du .c et non du .h !
Du coup la modification du .h n’entraine pas la recompilation de fichier source qui en dépend … et c’est le drame.

Mais make gère ce problème, il suffit de passer en arguments les fichiers de dépendances à une règle spécialisée (non générique).

lib.o: lib.h

%.o: %.c
(tabulation) $(CC) $(CFLAGS) -c -o $@ $<

Problème réglé 🙂

Note personnelle: je précise quasiment jamais les dépendances de mes cibles avec les fichiers headers.
Quand je code un projet je fais toujours les fichiers .h en premier pour définir l’API de mon programme puis je n’y touche plus par la suite.
Ça permet de travailler à plusieurs sur un même projet sans que le non-avancement d’un membre ne bloque l’avancement d’un autre membre.
Mais dans l’idéal il faudrait que je le fasse systématiquement … ce serait une bonne résolution pour l’année 2013 à prendre 🙂

La cible .PHONY

Avant de vous parler des modificateurs de listes et des makefile conditionnels je voulais vous parler d’un truc que j’adore, les cibles .PHONY.

Un truc que je fait très souvent c’est des makefile « en profondeur ».
En gros, dans la racine du projet se trouve un makefile qui ne fait qu’appeler les makefiles spécialisés des sous dossiers et ainsi de suite.
C’est le principe utilisé par tout les « gros » projets, tel que buildroot par exemple (compilateur pour le kernel linux).

Chaque développeur fait son makefile spécialisé dans son coin en respectant une série de cibles prédéfinies et c’est le makefile de plus haut niveau qui se charge de l’appeler.
Le but étant de pouvoir travailler à plusieurs sur un même projet comme je le disais plus haut.

La cible .PHONY permet de toujours reconstruire une ou plusieurs cible.

Imaginons que j’ai un sous dossier (ou un fichier) « lib » du même nom que ma cible « lib », sans dépendances.
La cible lib ne sera jamais exécuté car « lib » (le dossier ou le fichier) sera toujours à jour !

La cible .PHONY est là pour régler le soucis, toutes les dépendances de .PHONY sont forcément reconstruites même si elle sont à jours, évitant ainsi les ambiguïtés entre nom de cible et dossier / fichier.

Remarque: pour appeler un sous makefile il existe plusieurs solutions :
– (cd sousdossier/ && $(MAKE) cible)
– make -C sousdossier
Je préfère de loin la seconde options qui permet d’exécuter une cible sur plusieurs sous dossiers de manière récursive.
(il est possible de mettre plusieurs -C à la suite, chaque -C étant relatif au -C précédent, c’est tiptop pour faire de la compilation récursive)

Les modificateurs de listes

Dans ma listes d’astuce concernant les makefile j’ai parler de deux variables : OBJ et SRC.

OBJ contient la liste des fichiers objets et SRC la liste des fichiers sources.
Comme pour les cibles on voit qu’il y a une règle de généricité, pour chaque .c on veut le .o correspondant !

Tout d’abord créons la liste des fichiers sources (facile) :
SRC=main.c lib.c

Pour générer la liste des fichiers objets make fourni une syntaxe :
OBJ=$(SRC:.c=.o)

Détails:
– pour chaque élément de SRC
– remplace l’extension .c par .o
– stocke dans OBJ

Reprenons notre makefile d’exemple :
CC=gcc
CFLAGS=-Wall
EXEC=trucmachin
SRC=main.c lib.c
OBJ=$(SRC:.c=.o)

all: trucmachin

trucmachin: $(OBJ)
(tabulation) $(CC) -o $(EXEC) $^ $(LDFLAGS)

lib.o: lib.h

%.o: %.c
(tabulation) $(CC) $(CFLAGS) -c -o $@ $<

install:
(tabulation) @echo "Pas de procédure d'installation nécessaire."

help:
(tabulation) @echo "Cibles disponible : all, install, clean, mrproper"

clean:
(tabulation) rm main.o lib.o

mrproper: clean
(tabulation) rm $(EXEC)

Vous avez remarqué l’astuce ?
trucmachin: $(OBJ)
En mettant la liste des fichiers objets en dépendance je m’assure qu’il seront compilés par la règle générique.

Voici quelques autres fonctions magiques bien pratique :
– les « wildcard » pour traiter toute une série de fichiers suivant un masque, exemple : $(wildcard *.c)
– les substituions de texte : $(subst from, to, text), exemple : $(subst « toto », « titi », « Moi c’est toto ! »)
– les suppressions de blanc : $(strip  » toto « )
Et plein d’autres, voir la doc complète :
http://www.gnu.org/software/make/manual/html_node/Text-Functions.html#Text-Functions

Les makefile conditionnels

Les makefile disposent aussi de plusieurs fonction proches de celles du pré-processeur C.

Les égalités de valeurs :
ifeq ($(CC), gcc)
(...)
else
(...)
endif

Les inégalités de valeurs :
ifneq ($(CC), gcc)
(...)
else
(...)
endif

Les définitions de variables :
ifdef toto
(...)
else
(...)
endif

Les non-définitions de variables (très pratique pour gérer des valeurs par défaut) :
ifdef toto
(...)
else
(...)
endif

Note: les conditions peuvent être (et sont même couramment) utilisées en complément des modificateurs de liste pour faire des compilations vraiment comme on le souhaite.

Voila qui conclut cette petite introduction aux makefile.

Si vous avez du courage en ces temps de fête et que vous voulez en savoir plus sur make, la doc vous tend les bras 😉
http://www.gnu.org/software/make/manual/html_node/index.html

Bon WE à tous et bon bidouillage 🙂

Publicités

Discussion

2 réflexions sur “[Tuto] Makefile

  1. Super tuto, merci.

    Publié par Philippe | 22 décembre 2012, 23 h 59 min
  2. Tuto sympa et clair .
    Un lien vers la doc et tous les horizons ont été touchés (contrairement à certains autres tutos) .
    Merci !
    P.S : mérite un meilleur référencement par Google !

    Publié par Egonyx | 19 juin 2013, 16 h 55 min

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion / Changer )

Connexion à %s

Skyduino devient Carnet du Maker

Le site Carnet du Maker remplace désormais le blog Skyduino pour tout ce qui touche à l'Arduino, l'informatique et au DIY.