Makefile - Introduction Au Langage C

Fiche de cours - Makefile - Une introduction simple (et funky)

Warning

Cette fiche contient en fait bien plus qu'une introduction aux Makefile. Si vous êtes tombés dessus par hasard, nous espérons que vous ne vous êtes pas faits mal. Si vous débutez votre apprentissage du C, nous vous conseillons fortement de potasser le début de cette fiche jusqu'à apprendre comment écrire un Makefile très simple, puis de la laisser de côté pour le moment. Vous aurez toujours le temps d'y revenir plus tard pour bien comprendre comment écrire des Makefile qui permettent de compiler des projets de plusieurs centaines de fichiers en 3 lignes et demi.

Un Makefile est un fichier, utilisé par le programme make, regroupant une série de commandes permettant d’exécuter un ensemble d’actions, typiquement la compilation d’un projet. Un Makefile peut être écrit à la main, ou généré automatiquement par un utilitaire de plus haut niveau comme par exemplecmake, automake ou gmake.

Un Makefile est constitué d’une ou de plusieurs règles de la forme :

1 2cible:dépendances commandes

Lors du parcours du fichier, le programme make évalue d’abord la première règle rencontrée, ou celle dont le nom est spécifié en argument. L’évaluation d’une règle se fait récursivement :

  1. Les dépendances sont analysées et évaluée : si une dépendance est la cible d’une autre règle, cette règle est à son tour évaluée ;

  2. Lorsque l’ensemble des dépendances a été analysé, et si la cible est plus ancienne que les dépendances, les commandes correspondant à la règle sont exécutées.

Commande : make

Un fichier dénommé Makefile ayant été écrit, on utilise la commande make sans argument pour lancer l'évaluation de la première règle rencontrée :

1make

Options utiles :

  • -n : affiche l'ensemble des commandes qui seraient exécutées, mais sans les exécuter réellement.

  • -d : affiche des informations de débogage en plus du traitement normal. Très verbeux, à utiliser quand vous ne trouvez pas le problème de votre Makefile. Les informations de débogage indiquent quels fichiers sont évalués pour la reconstruction, quelles dates de fichiers sont comparées et avec quels résultats, quels fichiers ont réellement besoin d’être recréés, quelles règles implicites sont prises en compte et lesquelles sont appliquées - bref, tout ce qu’il y a d’intéressant à savoir sur la manière dont make décide de ce qu’il doit faire.

Exemple de Makefile

Ce qui suit présente la création d’un Makefile pour un exemple de projet. Supposons pour commencer que ce projet regroupe trois fichiers exemple.h, exemple.c et main.c.

Compilation séparée

Pour compiler l'exemple étudié, une ligne de commande basique serait par exemple :

1gcc-omon_executableexemple.cmain.c-std=c99-Wall-Wextra

Cette ligne de commande compile les 2 fichiers sources en fichiers objets et génère le programme exécutable final en faisant l'édition de liens. L'inconvénient est qu'on (re)compile systématiquement exemple.c et main.c même si l'un d'eux n'a pas été modifié.

Pour compiler séparément, on procède de la façon suivante :

1 2 3 4 5 6 7 8 9# On compile exemple.c et on génère le fichier objet exemple.o gcc-oexemple.o-cexemple.c-std=c99-Wall-Wextra-g # On compile main.c et on génère le fichier objet main.o gcc-omain.o-cmain.c-std=c99-Wall-Wextra-g # On fait l'édition de liens pour lier main.o et exemple.o en # un programme exécutable mon_programme gcc-omon_executableexemple.omain.o

Un Makefile est basé sur cette notion de compilation séparée.

L'objectif d'un Makefile est ainsi de ne (re)compiler que les fichiers sources ayant été modifiés depuis la dernière compilation et de générer une nouvelle version du programme exécutable final.

Makefile de base

Un fichier Makefile de base de ce projet s’écrit comme suit :

1 2 3 4 5 6 7 8mon_executable:exemple.o main.o gcc-omon_executableexemple.omain.o exemple.o:exemple.c gcc-oexemple.o-cexemple.c-std=c99-Wall-Wextra-g main.o:main.c exemple.h gcc-omain.o-cmain.c-std=c99-Wall-Wextra-g

