Introduction aux makefiles
Un article de GuruMed.
par Olivier 'StAn' Fabre.
Sommaire |
Le problème
Tant qu'on écrit de petits programmes qui se compilent vite, on peut se contenter d'un unique fichier source par programme, et taper une ligne de commande dans le shell pour les compiler. Cependant, plus les programmes deviennent grands et complexes, plus il devient important de séparer ses sources en plusieurs fichiers afin de les clarifier, éventuellement permettre la réutilisation de certaines parties telles-quelles dans d'autres projets, et diminuer les temps de compilation. Un besoin d'automatisation se fait alors sentir pour ne pas avoir à taper une ligne de commande par fichier.
La solution
Si vous utilisez un IDE comme StormC, ce problème ne devrait pas se poser car l'IDE s'occupe de tout (ou presque). Avec un compilateur en ligne de commande comme GCC ou VBCC, vous pouvez utiliser la commande make pour exécuter des makefiles automatisant la compilation de votre projet. En plus de n'avoir qu'une ligne à taper dans le shell pour construire l'exécutable, cela permet de ne compiler que les fichiers source ayant été modifiés depuis la dernière compilation, ce qui diminue grandement le temps de compilation sur les gros projets.
Les outils
Il existe différentes variantes de make, dont notamment: smake livré avec SAS/C, dmake livré avec Dice C, GNU make version GeekGadgets et d'autres portages de ce même GNU make pour Amiga que l'on peut trouver sur Aminet.
Je ne parlerai pas de smake et dmake qui ont leur propre syntaxe, et sont peu utilisés (ou presque introuvable légalement en ce qui concerne smake).
Deux versions de GNU Make seront abordées:
- Celle de GeekGadgets : make-3.77-bin.tgz (info)
- La version 3.76.1 dispo sur Aminet: make_bin.lha (readme)
La version de GeekGadgets a besoin de l'ixemul.library, mais pas celle d'Aminet. D'autres différences entre ces deux versions de make seront mentionnées un peu plus bas.
Le contenu d'un makefile
Ce qu'il faut mettre dans un makefile dépend grandement de votre projet. Un makefile peut servir à bien d'autres chose que la compilation d'un programme ! Mais nous n'en parlerons pas ici :-). Commençons par réaliser un makefile très simple destiné à compiler un programme composé de trois fichiers source.
Voici les fichiers source :
toto.c
#include "titi.h"
int main( void )
{
ma_jolie_fonction();
return 0;
}
titi.c
#include <stdio.h>
#include "titi.h"
void ma_jolie_fonction( void )
{
puts("bonjour");
}
titi.h
void ma_jolie_fonction( void );
Si vous vous demandez la raison de l'inclusion de titi.h dans titi.c, sachez que c'est pour être sûr que le prototype de ma_jolie_fonction() se trouvant dans le .h est bien conforme à la définition de la fonction dans le .c. En effet, il arrive parfois que l'on modifie les paramètres d'une fonction dans un fichier en oubliant de les modifier dans l'autre...
Et voici le makefile correspondant :
# makefile pour créer programme_executable # à partir de toto.c et titi.c programme_executable : toto.o titi.o vc -o programme_executable toto.o titi.o toto.o : toto.c titi.h vc -c -o toto.o toto.c titi.o : titi.c titi.h vc -c -o titi.o titi.c
Les '#' servent aux commentaires. Pour le reste, nous avons ici trois règles. Les règles sont la base d'un makefile. Elle sont de la forme:
cible(s) : dépendance(s) commande ...
Une cible est souvent le nom d'un fichier à générer, comme ici "programme_executable", "toto.o" et "titi.o"; cependant il peut aussi s'agir du simple nom d'une action à effectuer. Nous verrons cela par la suite.
Une dépendance est le nom d'un fichier dont dépend la cible de cette règle. Si la cible est un fichier, alors la règle ne sera exécutée que si une des dépendances est plus récente que la cible. Par exemple, ici, la première règle ne sera exécutée que si "toto.o" ou "titi.o" ont une date plus récente que "programme_executable".
Quand aux commandes, il s'agit simplement de commandes telles qu'on pourrait les taper dans un Shell AmigaDOS. Il peut y avoir plusieurs commandes par règle, sur plusieurs lignes consécutives, tout comme il peut n'y avoir aucune commande.
Attention: les lignes de commande doivent commencer par une tabulation (et pas par des espaces !). Pensez-y si vous utilisez un vieil éditeur de textes pourri comme GoldEd 5 ;-).
Différences entre le make de GeekGadgets et celui d'Aminet:
Le make de GG lance chaque commande dans un shell Unix "sh", tandis que le port dispo sur Aminet utilise un shell AmigaDOS normal. En pratique, cela a surtout une influence sur l'utilisation des jokers.
Si vous voulez par exemple lancer la commande suivante:
delete #?.o
il n'y aura pas de problème avec le make d'Aminet; mais cela ne marchera pas avec celui de GG. Pour ce dernier, il vous faudra écrire:
delete \#\?.o
De plus, le make de GG ne reconnait pas les chemins d'accès du type "VOLUME:chemin". Il faut écrire "/VOLUME/chemin", et ceci aussi bien dans les cibles que les dépendances et les commandes.
Les règles en détail
Examinons maintenant une par une les règles de notre makefile :
programme_executable : toto.o titi.o
vc -o programme_executable toto.o titi.o
Cette règle dit à make comment générer la cible "programme_executable". Les dépendances étant "toto.o" et "titi.o", la commande ne sera exécutée que si au moins un de ces deux fichiers est plus récent que "programme_executable", ou bien sûr si ce dernier n'existe pas encore.
Quand à la commande, c'est un simple appel à vc (le frontend de VBCC) servant à linker "toto.o" avec "titi.o" pour former l'exécutable.
Passons à la deuxième règle :
toto.o : toto.c titi.h vc -c -o toto.o toto.c
Celle-ci décrit la façon de générer "toto.o". Elle sera déclenchée si "toto.c" ou "titi.h" sont plus récents que "toto.o", c'est à dire si les fichiers source ont été modifiés depuis la dernière compilation. C'est très intéressant car cela signifie que seuls les fichiers modifiés seront recompilés, évitant par là une grosse perte de temps.
Puisque le fichier "toto.c" inclut "titi.h", "toto.o" peut changer si l'un ou l'autre de ces fichiers source a été modifié. C'est pourquoi ils sont tous deux dans les dépendances.
La commande "vc -c -o toto.o toto.c" crée "toto.o" à partir de "toto.c". Le "-c" dit à VBCC de ne pas générer d'exécutable, mais au contraire de juste générer le fichier objet (GCC fonctionne pareil).
La troisième et dernière règle est identique à celle que nous venons de voir, hormis les noms de fichiers qui diffèrent.
Vous pouvez essayer ce makefile en tapant tout simplement "make" dans un shell, en étant bien sûr dans le répertoire où se trouvent le makefile et les différents fichiers source. make devrait lancer la compilation de "toto.o", puis de "titi.o", puis finalement le link de ces deux fichiers pour obtenir "programme_executable".
Améliorations
Ce makefile est parfaitement fonctionnel, mais il a néanmoins plusieurs défauts :
Il ne fonctionne qu'avec VBCC, et le passer à un autre compilateur demande de changer plusieurs lignes de commande. Il y a des répêtitions. Si on change le nom d'une cible, il faut aussi le changer dans la ligne de commande associée. Pareil pour une dépendance. De même, chaque fichier objet ("toto.o", "titi.o") est généré par une ligne de commande similaire aux autres fichiers objets (seul les noms de fichier changent). Pourtant, on est obligé de la réécrire pour chaque fichier.
Ces défauts ne sont pas vraiment importants pour un aussi petit makefile, mais il deviendraient gênants lors d'un travail sur un projet de dizaines de fichiers source.
Nous allons améliorer ce makefile en utilisant des variables et une règle dite "de motif" (pattern rule en anglais).
# makefile pour créer programme_executable # à partir de toto.c et titi.c CC = vc programme_executable : toto.o titi.o $(CC) -o $@ $^ %.o : %.c $(CC) -c -o $@ $< toto.o : toto.c titi.h titi.o : titi.c titi.h
La première variable rajoutée ici est CC. C'est la variable standard utilisée pour représenter le nom du compilateur. Nous lui donnons donc tout naturellement la valeur "vc" pour utiliser VBCC.
Pour ensuite utiliser cette variable, nous écrirons "$(CC)". Chaque occurence de "$(CC)" sera remplacée par "vc".
Passons à la première règle :
programme_executable : toto.o titi.o
$(CC) -o $@ $^
La cible et les dépendances n'ont pas changé; par contre la ligne de commande utilise maintenant trois variables. Nous avons déjà vu "$(CC)". Les deux autres, "$@" et "$^", sont des variables dites automatiques car elles sont créées automatiquement par make.
"$@" va tout simplement être remplacé par la cible de la règle en cours (ici, "programme_executable").
"$^" va elle être remplacé par toutes les dépendances (ici, "toto.o titi.o").
Cette ligne est donc équivalente à :
vc -o programme_executable toto.o titi.o
La deuxième règle introduit la notion de règle implicite :
%.o : %.c vc -c -o $@ $<
Cette règle (de type "règle de motif" ou pattern rule) défini une règle implicite disant à make comment créer un fichier ".o" à partir de son ".c" correspondant.
Nous avons déjà vu la variable automatique "$@", qui sera remplacée par la cible, c'est à dire ici le fichier .o.
"$<" est aussi une variable automatique, et sera elle remplacée par la première dépendance, c'est à dire le fichier .c.
Les deux règles suivantes sont les mêmes que dans la première version du makefile, mais sans les commandes associées :
toto.o : toto.c titi.h titi.o : titi.c titi.h
En l'absence de commande spécifique, make va utiliser la règle implicite que nous venons de définir. Ces deux règles couplées à la règle de motif donnent donc un résultat identique aux deux dernières règles du premier makefile.
Avec ce nouveau makefile, vous pouvez :
Changer de compilateur en modifiant une seule ligne ("CC = gcc"). Ajouter un fichier objet en ajoutant une seule ligne ("objet : dépendances").
A propos des dépendances, il peut être assez fastidieux de les maintenir quand on commence à avoir de nombreux fichiers source. Vous pourrez alors utiliser un outil pour les générer automatiquement. Si vous utilisez GCC, vous pouvez vous en servir avec l'option -M. Sinon, LFMakeMaker de Laurent Faillie fera l'affaire (lfmakema.lha (readme) sur Aminet). Tapez simplement "LFMakeMaker #?.c" dans le shell pour avoir vos dépendances.
Nettoyage
Une cible "clean" est généralement incluse dans un makefile pour effacer tous les fichiers objet, afin de forcer la recompilation de tout le projet :
clean : delete #?.o
(attention, vous devez écrire "\#\?.o" si vous utilisez le make de GeekGadgets)
Comme vous pouvez le constater, une règle n'a pas forcément de dépendance, et sa cible n'est pas forcément un nom de fichier.
Pour exécuter cette règle, vous n'avez qu'à taper "make clean" dans le shell (en l'absence d'argument, make exécute par défaut la première règle du makefile).
Le mot de la fin
Cette introduction devrait vous avoir appris le nécessaire pour faire des makefiles basiques, permettant de compiler des programmes composés de nombreux fichiers source sans souci.
Il reste cependant de nombreuses possibilités d'extension : faire un makefile qui gère plusieurs compilateurs, des options de compilation différentes (avec ou sans débug, pour 68k ou PPC...), la création de l'archive lha contenant votre programme et sa doc, etc.
Si le besoin s'en fait sentir, un deuxième article sur les makefiles pourrait voir le jour; mais en attendant vous pouvez examiner les makefiles qui vous passeront sous les yeux et consulter la doc AmigaGuide fournie avec le make de GeekGadgets pour plus de précisions.
Le forum de GuruMed est évidemment lui aussi prêt à recevoir vos questions ;-).
