web-dev-qa-db-fra.com

Organisation de projet C ++ (avec gtest, cmake et doxygen)

Comme je suis novice en programmation en général, j'ai donc décidé de commencer par créer une classe de vecteurs simple en C++. Cependant, je voudrais prendre de bonnes habitudes dès le départ plutôt que d'essayer de modifier mon flux de travail plus tard.

Je n'ai actuellement que deux fichiers vector3.hpp Et vector3.cpp. Ce projet commencera lentement à prendre de l'ampleur (ce qui en fera beaucoup plus d'une bibliothèque d'algèbre linéaire générale) au fur et à mesure que je me familiariserai avec tout. J'aimerais donc adopter une présentation de projet "standard" pour rendre la vie plus facile par la suite. Après avoir jeté un coup d’œil, j’ai trouvé deux moyens d’organiser les fichiers hpp et cpp, le premier étant:

project
└── src
    ├── vector3.hpp
    └── vector3.cpp

et le second étant:

project
├── inc
│   └── project
│       └── vector3.hpp
└── src
    └── vector3.cpp

Lequel recommandez vous et pourquoi?

Deuxièmement, j'aimerais utiliser Google C++ Testing Framework pour tester mon code car il semble assez facile à utiliser. Suggérez-vous de regrouper ceci avec mon code, par exemple dans un dossier inc/gtest Ou contrib/gtest? Si groupé, suggérez-vous d’utiliser le script Fuse_gtest_files.py Pour réduire le nombre de fichiers, ou de le laisser tel quel? Si non groupé, comment cette dépendance est-elle gérée?

En ce qui concerne les tests, comment sont-ils généralement organisés? Je pensais avoir un fichier cpp pour chaque classe (test_vector3.cpp Par exemple), mais tous compilés dans un binaire pour pouvoir tous être exécutés ensemble facilement?

Comme la bibliothèque gtest est généralement construite avec cmake et make, je pensais qu'il serait logique que mon projet soit aussi construit comme ça? Si j'ai décidé d'utiliser la disposition de projet suivante:

├── CMakeLists.txt
├── contrib
│   └── gtest
│       ├── gtest-all.cc
│       └── gtest.h
├── docs
│   └── Doxyfile
├── inc
│   └── project
│       └── vector3.cpp
├── src
│   └── vector3.cpp
└── test
    └── test_vector3.cpp

À quoi ressemblerait le CMakeLists.txt Pour pouvoir construire uniquement la bibliothèque ou la bibliothèque et les tests? J'ai aussi vu pas mal de projets qui ont un répertoire build et un répertoire bin. La construction a-t-elle lieu dans le répertoire de construction, puis les fichiers binaires ont été déplacés vers le répertoire bin? Les fichiers binaires pour les tests et la bibliothèque vivraient-ils au même endroit? Ou serait-il plus logique de le structurer comme suit:

test
├── bin
├── build
└── src
    └── test_vector3.cpp

Je voudrais aussi utiliser doxygen pour documenter mon code. Est-il possible de le faire fonctionner automatiquement avec cmake et make?

Désolé pour tant de questions, mais je n'ai pas trouvé de livre sur C++ qui réponde de manière satisfaisante à ce type de questions.

112
rozzy

Les systèmes de construction C++ sont un peu un art noir et plus le projet est ancien, plus on trouve de choses étranges. Il n’est donc pas surprenant que de nombreuses questions se posent. Je vais essayer de passer en revue les questions une par une et de mentionner certaines choses générales concernant la construction de bibliothèques C++.

Séparer les en-têtes et les fichiers cpp dans des répertoires. Cela n’est essentiel que si vous construisez un composant qui est supposé être utilisé comme une bibliothèque, par opposition à une application réelle. Vos en-têtes sont la base sur laquelle les utilisateurs peuvent interagir avec ce que vous proposez et doivent être installés. Cela signifie qu'ils doivent être dans un sous-répertoire (personne ne veut que beaucoup d'en-têtes se retrouvent dans le niveau supérieur /usr/include/) Et vos en-têtes doivent pouvoir s'inclure dans une telle configuration.

└── prj
 ├── include
 │   └── prj
 │       ├── header2.h
 │       └── header.h
 └── src
     └── x.cpp

fonctionne bien, parce que les chemins d’inclusion fonctionnent et que vous pouvez utiliser une navigation facile pour les cibles d’installation.

