Conduite de Projet en C sous GNU/Linux
PrécédentChapitre 4. L'environnement de programmation 

Le gestionnaire de compilation

Le gestionnaire de compilation make permet d'automatiser la phase de compilation, celle de votre programme mais aussi celle de la documentation associée, et permet aussi d'automatiser l'installation, la mise en paquetage pour distribuer plus facilement votre application, l'élimination de fichiers temporaires créés par votre éditeur de texte, le compilateur, etc...

Le gestionnaire de compilation permet aussi d'accélérer des recompilations successives lorsque vous travaillez sur les sources de votre projet. Plutôt que de toujours recompiler tous les fichiers sources, le gestionnaire détermine les sources à recompiler et n'exécute que les compilations nécessaires.

Prenons un exemple concret pour comprendre l'utilisation du gestionnaire de compilation. Supposons que les sources de votre programme soit composé de plusieurs sources en C : main.c, utils.c, utils.h, getopt.c, getopt.h et getopt1.c (les fichiers getopt.c, getopt.h et getopt1.c du projet GNU sont utilisés pour analyser les options passées sur la ligne de commande).

Le fichier main.c implémente la fonction principale main, le fichier utils.h définit divers utilitaires que le fichier utils.c implémente, le fichier getopt.h définit des fonctions d'analyse des options de la ligne de commande, le fichier getopt.c implémente les fonctions relatives aux options courtes et getopt1.c implémente les fonctions relatives aux options longues.

Si vous désirez créer l'exécutable de votre programme à partir de la ligne de commande, vous pourrez taper :


             gcc main.c utils.c getopt.c getopt1.c -o programme
     

Si vous travaillez sur les sources de votre projet et les recompilez souvent, vous remarquerez vite que cette commande peut être longue à s'exécuter, surtout si votre projet s'agrandit.

Il est alors préférable d'utiliser la compilation incrémentale, qui permet de recompiler seulement les fichiers sources que vous avez modifié depuis la dernière compilation. Lors de la première compilation incrémentale, vous devrez créer les fichiers objets pour chacun des fichiers source, puis relier ces fichiers objets, de cette manière :


             gcc -c main.c
             gcc -c utils.c
             gcc -c getopt.c
             gcc -c getopt1.c
             gcc main.o utils.o getopt.o getopt1.o -o programme
     

Si vous modifiez maintenant le fichier main.c, il suffit, pour recompiler votre programme, de taper :


             gcc -c main.c
             gcc main.o utils.o getopt.o getopt1.o -o programme
     

Ainsi, vous vous épargnez la recompilation des fichiers utils.c, getopt.c et getopt1.c qui n'était pas nécessaire.

Le gestionnaire de compilation make permet d'automatiser cette compilation incrémentale. Il vous suffit de taper make sur la ligne de commande, et le gestionnaire recherche les sources plus récentes que les objets associés (qu'il faut donc recompiler), les compile, puis lie tous les fichiers objets pour créer l'exécutable.

Pour ce faire, la commande make a besoin de conna^itre un certain nombre de choses :

Dans notre exemple,

Pour indiquer au gestionnaire de compilation toutes ces choses, il faut créer un fichier Makefile, dans le répertoire courant, contenant toutes ces informations.


             #
             # Fichier Makefile pour notre programme d'exemple
             #
     
             # On indique que si un des fichiers objet est modifié,
             # il faut recréer l'exécutable...
             programme: main.o utils.o getopt.o getopt1.o
                     # ... et la commande pour le recréer
                     gcc main.o utils.o getopt.o getopt1.o -o programme
     
             # On indique que si l'un des fichiers main.c, utils.h ou getopt.h
             # est modifié, il faut recréer main.o...
             main.o: main.c utils.h getopt.h
                     # ... avec cette commande
                     gcc -c main.c
     
             # même chose pour les autres fichiers objets
     
             utils.o: utils.c utils.h
                     gcc -c utils.c
     
             getopt.o: getopt.c getopt.h
                     gcc -c getopt.c
     
             getopt1.o: getopt1.c getopt.h
                     gcc -c getopt1.c
     
     