En effet pour créer l’exécutable mon_executable, nous avons besoin des fichiers objets exemple.o et main.o. make va d’abord rencontrer la dépendance exemple.o, et va donc évaluer la règle dont ce fichier est la cible. La seule dépendance de cette règle étant exemple.c, qui n’est pas cible d’une autre règle, make va exécuter la commande :

1gcc -o exemple.o -c exemple.c -std=c99 -Wall -Wextra -g

De la même façon, make va ensuite rencontrer la dépendance main.o et donc évaluer la règle dont main.o est la cible et exécuter la commande :

1gcc -o main.o -c main.c -std=c99 -Wall -Wextra -g

Finalement, toutes les dépendances de la règle initiale ayant été analysées, make va exécuter la commande :

1gcc -o monexecutable exemple.o main.o

NOTA BENE :

  • la syntaxe du Makefile impose l'utilisation de la tabulation pour décaler les lignes de commandes.

  • les décalages observés dans l'exemple de Makefile ci-dessus au niveau des lignes gcc ... doivent donc être des tabulations et non des espaces.

Un premier exercice pour tester par vous même l'utilisation des notions ci-dessus :

  • Télécharger l'exercice n°1

  • Le programme exécutable final à générer sera nommé : prog

  • Le programme est composé de 3 fichiers sources : main.c (programme principal), interface.c (interface utilisateur), fonctions.c (fonctions de calcul : factorielle et 2 puissance n)

  • Écrivez le Makefile selon le modèle expliqué ci-dessus et lancer le programme prog généré

Un premier raffinement

Pour améliorer ce Makefile, on peut rajouter quelques cibles standards :

  • all : à placer généralement au début du fichier ; les dépendances associées correspondent à l’ensemble des exécutables à produire ;

  • clean : normalement pas de dépendance ; la commande associée supprime tous les fichiers intermédiaires (notamment les fichiers objets .o) ;

  • mrproper : la commande correspondante supprime tout ce qui peut être regénéré (fichiers objets .o, exécutable, ...), ce qui permet une reconstruction complète du projet lors de l’appel suivant à make.

Notre Makefile devient donc ici :

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16all:mon_executable mon_executable:exemple.o main.o gcc-omon_executableexemple.omain.o exemple.o:exemple.c gcc-oexemple.o-cexemple.c-std=c99-Wall-Wextra-g main.o:main.c exemple.h gcc-omain.o-cmain.c-std=c99-Wall-Wextra-g clean: rm-f*.o mrproper:clean rm-fmon_executable

Reprenez l'exercice n°1 téléchargé précédemment et ajoutez ces raffinements dans votre Makefile.

Introduction de variables

Il est possible de définir des variables dans un Makefile. Elles se déclarent sous la forme NOM=valeur et sont appelées sous la forme $(NOM), à peu près comme dans un shellscript.

Parmi quelques variables standards pour un Makefile de projet C ou C++, on trouve :

  • CC qui désigne le compilateur utilisé ;
  • CFLAGS qui regroupe les options de compilation ;
  • LDFLAGS qui regroupe les options d’édition de liens ;
  • EXEC ou TARGET qui regroupe les exécutables.

Pour notre projet exemple, cela donne :

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21CC=gcc CFLAGS=-std=c99-Wall-Wextra-g LDFLAGS= EXEC=mon_executable all:$(EXEC) mon_executable:exemple.o main.o $(CC)-omon_executableexemple.omain.o$(LDFLAGS) exemple.o:exemple.c $(CC)-oexemple.o-cexemple.c$(CFLAGS) main.o:main.c exemple.h $(CC)-omain.o-cmain.c$(CFLAGS) clean: rm-f*.o mrproper:clean rm-f$(EXEC)

Reprenez l'exercice n°1 téléchargé précédemment et ajoutez ce type de variables dans votre Makefile.

Il existe aussi, et c’est encore plus intéressant car très puissant, des variables internes au Makefile, utilisables dans les commandes ; notamment :

  • $@ : nom de la cible ;
  • $< : nom de la première dépendance ;
  • : liste des dépendances ;
  • $? : liste des dépendances plus récentes que la cible ;
  • $* : nom d’un fichier sans son suffixe.

