Dans l’article précédent, nous avons vu comment personnaliser votre projet en fonction du contexte et des entrées utilisateur.
Si cela suffit pour écrire une application simple, un logiciel plus complexe dépend souvent d’autres projets, bibliothèques, ou outils. Dans cet article, vous découvrirez plusieurs manières1 d’intégrer ces dépendances avec CMake.
La méthode simple (mais moins fiable)
Comme vu dans le premier article, on peut utiliser add_subdirectory
pour inclure un dossier dans son projet CMake. Cela peut également s’appliquer à des projets externes : il suffit d’écrire add_subdirectory(chemin/vers/dependance)
pour l’ajouter à votre build.
Si vous souhaitez définir une nouvelle valeur par défaut pour certaines options de votre dépendance, vous pouvez le faire avant l’appel à add_subdirectory
.
Prenons l’exemple suivant d’un CMakeLists.txt
situé dans votre dossier external
:
set(CMAKE_FOLDER external) # Indique à CMake de regrouper les cibles dans le dossier "external" pour les IDEs compatibles.
# Si vous ne souhaitez pas compiler/exécuter les tests des dépendances.
set(BUILD_TESTING_BCKP ${BUILD_TESTING}) # Sauvegarde de la valeur actuelle
set(BUILD_TESTING OFF CACHE BOOL "Force disable of tests for external dependencies" FORCE)
# Ces options sont définies par Tracy, mais comme nous les définissons en amont,
# la valeur fournie sera utilisée comme valeur par défaut.
# Il est conseillé d’utiliser le même commentaire, car c’est aussi celui qui sera affiché.
option(TRACY_ON_DEMAND "On-demand profiling" ON)
option(TRACY_NO_SAMPLING "Disable call stack sampling" ON)
add_subdirectory(tracy) # Essayez-le, c’est un excellent profiler !
# Restauration de la valeur initiale de BUILD_TESTING
set(BUILD_TESTING ${BUILD_TESTING_BCKP} CACHE BOOL "Build tests (default variable for CTest)" FORCE)
Comme vous pouvez le constater, on commence à écrire du code pour gérer des éléments qu’on ne souhaite pas forcément dans notre build. De plus, cette méthode présente plusieurs limitations :
- La dépendance sera toujours construite avec le reste du projet
- Cela ne scale pas très bien pour les gros projets.
- Vous ne souhaitez peut-être pas compiler toutes les cibles des dépendances (comme les tests, cf. les acrobaties de l’exemple).
- Les sous-projets peuvent avoir des effets de bord dans leurs
CMakeLists.txt
:- Ils peuvent utiliser une liste personnalisée de configurations.
- Quelqu’un peut modifier des variables du cache comme
CMAKE_CXX_FLAGS
(ne faites surtout pas ça ! 2). - Risque de conflits si plusieurs dépendances partagent les mêmes sous-dépendances (ça ne scale pas très bien).
- Et tout un tas d’autres bricoles…
- Vous devez avoir accès au code source de la dépendance, qui doit utiliser CMake.
Cela reste acceptable pour des petits projets non destinés à être partagés.
En revanche, pour un projet plus costaud ou une bibliothèque destinée à être partagée, je vous invite à lire la suite.
Les packages : la “bonne” méthode
La plupart des gestionnaires de paquets ou mécanismes d’intégration de dépendances dans CMake s’appuient sur un outil plus adapté : find_package
.
Comme son nom l’indique, cette commande permet de rechercher un package (souvent précompilé). C’est la méthode recommandée pour intégrer des dépendances dans un projet.
Au moment d’écrire ces lignes, find_package
supporte trois modes : Module, Config et redirection via FetchContent.
Si vous vous contentez de consommer une dépendance, les différences entre ces modes importent peu. Sachez simplement que le mode Module est principalement utilisé pour des bibliothèques système/préinstallées qui ne supportent pas nativement CMake (via des scripts intégrés), tandis que le mode Config est à préfer pour toute dépendance installée manuellement ou via un gestionnaire de paquets moderne : c’est l’approche que vous devriez privilégier.
Rechercher des packages
Voyons les principaux paramètres de cette commande :
find_package(<NomDuPackage> [version] [EXACT] [MODULE|CONFIG] [REQUIRED] [COMPONENTS])
version
permet de spécifier une version compatible (la notion de compatibilité est définie par le package).- Pour une version précise :
major[.minor[.patch[.tweak]]]
- Pour une plage de versions :
versionMin...[<]versionMax
(les bornes sont incluses par défaut, sauf si l’on ajoute<
pour exclure la borne supérieure /versionMax
). - Pour une version exacte, ajoutez
EXACT
.
- Pour une version précise :
REQUIRED
: provoque une erreur si le package n’est pas trouvé.- Sinon, vous pouvez tester la variable
<NomDuPackage>_FOUND
.
- Sinon, vous pouvez tester la variable
COMPONENTS
: permet de ne rechercher que certaines parties du package (utile pour des bibliothèques commeQt
,Boost
, etc.).MODULE|CONFIG
: force l’un des deux modes.CONFIG
est utile si la dépendance fournit son propre fichier de configuration de package, et que vous ne souhaitez pas utiliser un module intégré.
Chaque package peut exposer ses cibles différemment, mais la convention en Modern CMake est de le faire via un namespace.
Exemple avec les bibliothèques fmt
et TracyClient
:
find_package(fmt CONFIG REQUIRED)
find_package(Tracy CONFIG REQUIRED)
target_link_libraries(MyTarget
PRIVATE
fmt::fmt # namespace = fmt, cible = fmt
Tracy::TracyClient # namespace = Tracy, cible = TracyClient
)
Où CMake cherche-t-il les packages ?
“Ça dépend”, comme on dit dang le jargon.
Mais la règle principale est la suivante : CMake recherche un fichier nommé <NomDuPackageEnMinuscules>-config.cmake
ou <NomDuPackage>Config.cmake
(en mode Config) ou FindXXXX.cmake
(en mode Module).
La procédure complète est un peu complexe, mais on peut la résumer ainsi : CMake recherche ces fichiers dans les répertoires pointés par les variables suivantes, dans l’ordre :
CMAKE_FIND_PACKAGE_REDIRECTS_DIR
: utilisée par les gestionnaires de paquets pour rediriger vers leurs propres packages.<NomDuPackage>_ROOT
: permet d’indiquer manuellement l’emplacement d’un package.CMAKE_PREFIX_PATH
: si vous installez toutes vos dépendances au même endroit, c’est le moyen le plus simple (peut être une variable CMake ou une variable d’environnement).- La variable d’environnement
PATH
. - Le registre de packages CMake : si vous installez/exportez un package, il peut être ajouté au registre (à moins que
CMAKE_EXPORT_NO_PACKAGE_REGISTRY
ne soit activée).
Dans la pratique, vous utiliserez généralement un gestionnaire de paquets ou CMAKE_PREFIX_PATH
.
Installation manuelle d’une dépendance
Si votre dépendance peut être construite avec CMake et supporte l’installation, il vous suffit de la configurer (cmake -B builddir
), de la compiler (cmake --build builddir
) puis de l’installer avec :
cmake --install builddir
Vous souhaiterez probablement ajouter les options suivantes :
- Répertoire d’installation :
--prefix <répertoire>
- Configuration à installer :
--config <cfg>
Exemple complet :
cmake --install out/build --prefix out/install --config RelWithDebInfo
Nous verrons comment supporter l’installation dans votre projet dans un prochain article.
Common Packge Specification
Alors que j’écrivais ces lignes, Kitware a annoncé un nouvau standard pour la description des packages: la Common Package Specification.
Bien qu’encore au stade experimental, la bonne nouvelle est que la partie qui permet de consommer un package n’a pas changé par rapport à ce qui a été dit ici: vous utiliserez toujours find_package
.
C’est tout pour cet article !
Dans le prochain, vous apprendrez à créer vos propres packages et à générer des installeurs avec CMake.
Footnotes
-
Il existe de nombreuses façons d’intégrer des dépendances dans CMake : modules préinstallés,
FetchContent
,ExternalProject
,add_subdirectory
, ou encore des gestionnaires de paquets tiers comme CPM ou VCPKG. ↩ -
Comme mentionné dans l’article précédent, les variables
CMAKE_*_FLAGS
sont destinées à être modifiées uniquement par l’utilisateur ou via des fichiers de toolchain ! ↩