Regroupement des dépendances: je pense que cela dépend en grande partie de la capacité du système de construction à localiser et à configurer les dépendances et de la dépendance de votre code pour une version unique. Cela dépend également de la capacité de vos utilisateurs et de la facilité d'installation de la dépendance sur leur plate-forme. CMake est livré avec un script find_package Pour Google Test. Cela rend les choses beaucoup plus faciles. J'irais avec le regroupement que lorsque nécessaire et l'éviterais autrement.

Comment compiler: Évitez les compilations in-source. CMake facilite la génération de sources et facilite beaucoup la vie.

Je suppose que vous souhaitez également utiliser CTest pour exécuter des tests sur votre système (il est également livré avec une prise en charge intégrée de GTest). Une décision importante pour la disposition des répertoires et l'organisation des tests sera la suivante: finissez-vous avec des sous-projets? Si tel est le cas, vous devez travailler davantage lors de la configuration de CMakeLists et diviser vos sous-projets en sous-répertoires, chacun avec ses propres fichiers include et src. Peut-être même leurs propres exécutions et sorties de doxygen (la combinaison de plusieurs projets de doxygen est possible, mais pas facile ou jolie).

Vous allez vous retrouver avec quelque chose comme ça:

└── prj
    ├── CMakeLists.txt <-- (1)
    ├── include
    │   └── prj
    │       ├── header2.hpp
    │       └── header.hpp
    ├── src
    │   ├── CMakeLists.txt <-- (2)
    │   └── x.cpp
    └── test
        ├── CMakeLists.txt <-- (3)
        ├── data
        │   └── testdata.yyy
        └── testcase.cpp

  • (1) configure les dépendances, les spécificités de la plate-forme et les chemins de sortie
  • (2) configure la bibliothèque que vous allez construire
  • (3) configure les exécutables de test et les cas de test

Si vous avez des sous-composants, je vous suggère d'ajouter une autre hiérarchie et d'utiliser l'arborescence ci-dessus pour chaque sous-projet. La situation devient alors délicate, car vous devez décider si les sous-composants recherchent et configurent leurs dépendances ou si vous le faites au niveau supérieur. Cela devrait être décidé au cas par cas.

Doxygen: Après avoir réussi à exécuter la danse de configuration de doxygen, il est trivial d’utiliser CMake add_custom_command Pour ajouter une cible doc.

C'est ainsi que mes projets se terminent et j'ai vu des projets très similaires, mais bien sûr, ce n'est pas un remède à tous.

Addendum À un moment donné, vous souhaiterez générer un fichier config.hpp Contenant une définition de version et éventuellement un identificateur de contrôle de version (un numéro de révision hachage Git ou SVN). CMake dispose de modules pour automatiser la recherche de ces informations et générer des fichiers. Vous pouvez utiliser configure_file De CMake pour remplacer des variables dans un fichier de modèle par des variables définies dans le CMakeLists.txt.

Si vous construisez des bibliothèques, vous aurez également besoin d’une définition d’exportation pour obtenir la différence entre les compilateurs, par exemple. __declspec Sous MSVC et les attributs visibility sous GCC/clang.

76
pmr

Pour commencer, il existe certains noms classiques de répertoires que vous ne pouvez pas ignorer. Ils sont basés sur la longue tradition du système de fichiers Unix. Ceux-ci sont:

trunk
├── bin     : for all executables (applications)
├── lib     : for all other binaries (static and shared libraries (.so or .dll))
├── include : for all header files
├── src     : for source files
└── doc     : for documentation

C'est probablement une bonne idée de s'en tenir à cette présentation de base, au moins au plus haut niveau.

En ce qui concerne le fractionnement des fichiers d’en-tête et des fichiers source (cpp), les deux schémas sont assez courants. Cependant, j'ai tendance à préférer les garder ensemble, il est plus pratique de regrouper les fichiers au quotidien. De plus, lorsque tout le code se trouve dans un dossier de niveau supérieur, c’est-à-dire le dossier trunk/src/, Vous remarquerez que tous les autres dossiers (bin, lib, include, doc et peut-être certains dossiers de test) du dossier de test. niveau supérieur, en plus du répertoire "build" pour une construction hors source, sont tous les dossiers qui ne contiennent rien de plus que des fichiers générés lors du processus de construction. Et ainsi, seul le dossier src doit être sauvegardé, ou mieux, conservé sous un système/serveur de contrôle de version (comme Git ou SVN).

