Compiler un projet C++ avec un Makefile
Document réservé
Vous consultez actuellement la présentation publique de ce chapitre.
Les documents PDF complets, comprenant les développements théoriques, les exemples détaillés et les exercices, sont disponibles sur demande.
Pour obtenir un accès, contactez-moi via la page Contact en indiquant les domaines qui vous intéressent (C++, ESP-IDF, électronique, etc.).
Source PDF : cpp-An1.pdf
(Limite 50000 octets — symratio=0.40, minlen=20, clean)
Compiler un projet C++ avec make Compilation et édition de liens . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Structure générale d’une règle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Cas A : un programme contenant uniquement main.cpp . . . . . . . . . . . . . . . . . . . Cas B : un projet comportant plusieurs fichiers .cpp et .h . . . . . . . . . . . . . . . . . . Générer automatiquement les dépendances . . . . . . . . . . . . . . . . . . . . . . . . . . . Cas C : un projet utilisant des fichiers .tpp . . . . . . . . . . . . . . . . . . . . . . . . . . Comparaison des trois organisations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Un modèle général de Makefile . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Annexe A - Compiler un projet C++ avec un makefile Compiler un projet C++ avec make Lorsqu’un programme C++ ne contient qu’un unique fichier source, sa compilation peut être réalisée directement depuis le terminal à l’aide du compilateur. Par exemple, si on a un programme main.cpp dans un clang++ -std=c++17 main.cpp -o programme Cette méthode devient cependant trèspeu pratique lorsque le projet comporte beaucoup de fichiers source. Dans ce cas, il faut indiquer au compilateur quels fichiers doivent être compilés, gérer les différentes étapes de la construction du programme et éviter de recompiler inutilement les fichiers qui n’ont pas été modifiés. C’est l’utilitaire make permet d’automatiser ces opérations. Les instructions nécessaires à la construction du programme sont placées dans un fichier nommé : Nous allons construire progressivement des Makefiles adaptés à trois organisations courantes d’un projet 1. un programme constitué uniquement d’un fichier main.cpp ; 2. un projet comportant plusieurs fichiers .cpp et leurs fichiers d’en-tête .h ; 3. un projet utilisant des classes templates dont les définitions sont placées dans des fichiers .tpp. Nous supposerons dans tous les exemples que les différents fichiers du projet se trouvent dans le même De quoi make s’occupe-t-il ? Il est important de distinguer le rôle de make de celui du compilateur. L’utilitaire make n’est pas un compilateur C++. Il analyse les règles contenues dans le Makefile et détermine quelles commandes doivent être exécutées pour construire le programme. Ces commandes peuvent appele ensuite le compilateur C++. Dans nos exemples, nous utiliserons : qui est le compilateur C++ fourni par Clang. Le rôle de make peut donc être résumé de la manière suivante : détermine ce qui doit être construit exécute les commandes nécessaires 1. Dans un projet plus important, les fichiers source, les fichiers d’en-tête et les fichiers générés sont souvent répartis dans plusieurs répertoires. Le principe de fonctionnement de make reste identique ; les chemins des fichiers et les répertoires de recherche doivent simplement être indiqués dans le Makefile. Cette organisation ne sera pas développée ici. Les projets ESP-IDF, étudiés séparément dans ce cours, utilisent un autre système de construction basé sur CMake.AccueilAnnexe A - Compiler un projet C++ avec un makefile Dans cette annexe, nous utilisons clang++, le compilateur C++ fourni avec l’environnement de développement Apple. Sur d’autres systèmes, notamment sous Linux, le compilateur GCC est également très répandu. La commande correspondante pour compiler du C++ est généralement : Dans de nombreux Makefiles, il suffit alors de remplacer : sans modifier les règles générales de construction du projet. Compilation et édition de liens Avant d’étudier la syntaxe d’un Makefile, il est nécessaire de distinguer deux étapes fondamentales de la construction d’un programme C++ : 2. l’édition de liens. Chaque fichier source .cpp peut être compilé séparément. Considérons par exemple : clang++ -std=c++17 -c Matrice.cpp -o Matrice.o produit un fichier objet (extension .o) demande au compilateur de compiler le fichier source sans effectuer l’édition de liens. −−−−−−−−→ Un fichier objet contient du code machine 2 produit à partir du fichier source, mais il ne constitue généralement pas encore un programme exécutable autonome. 2. Le code machine est constitué des instructions directement exécutables par le processeur. Il est représenté par une suite de bits dont la signification dépend de l’architecture du processeur. L’assembleur en fournit une représentation symbolique plus facilement lisible par l’être humain. Une instruction assembleur correspond généralement de manière très directe à une instruction machine. Le code machine ARM64, par exemple, est différent du code machine x86-64 ou RISC-V.AccueilAnnexe A - Compiler un projet C++ avec un makefile L’édition de liens Lorsque le programme est réparti entre plusieurs fichiers source, chacun d’eux peut produire son propre fichier objet, par exemple : Ces fichiers objets doivent ensuite être réunis pour produire l’exécutable. Cette opération est appelée édition de liens. clang++ main.o Matrice.o Complexe.o -o programme Nous pouvons représenter l’ensemble du processus par : main.cpp −→ main.o Matrice.cpp −→ Matrice.o Complexe.cpp −→ Complexe.o Les premières transformations correspondent à la compilation et la dernière étape correspond à l’édition de Pourquoi utiliser make ? Nous pourrions naturellement exécuter toutes ces commandes manuellement et pour un projet de quelques fichiers, cela reste possible. Cette méthode présente cependant un inconvénient important : après la modification d’un seul fichier source, il serait inutile de recompiler tous les autres fichiers. Supposons que seul : ait été modifié, il suffit alors de reconstruire : puis d’effectuer une nouvelle édition de liens. Les fichiers : peuvent être conservés. C’est précisément l’un des rôles essentiels de make qui, pour déterminer ce qui doit être reconstruit, make examine les dépendances entre les fichiers et leurs dates de modification. make ne recompile pas nécessairement tout le projet. Il reconstruit ce qui est devenu périmé après une modification.AccueilAnnexe A - Compiler un projet C++ avec un makefile Structure générale d’une règle Un Makefile est constitué de règles dont la forme générale est : Chaque élément a son rôle. La cible est généralement le fichier que nous voulons construire. Par exemple : indique que la règle permet de construire le fichier Matrice.o. Après les deux-points apparaissent les fichiers dont dépend la cible : Matrice.o: Matrice.cpp Matrice.h Cette règle indique que Matrice.o dépend de Matrice.cpp et de Matrice.h. Si l’un de ces fichiers est plus récent que Matrice.o, make considère que le fichier objet doit être reconstruit. La ligne suivante indique comment construire la cible : Matrice.o: Matrice.cpp Matrice.h clang++ -std=c++17 -c Matrice.cpp -o Matrice.o Nous pouvons donc lire cette règle de la manière suivante : Pour construire Matrice.o, nous avons besoin de Matrice.cpp et de Matrice.h. Si la cible doit être reconstruite, exécuter la commande indiquée. Attention aux tabulations La syntaxe historique de make impose une règle qui peut surprendre : les lignes contenant les commandes doivent normalement commencer par une véritable tabulation. Matrice.o: Matrice.cpp Matrice.h clang++ -std=c++17 -c Matrice.cpp -o Matrice.o la seconde ligne commence par une tabulation et des espaces placés à la place de cette tabulation peuvent provoquer une erreur lors de l’interprétation du Makefile. Dans un Makefile : tabulation, pas indentation C++.AccueilAnnexe A - Compiler un projet C++ avec un makefile Cas A : un programme contenant uniquement main.cpp Commençons par le cas le plus simple : un programme C++ constitué d’un seul fichier source. Dans le répertoire Projet on a : Supposons que nous voulions un exécutable nommé : Une compilation manuelle pourrait être effectuée avec : clang++ -std=c++17 main.cpp -o programme Nous allons maintenant placer cette opération dans un Makefile. Utiliser des variables Un Makefile peut contenir des variables afin d’éviter de répéter plusieurs fois les mêmes informations, nous utiliserons entre autres, CXXFLAGS = -std=c++17 -O0 -Wall -Wextra -pedantic contient le nom du compilateur C++, la variable : contient les options utilisées lors de la compilation et enfin contient le nom de l’exécutable que nous souhaitons produire. Les options choisies sont : • -std=c++17 : utiliser la norme C++ 17 ; • -Wall : activer un ensemble important d’avertissements ; • -Wextra : activer des avertissements supplémentaires ; • -O0 jusqu’à -O3 : optimisation croissante du code, la valeur maximum recommandée est -02. • -pedantic : demander au compilateur de signaler certaines constructions non conformes strictement à la norme choisie (peut devenir super pénible !). Notre premier Makefile sera :AccueilAnnexe A - Compiler un projet C++ avec un makefile CXXFLAGS = -std=c++17 -Wall -O0 -Wextra -pedantic .PHONY: build run clean build: (TARGET): main.cpp (CXXFLAGS) main.cpp -o $(TARGET) Pour utiliser le contenu d’une variable dans un Makefile, nous écrivons : indique que la cible build dépend de l’exécutable. Lorsque nous exécutons : make commence par vérifier si l’exécutable doit être construit ou mis à jour. La règle correspondante est : (CXXFLAGS) main.cpp -o $(TARGET) Si main.cpp est plus récent que l’exécutable, ou si celui-ci n’existe pas encore, la commande de compilationAccueilAnnexe A - Compiler un projet C++ avec un makefile indique que l’exécution dépend d’abord de la cible build. demande d’abord à make de vérifier que le programme est à jour, puis lance l’exécutable. Cette organisation est pratique, car il n’est pas nécessaire de lancer manuellement une compilation avant chaque exécution. supprime l’exécutable produit par la compilation. Nous pouvons écrire : pour revenir à un répertoire ne contenant que les fichiers source. de la commande rm évite qu’une erreur soit produite si le fichier à supprimer n’existe pas. ne correspondent à des commandes logiques. Nous les déclarons donc comme cibles particulières à l’aide de : .PHONY: build run clean Cette déclaration (ce n’est pas une commande) indique à make que build run clean doivent toujours être interprétés comme des cibles à exécuter et non comme des fichiers à examiner sur le disque.AccueilAnnexe A - Compiler un projet C++ avec un makefile Les trois commandes essentielles de notre Makefile sont : Nous disposons à présent d’un système simple permettant de construire, exécuter et nettoyer un petit Cas B : un projet comportant plusieurs fichiers .cpp et .h Considérons maintenant un projet C++ constitué de plusieurs fichiers source qui se trouvent dans le même Le fichier main.cpp utilise les classes déclarées dans les fichiers d’en-tête : #include "Matrice.h" #include "Complexe.h" et les définitions des méthodes contenues dans ces fichiers d’en-tête .h, sont dans Matrice.cpp et Dans cette situation, il serait pratique de demander au compilateur de traiter tous les fichiers source en une seule commande : clang++ -std=c++17 main.cpp Matrice.cpp Complexe.cpp -o programme Cette commande fonctionne, mais elle présente un inconvénient : chaque exécution de la commande provoque la recompilation de tous les fichiers source. Nous allons donc introduire la compilation séparée. Construire séparément les fichiers objets Avec la commande qui suit, chaque fichier .cpp est compilé indépendamment afin de produire un fichier clang++ -std=c++17 -c main.cpp -o main.o clang++ -std=c++17 -c Matrice.cpp -o Matrice.o clang++ -std=c++17 -c Complexe.cpp -o Complexe.o Nous obtenons donc : main.cpp −→ main.o Matrice.cpp −→ Matrice.o Complexe.cpp −→ Complexe.oAccueilAnnexe A - Compiler un projet C++ avec un makefile demande au compilateur de s’arrêter après la production du fichier objet. Aucun exécutable n’est encore Construire l’exécutable Une fois les trois fichiers objets disponibles, ils peuvent être réunis par l’édition de liens : clang++ main.o Matrice.o Complexe.o -o programme Nous obtenons alors l’exécutable : Le processus complet peut être représenté par : main.cpp −→ main.o Matrice.cpp −→ Matrice.o Complexe.cpp −→ Complexe.o L’intérêt de la compilation séparée Supposons maintenant que nous modifiions uniquement le fichier : Il n’est pas nécessaire de recompiler main.cpp ni Complexe.cpp. Seul le fichier objet correspondant doit être reconstruit : clang++ -std=c++17 -c Matrice.cpp -o Matrice.o Une nouvelle édition de liens est ensuite nécessaire : clang++ main.o Matrice.o Complexe.o -o programme Les fichiers main.o et Complexe.o sont réutilisés tels quels et c’est précisément ce travail de vérification que make peut effectuer automatiquement. Un Makefile utilisant la compilation séparée Dans ce cas, nous écrirons explicitement une règle pour chaque fichier objet : CXXFLAGS = -std=c++17 -Wall -Wextra -pedantic OBJECTS = main.o Matrice.o Complexe.o .PHONY: build run clean main.o: main.cpp Matrice.h Complexe.h (CXXFLAGS) -c main.cpp -o main.oAccueilAnnexe A - Compiler un projet C++ avec un makefile Matrice.o: Matrice.cpp Matrice.h (CXXFLAGS) -c Complexe.cpp -o Complexe.o Nous remarquons une nouvelle variable, la variable OBJECTS. La liste des fichiers objets La nouvelle variable OBJECTS a pour valeurs : OBJECTS = main.o Matrice.o Complexe.o Elle contient donc la liste des fichiers objets nécessaires à la construction de l’exécutable. indique que l’exécutable dépend de tous ces fichiers objets. Si l’un d’eux est reconstruit, l’exécutable devient plus ancien que l’un de ses fichiers dépendants et l’édition de liens doit être effectuée à nouveau. Le rôle des fichiers d’en-tête dans les dépendances Considérons la règle : Matrice.o: Matrice.cpp Matrice.h (CXXFLAGS) -c Matrice.cpp -o Matrice.o Le fichier Matrice.h n’est pas compilé séparément mais il apparaît néanmoins parmi les dépendances de Matrice.o. En effet, Matrice.cpp contient normalement l’appel : #include "Matrice.h" Une modification de la déclaration de la classe dans Matrice.h peut donc modifier le résultat de la compilation de Matrice.cpp. Il faut par conséquent reconstruire Matrice.o si Matrice.h a été modifié. Que se passe-t-il après une modification ? Supposons que le projet ait déjà été entièrement construit et que nous modifions uniquement : make constate que Matrice.cpp est plus récent que Matrice.o, il doit donc être reconstruit et Matrice.cpp −→ Matrice.oAccueilAnnexe A - Compiler un projet C++ avec un makefile Le nouveau fichier Matrice.o étant maintenant plus récent que l’exécutable, celui-ci doit à son tour être reconstruit par édition de liens. ne sont pas recompilés. Si nous modifions maintenant Matrice.h, la situation est différente car d’après notre Makefile, ce fichier intervient dans les règles : main.o: main.cpp Matrice.h Complexe.h Matrice.o: Matrice.cpp Matrice.h make reconstruira donc : puis effectuera une nouvelle édition de liens. Le fichier Complexe.o, qui ne dépend pas de Matrice.h, ne sera pas recompilé. make ne cherche pas simplement quels fichiers ont été modifiés. Il détermine quelles cibles dépendent des fichiers modifiés. Simplifier le Makefile Le Makefile précédent fonctionne correctement, mais il contient plusieurs règles très similaires : main.o: main.cpp Matrice.h Complexe.h (CXXFLAGS) -c main.cpp -o main.o Matrice.o: Matrice.cpp Matrice.h (CXXFLAGS) -c Complexe.cpp -o Complexe.o La structure de la commande de compilation est toujours la même et seuls les noms des fichiers changent. L’utilitaire make permet de factoriser ce type de répétition à l’aide de règles génériques. Définir les fichiers source Nous pouvons commencer par regrouper les fichiers source dans une variable : SOURCES = main.cpp Matrice.cpp Complexe.cpp À partir de cette liste, nous pouvons demander à make de construire automatiquement les noms des fichiers objets correspondants : OBJECTS = $(SOURCES:.cpp=.o) Cette écriture remplace le suffixe :AccueilAnnexe A - Compiler un projet C++ avec un makefile dans chacun des éléments de la variable SOURCES. Utiliser une règle générique Tous les fichiers .cpp sont compilés selon le même principe : clang++ ... -c fichier.cpp -o fichier.o Nous pouvons donc écrire une seule règle capable de s’appliquer à tous les fichiers source : représente ici une partie variable du nom du fichier. « pour construire un fichier objet à partir d’un fichier source portant le même nom de base » Les variables automatiques Dans une règle, make fournit certaines variables automatiques dont la valeur dépend de la cible en cours nous utilisons deux de ces variables : premier fichier dépendant de la règle nom de la cible en cours de construction Supposons que make doive construire : La règle générique : est alors interprétée conceptuellement comme : clang++ -std=c++17 -Wall -Wextra -pedantic \ -c Matrice.cpp -o Matrice.o Dans cette instanciation de la règle :AccueilAnnexe A - Compiler un projet C++ avec un makefile La variable automatique $^ Une autre variable automatique particulièrement utile est : ^ représente la liste de toutes les dépendances placées après les deux-points : dans la règle en cours. Nous pouvons donc écrire : (OBJECTS) signifie : "donne-moi le contenu de la variable appelée OBJECTS" "donne-moi toutes les dépendances de la règle que je suis actuellement en train Un Makefile plus général Nous pouvons maintenant remplacer les règles individuelles par un Makefile plus compact : SOURCES = main.cpp Matrice.cpp Complexe.cpp OBJECTS = $(SOURCES:.cpp=.o) .PHONY: build run clean Ce Makefile contient maintenant une seule règle de compilation pour tous les fichiers .cpp, et ajouter un nouveau fichier source, par exemple :AccueilAnnexe A - Compiler un projet C++ avec un makefile nécessite simplement de modifier la variable : SOURCES = main.cpp Matrice.cpp Complexe.cpp Vecteur.cpp sera automatiquement ajouté à la variable OBJECTS et compilé par la règle générique. Une limitation de cette simplification Notre règle générique : ne mentionne plus explicitement les fichiers d’en-tête donc, si nous modifions uniquement : make ne peut pas nécessairement déduire, avec ce Makefile seul, qu’il doit reconstruire les fichiers objets qui dépendent de cet en-tête. Nous avons donc simplifié le Makefile, mais perdu une partie de l’information concernant les dépendances. Générer automatiquement les dépendances Dans la section précédente, nous avons simplifié les règles de compilation grâce à une règle générique : Cette solution présente cependant une difficulté : les fichiers d’en-tête inclus par les fichiers source n’apparaissent plus explicitement dans les dépendances. Il serait possible de maintenir ces informations manuellement, mais cette solution devient rapidement fastidieuse lorsque le nombre de fichiers augmente. Les options -MMD et -MP Nous allons ajouter deux options aux options de compilation : Notre variable devient donc : L’option -MMD demande au compilateur de produire, en plus du fichier objet, un fichier décrivant les dépendances du fichier source. Ainsi, la compilation : clang++ -MMD -c Matrice.cpp -o Matrice.o peut produire simultanément :AccueilAnnexe A - Compiler un projet C++ avec un makefile Le fichier Matrice.o contient le code objet et fichier Matrice.d contient les informations permettant à make de connaître les fichiers dont dépend cette compilation. Le contenu d’un fichier de dépendances Supposons que Matrice.cpp contienne : #include "Matrice.h" Le fichier Matrice.d généré par le compilateur pourra contenir une règle comparable à : Matrice.o: Matrice.cpp Matrice.h Nous retrouvons donc automatiquement l’information que nous avions précédemment écrite à la main : Matrice.o: Matrice.cpp Matrice.h Le compilateur connaît les fichiers réellement inclus pendant la compilation. Il est donc particulièrement bien placé pour générer cette liste. make utilise les dépendances, mais le compilateur peut les déterminer automatiquement. Le rôle de l’option -MP L’option -MP complète la génération des dépendances en ajoutant des règles auxiliaires pour les fichiers d’en-tête rencontrés. Elle permet notamment d’éviter certaines erreurs de make lorsqu’un fichier d’en-tête précédemment utilisé est ensuite supprimé ou renommé. Dans nos Makefiles, nous utiliserons donc généralement les deux options ensemble : La liste des fichiers de dépendances Nous disposons déjà de SOURCES = main.cpp Matrice.cpp Complexe.cpp OBJECTS = $(SOURCES:.cpp=.o) Nous pouvons appliquer le même principe aux fichiers de dépendances : DEPS = $(OBJECTS:.o=.d) Si OBJECTS contient : main.o Matrice.o Complexe.o alors DEPS contiendra : main.d Matrice.d Complexe.d Matrice.o, Matrice.d Complexe.cpp −→ Complexe.o, Complexe.dAccueilAnnexe A - Compiler un projet C++ avec un makefile Inclure les dépendances dans le Makefile Les fichiers .d contiennent des règles compréhensibles par make et il faut donc demander à make de les lire en ajoutant à la fin du Makefile : Cette instruction demande d’inclure le contenu des fichiers indiqués dans la variable DEPS (le signe - placé devant include est important). Lors de la toute première compilation, les fichiers : n’existent pas encore mais ils seront précisément créés pendant cette première compilation. demande à make de poursuivre son travail si ces fichiers ne sont pas encore disponibles. Un Makefile avec dépendances automatiques Nous pouvons maintenant réunir tous les éléments étudiés : SOURCES = main.cpp Matrice.cpp Complexe.cpp OBJECTS = (OBJECTS:.o=.d) .PHONY: build run cleanAccueilAnnexe A - Compiler un projet C++ avec un makefile Exemple de reconstruction Supposons que le projet soit entièrement compilé. Nous modifions uniquement : Le fichier de dépendances généré précédemment indique à make quels fichiers objets dépendent de cet seuls les fichiers objets concernés sont recompilés et l’exécutable est ensuite reconstruit par édition de liens. Il n’est donc plus nécessaire de maintenir manuellement dans le Makefile la liste des fichiers d’en-tête utilisés par chaque fichier source. 1. Les fichiers .cpp sont indiqués dans le Makefile. 2. Les dépendances vers les fichiers .h peuvent être déterminées automatiquement par le compilateur. Cas C : un projet utilisant des fichiers .tpp Les templates introduisent une organisation légèrement différente des fichiers source. Dans le chapitre consacré à Matrice<T>, nous avons par exemple utilisé une structure de la forme : |-- MatriceCarree.tpp À première vue, nous pourrions être tentés d’ajouter les fichiers .tpp à la liste des fichiers à compiler mais ce serait une erreur car les fichiers .tpp jouent ici un rôle différent de celui des fichiers .cpp.AccueilAnnexe A - Compiler un projet C++ avec un makefile Pourquoi le fichier .tpp n’est-il pas compilé séparément ? Pour une classe ordinaire, nous pouvons généralement séparer la déclaration et la définition : Le fichier Matrice.cpp constitue alors une unité de compilation qui peut produire son propre fichier objet : Matrice.cpp −→ Matrice.o Avec un template, la situation est différente. Lorsque le compilateur rencontre par exemple : Matrice<int> A(3, 3); il doit disposer de la définition du template afin de pouvoir produire le code correspondant à l’instanciation demandée, c’est pour cela que nous avons placé les définitions des méthodes dans Matrice.tpp et inclus celles-ci à la fin de Matrice.h avec la commande #include "Matrice.tpp" Le compilateur rencontre ainsi la chaîne d’inclusions suivante : main.cpp −→ Matrice.h −→ Matrice.tpp Le contenu du fichier Matrice.tpp devient donc visible lors de la compilation du fichier qui utilise le template et par conséquent il n’est pas nécessaire de compiler séparément Matrice.tpp. Ne pas ajouter les fichiers .tpp à SOURCES Considérons la variable SOURCES utilisée dans notre Makefile : Il est faut d’écrire SOURCES = main.cpp Matrice.tpp MatriceCarree.tpp Dans l’organisation adoptée ici, les fichiers .tpp ne constituent pas des unités de compilation indépendantes. Ils sont inclus dans les fichiers d’en-tête correspondants. .cpp : unité de compilation .tpp : définitions de templates incluses Les fichiers .tpp comme dépendances Même s’il n’est pas compilé séparément, un fichier .tpp reste une dépendance du fichier source qui finit par l’inclure. Supposons que main.cpp contienne : #include "Matrice.h" et que Matrice.h contienne : #include "Matrice.tpp" Le compilateur voit alors également le contenu de Matrice.tpp lors de la compilation de main.cpp et grâce à l’option −MDD cette dépendance peut être enregistrée automatiquement dans le fichier main.d. Par exemple, le fichier de dépendances pourra contenir une règle comparable à :AccueilAnnexe A - Compiler un projet C++ avec un makefile main.o: main.cpp Matrice.h Matrice.tpp \ MatriceCarree.h MatriceCarree.tpp Ainsi, si nous modifions uniquement : make sait que main.o doit être reconstruit. Le fichier .tpp n’est donc pas une source compilée séparément, mais il participe pleinement au système de Ne pas compiler un fichier séparément ne signifie pas qu’il ne constitue pas une dépendance. Makefile du projet Matrice<T> Dans notre projet, les implémentations des classes templates se trouvent entièrement dans les fichiers .tpp inclus par les fichiers d’en-tête. Le seul fichier .cpp est donc : Nous pouvons utiliser : OBJECTS = (OBJECTS:.o=.d) .PHONY: build run cleanAccueilAnnexe A - Compiler un projet C++ avec un makefile n’apparaissent pas explicitement dans ce Makefile car ils sont découverts par le compilateur lors du traitement des directives #include, puis enregistrés dans les fichiers de dépendances générés automatiquement. Mélanger fichiers .cpp et .tpp La présence de fichiers .tpp n’interdit naturellement pas l’utilisation simultanée de fichiers .cpp ordinaires. Un projet pourrait par exemple contenir : Dans ce cas, la liste des sources serait : SOURCES = main.cpp Complexe.cpp SOURCES = main.cpp Complexe.cpp Matrice.tpp Les fichiers .cpp sont compilés séparément et les fichiers .tpp sont inclus là où les définitions des templates doivent être visibles. fichiers qui doivent être compilés séparément Comparaison des trois organisations Les trois situations étudiées utilisent les mêmes principes de make. Ce qui change essentiellement est la nature des fichiers qui doivent être compilés séparément, il est donc utile de comparer les trois organisations. Cas A : un unique fichier main.cpp La structure la plus élémentaire est : Un seul fichier source doit être compilé Pour un projet aussi simple, il n’est pas indispensable de produire explicitement un fichier objet intermédiaire, ainsi nous avions utilisé : (CXXFLAGS) main.cpp -o $(TARGET) Le compilateur effectue alors successivement la compilation et l’édition de liens.AccueilAnnexe A - Compiler un projet C++ avec un makefile Cas B : plusieurs fichiers .cpp Nous considérons à présent : Les fichiers qui doivent être compilés séparément sont : SOURCES = main.cpp Matrice.cpp Complexe.cpp Lesquels produisent respectivement : OBJECTS = main.o Matrice.o Complexe.o Les fichiers .h ne sont pas compilés séparément mais ils constituent cependant des dépendances des fichiers source qui les incluent. Cas C : présence de fichiers .tpp |-- MatriceCarree.tpp Dans notre organisation des templates, les fichiers .tpp sont inclus par les fichiers d’en-tête correspondants et le seul fichier compilé séparément reste donc : Les fichiers .h et .tpp seront automatiquement identifiés comme dépendances grâce aux options de génération des dépendances. Tableau récapitulatif Dans l’organisation adoptée dans ce cours, la règle fondamentale peut donc être formulée simplement : • Les fichiers .cpp sont des unités de compilation. • Les fichiers .h et .tpp sont inclus et peuvent constituer des dépendances.AccueilAnnexe A - Compiler un projet C++ avec un makefile Un modèle général de Makefile Pour un petit projet C++ dont tous les fichiers se trouvent dans le même répertoire, nous pouvons maintenant utiliser le modèle suivant : SOURCES = main.cpp Matrice.cpp Complexe.cpp OBJECTS = (OBJECTS:.o=.d) .PHONY: build run clean Dans la plupart des petits projets, seules deux lignes doivent être adaptées : SOURCES = main.cpp Matrice.cpp Complexe.cpp La première définit le nom de l’exécutable et la seconde contient la liste des fichiers .cpp qui doivent être compilés séparément. Les variables OBJECTS et DEPS sont ensuite construites automatiquement à partir de cette liste. Si le projet contient également des fichiers .tpp, ceux-ci ne doivent pas être ajoutés à SOURCES lorsqu’ils sont inclus par les fichiers d’en-tête.AccueilAnnexe A - Compiler un projet C++ avec un makefile conduit simplement à : SOURCES = main.cpp Complexe.cpp La syntaxe d’un Makefile est relativement simple, mais certaines erreurs sont particulièrement fréquentes. Utiliser des espaces à la place d’une tabulation Une commande associée à une règle doit normalement commencer par une véritable tabulation : Une indentation réalisée uniquement avec des espaces peut provoquer une erreur de syntaxe. Cette particularité est propre à la syntaxe traditionnelle des Makefiles et ne doit pas être confondue avec les règles d’indentation du code C++. Ajouter un fichier d’en-tête à SOURCES Une déclaration telle que : SOURCES = main.cpp Matrice.cpp Matrice.h est incorrecte dans notre organisation. Le fichier Matrice.h n’est pas une unité de compilation et il faut écrire : SOURCES = main.cpp Matrice.cpp Les dépendances vers Matrice.h seront déterminées automatiquement. Ajouter un fichier .tpp à SOURCES De même, dans l’organisation utilisée pour nos templates : SOURCES = main.cpp Matrice.tpp n’est pas approprié. Le fichier Matrice.tpp est inclus depuis Matrice.h. Il doit être visible du compilateur, mais il ne constitue pas une unité de compilation séparée. Confondre compilation et édition de liens clang++ -c Matrice.cpp -o Matrice.o produit un fichier objet. clang++ main.o Matrice.o -o programme réunit les fichiers objets pour produire l’exécutable. Ces deux opérations correspondent à deux étapes différentes de la construction du programme.AccueilAnnexe A - Compiler un projet C++ avec un makefile Erreur de compilation ou erreur d’édition de liens ? Une erreur détectée pendant la compilation concerne généralement le contenu d’un fichier source : syntaxe incorrecte, type incompatible, nom inconnu, etc. Une erreur détectée pendant l’édition de liens signifie souvent que le compilateur a accepté les déclarations rencontrées, mais qu’une définition nécessaire n’a pas été trouvée parmi les fichiers objets fournis à l’éditeur de liens. Cette distinction peut être très utile lors de la recherche d’une erreur dans un projet comportant plusieurs Fiche de référence Les principaux éléments rencontrés dans cette annexe sont regroupés ci-dessous. Compilateur C++ utilisé Options utilisées lors de la compilation Nom de l’exécutable à produire Liste des fichiers .cpp à compiler Liste des fichiers objets .o Liste des fichiers de dépendances .d Les noms TARGET, SOURCES, OBJECTS et DEPS ne sont pas imposés par make. Ce sont des noms choisis par En revanche, certains noms tels que CXX et CXXFLAGS sont conventionnellement utilisés par make pour représenter respectivement le compilateur C++ et ses options de compilation. Variables automatiques Certaines variables sont automatiquement définies par make lorsqu’une règle est exécutée. Les trois utilisées dans cette annexe sont : Première dépendance Ensemble des dépendances Considérons par exemple : Lors de la construction de Matrice.o à partir de Matrice.cpp, nous avons : Dans la règle d’édition de liens : ^ représente tous les fichiers objets indiqués dans OBJECTS.AccueilAnnexe A - Compiler un projet C++ avec un makefile Dans une règle générique, le symbole % représente la partie variable d’un nom de fichier. permet par exemple d’établir les correspondances : La même règle peut ainsi être utilisée pour plusieurs fichiers. Options du compilateur utilisées Utiliser la norme C++17 Activer un ensemble important d’avertissements Activer des avertissements supplémentaires Signaler certaines extensions non conformes à la norme Optimisation plus ou moins importante de la compilation Compiler sans effectuer l’édition de liens Indiquer le nom du fichier produit Générer les dépendances vers les fichiers inclus Ajouter des règles auxiliaires aux dépendances générées L’option -o signifie simplement que nous choisissons le nom du fichier produit, donc : clang++ -c Matrice.cpp -o Matrice.o produit le fichier objet Matrice.o, tandis que clang++ main.o Matrice.o -o programme produit l’exécutable programme. L’option -o signifie simplement que nous choisissons le nom du fichier produit, ainsi : clang++ -c Matrice.cpp -o Matrice.o produit le fichier objet Matrice.o, tandis que : clang++ main.o Matrice.o -o programme produit l’exécutable programme. Dans nos Makefiles, nous avons adopté trois cibles pratiques : .PHONY: build run cleanAccueilAnnexe A - Compiler un projet C++ avec un makefile construit le programme si cela est nécessaire. vérifie d’abord que le programme est construit et à jour, puis l’exécute et enfin supprime les fichiers générés par la construction. Avec notre Makefile général, il s’agit notamment de : • l’exécutable ; • les fichiers objets .o ; • les fichiers de dépendances .d. Le chemin suivi par make Lorsque nous exécutons make ne commence pas nécessairement par exécuter une commande, il examine d’abord les dépendances. Pour un projet comportant plusieurs fichiers source, le raisonnement peut être représenté schématiquement l’exécutable est-il à jour ? les fichiers objets sont-ils à jour ? les sources ou les fichiers inclus ont-ils changé ? reconstruire uniquement ce qui est nécessaire Si tout est déjà à jour, aucune compilation n’est nécessaire. C’est cette gestion des dépendances qui constitue l’intérêt principal de make. Un Makefile n’est pas un programme C++ et make n’est pas un compilateur, mais le Makefile décrit les relations entre les différents éléments nécessaires à la construction d’un programme et les commandes permettant de les produire. Dans le cas d’un projet C++ classique, nous avons rencontré trois catégories principales de fichiers : • les fichiers .cpp, qui constituent les unités de compilation ;AccueilAnnexe A - Compiler un projet C++ avec un makefile • les fichiers .h, qui sont inclus dans ces unités et constituent souvent des dépendances ; • les fichiers .tpp, utilisés dans notre organisation pour rendre les définitions des templates disponibles au compilateur et qui constituent également des dépendances. La compilation séparée permet de transformer indépendamment les différents fichiers .cpp en fichiers objets L’édition de liens réunit ensuite ces fichiers objets afin de produire l’exécutable. L’utilitaire make automatise ces opérations et évite de reconstruire inutilement les parties du projet qui n’ont pas été affectées par une modification. Grâce à la génération automatique des dépendances avec -MMD et -MP, le Makefile peut rester relativement compact tout en tenant compte des fichiers inclus par le programme. Le compilateur construit le programme. make décide ce qui doit être reconstruit Dans cette annexe, nous utilisons clang++, le compilateur C++ fourni avec l’environnement de développement Apple. Sur d’autres systèmes, notamment sous Linux, le compilateur GCC est galement très répandu. La commande correspondante pour compiler du C++ est généralement : Dans de nombreux Makefiles, il suffit alors de remplacer sans modifier les règles générales de construction du projet. Le modèle "passe-partout" est : # Options de compilation # Optimisation facultative pour une version finale : # ajouter -O2 a CXXFLAGSAccueilAnnexe A - Compiler un projet C++ avec un makefile # Fichiers sources a compiler SOURCES = main.cpp Matrice.cpp Complexe.cpp # Fichiers objets et dependances generes automatiquement OBJECTS = (OBJECTS:.o=.d) # Cibles artificielles .PHONY: build run clean # Compilation separee # Compilation si necessaire, puis execution # Suppression des fichiers generes # Inclusion des dependances generees automatiquement Il suffit donc d’entrer les noms correspondant et lancer make. Vous trouverez un lien de téléchargement du Makefile ci-dessus sur mon site. le Makefile décrit la construction du processus, c’est make qui est le programme exécutable. Makefile n’a pas besoin d’être rendu exécutable avec la commande : chmod +x.Accueilcpp-An1- Annexe A - Compiler un projet C++ avec un makefile https://cpp-iot-etc.ch
Source PDF : cpp-An1.pdf
(Limite 50000 octets — symratio=0.40, minlen=20, clean)
Compiler un projet C++ avec make Compilation et édition de liens . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Structure générale d’une règle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Cas A : un programme contenant uniquement main.cpp . . . . . . . . . . . . . . . . . . . Cas B : un projet comportant plusieurs fichiers .cpp et .h . . . . . . . . . . . . . . . . . . Générer automatiquement les dépendances . . . . . . . . . . . . . . . . . . . . . . . . . . . Cas C : un projet utilisant des fichiers .tpp . . . . . . . . . . . . . . . . . . . . . . . . . . Comparaison des trois organisations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Un modèle général de Makefile . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Annexe A - Compiler un projet C++ avec un makefile Compiler un projet C++ avec make Lorsqu’un programme C++ ne contient qu’un unique fichier source, sa compilation peut être réalisée directement depuis le terminal à l’aide du compilateur. Par exemple, si on a un programme main.cpp dans un clang++ -std=c++17 main.cpp -o programme Cette méthode devient cependant trèspeu pratique lorsque le projet comporte beaucoup de fichiers source. Dans ce cas, il faut indiquer au compilateur quels fichiers doivent être compilés, gérer les différentes étapes de la construction du programme et éviter de recompiler inutilement les fichiers qui n’ont pas été modifiés. C’est l’utilitaire make permet d’automatiser ces opérations. Les instructions nécessaires à la construction du programme sont placées dans un fichier nommé : Nous allons construire progressivement des Makefiles adaptés à trois organisations courantes d’un projet 1. un programme constitué uniquement d’un fichier main.cpp ; 2. un projet comportant plusieurs fichiers .cpp et leurs fichiers d’en-tête .h ; 3. un projet utilisant des classes templates dont les définitions sont placées dans des fichiers .tpp. Nous supposerons dans tous les exemples que les différents fichiers du projet se trouvent dans le même De quoi make s’occupe-t-il ? Il est important de distinguer le rôle de make de celui du compilateur. L’utilitaire make n’est pas un compilateur C++. Il analyse les règles contenues dans le Makefile et détermine quelles commandes doivent être exécutées pour construire le programme. Ces commandes peuvent appele ensuite le compilateur C++. Dans nos exemples, nous utiliserons : qui est le compilateur C++ fourni par Clang. Le rôle de make peut donc être résumé de la manière suivante : détermine ce qui doit être construit exécute les commandes nécessaires 1. Dans un projet plus important, les fichiers source, les fichiers d’en-tête et les fichiers générés sont souvent répartis dans plusieurs répertoires. Le principe de fonctionnement de make reste identique ; les chemins des fichiers et les répertoires de recherche doivent simplement être indiqués dans le Makefile. Cette organisation ne sera pas développée ici. Les projets ESP-IDF, étudiés séparément dans ce cours, utilisent un autre système de construction basé sur CMake.AccueilAnnexe A - Compiler un projet C++ avec un makefile Dans cette annexe, nous utilisons clang++, le compilateur C++ fourni avec l’environnement de développement Apple. Sur d’autres systèmes, notamment sous Linux, le compilateur GCC est également très répandu. La commande correspondante pour compiler du C++ est généralement : Dans de nombreux Makefiles, il suffit alors de remplacer : sans modifier les règles générales de construction du projet. Compilation et édition de liens Avant d’étudier la syntaxe d’un Makefile, il est nécessaire de distinguer deux étapes fondamentales de la construction d’un programme C++ : 2. l’édition de liens. Chaque fichier source .cpp peut être compilé séparément. Considérons par exemple : clang++ -std=c++17 -c Matrice.cpp -o Matrice.o produit un fichier objet (extension .o) demande au compilateur de compiler le fichier source sans effectuer l’édition de liens. −−−−−−−−→ Un fichier objet contient du code machine 2 produit à partir du fichier source, mais il ne constitue généralement pas encore un programme exécutable autonome. 2. Le code machine est constitué des instructions directement exécutables par le processeur. Il est représenté par une suite de bits dont la signification dépend de l’architecture du processeur. L’assembleur en fournit une représentation symbolique plus facilement lisible par l’être humain. Une instruction assembleur correspond généralement de manière très directe à une instruction machine. Le code machine ARM64, par exemple, est différent du code machine x86-64 ou RISC-V.AccueilAnnexe A - Compiler un projet C++ avec un makefile L’édition de liens Lorsque le programme est réparti entre plusieurs fichiers source, chacun d’eux peut produire son propre fichier objet, par exemple : Ces fichiers objets doivent ensuite être réunis pour produire l’exécutable. Cette opération est appelée édition de liens. clang++ main.o Matrice.o Complexe.o -o programme Nous pouvons représenter l’ensemble du processus par : main.cpp −→ main.o Matrice.cpp −→ Matrice.o Complexe.cpp −→ Complexe.o Les premières transformations correspondent à la compilation et la dernière étape correspond à l’édition de Pourquoi utiliser make ? Nous pourrions naturellement exécuter toutes ces commandes manuellement et pour un projet de quelques fichiers, cela reste possible. Cette méthode présente cependant un inconvénient important : après la modification d’un seul fichier source, il serait inutile de recompiler tous les autres fichiers. Supposons que seul : ait été modifié, il suffit alors de reconstruire : puis d’effectuer une nouvelle édition de liens. Les fichiers : peuvent être conservés. C’est précisément l’un des rôles essentiels de make qui, pour déterminer ce qui doit être reconstruit, make examine les dépendances entre les fichiers et leurs dates de modification. make ne recompile pas nécessairement tout le projet. Il reconstruit ce qui est devenu périmé après une modification.AccueilAnnexe A - Compiler un projet C++ avec un makefile Structure générale d’une règle Un Makefile est constitué de règles dont la forme générale est : Chaque élément a son rôle. La cible est généralement le fichier que nous voulons construire. Par exemple : indique que la règle permet de construire le fichier Matrice.o. Après les deux-points apparaissent les fichiers dont dépend la cible : Matrice.o: Matrice.cpp Matrice.h Cette règle indique que Matrice.o dépend de Matrice.cpp et de Matrice.h. Si l’un de ces fichiers est plus récent que Matrice.o, make considère que le fichier objet doit être reconstruit. La ligne suivante indique comment construire la cible : Matrice.o: Matrice.cpp Matrice.h clang++ -std=c++17 -c Matrice.cpp -o Matrice.o Nous pouvons donc lire cette règle de la manière suivante : Pour construire Matrice.o, nous avons besoin de Matrice.cpp et de Matrice.h. Si la cible doit être reconstruite, exécuter la commande indiquée. Attention aux tabulations La syntaxe historique de make impose une règle qui peut surprendre : les lignes contenant les commandes doivent normalement commencer par une véritable tabulation. Matrice.o: Matrice.cpp Matrice.h clang++ -std=c++17 -c Matrice.cpp -o Matrice.o la seconde ligne commence par une tabulation et des espaces placés à la place de cette tabulation peuvent provoquer une erreur lors de l’interprétation du Makefile. Dans un Makefile : tabulation, pas indentation C++.AccueilAnnexe A - Compiler un projet C++ avec un makefile Cas A : un programme contenant uniquement main.cpp Commençons par le cas le plus simple : un programme C++ constitué d’un seul fichier source. Dans le répertoire Projet on a : Supposons que nous voulions un exécutable nommé : Une compilation manuelle pourrait être effectuée avec : clang++ -std=c++17 main.cpp -o programme Nous allons maintenant placer cette opération dans un Makefile. Utiliser des variables Un Makefile peut contenir des variables afin d’éviter de répéter plusieurs fois les mêmes informations, nous utiliserons entre autres, CXXFLAGS = -std=c++17 -O0 -Wall -Wextra -pedantic contient le nom du compilateur C++, la variable : contient les options utilisées lors de la compilation et enfin contient le nom de l’exécutable que nous souhaitons produire. Les options choisies sont : • -std=c++17 : utiliser la norme C++ 17 ; • -Wall : activer un ensemble important d’avertissements ; • -Wextra : activer des avertissements supplémentaires ; • -O0 jusqu’à -O3 : optimisation croissante du code, la valeur maximum recommandée est -02. • -pedantic : demander au compilateur de signaler certaines constructions non conformes strictement à la norme choisie (peut devenir super pénible !). Notre premier Makefile sera :AccueilAnnexe A - Compiler un projet C++ avec un makefile CXXFLAGS = -std=c++17 -Wall -O0 -Wextra -pedantic .PHONY: build run clean build: (TARGET): main.cpp (CXXFLAGS) main.cpp -o $(TARGET) Pour utiliser le contenu d’une variable dans un Makefile, nous écrivons : indique que la cible build dépend de l’exécutable. Lorsque nous exécutons : make commence par vérifier si l’exécutable doit être construit ou mis à jour. La règle correspondante est : (CXXFLAGS) main.cpp -o $(TARGET) Si main.cpp est plus récent que l’exécutable, ou si celui-ci n’existe pas encore, la commande de compilationAccueilAnnexe A - Compiler un projet C++ avec un makefile indique que l’exécution dépend d’abord de la cible build. demande d’abord à make de vérifier que le programme est à jour, puis lance l’exécutable. Cette organisation est pratique, car il n’est pas nécessaire de lancer manuellement une compilation avant chaque exécution. supprime l’exécutable produit par la compilation. Nous pouvons écrire : pour revenir à un répertoire ne contenant que les fichiers source. de la commande rm évite qu’une erreur soit produite si le fichier à supprimer n’existe pas. ne correspondent à des commandes logiques. Nous les déclarons donc comme cibles particulières à l’aide de : .PHONY: build run clean Cette déclaration (ce n’est pas une commande) indique à make que build run clean doivent toujours être interprétés comme des cibles à exécuter et non comme des fichiers à examiner sur le disque.AccueilAnnexe A - Compiler un projet C++ avec un makefile Les trois commandes essentielles de notre Makefile sont : Nous disposons à présent d’un système simple permettant de construire, exécuter et nettoyer un petit Cas B : un projet comportant plusieurs fichiers .cpp et .h Considérons maintenant un projet C++ constitué de plusieurs fichiers source qui se trouvent dans le même Le fichier main.cpp utilise les classes déclarées dans les fichiers d’en-tête : #include "Matrice.h" #include "Complexe.h" et les définitions des méthodes contenues dans ces fichiers d’en-tête .h, sont dans Matrice.cpp et Dans cette situation, il serait pratique de demander au compilateur de traiter tous les fichiers source en une seule commande : clang++ -std=c++17 main.cpp Matrice.cpp Complexe.cpp -o programme Cette commande fonctionne, mais elle présente un inconvénient : chaque exécution de la commande provoque la recompilation de tous les fichiers source. Nous allons donc introduire la compilation séparée. Construire séparément les fichiers objets Avec la commande qui suit, chaque fichier .cpp est compilé indépendamment afin de produire un fichier clang++ -std=c++17 -c main.cpp -o main.o clang++ -std=c++17 -c Matrice.cpp -o Matrice.o clang++ -std=c++17 -c Complexe.cpp -o Complexe.o Nous obtenons donc : main.cpp −→ main.o Matrice.cpp −→ Matrice.o Complexe.cpp −→ Complexe.oAccueilAnnexe A - Compiler un projet C++ avec un makefile demande au compilateur de s’arrêter après la production du fichier objet. Aucun exécutable n’est encore Construire l’exécutable Une fois les trois fichiers objets disponibles, ils peuvent être réunis par l’édition de liens : clang++ main.o Matrice.o Complexe.o -o programme Nous obtenons alors l’exécutable : Le processus complet peut être représenté par : main.cpp −→ main.o Matrice.cpp −→ Matrice.o Complexe.cpp −→ Complexe.o L’intérêt de la compilation séparée Supposons maintenant que nous modifiions uniquement le fichier : Il n’est pas nécessaire de recompiler main.cpp ni Complexe.cpp. Seul le fichier objet correspondant doit être reconstruit : clang++ -std=c++17 -c Matrice.cpp -o Matrice.o Une nouvelle édition de liens est ensuite nécessaire : clang++ main.o Matrice.o Complexe.o -o programme Les fichiers main.o et Complexe.o sont réutilisés tels quels et c’est précisément ce travail de vérification que make peut effectuer automatiquement. Un Makefile utilisant la compilation séparée Dans ce cas, nous écrirons explicitement une règle pour chaque fichier objet : CXXFLAGS = -std=c++17 -Wall -Wextra -pedantic OBJECTS = main.o Matrice.o Complexe.o .PHONY: build run clean main.o: main.cpp Matrice.h Complexe.h (CXXFLAGS) -c main.cpp -o main.oAccueilAnnexe A - Compiler un projet C++ avec un makefile Matrice.o: Matrice.cpp Matrice.h (CXXFLAGS) -c Complexe.cpp -o Complexe.o Nous remarquons une nouvelle variable, la variable OBJECTS. La liste des fichiers objets La nouvelle variable OBJECTS a pour valeurs : OBJECTS = main.o Matrice.o Complexe.o Elle contient donc la liste des fichiers objets nécessaires à la construction de l’exécutable. indique que l’exécutable dépend de tous ces fichiers objets. Si l’un d’eux est reconstruit, l’exécutable devient plus ancien que l’un de ses fichiers dépendants et l’édition de liens doit être effectuée à nouveau. Le rôle des fichiers d’en-tête dans les dépendances Considérons la règle : Matrice.o: Matrice.cpp Matrice.h (CXXFLAGS) -c Matrice.cpp -o Matrice.o Le fichier Matrice.h n’est pas compilé séparément mais il apparaît néanmoins parmi les dépendances de Matrice.o. En effet, Matrice.cpp contient normalement l’appel : #include "Matrice.h" Une modification de la déclaration de la classe dans Matrice.h peut donc modifier le résultat de la compilation de Matrice.cpp. Il faut par conséquent reconstruire Matrice.o si Matrice.h a été modifié. Que se passe-t-il après une modification ? Supposons que le projet ait déjà été entièrement construit et que nous modifions uniquement : make constate que Matrice.cpp est plus récent que Matrice.o, il doit donc être reconstruit et Matrice.cpp −→ Matrice.oAccueilAnnexe A - Compiler un projet C++ avec un makefile Le nouveau fichier Matrice.o étant maintenant plus récent que l’exécutable, celui-ci doit à son tour être reconstruit par édition de liens. ne sont pas recompilés. Si nous modifions maintenant Matrice.h, la situation est différente car d’après notre Makefile, ce fichier intervient dans les règles : main.o: main.cpp Matrice.h Complexe.h Matrice.o: Matrice.cpp Matrice.h make reconstruira donc : puis effectuera une nouvelle édition de liens. Le fichier Complexe.o, qui ne dépend pas de Matrice.h, ne sera pas recompilé. make ne cherche pas simplement quels fichiers ont été modifiés. Il détermine quelles cibles dépendent des fichiers modifiés. Simplifier le Makefile Le Makefile précédent fonctionne correctement, mais il contient plusieurs règles très similaires : main.o: main.cpp Matrice.h Complexe.h (CXXFLAGS) -c main.cpp -o main.o Matrice.o: Matrice.cpp Matrice.h (CXXFLAGS) -c Complexe.cpp -o Complexe.o La structure de la commande de compilation est toujours la même et seuls les noms des fichiers changent. L’utilitaire make permet de factoriser ce type de répétition à l’aide de règles génériques. Définir les fichiers source Nous pouvons commencer par regrouper les fichiers source dans une variable : SOURCES = main.cpp Matrice.cpp Complexe.cpp À partir de cette liste, nous pouvons demander à make de construire automatiquement les noms des fichiers objets correspondants : OBJECTS = $(SOURCES:.cpp=.o) Cette écriture remplace le suffixe :AccueilAnnexe A - Compiler un projet C++ avec un makefile dans chacun des éléments de la variable SOURCES. Utiliser une règle générique Tous les fichiers .cpp sont compilés selon le même principe : clang++ ... -c fichier.cpp -o fichier.o Nous pouvons donc écrire une seule règle capable de s’appliquer à tous les fichiers source : représente ici une partie variable du nom du fichier. « pour construire un fichier objet à partir d’un fichier source portant le même nom de base » Les variables automatiques Dans une règle, make fournit certaines variables automatiques dont la valeur dépend de la cible en cours nous utilisons deux de ces variables : premier fichier dépendant de la règle nom de la cible en cours de construction Supposons que make doive construire : La règle générique : est alors interprétée conceptuellement comme : clang++ -std=c++17 -Wall -Wextra -pedantic \ -c Matrice.cpp -o Matrice.o Dans cette instanciation de la règle :AccueilAnnexe A - Compiler un projet C++ avec un makefile La variable automatique $^ Une autre variable automatique particulièrement utile est : ^ représente la liste de toutes les dépendances placées après les deux-points : dans la règle en cours. Nous pouvons donc écrire : (OBJECTS) signifie : "donne-moi le contenu de la variable appelée OBJECTS" "donne-moi toutes les dépendances de la règle que je suis actuellement en train Un Makefile plus général Nous pouvons maintenant remplacer les règles individuelles par un Makefile plus compact : SOURCES = main.cpp Matrice.cpp Complexe.cpp OBJECTS = $(SOURCES:.cpp=.o) .PHONY: build run clean Ce Makefile contient maintenant une seule règle de compilation pour tous les fichiers .cpp, et ajouter un nouveau fichier source, par exemple :AccueilAnnexe A - Compiler un projet C++ avec un makefile nécessite simplement de modifier la variable : SOURCES = main.cpp Matrice.cpp Complexe.cpp Vecteur.cpp sera automatiquement ajouté à la variable OBJECTS et compilé par la règle générique. Une limitation de cette simplification Notre règle générique : ne mentionne plus explicitement les fichiers d’en-tête donc, si nous modifions uniquement : make ne peut pas nécessairement déduire, avec ce Makefile seul, qu’il doit reconstruire les fichiers objets qui dépendent de cet en-tête. Nous avons donc simplifié le Makefile, mais perdu une partie de l’information concernant les dépendances. Générer automatiquement les dépendances Dans la section précédente, nous avons simplifié les règles de compilation grâce à une règle générique : Cette solution présente cependant une difficulté : les fichiers d’en-tête inclus par les fichiers source n’apparaissent plus explicitement dans les dépendances. Il serait possible de maintenir ces informations manuellement, mais cette solution devient rapidement fastidieuse lorsque le nombre de fichiers augmente. Les options -MMD et -MP Nous allons ajouter deux options aux options de compilation : Notre variable devient donc : L’option -MMD demande au compilateur de produire, en plus du fichier objet, un fichier décrivant les dépendances du fichier source. Ainsi, la compilation : clang++ -MMD -c Matrice.cpp -o Matrice.o peut produire simultanément :AccueilAnnexe A - Compiler un projet C++ avec un makefile Le fichier Matrice.o contient le code objet et fichier Matrice.d contient les informations permettant à make de connaître les fichiers dont dépend cette compilation. Le contenu d’un fichier de dépendances Supposons que Matrice.cpp contienne : #include "Matrice.h" Le fichier Matrice.d généré par le compilateur pourra contenir une règle comparable à : Matrice.o: Matrice.cpp Matrice.h Nous retrouvons donc automatiquement l’information que nous avions précédemment écrite à la main : Matrice.o: Matrice.cpp Matrice.h Le compilateur connaît les fichiers réellement inclus pendant la compilation. Il est donc particulièrement bien placé pour générer cette liste. make utilise les dépendances, mais le compilateur peut les déterminer automatiquement. Le rôle de l’option -MP L’option -MP complète la génération des dépendances en ajoutant des règles auxiliaires pour les fichiers d’en-tête rencontrés. Elle permet notamment d’éviter certaines erreurs de make lorsqu’un fichier d’en-tête précédemment utilisé est ensuite supprimé ou renommé. Dans nos Makefiles, nous utiliserons donc généralement les deux options ensemble : La liste des fichiers de dépendances Nous disposons déjà de SOURCES = main.cpp Matrice.cpp Complexe.cpp OBJECTS = $(SOURCES:.cpp=.o) Nous pouvons appliquer le même principe aux fichiers de dépendances : DEPS = $(OBJECTS:.o=.d) Si OBJECTS contient : main.o Matrice.o Complexe.o alors DEPS contiendra : main.d Matrice.d Complexe.d Matrice.o, Matrice.d Complexe.cpp −→ Complexe.o, Complexe.dAccueilAnnexe A - Compiler un projet C++ avec un makefile Inclure les dépendances dans le Makefile Les fichiers .d contiennent des règles compréhensibles par make et il faut donc demander à make de les lire en ajoutant à la fin du Makefile : Cette instruction demande d’inclure le contenu des fichiers indiqués dans la variable DEPS (le signe - placé devant include est important). Lors de la toute première compilation, les fichiers : n’existent pas encore mais ils seront précisément créés pendant cette première compilation. demande à make de poursuivre son travail si ces fichiers ne sont pas encore disponibles. Un Makefile avec dépendances automatiques Nous pouvons maintenant réunir tous les éléments étudiés : SOURCES = main.cpp Matrice.cpp Complexe.cpp OBJECTS = (OBJECTS:.o=.d) .PHONY: build run cleanAccueilAnnexe A - Compiler un projet C++ avec un makefile Exemple de reconstruction Supposons que le projet soit entièrement compilé. Nous modifions uniquement : Le fichier de dépendances généré précédemment indique à make quels fichiers objets dépendent de cet seuls les fichiers objets concernés sont recompilés et l’exécutable est ensuite reconstruit par édition de liens. Il n’est donc plus nécessaire de maintenir manuellement dans le Makefile la liste des fichiers d’en-tête utilisés par chaque fichier source. 1. Les fichiers .cpp sont indiqués dans le Makefile. 2. Les dépendances vers les fichiers .h peuvent être déterminées automatiquement par le compilateur. Cas C : un projet utilisant des fichiers .tpp Les templates introduisent une organisation légèrement différente des fichiers source. Dans le chapitre consacré à Matrice<T>, nous avons par exemple utilisé une structure de la forme : |-- MatriceCarree.tpp À première vue, nous pourrions être tentés d’ajouter les fichiers .tpp à la liste des fichiers à compiler mais ce serait une erreur car les fichiers .tpp jouent ici un rôle différent de celui des fichiers .cpp.AccueilAnnexe A - Compiler un projet C++ avec un makefile Pourquoi le fichier .tpp n’est-il pas compilé séparément ? Pour une classe ordinaire, nous pouvons généralement séparer la déclaration et la définition : Le fichier Matrice.cpp constitue alors une unité de compilation qui peut produire son propre fichier objet : Matrice.cpp −→ Matrice.o Avec un template, la situation est différente. Lorsque le compilateur rencontre par exemple : Matrice<int> A(3, 3); il doit disposer de la définition du template afin de pouvoir produire le code correspondant à l’instanciation demandée, c’est pour cela que nous avons placé les définitions des méthodes dans Matrice.tpp et inclus celles-ci à la fin de Matrice.h avec la commande #include "Matrice.tpp" Le compilateur rencontre ainsi la chaîne d’inclusions suivante : main.cpp −→ Matrice.h −→ Matrice.tpp Le contenu du fichier Matrice.tpp devient donc visible lors de la compilation du fichier qui utilise le template et par conséquent il n’est pas nécessaire de compiler séparément Matrice.tpp. Ne pas ajouter les fichiers .tpp à SOURCES Considérons la variable SOURCES utilisée dans notre Makefile : Il est faut d’écrire SOURCES = main.cpp Matrice.tpp MatriceCarree.tpp Dans l’organisation adoptée ici, les fichiers .tpp ne constituent pas des unités de compilation indépendantes. Ils sont inclus dans les fichiers d’en-tête correspondants. .cpp : unité de compilation .tpp : définitions de templates incluses Les fichiers .tpp comme dépendances Même s’il n’est pas compilé séparément, un fichier .tpp reste une dépendance du fichier source qui finit par l’inclure. Supposons que main.cpp contienne : #include "Matrice.h" et que Matrice.h contienne : #include "Matrice.tpp" Le compilateur voit alors également le contenu de Matrice.tpp lors de la compilation de main.cpp et grâce à l’option −MDD cette dépendance peut être enregistrée automatiquement dans le fichier main.d. Par exemple, le fichier de dépendances pourra contenir une règle comparable à :AccueilAnnexe A - Compiler un projet C++ avec un makefile main.o: main.cpp Matrice.h Matrice.tpp \ MatriceCarree.h MatriceCarree.tpp Ainsi, si nous modifions uniquement : make sait que main.o doit être reconstruit. Le fichier .tpp n’est donc pas une source compilée séparément, mais il participe pleinement au système de Ne pas compiler un fichier séparément ne signifie pas qu’il ne constitue pas une dépendance. Makefile du projet Matrice<T> Dans notre projet, les implémentations des classes templates se trouvent entièrement dans les fichiers .tpp inclus par les fichiers d’en-tête. Le seul fichier .cpp est donc : Nous pouvons utiliser : OBJECTS = (OBJECTS:.o=.d) .PHONY: build run cleanAccueilAnnexe A - Compiler un projet C++ avec un makefile n’apparaissent pas explicitement dans ce Makefile car ils sont découverts par le compilateur lors du traitement des directives #include, puis enregistrés dans les fichiers de dépendances générés automatiquement. Mélanger fichiers .cpp et .tpp La présence de fichiers .tpp n’interdit naturellement pas l’utilisation simultanée de fichiers .cpp ordinaires. Un projet pourrait par exemple contenir : Dans ce cas, la liste des sources serait : SOURCES = main.cpp Complexe.cpp SOURCES = main.cpp Complexe.cpp Matrice.tpp Les fichiers .cpp sont compilés séparément et les fichiers .tpp sont inclus là où les définitions des templates doivent être visibles. fichiers qui doivent être compilés séparément Comparaison des trois organisations Les trois situations étudiées utilisent les mêmes principes de make. Ce qui change essentiellement est la nature des fichiers qui doivent être compilés séparément, il est donc utile de comparer les trois organisations. Cas A : un unique fichier main.cpp La structure la plus élémentaire est : Un seul fichier source doit être compilé Pour un projet aussi simple, il n’est pas indispensable de produire explicitement un fichier objet intermédiaire, ainsi nous avions utilisé : (CXXFLAGS) main.cpp -o $(TARGET) Le compilateur effectue alors successivement la compilation et l’édition de liens.AccueilAnnexe A - Compiler un projet C++ avec un makefile Cas B : plusieurs fichiers .cpp Nous considérons à présent : Les fichiers qui doivent être compilés séparément sont : SOURCES = main.cpp Matrice.cpp Complexe.cpp Lesquels produisent respectivement : OBJECTS = main.o Matrice.o Complexe.o Les fichiers .h ne sont pas compilés séparément mais ils constituent cependant des dépendances des fichiers source qui les incluent. Cas C : présence de fichiers .tpp |-- MatriceCarree.tpp Dans notre organisation des templates, les fichiers .tpp sont inclus par les fichiers d’en-tête correspondants et le seul fichier compilé séparément reste donc : Les fichiers .h et .tpp seront automatiquement identifiés comme dépendances grâce aux options de génération des dépendances. Tableau récapitulatif Dans l’organisation adoptée dans ce cours, la règle fondamentale peut donc être formulée simplement : • Les fichiers .cpp sont des unités de compilation. • Les fichiers .h et .tpp sont inclus et peuvent constituer des dépendances.AccueilAnnexe A - Compiler un projet C++ avec un makefile Un modèle général de Makefile Pour un petit projet C++ dont tous les fichiers se trouvent dans le même répertoire, nous pouvons maintenant utiliser le modèle suivant : SOURCES = main.cpp Matrice.cpp Complexe.cpp OBJECTS = (OBJECTS:.o=.d) .PHONY: build run clean Dans la plupart des petits projets, seules deux lignes doivent être adaptées : SOURCES = main.cpp Matrice.cpp Complexe.cpp La première définit le nom de l’exécutable et la seconde contient la liste des fichiers .cpp qui doivent être compilés séparément. Les variables OBJECTS et DEPS sont ensuite construites automatiquement à partir de cette liste. Si le projet contient également des fichiers .tpp, ceux-ci ne doivent pas être ajoutés à SOURCES lorsqu’ils sont inclus par les fichiers d’en-tête.AccueilAnnexe A - Compiler un projet C++ avec un makefile conduit simplement à : SOURCES = main.cpp Complexe.cpp La syntaxe d’un Makefile est relativement simple, mais certaines erreurs sont particulièrement fréquentes. Utiliser des espaces à la place d’une tabulation Une commande associée à une règle doit normalement commencer par une véritable tabulation : Une indentation réalisée uniquement avec des espaces peut provoquer une erreur de syntaxe. Cette particularité est propre à la syntaxe traditionnelle des Makefiles et ne doit pas être confondue avec les règles d’indentation du code C++. Ajouter un fichier d’en-tête à SOURCES Une déclaration telle que : SOURCES = main.cpp Matrice.cpp Matrice.h est incorrecte dans notre organisation. Le fichier Matrice.h n’est pas une unité de compilation et il faut écrire : SOURCES = main.cpp Matrice.cpp Les dépendances vers Matrice.h seront déterminées automatiquement. Ajouter un fichier .tpp à SOURCES De même, dans l’organisation utilisée pour nos templates : SOURCES = main.cpp Matrice.tpp n’est pas approprié. Le fichier Matrice.tpp est inclus depuis Matrice.h. Il doit être visible du compilateur, mais il ne constitue pas une unité de compilation séparée. Confondre compilation et édition de liens clang++ -c Matrice.cpp -o Matrice.o produit un fichier objet. clang++ main.o Matrice.o -o programme réunit les fichiers objets pour produire l’exécutable. Ces deux opérations correspondent à deux étapes différentes de la construction du programme.AccueilAnnexe A - Compiler un projet C++ avec un makefile Erreur de compilation ou erreur d’édition de liens ? Une erreur détectée pendant la compilation concerne généralement le contenu d’un fichier source : syntaxe incorrecte, type incompatible, nom inconnu, etc. Une erreur détectée pendant l’édition de liens signifie souvent que le compilateur a accepté les déclarations rencontrées, mais qu’une définition nécessaire n’a pas été trouvée parmi les fichiers objets fournis à l’éditeur de liens. Cette distinction peut être très utile lors de la recherche d’une erreur dans un projet comportant plusieurs Fiche de référence Les principaux éléments rencontrés dans cette annexe sont regroupés ci-dessous. Compilateur C++ utilisé Options utilisées lors de la compilation Nom de l’exécutable à produire Liste des fichiers .cpp à compiler Liste des fichiers objets .o Liste des fichiers de dépendances .d Les noms TARGET, SOURCES, OBJECTS et DEPS ne sont pas imposés par make. Ce sont des noms choisis par En revanche, certains noms tels que CXX et CXXFLAGS sont conventionnellement utilisés par make pour représenter respectivement le compilateur C++ et ses options de compilation. Variables automatiques Certaines variables sont automatiquement définies par make lorsqu’une règle est exécutée. Les trois utilisées dans cette annexe sont : Première dépendance Ensemble des dépendances Considérons par exemple : Lors de la construction de Matrice.o à partir de Matrice.cpp, nous avons : Dans la règle d’édition de liens : ^ représente tous les fichiers objets indiqués dans OBJECTS.AccueilAnnexe A - Compiler un projet C++ avec un makefile Dans une règle générique, le symbole % représente la partie variable d’un nom de fichier. permet par exemple d’établir les correspondances : La même règle peut ainsi être utilisée pour plusieurs fichiers. Options du compilateur utilisées Utiliser la norme C++17 Activer un ensemble important d’avertissements Activer des avertissements supplémentaires Signaler certaines extensions non conformes à la norme Optimisation plus ou moins importante de la compilation Compiler sans effectuer l’édition de liens Indiquer le nom du fichier produit Générer les dépendances vers les fichiers inclus Ajouter des règles auxiliaires aux dépendances générées L’option -o signifie simplement que nous choisissons le nom du fichier produit, donc : clang++ -c Matrice.cpp -o Matrice.o produit le fichier objet Matrice.o, tandis que clang++ main.o Matrice.o -o programme produit l’exécutable programme. L’option -o signifie simplement que nous choisissons le nom du fichier produit, ainsi : clang++ -c Matrice.cpp -o Matrice.o produit le fichier objet Matrice.o, tandis que : clang++ main.o Matrice.o -o programme produit l’exécutable programme. Dans nos Makefiles, nous avons adopté trois cibles pratiques : .PHONY: build run cleanAccueilAnnexe A - Compiler un projet C++ avec un makefile construit le programme si cela est nécessaire. vérifie d’abord que le programme est construit et à jour, puis l’exécute et enfin supprime les fichiers générés par la construction. Avec notre Makefile général, il s’agit notamment de : • l’exécutable ; • les fichiers objets .o ; • les fichiers de dépendances .d. Le chemin suivi par make Lorsque nous exécutons make ne commence pas nécessairement par exécuter une commande, il examine d’abord les dépendances. Pour un projet comportant plusieurs fichiers source, le raisonnement peut être représenté schématiquement l’exécutable est-il à jour ? les fichiers objets sont-ils à jour ? les sources ou les fichiers inclus ont-ils changé ? reconstruire uniquement ce qui est nécessaire Si tout est déjà à jour, aucune compilation n’est nécessaire. C’est cette gestion des dépendances qui constitue l’intérêt principal de make. Un Makefile n’est pas un programme C++ et make n’est pas un compilateur, mais le Makefile décrit les relations entre les différents éléments nécessaires à la construction d’un programme et les commandes permettant de les produire. Dans le cas d’un projet C++ classique, nous avons rencontré trois catégories principales de fichiers : • les fichiers .cpp, qui constituent les unités de compilation ;AccueilAnnexe A - Compiler un projet C++ avec un makefile • les fichiers .h, qui sont inclus dans ces unités et constituent souvent des dépendances ; • les fichiers .tpp, utilisés dans notre organisation pour rendre les définitions des templates disponibles au compilateur et qui constituent également des dépendances. La compilation séparée permet de transformer indépendamment les différents fichiers .cpp en fichiers objets L’édition de liens réunit ensuite ces fichiers objets afin de produire l’exécutable. L’utilitaire make automatise ces opérations et évite de reconstruire inutilement les parties du projet qui n’ont pas été affectées par une modification. Grâce à la génération automatique des dépendances avec -MMD et -MP, le Makefile peut rester relativement compact tout en tenant compte des fichiers inclus par le programme. Le compilateur construit le programme. make décide ce qui doit être reconstruit Dans cette annexe, nous utilisons clang++, le compilateur C++ fourni avec l’environnement de développement Apple. Sur d’autres systèmes, notamment sous Linux, le compilateur GCC est galement très répandu. La commande correspondante pour compiler du C++ est généralement : Dans de nombreux Makefiles, il suffit alors de remplacer sans modifier les règles générales de construction du projet. Le modèle "passe-partout" est : # Options de compilation # Optimisation facultative pour une version finale : # ajouter -O2 a CXXFLAGSAccueilAnnexe A - Compiler un projet C++ avec un makefile # Fichiers sources a compiler SOURCES = main.cpp Matrice.cpp Complexe.cpp # Fichiers objets et dependances generes automatiquement OBJECTS = (OBJECTS:.o=.d) # Cibles artificielles .PHONY: build run clean # Compilation separee # Compilation si necessaire, puis execution # Suppression des fichiers generes # Inclusion des dependances generees automatiquement Il suffit donc d’entrer les noms correspondant et lancer make. Vous trouverez un lien de téléchargement du Makefile ci-dessus sur mon site. le Makefile décrit la construction du processus, c’est make qui est le programme exécutable. Makefile n’a pas besoin d’être rendu exécutable avec la commande : chmod +x.Accueilcpp-An1- Annexe A - Compiler un projet C++ avec un makefile https://cpp-iot-etc.ch
Téléchargement d’un Makefile générique adaptable
Makefile
Fichier Makefile que vous pourrez adapter à votre projet