TP7 : Build system
Le but de ce TP est de comprendre les points suivants :
Structurer son projet¶
Voici la structure courante de votre projet :
TP7/
└── main.cpp#include <iostream>
#include <string>
#include <vector>
class Book {
private:
std::string mTitle;
std::string mAuthor;
public:
Book(const std::string& title, const std::string& author) : mTitle(title), mAuthor(author) {}
std::string getTitle() const {
return mTitle;
}
std::string getAuthor() const {
return mAuthor;
}
void display() const {
std::cout << mTitle << " by " << mAuthor << std::endl;
}
};
class Library {
private:
std::string mName;
std::vector<Book> mBooks;
public:
Library(const std::string& name) : mName(name) {}
void addBook(const Book& book) {
mBooks.push_back(book);
}
void displayBooks() const {
std::cout << "Library: " << mName << std::endl;
for (const Book& book : mBooks) {
book.display();
}
}
};
int main() {
Book gameOfThrones("Game of Thrones", "G. R. R. Martin");
Book theHobbit("The Hobbit", "J. R. R. Tolkien");
Library library("City Library");
library.addBook(gameOfThrones);
library.addBook(theHobbit);
library.displayBooks();
return 0;
}Compilez et exécutez le programme. Observez la sortie.
Nettoyez le projet en supprimant les fichiers générés par la compilation.
Restructurez votre projet comme en cours en utilisant la structure et le makefile suivants :
TP7/
├── include/
│ ├── book.h
│ └── library.h
├── source/
│ ├── book.cpp
│ ├── library.cpp
│ └── main.cpp
└── makefileall: library-app
library-app: source/book.o source/library.o source/main.o
g++ source/book.o source/library.o source/main.o -o library-app
source/book.o: source/book.cpp include/book.h
g++ -std=c++17 -Iinclude -c source/book.cpp -o source/book.o
source/library.o: source/library.cpp include/library.h include/book.h
g++ -std=c++17 -Iinclude -c source/library.cpp -o source/library.o
source/main.o: source/main.cpp include/library.h include/book.h
g++ -std=c++17 -Iinclude -c source/main.cpp -o source/main.o
clean:
rm -f source/book.o source/library.o source/main.o library-app
run:
./library-app
.PHONY: all clean runEn faisant
makepuismake runen se plaçant dansTP7/, est-ce que votre programme produit la bonne sortie ?
Erreur : séparateur manquant
Si vous rencontrez l’erreur “séparateur manquant” en exécutant make, il est possible que le copier-coller du contenu du makefile dans votre éditeur ait remplacé les tabulations par des espaces. Assurez-vous que chaque ligne de commande dans le makefile est bien précédée d’une tabulation (Tab au lieu de quatre espaces).
Nettoyez le projet en utilisant
make clean.
Compilation automatique¶
Revenez au makefile précédent et essayez de le comprendre grâce au cours. Vous pouvez utiliser les commandes
touchetmake -dpour mieux comprendre les différentes étapes du processus demake.
std=c++17
std=c++17Nous utilisons c++17 pour pouvoir utiliser les syntaxes modernes de C++ comme for (const Book& book : mBooks).
-Iinclude
-Iinclude-Iinclude est une option qui indique au compilateur que les headers comme #include "book.h" sont situés dans le dossier include, ce qui nous permet d’écrire #include "book.h" alors que le header book.h ne se trouve pas au même endroit que le code source book.cpp.
Dans les prérequis des fichiers sources, nous avons ajouté manuellement les headers utilisés, mais nous voulons pouvoir facilement en ajouter ou en supprimer sans modifier notre script de compilation automatique (makefile). Autrement dit, nous souhaitons que la génération des dépendances soit également automatisée.
Exécutez
make cleanavant de passer à l’étape suivante.Apportez les modifications suivantes au makefile :
all: library-app
library-app: source/book.o source/library.o source/main.o
g++ source/book.o source/library.o source/main.o -o library-app
source/book.o: source/book.cpp
g++ -std=c++17 -Iinclude -MMD -MF source/book.d -c source/book.cpp -o source/book.o
source/library.o: source/library.cpp
g++ -std=c++17 -Iinclude -MMD -MF source/library.d -c source/library.cpp -o source/library.o
source/main.o: source/main.cpp
g++ -std=c++17 -Iinclude -MMD -MF source/main.d -c source/main.cpp -o source/main.o
clean:
rm -f source/book.o source/library.o source/main.o source/book.d source/library.d source/main.d library-app
run:
./library-app
.PHONY: all clean run
-include source/book.d source/library.d source/main.dExécutez la commande
makeet examinez le contenu des fichiers.dgénérés.
Fichiers .d (dépendances)
.d (dépendances)Afin d’éviter d’avoir à spécifier manuellement les headers pour la compilation de chaque fichier source, nous générons automatiquement les fichiers de dépendances .d en utilisant l’option -MMD (qui inclut les prérequis avec les headers que nous avons créés, mais pas ceux des bibliothèques C++ stables) et demandons à make d’inclure ces prérequis avec l’option -include *.d.
En raison de l’ordre d’exécution, la génération de main.d par exemple se produit après que les prérequis de main.o aient été traités. Ce n’est pas un problème, car l’objectif d’ajouter les headers aux prérequis est d’éviter de recompiler l’intégralité du projet une fois qu’il a déjà été compilé. La première compilation générera donc ces dépendances, et lors des compilations suivantes, les fichiers .d seront inclus, permettant à make de détecter les changements dans les headers.
-include *.d à la fin
-include *.d à la finLa directive -include doit être placée à la fin du makefile (contrairement aux directives include habituelles qui se trouvent en début de fichier). Si -include est au début, elle remplace le comportement par défaut de make, qui est make all, par make des cibles incluses.
Les fichiers générés par la compilation sont pour le moment mélangés avec les fichiers sources, pour garder notre projet propre, nous allons les mettre dans un autre sous-répertoire build/.
Exécutez
make cleanavant de passer à l’étape suivante.Apportez les modifications suivantes au makefile :
all: build/binaries/library-app
build:
mkdir -p build/objects build/dependencies build/binaries
build/binaries/library-app: build/objects/book.o build/objects/library.o build/objects/main.o | build
g++ build/objects/book.o build/objects/library.o build/objects/main.o -o build/binaries/library-app
build/objects/book.o: source/book.cpp | build
g++ -std=c++17 -Iinclude -MMD -MF build/dependencies/book.d -c source/book.cpp -o build/objects/book.o
build/objects/library.o: source/library.cpp | build
g++ -std=c++17 -Iinclude -MMD -MF build/dependencies/library.d -c source/library.cpp -o build/objects/library.o
build/objects/main.o: source/main.cpp | build
g++ -std=c++17 -Iinclude -MMD -MF build/dependencies/main.d -c source/main.cpp -o build/objects/main.o
clean:
rm -rf build
run:
./build/binaries/library-app
.PHONY: all build clean run
-include build/dependencies/book.d build/dependencies/library.d build/dependencies/main.dbuild/
build/Afin d’éviter de mélanger les fichiers de compilation avec le code source, nous allons les générer dans un répertoire nommé build et les organiser dans des sous-répertoires : dependencies/ pour les fichiers .d, objects/ pour les fichiers .o et binaries/ pour les exécutables.
Pour ce faire, nous créons une cible build qui génère les répertoires nécessaires à l’aide de l’option -p (pour parents), permettant de créer tous les répertoires (y compris les parents) s’ils n’existent pas déjà. Si les répertoires existent déjà, la commande ne les écrase pas.
La syntaxe | build indique un prérequis d’ordre uniquement.
Le traitement des prérequis n’est pas forcément ordonné, ce qui permet la parallélisation dans les grands projets où plusieurs cibles peuvent être traitées en même temps.
Un prérequis d’ordre uniquement est traité avant la cible, mais il n’est pas utilisé pour déterminer si la cible est à jour.
Autrement dit, | garantit l’ordre d’exécution, mais n’influence pas la recompilation.
Puisque nous ne générons plus tous les fichiers au même endroit, nous spécifions avec l’option -o l’emplacement et le nom du fichier .o à créer. Par exemple, -o build/objects/main.o indique que main.o doit être généré dans build/objects/ sous ce nom. Il en va de même pour -MF build/dependencies/main.d pour les fichiers de dépendances contenant les prérequis.
Maintenant, pour nettoyer le projet (make clean), il suffit de supprimer le répertoire build/. L’option -rf de la commande rm combine les options recursive et force, permettant ainsi de supprimer les répertoires et tous leurs sous-répertoires sans confirmation.
Nous ajoutons également build à .PHONY au cas où le répertoire build/ existe, mais pas ses sous-répertoires. Cela pourrait induire make en erreur, le faisant penser que build est à jour.
Exécutez les différentes commandes
make,make runetmake cleanet observez l’état des répertoires après chaque commande.Exécutez
make cleanavant de passer à l’étape suivante.
Idéalement, nous voulons un makefile qui fonctionnerait pour tous nos projets qui ont la structure :
project/
├── include/
│ ├── header.h
│ └── ...
├── source/
│ ├── code.cpp
│ ├── ...
│ └── ...
└── makefilePour cela, nous allons utiliser des variables pour refactoriser le makefile.
Apportez les modifications suivantes au makefile :
EXECUTABLE = library-app
CXX = g++
CXXFLAGS = -std=c++17 -Iinclude
SOURCES = $(wildcard source/*.cpp)
OBJECTS = $(patsubst source/%.cpp,build/objects/%.o,$(SOURCES))
DEPENDENCIES = $(patsubst source/%.cpp,build/dependencies/%.d,$(SOURCES))
all: build/binaries/$(EXECUTABLE)
build:
mkdir -p build/objects build/dependencies build/binaries
build/binaries/$(EXECUTABLE): $(OBJECTS) | build
$(CXX) $(OBJECTS) -o build/binaries/$(EXECUTABLE)
build/objects/%.o: source/%.cpp | build
$(CXX) $(CXXFLAGS) -MMD -MF build/dependencies/$*.d -c $< -o $@
clean:
rm -rf build
run:
./build/binaries/$(EXECUTABLE)
.PHONY: all build clean run
-include $(DEPENDENCIES)Variables makefile
Nos variables makefile seront toujours globales pour nos cas d’utilisation (donc en UPPER_SNAKE_CASE).
Nous allons uniquement utiliser des variables dont les valeurs sont des chaînes de caractères.
VARIABLE est utilisée pour modifier la variable et $(VARIABLE) correspond à sa valeur.
EXECUTABLE: le nom de l’exécutable, le seul élément que nous allons modifier selon le projet; le reste du makefile ne dépend que de la structure du projet et non de son contenu.CXX: le compilateur C++ que nous utilisons (icig++mais d’autres compilateurs existent).CXXFLAGS: les options de compilation (ici-std=c++17et-Iinclude). Souvent, nous pouvons ajouter d’autres options selon nos besoins (par exemple,-Wallpour activer les warnings). Parfois, nous pouvons aussi ajouter des options au linker, auquel cas, il faut définir une autre variable.SOURCES: la liste des fichiers de codes sources (ici$(SOURCE)est égal à la chaîne de caractèressource/book.cpp source/library.cpp source/main.cpp).OBJECTS: la liste des fichiers objets (ici$(OBJECTS)est égal à la chaîne de caractèresbuild/objects/book.o build/objects/library.o build/objects/main.o).DEPENDENCIES: la liste des fichiers de dépendances (ici$(DEPENDENCIES)est égal à la chaîne de caractèresbuild/dependencies/book.d build/dependencies/library.d build/dependencies/main.d).
wildcard et patsubst
wildcard et patsubstIci, la fonction wildcard permet de lister tous les noms de fichiers dans source/ qui finissent par .cpp.
La fonction patsubst prend deux motifs (par exemple, source/%.cpp et build/objects/%.o) et remplace le premier par le second (par exemple, source/main.cpp devient build/objects/main.o) dans la chaîne de caractères donné (par exemple, $(SOURCES)).
Dans notre projet, le code suivant
build/binaries/$(EXECUTABLE): $(OBJECTS) | build
$(CXX) $(OBJECTS) -o build/binaries/$(EXECUTABLE)devient
build/binaries/library-app: build/objects/book.o build/objects/library.o build/objects/main.o | build
g++ build/objects/book.o build/objects/library.o build/objects/main.o -o build/binaries/library-app$<, $@, % et $*
$<, $@, % et $*Les variables spéciales $< et $@ représentent respectivement le premier prérequis et la cible.
La variable % est utilisée comme avant (dans les expressions régulières) pour représenter une certaine chaîne de caractère dans le motif définissant la cible et peut être réutilisée dans les prérequis. La variable $* permet de reprendre la valeur de % mais dans les commandes.\
Dans notre projet, le code suivant
build/objects/%.o: source/%.cpp | build
$(CXX) $(FLAGS) -MMD -MF build/dependencies/$*.d -c $< -o $@devient
build/objects/main.o: source/main.cpp | build
g++ -std=c++17 -Iinclude -MMD -MF build/dependencies/main.d -c source/main.cpp -o build/objects/main.opour la cible build/objects/main.o. La même règle s’applique aux autres cibles .o.
Revenez aux objectifs et cochez les points que vous avez maîtrisés. Revenez sur les points que vous n’avez pas encore bien compris. Appelez votre encadrant si besoin.