Ingénierie et programmation des logiciels
Comprendre et gérer les dépendances des programmes C avec les fichiers Makefiles
Table of Contents
Présentation
Dans la programmation C, un programme est souvent divisé en plusieurs fichiers sources et fichiers d'en-têtes pour améliorer l'organisation, la réutilisabilité et la vitesse de compilation. Cependant, cette modularité introduit un défi : lorsqu'un fichier d'en-tête change, chaque fichier source qui l'inclut doit être recompilé. Ceci manuellement est sujet à erreur et prend du temps. Makefiles, entraîné par l'outil make build automation, résolvez ce problème en encodant les dépendances et en automatisant le processus de construction.
Qu'est-ce que les Makefiles?
Un Makefile est un fichier en texte simple qui définit un ensemble de règles pour la construction d'un projet. L'utilitaire make lit ces règles, vérifie les horodatages de fichiers et n'exécute que les commandes nécessaires pour mettre le projet à jour. L'idée principale est simple : chaque règle a un cible (le fichier à produire), préalables (les fichiers nécessaires pour construire la cible), et recettes (commandes de coque à exécuter). En enregistrant les fichiers dont dépend, make peut reconstruire progressivement seulement les parties qui ont changé.
Makefiles fait partie d'Unix depuis les années 70, et GNU Make est le standard de facto sur Linux et macOS. La syntaxe est concise mais peut être subtile; obtenir des dépendances est la compétence première dont un développeur C a besoin.
Comprendre les dépendances
Dans un projet C, les dépendances ne se limitent pas aux fichiers . Chaque fichier source comprend un ou plusieurs fichiers d'en-tête (p. ex. . Si un en-tête est modifié, tous les fichiers qui comprennent ces fichiers doivent être recompilés. De même, les fichiers objets dépendent de leurs fichiers correspondants et l'exécutable final dépend de tous les fichiers objets.
Dépendances explicites ou implicites
Dans les premiers Makefiles, les programmeurs ont énuméré manuellement toutes les conditions préalables. Cette approche est fragile : oublier un en-tête signifie des constructions discontinues, tout en listant trop de déclencheurs des recompilations inutiles. Pire, à mesure que le projet grandit, les listes manuelles deviennent inmainables. La solution moderne est de faire en sorte que le compilateur génère automatiquement les dépendances, en les transformant en conditions préalables implicites et vérifiées par la machine.
Un Makefile bien conçu traite les dépendances comme une préoccupation de première classe. L'objectif est de ne jamais reconstruire quoi que ce soit qui n'a pas besoin de reconstruire, et de toujours reconstruire tout ce qui fait. C'est l'essence de correctitude[ et efficacité dans les constructions incrémentales.
Structure de base d'un fichier Makefile
Un Makefile typique contient des définitions, des règles et des cibles fallacieuses. Voici un exemple minimal mais fonctionnel pour un projet avec et :
CC = gcc
CFLAGS = -Wall -Wextra -O2
main: main.o utils.o
$(CC) $(CFLAGS) -o main main.o utils.o
main.o: main.c
$(CC) $(CFLAGS) -c main.c
utils.o: utils.c
$(CC) $(CFLAGS) -c utils.c
clean:
rm -f main main.o utils.o
Ce Makefile a quatre cibles : (l'exécutable), deux fichiers objets et une cible fausse . Les lignes de dépendance après le côlon indiquent make quels fichiers doivent être vérifiés avant de décider de reconstruire la cible.
Cibles de contrefaçon
Les cibles comme ou ne représentent pas les fichiers. Pour empêcher make de les confondre avec des noms de fichiers, elles doivent être déclarées comme phony[:
.PHONY: clean all
Sans cela, si un fichier nommé existait, make le considérerait à jour et sauterait la recette.
Gérer efficacement les dépendances
Le manuel Makefile ci-dessus a une faille grave: la dépendance de sur est correcte, mais que dire des en-têtes? Si change, (qui l'inclut) doit être reconstruite, mais la règle dit que cela dépend seulement de . La correction est de laisser le compilateur produire la liste de dépendance réelle.
Génération automatique de dépendance avec GCC
GCC (et Clang) peut générer des informations de dépendance en utilisant la famille de drapeaux . La combinaison la plus pratique pour la plupart des projets est :
- – écrit un fichier de dépendance ([) lors de la compilation, en listant uniquement les en-têtes définis par l'utilisateur (pas les en-têtes système).
- – spécifie le nom du fichier de dépendance.
- – ajoute des cibles fausses pour chaque en-tête, empêchant les erreurs lorsqu'un en-tête est supprimé.
Voici comment intégrer le suivi automatique de la dépendance dans un Makefile :
CC = gcc
CFLAGS = -Wall -Wextra -O2 -MMD -MP
SRCDIR = src
OBJDIR = obj
SRCS = $(wildcard $(SRCDIR)/*.c)
OBJS = $(patsubst $(SRCDIR)/%.c,$(OBJDIR)/%.o,$(SRCS))
DEPS = $(OBJS:.o=.d)
all: myprogram
myprogram: $(OBJS)
$(CC) $(CFLAGS) -o $@ $^
$(OBJDIR)/%.o: $(SRCDIR)/%.c
@mkdir -p $(OBJDIR)
$(CC) $(CFLAGS) -c $< -o $@
-include $(DEPS)
.PHONY: all clean
clean:
rm -rf $(OBJDIR) myprogram
Explication :
- recueille tous les fichiers dans le répertoire source.
- les transforme en chemins de fichiers objets.
- liste les fichiers de dépendance (p. ex. ).
- La règle de motif compile chaque fichier et, à cause de , génère un fichier comme effet secondaire.
- La ligne lit les fichiers générés , les transformant en véritables prérequis Makefile. Le tiret () supprime les erreurs lorsque les fichiers n'existent pas encore (par exemple, sur la première construction).
Maintenant, si est modifié, la prochaine invocation de recompilera automatiquement parce que le fichier pour inclut comme préalable.
Techniques avancées
Manipulation des dépendances générées en toute sécurité
Lorsqu'un en-tête est supprimé, le fichier peut toujours le renvoyer, ce qui fait que make échoue avec une erreur cible manquante. Le drapeau s'adresse à cela en ajoutant des règles fictives vides pour chaque en-tête de dépendance. Si l'en-tête est parti, make exécute simplement la règle false (qui ne fait rien) et continue.
Inclure les fichiers de dépendance après les changements de source
Une subtilité : si un fichier source ajoute ou supprime un , le fichier correspondant doit être régénéré. Parce que le fichier est lui-même une condition préalable du fichier objet, make remarquera le changement d'horodatage et de recompilation, qui régénère le fichier . Cette récursion fonctionne automatiquement une fois la première construction terminée.
Utiliser les prérequis de l'ordre seulement
Parfois, vous avez besoin d'un répertoire pour exister avant de construire, mais vous ne voulez pas que son horodatage déclenche une reconstruction. C'est le rôle de preconditions seulement de l'ordre (séparé par ). Dans la règle de motif ci-dessus, nous avons utilisé à l'intérieur de la recette; une alternative est:
$(OBJDIR)/%.o: $(SRCDIR)/%.c | $(OBJDIR)
$(CC) $(CFLAGS) -c $< -o $@
$(OBJDIR):
mkdir -p $@
Cela garantit que le répertoire est créé avant toute compilation, mais un changement au répertoire lui-même (par exemple, un nouveau fichier à l'intérieur) ne déclenchera pas la recompilation.
Meilleures pratiques de gestion des dépendances
- Utilisez la génération automatique de dépendance dès le premier jour. Même pour un projet à dossier unique, il est une bonne habitude.
- Garder les fichiers de dépendance séparés des fichiers sources. Mettez-les dans un répertoire ou . Cela facilite le nettoyage et évite d'encombrer l'arborescence source.
- Inclure les fichiers générés après les règles qui les créent. La directive est bonne, mais la placer après la règle de motif garantit que make sait d'abord comment construire des fichiers objets avant d'essayer de lire les fichiers .
- Utiliser des cibles fallacieuses pour l'entretien ménager. , , et sont fréquents.
- Les variables de levier pour les drapeaux du compilateur, les répertoires sources et les listes de fichiers. Cela rend le Makefile réutilisable à travers les projets et plus facile à personnaliser.
- Testez votre Makefile avec un changement d'en-tête délibéré. Modifier un en-tête, exécuter , et confirmer que seuls les fichiers objets touchés sont recompilés. Si une reconstruction complète se produit, quelque chose ne va pas avec le suivi de la dépendance.
- Gardez le Makefile simple mais pas plus simple que nécessaire. La suringénierie avec des fonctions avancées comme peut rendre le débogage douloureux. Commencez par le motif indiqué dans cet article; il s'équilibre bien à des dizaines de fichiers.
Ressources extérieures
Pour approfondir votre compréhension, consultez ces références faisant autorité :
- GNU Make Manual – Le guide définitif pour faire la syntaxe, les fonctions et les fonctionnalités avancées.
- Options de préprocesseur GCC[ – Documentation pour , , , et drapeaux connexes.
- Makefile Tutorial by Chase Lambert – Un tutoriel pratique et bien structuré couvrant de nombreux cas d'utilisation dans le monde réel.
Conclusion
La gestion des dépendances des programmes C avec Makefiles n'est pas un luxe, mais une nécessité pour tout projet qui dépasse un seul fichier. En combinant les règles de patron, la génération de dépendance automatique avec , et l'inclusion soigneuse des fichiers générés , vous pouvez créer un système de construction qui est à la fois rapide et correct. Les techniques présentées ici éliminent le suivi manuel, réduisent les temps de construction et empêchent les bogues subtils causés par les fichiers objets stal. Une fois que vous internalisez ces modèles, vous n'écrirez plus jamais une liste de dépendance manuelle.