On peut donc encore compacter notre Makefile :

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21CC=gcc CFLAGS=-std=c99-Wall-Wextra-g LDFLAGS= EXEC=mon_executable all:$(EXEC) mon_executable:exemple.o main.o $(CC)-o$@$^$(LDFLAGS) exemple.o:exemple.c $(CC)-o$@-c$<$(CFLAGS) main.o:main.c exemple.h $(CC)-o$@-c$<$(CFLAGS) clean: rm-f*.o mrproper:clean rm-f$(EXEC)

Reprenez l'exercice n°1 téléchargé précédemment et ajoutez ce type de variables dans votre Makefile.

Règles d’inférences

On voit néanmoins qu’il y a encore moyen d’améliorer ce Makefile. En effet les règles dont les deux fichiers objets sont les cibles se ressemblent fortement. Or justement, on peut créer des règles génériques dans un Makefile. Pour cela il suffit d’utiliser le symbole % à la fois pour la cible et pour la dépendance. Il est également possible de préciser séparément d’autres dépendances pour les cas particuliers, par exemple pour ne pas oublier la dépendance au fichier exemple.h pour la règle dont main.o est la cible. Ceci nous donne :

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20CC=gcc CFLAGS=-std=c99-Wall-Wextra-g LDFLAGS= EXEC=mon_executable all:$(EXEC) mon_executable:exemple.o main.o $(CC)-o$@$^$(LDFLAGS) main.o:exemple.h %.o:%.c $(CC)-o$@-c$<$(CFLAGS) clean: rm-f*.o mrproper:clean rm-f$(EXEC)

Tester par vous même l'utilisation des notions ci-dessus :

  • Télécharger l'exercice n°2

  • Le programme principal est affiche.c, il fait appel aux fonctions contenues dans les 20 fichiers fct01.c à fct20.c

  • Le programme exécutable final sera nommé : affiche

  • Votre Makefile ne devra contenir aucun des noms : fct01.c à fct20.c

Liste des fichiers objets et liste des fichiers sources

On peut encore simplifier ce Makefile. En effet, on constate que la génération de l’exécutable dépend de tous les fichiers objets. Ceci peut être long à écrire dans le cas d’un gros projet !

Pour simplifier l’écriture, sachant que tous les fichiers objets correspondent aux fichiers sources en remplaçant l’extension .c par l’extension .o, on peut utiliser les variables SRC et OBJ et définir OBJ=$(SRC :.c=.o). SRC sera définie par la liste des fichiers sources... ce qui n’a fait que déplacer le problème ! Là encore, une syntaxe particulière permet de simplifier le Makefile : SRC=$(wildcard *.c).

Le Makefile du projet exemple devient finalement :

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22CC=gcc CFLAGS=-std=c99-Wall-Wextra-g LDFLAGS= EXEC=mon_executable SRC=$(wildcard*.c) OBJ=$(SRC:.c=.o) all:$(EXEC) mon_executable:$(OBJ) $(CC)-o$@$^$(LDFLAGS) main.o:exemple.h %.o:%.c $(CC)-o$@-c$<$(CFLAGS) clean: rm-f*.o mrproper:clean rm-f$(EXEC)

Reprenez l'exercice n°2 téléchargé précédemment et ajoutez ces améliorations dans votre Makefile :

  • Votre Makefile ne devra contenir aucune des noms : fct01 à fct20

  • Le programme exécutable final sera nommé : affiche

Un dernier exercice pour vérifier par vous même que vous avez vraiment tout compris :

  • Télécharger l'exercice n°3

  • 2 programmes exécutables sont à générer par votre Makefile : affiche1 et affiche2

  • affiche1.c fait appel aux fonctions fct01.c à fct09.c. Vous vous attacherez à ne compiler que les fichiers concernés pour générer ce premier programme affiche1.

  • affiche2.c fait appel aux fonctions fct10.c à fct20.c. Vous vous attacherez à ne compiler que les fichiers concernés pour générer ce deuxième programme affiche2.

  • Votre Makefile ne devra contenir aucune des noms de fonctions : fct01 à fct19

Il existe encore bien d’autres possibilités pour simplifier un Makefile. Il est notamment possible de créer des Makefile pour des sous-répertoires correspondant à des sous-parties du projet, et d’avoir un Makefile “maître” très simple qui appelle ces “sous-Makefile”, avec la variable MAKE. Il existe également des outils de génération automatique.

Tag » Apprendre Makefile