Et quand il s’agit d’installer vos fichiers d’en-tête sur le système de destination (si vous voulez éventuellement distribuer votre bibliothèque), eh bien, CMake a une commande pour installer des fichiers (crée implicitement une cible "install", à faire "make install") qui vous pouvez utiliser pour mettre tous les en-têtes dans le répertoire /usr/include/. Je viens d'utiliser la macro cmake suivante à cet effet:

# custom macro to register some headers as target for installation:
#  setup_headers("/path/to/header/something.h" "/relative/install/path")
macro(setup_headers HEADER_FILES HEADER_PATH)
  foreach(CURRENT_HEADER_FILE ${HEADER_FILES})
    install(FILES "${SRCROOT}${CURRENT_HEADER_FILE}" DESTINATION "${INCLUDEROOT}${HEADER_PATH}")
  endforeach(CURRENT_HEADER_FILE)
endmacro(setup_headers)

SRCROOT est une variable cmake que j'ai définie dans le dossier src, et INCLUDEROOT est une variable cmake que je configure dans les endroits où les en-têtes doivent aller. Bien sûr, il y a beaucoup d'autres façons de faire cela, et je suis sûr que mon chemin n'est pas le meilleur. Le fait est qu’il n’ya aucune raison de séparer les en-têtes et les sources simplement parce que seuls les en-têtes doivent être installés sur le système cible, car il est très facile, notamment avec CMake (ou CPack), de sélectionner et de configurer les en-têtes être installé sans avoir à les avoir dans un répertoire séparé. Et c'est ce que j'ai vu dans la plupart des bibliothèques.

Quote: Deuxièmement, j'aimerais utiliser le framework de test Google C++ pour le test unitaire de mon code, car il semble assez facile à utiliser. Suggérez-vous de regrouper cela avec mon code, par exemple dans un dossier "inc/gtest" ou "contrib/gtest"? Si groupé, proposez-vous d’utiliser le script Fuse_gtest_files.py pour réduire le nombre de fichiers, ou de le laisser tel quel? Si non groupé, comment cette dépendance est-elle gérée?

Ne regroupez pas les dépendances avec votre bibliothèque. C'est généralement une idée assez horrible, et je déteste toujours quand je suis coincé en essayant de construire une bibliothèque qui a fait cela. Ce devrait être votre dernier recours, et méfiez-vous des pièges. Souvent, les gens regroupent des dépendances avec leur bibliothèque, soit parce qu’ils ciblent un environnement de développement épouvantable (par exemple, Windows), soit parce qu’ils ne prennent en charge qu’une ancienne version (dépréciée) de la bibliothèque (dépendance) en question. Le principal inconvénient est que votre dépendance intégrée peut entrer en conflit avec des versions déjà installées de la même bibliothèque/application (par exemple, vous avez fourni gtest, mais que la personne qui tente de construire votre bibliothèque possède déjà une version plus récente (ou plus ancienne) de gtest, puis les deux pourraient entrer en conflit et donner à cette personne un très mauvais mal de tête). Donc, comme je l'ai dit, faites-le à vos risques et périls, et je dirais seulement en dernier recours. Demander aux gens d'installer quelques dépendances avant de pouvoir compiler votre bibliothèque est un mal beaucoup moins grave que d'essayer de résoudre des conflits entre vos dépendances intégrées et les installations existantes.

Citation: Quand est-il écrit, comment sont-ils organisés? Je pensais avoir un fichier cpp pour chaque classe (test_vector3.cpp par exemple), mais tous compilés dans un binaire pour pouvoir tous être exécutés ensemble facilement?

Un fichier cpp par classe (ou un petit groupe cohérent de classes et de fonctions) est plus habituel et pratique à mon avis. Cependant, ne les compilez certainement pas dans un seul fichier binaire pour "pouvoir les exécuter tous ensemble". C'est une très mauvaise idée. Généralement, quand il s'agit de coder, vous voulez diviser les choses autant qu'il est raisonnable de le faire. Dans le cas des tests unitaires, vous ne voulez pas qu'un seul fichier binaire exécute tous les tests, car cela signifie que toute modification minime apportée à un élément de votre bibliothèque est susceptible de provoquer une recompilation quasi totale de ce programme. et ce n’est que quelques minutes/heures perdues dans l’attente de la recompilation. Il suffit de s'en tenir à un schéma simple: 1 unité = 1 programme de test unitaire. Ensuite, utilisez un script ou une structure de test unitaire (telle que gtest et/ou CTest) pour exécuter tous les programmes de test et faire un rapport sur les taux d'échec/de réussite.