Un des grands principes de la programmation est de ne jamais écrire deux fois la même chose : car si vous vous trompez en l'écrivant ou changez d'avis après l'avoir écrit, il est préférable de n'avoir à le modifier qu'une seule fois.

On voit dans le fichier Makefile précédent que certaines choses sont réécrites plusieurs fois : par exemple la liste des fichiers objet, ou encore le compilateur utilisé (gcc).

Nous allons donc définir des variables, une pour la liste des fichiers objet, et une pour le compilateur utilisé.


             #
             # Fichier Makefile pour notre programme d'exemple
             #
     
             # Une variable définissant la liste des fichiers objet
             OBJS= main.o utils.o getopt.o getopt1.o
     
             # et une variable pour le compilateur
             CC= gcc
     
             programme: $(OBJS)
                     $(CC) $(OBJS) -o programme
     
             main.o: main.c utils.h getopt.h
                     $(CC) -c main.c
     
             utils.o: utils.c utils.h
                     $(CC) -c utils.c
     
             getopt.o: getopt.c getopt.h
                     $(CC) -c getopt.c
     
             getopt1.o: getopt1.c getopt.h
                     $(CC) -c getopt1.c
     
     

Étudions plus en détail la structure de ce fichier Makefile.

Tout d'abord, nous définissons des variables, que nous utiliserons plus tard dans le fichier.

Ensuite, nous définissons les dépendances entre les différents fichiers, et la manière de recréer un fichier si l'un des fichiers desquels il dépend a été modifié. Par exemple, le bloc


             programme: $(OBJS)
                     $(CC) $(OBJS) -o programme
     
     

indique que le fichier programme dépend des fichiers $(OBJS), et que si l'un des fichiers $(OBJS) est plus récent en date que le fichier programme, il faut recréer le fichier programme avec la commande $(CC) $(OBJS) -o programme.

Ces blocs sont appelés des règles. La partie à gauche de : est appelé la cible et la partie à droite les dépendances.

Des variables automatiques sont aussi définies par le gestionnaire de compilation. Trois exemples de variables automatiques sont les variables $^, $< et $@. La variable automatique ^ définit, jusqu'à la fin de la règle, la liste des dépendances, la variable automatique $< définit la première dépendance alors que la variable automatique $@ définit la cible.

On peut donc réécrire notre fichier Makefile de cette manière :


             #
             # Fichier Makefile pour notre programme d'exemple
             #
     
             OBJS= main.o utils.o getopt.o getopt1.o
             CC= gcc
     
             # Ici, $^ représente $(OBJS)
             # et $@ représente programme
             programme: $(OBJS)
                     $(CC) $^ -o $@
     
             # $< représente main.c
             main.o: main.c utils.h getopt.h
                     $(CC) -c $<
     
             utils.o: utils.c utils.h
                     $(CC) -c $<
     
             getopt.o: getopt.c getopt.h
                     $(CC) -c $<
     
             getopt1.o: getopt1.c getopt.h
                     $(CC) -c $<
     
     

Pour revenir à notre souci de ne jamais écrire deux fois la même chose, on voit dans notre dernier fichier Makefile que les règles pour les fichiers objets sont redondantes. En effet, toutes ces règles indiquent que pour créer un fichier objet fichier.o, il faut exécuter la commande $(CC) -c $<.

Le gestionnaire de compilation make permet de créer des règles implicites. Ces règles implicites permettent de définir, pour un certain type de fichier cible dépendant d'un certain type de fichier, la commande à exécuter pour créer le fichier cible à partir des dépendances.

Dans notre cas, il est avantageux de créer une règle implicite définissant la manière de créer un fichier objet dépendant d'un fichier source C.


             #
             # Fichier Makefile pour notre programme d'exemple
             #
     
             OBJS= main.o utils.o getopt.o getopt1.o
             CC= gcc
     
             programme: $(OBJS)
                     $(CC) $^ -o $@
     
             %.o: %.c
                     $(CC) -c $<
     
             main.o: main.c utils.h getopt.h
             utils.o: utils.c utils.h
             getopt.o: getopt.c getopt.h
             getopt1.o: getopt1.c getopt.h
     
     


PrécédentDébut du document 
L'environnement de programmationDébut du chapitre