Citation: Etant donné que la bibliothèque gtest est généralement construite avec cmake et make, je pensais qu'il serait logique que mon projet soit aussi construit comme ça? Si j'ai décidé d'utiliser la disposition de projet suivante:

Je suggérerais plutôt cette mise en page:

trunk
├── bin
├── lib
│   └── project
│       └── libvector3.so
│       └── libvector3.a        products of installation / building
├── docs
│   └── Doxyfile
├── include
│   └── project
│       └── vector3.hpp
│_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
│
├── src
│   └── CMakeLists.txt
│   └── Doxyfile.in
│   └── project                 part of version-control / source-distribution
│       └── CMakeLists.txt
│       └── vector3.hpp
│       └── vector3.cpp
│       └── test
│           └── test_vector3.cpp
│_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
│
├── build
└── test                        working directories for building / testing
    └── test_vector3

Quelques points à noter ici. Tout d’abord, les sous-répertoires de votre répertoire src doivent refléter ceux de votre répertoire d’inclusion. C’est juste pour que les choses restent intuitives (aussi, essayez de garder votre structure de sous-répertoires raisonnablement plate (peu profonde), car une imbrication profonde des dossiers est souvent plus embêtant qu’autre chose). Deuxièmement, le répertoire "include" n’est qu’un répertoire d’installation, son contenu correspond aux en-têtes choisis dans le répertoire src.

Troisièmement, le système CMake est destiné à être distribué sur les sous-répertoires sources, et non sous la forme d’un fichier CMakeLists.txt au niveau supérieur. Cela garde les choses locales, et c'est bien (dans l'esprit de séparer les choses en morceaux indépendants). Si vous ajoutez une nouvelle source, un nouvel en-tête ou un nouveau programme de test, il vous suffit d'éditer un fichier CMakeLists.txt simple et petit dans le sous-répertoire en question, sans rien modifier d'autre. Cela vous permet également de restructurer facilement les répertoires (les CMakeLists sont locales et contenues dans les sous-répertoires déplacés). Les CMakeLists de niveau supérieur doivent contenir la plupart des configurations de niveau supérieur, telles que la configuration de répertoires de destination, des commandes personnalisées (ou macros) et la recherche de packages installés sur le système. Les CMakeLists de niveau inférieur ne doivent contenir que de simples listes d’en-têtes, de sources et de sources de tests unitaires, ainsi que les commandes cmake qui les enregistrent dans les cibles de compilation.

Citation: À quoi ressemblerait CMakeLists.txt pour pouvoir créer uniquement la bibliothèque ou la bibliothèque et les tests?

La réponse de base est que CMake vous permet d'exclure spécifiquement certaines cibles de "toutes" (ce qui est construit lorsque vous tapez "make"), et vous pouvez également créer des ensembles spécifiques de cibles. Je ne peux pas faire un tutoriel CMake ici, mais il est assez simple de le découvrir par vous-même. Dans ce cas spécifique, cependant, la solution recommandée consiste bien entendu à utiliser CTest, qui est simplement un ensemble supplémentaire de commandes que vous pouvez utiliser dans les fichiers CMakeLists pour enregistrer un certain nombre de cibles (programmes) marquées comme unités. tests. Donc, CMake mettra tous les tests dans une catégorie spéciale de builds, et c’est exactement ce que vous avez demandé, donc, le problème est résolu.

Quote: J'ai aussi vu pas mal de projets qui ont une construction et un répertoire bin. La construction a-t-elle lieu dans le répertoire de construction, puis les fichiers binaires ont été déplacés vers le répertoire bin? Les fichiers binaires pour les tests et la bibliothèque vivraient-ils au même endroit? Ou serait-il plus logique de le structurer comme suit:

Avoir un répertoire de construction en dehors de la source (construction "hors source") est vraiment la seule chose sensée à faire, c'est la norme de facto de nos jours. Donc, définitivement, ayez un répertoire "build" séparé, en dehors du répertoire source, comme le recommandent les utilisateurs de CMake, et comme tous les programmeurs que j'ai rencontrés le font. En ce qui concerne le répertoire bin, eh bien, il s’agit d’une convention et c’est probablement une bonne idée de la respecter, comme je l’ai dit au début de ce billet.

Citation: J'aimerais également utiliser doxygen pour documenter mon code. Est-il possible de le faire fonctionner automatiquement avec cmake et make?

Oui. C'est plus que possible, c'est génial. En fonction de votre envie, vous avez plusieurs possibilités. CMake a un module pour Doxygen (c'est-à-dire find_package(Doxygen)) qui vous permet d'enregistrer des cibles qui exécuteront Doxygen sur certains fichiers. Si vous voulez faire des choses plus sophistiquées, comme mettre à jour le numéro de version dans le Doxyfile, ou entrer automatiquement un timbre date/auteur pour les fichiers source, etc., tout est possible avec un peu de CMake kung-fu. En général, cela implique que vous conserviez un fichier Doxyfile source (par exemple, le "Doxyfile.in" que j'ai mis dans la disposition des dossiers ci-dessus), qui doit être trouvé et remplacé par les commandes d'analyse de CMake. Dans mon fichier CMakeLists de niveau supérieur , vous trouverez un tel morceau de CMake kung-fu qui fait quelques choses de fantaisie avec cmake-doxygen ensemble.

37
Mikael Persson

Structurer le projet

Je préférerais généralement ce qui suit:

├── CMakeLists.txt
|
├── docs/
│   └── Doxyfile
|
├── include/
│   └── project/
│       └── vector3.hpp
|
├── src/
    └── project/
        └── vector3.cpp
        └── test/
            └── test_vector3.cpp

Cela signifie que vous avez un ensemble de fichiers API très clairement défini pour votre bibliothèque et que la structure signifie que les clients de votre bibliothèque le feront.

#include "project/vector3.hpp"

plutôt que le moins explicite

#include "vector3.hpp"


J'aime que la structure de l’arborescence/src corresponde à celle de l’arbre/include, mais c’est vraiment une préférence personnelle. Toutefois, si votre projet se développe pour contenir des sous-répertoires au sein de/include/project, il serait généralement utile de faire correspondre ceux de l'arborescence/src.

Pour les tests, je préfère les garder "proches" des fichiers qu’ils testent, et si vous vous retrouvez avec des sous-répertoires dans/src, c’est un paradigme assez facile à suivre pour ceux qui veulent trouver le code de test d’un fichier donné.


Essai

Deuxièmement, j'aimerais utiliser Google C++ Testing Framework pour tester mon code car il semble assez facile à utiliser.

Gtest est en effet simple à utiliser et assez complet en termes de capacités. Il peut être utilisé aux côtés de gmock très facilement pour étendre ses capacités, mais mes propres expériences avec gmock ont ​​été moins favorables. Je suis tout à fait disposé à accepter le fait que cela pourrait bien être dû à mes propres lacunes, mais les tests génériques ont tendance à être plus difficiles à créer et beaucoup plus fragiles/difficiles à maintenir. Un gros clou dans le cercueil de gmock est qu’il ne joue pas vraiment contre Nice avec des indicateurs intelligents.

C'est une réponse très triviale et subjective à une question énorme (qui n'appartient probablement pas à S.O.)

Suggérez-vous de regrouper cela avec mon code, par exemple dans un dossier "inc/gtest" ou "contrib/gtest"? Si groupé, proposez-vous d’utiliser le script Fuse_gtest_files.py pour réduire le nombre de fichiers, ou de le laisser tel quel? Si non groupé, comment cette dépendance est-elle gérée?

Je préfère utiliser CMake's ExternalProject_Add module. Cela vous évite d'avoir à garder le code source de gtest dans votre référentiel, ou à l'installer n'importe où. Il est téléchargé et intégré automatiquement dans votre arbre de compilation.

Voir mon réponse traitant des détails ici .

En ce qui concerne les tests, comment sont-ils généralement organisés? Je pensais avoir un fichier cpp pour chaque classe (test_vector3.cpp par exemple), mais tous compilés dans un binaire pour pouvoir tous être exécutés ensemble facilement?

Bon plan.


Bâtiment

Je suis fan de CMake, mais comme pour vos questions relatives aux tests, S.O. n’est probablement pas le meilleur endroit pour demander des avis sur une question aussi subjective.

À quoi le CMakeLists.txt doit-il ressembler pour pouvoir créer uniquement la bibliothèque ou la bibliothèque et les tests?

add_library(ProjectLibrary <All library sources and headers>)
add_executable(ProjectTest <All test files>)
target_link_libraries(ProjectTest ProjectLibrary)

La bibliothèque apparaîtra comme cible "ProjectLibrary" et la suite de tests comme cible "ProjectTest". En spécifiant la bibliothèque en tant que dépendance du test exe, la construction du test exécutera automatiquement la reconstruction de la bibliothèque si elle est obsolète.

J'ai aussi vu pas mal de projets qui ont une construction et un répertoire bin. La construction a-t-elle lieu dans le répertoire de construction, puis les fichiers binaires ont été déplacés vers le répertoire bin? Les fichiers binaires pour les tests et la bibliothèque vivraient-ils au même endroit?

CMake recommande les versions "hors source", c’est-à-dire que vous créez votre propre répertoire de construction en dehors du projet et que vous exécutez CMake à partir de là. Cela évite de "polluer" votre arborescence source avec les fichiers de construction, ce qui est hautement souhaitable si vous utilisez un vcs.

Vous pouvez spécifier que les fichiers binaires sont déplacés ou copiés vers un autre répertoire une fois qu'ils ont été créés, ou qu'ils sont créés par défaut dans un autre répertoire, mais généralement aucun avoir besoin. CMake fournit des moyens complets pour installer votre projet si vous le souhaitez ou pour que d'autres projets CMake puissent facilement "trouver" les fichiers pertinents de votre projet.

En ce qui concerne CMake support pour la recherche et l'exécution de tests gtest , cela serait en grande partie inapproprié si vous construisez gtest dans le cadre de votre projet. Le module FindGtest est vraiment conçu pour être utilisé dans le cas où gtest a été construit séparément en dehors de votre projet.

CMake fournit son propre cadre de test (CTest) et, idéalement, chaque cas gtest serait ajouté en tant que cas CTest.

Cependant, le GTEST_ADD_TESTS La macro fournie par FindGtest pour permettre l'ajout facile de cas gtest car des cas ctest individuels manque quelque peu car elle ne fonctionne pas pour les macros de gtest autres que TEST et TEST_F. Valeur - ou Type paramétré teste avec TEST_P, TYPED_TEST_P, etc. ne sont pas gérés du tout.

Le problème n'a pas de solution facile à ma connaissance. Le moyen le plus robuste d’obtenir une liste des cas gtest est d’exécuter le test exe avec le drapeau --gtest_list_tests. Cependant, ceci ne peut être fait que lorsque l'exe est construit, donc CMake ne peut pas s'en servir. Ce qui vous laisse deux choix. CMake doit essayer d’analyser le code C++ pour déduire les noms des tests (non trivial à l’extrême si vous souhaitez prendre en compte toutes les macros gtest, tests commentés, tests désactivés), ou des cas de test sont ajoutés manuellement à la liste. Fichier CMakeLists.txt.

Je voudrais aussi utiliser doxygen pour documenter mon code. Est-il possible de le faire fonctionner automatiquement avec cmake et make?

Oui, bien que je n’ai aucune expérience sur ce front. CMake fournit FindDoxygen à cette fin.

17
Fraser

En plus des autres (excellentes) réponses, je vais décrire une structure que j'ai utilisée pour des projets relativement à grande échelle .
Je ne vais pas aborder la sous-question à propos de Doxygen, car je ne ferais que répéter ce qui est dit dans les autres réponses.


Raisonnement

Pour la modularité et la facilité de maintenance, le projet est organisé en un ensemble de petites unités. Pour plus de clarté, appelons-les UnitX, avec X = A, B, C, ... (mais ils peuvent avoir un nom quelconque). La structure du répertoire est ensuite organisée pour refléter ce choix, avec la possibilité de regrouper les unités si nécessaire.

Solution

La structure de base des répertoires est la suivante (le contenu des unités est détaillé ultérieurement):

project
├── CMakeLists.txt
├── UnitA
├── UnitB
├── GroupA
│   └── CMakeLists.txt
│   └── GroupB
│       └── CMakeLists.txt
│       └── UnitC
│       └── UnitD
│   └── UnitE

project/CMakeLists.txt pourrait contenir les éléments suivants:

cmake_minimum_required(VERSION 3.0.2)
project(project)
enable_testing() # This will be necessary for testing (details below)

add_subdirectory(UnitA)
add_subdirectory(UnitB)
add_subdirectory(GroupA)

et project/GroupA/CMakeLists.txt:

add_subdirectory(GroupB)
add_subdirectory(UnitE)

et project/GroupB/CMakeLists.txt:

add_subdirectory(UnitC)
add_subdirectory(UnitD)

Passons maintenant à la structure des différentes unités (prenons, à titre d'exemple, UnitD)

project/GroupA/GroupB/UnitD
├── README.md
├── CMakeLists.txt
├── lib
│   └── CMakeLists.txt
│   └── UnitD
│       └── ClassA.h
│       └── ClassA.cpp
│       └── ClassB.h
│       └── ClassB.cpp
├── test
│   └── CMakeLists.txt
│   └── ClassATest.cpp
│   └── ClassBTest.cpp
│   └── [main.cpp]

Aux différents composants:

  • J'aime avoir la source (.cpp) et les en-têtes (.h) dans le même dossier. Cela évite une hiérarchie de répertoires en double, facilite la maintenance. Pour l’installation, filtrer les fichiers d’en-tête ne pose aucun problème (surtout avec CMake).
  • Le rôle du répertoire UnitD est de permettre ultérieurement d'inclure des fichiers avec #include <UnitD/ClassA.h>. De plus, lors de l’installation de cette unité, vous pouvez simplement copier la structure de répertoires en l’état. Notez que vous pouvez également organiser vos fichiers source dans des sous-répertoires.
  • J'aime bien un fichier README pour résumer le fonctionnement de l'unité et spécifier des informations utiles à ce sujet.
  • CMakeLists.txt pourrait simplement contenir:

    add_subdirectory(lib)
    add_subdirectory(test)
    
  • lib/CMakeLists.txt:

    project(UnitD)
    
    set(headers
        UnitD/ClassA.h
        UnitD/ClassB.h
        )
    
    set(sources
        UnitD/ClassA.cpp
        UnitD/ClassB.cpp    
        )
    
    add_library(${TARGET_NAME} STATIC ${headers} ${sources})
    
    # INSTALL_INTERFACE: folder to which you will install a directory UnitD containing the headers
    target_include_directories(UnitD
                               PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
                               PUBLIC $<INSTALL_INTERFACE:include/SomeDir>
                               )
    
    target_link_libraries(UnitD
                          PUBLIC UnitA
                          PRIVATE UnitC
                          )
    

    Notez ici qu’il n’est pas nécessaire de dire à CMake que nous voulons les répertoires d’inclusion pour UnitA et UnitC, car cela a déjà été spécifié lors de la configuration de ces unités. De plus, PUBLIC indiquera à toutes les cibles dépendantes de UnitD qu'elles devraient inclure automatiquement la dépendance UnitA, alors que UnitC ne sera pas nécessaire à ce moment-là (PRIVATE).

  • test/CMakeLists.txt _ (voir plus bas si vous voulez utiliser GTest pour cela):

    project(UnitDTests)
    
    add_executable(UnitDTests
                   ClassATest.cpp
                   ClassBTest.cpp
                   [main.cpp]
                   )
    
    target_link_libraries(UnitDTests
                          PUBLIC UnitD
    )
    
    add_test(
            NAME UnitDTests
            COMMAND UnitDTests
    )
    

Utiliser GoogleTest

Pour Google Test, le plus simple est de savoir si sa source est présente quelque part dans votre répertoire source, mais vous n'avez pas à l'ajouter vous-même. J'utilisais ce projet pour le télécharger automatiquement, et j'emballe son utilisation dans une fonction pour s'assurer qu'il n'est téléchargé qu'une seule fois, même si nous avons plusieurs cibles de test.

Cette fonction CMake est la suivante:

function(import_gtest)
  include (DownloadProject)
  if (NOT TARGET gmock_main)
    include(DownloadProject)
    download_project(PROJ                googletest
                     GIT_REPOSITORY      https://github.com/google/googletest.git
                     GIT_TAG             release-1.8.0
                     UPDATE_DISCONNECTED 1
                     )
    set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) # Prevent GoogleTest from overriding our compiler/linker options when building with Visual Studio
    add_subdirectory(${googletest_SOURCE_DIR} ${googletest_BINARY_DIR} EXCLUDE_FROM_ALL)
  endif()
endfunction()

et puis, quand je veux l'utiliser dans l'une de mes cibles de test, j'ajouterai les lignes suivantes au CMakeLists.txt (c'est pour l'exemple ci-dessus, test/CMakeLists.txt):

import_gtest()
target_link_libraries(UnitDTests gtest_main gmock_main)
5
oLen