🔝 Retour au Sommaire
Objectif : Maîtriser la gestion des chemins d'inclusion dans CMake moderne — de la déclaration basique aux generator expressions
BUILD_INTERFACE/INSTALL_INTERFACE, en passant par les includes système, les pièges courants et les patterns avancés.
Quand le compilateur rencontre une directive #include, il doit savoir où chercher le fichier demandé. Les include directories sont les chemins que le compilateur parcourt pour résoudre ces inclusions. En ligne de commande, chaque chemin correspond à un flag -I (ou -isystem pour les headers système) :
# Ce que CMake génère en coulisse
g++ -I/home/dev/my_project/include \
-I/home/dev/my_project/libs/utils/include \
-isystem /usr/include/openssl \
-c core.cpp -o core.oLa commande target_include_directories() est le mécanisme CMake pour déclarer ces chemins de manière propre, ciblée et propageable.
target_include_directories(<cible> [SYSTEM] [AFTER|BEFORE]
<PRIVATE|PUBLIC|INTERFACE> [dir1 ...]
[<PRIVATE|PUBLIC|INTERFACE> [dir2 ...]]
)Les paramètres optionnels SYSTEM, AFTER et BEFORE modifient le comportement de la commande — nous les détaillerons plus loin. Le cœur de la commande est la combinaison visibilité + chemins, qui suit exactement les mêmes règles de propagation que target_link_libraries() (section 26.2.2).
Le choix de visibilité suit la règle d'or introduite en section 26.2 : le chemin apparaît-il dans les headers publics de la cible ?
target_include_directories(my_project_core
PUBLIC
${PROJECT_SOURCE_DIR}/include
)Le répertoire include/ contient les headers publics de my_project_core. Ces headers sont inclus à la fois par les fichiers .cpp de my_project_core (pour compiler la bibliothèque elle-même) et par les consommateurs (pour utiliser l'API). PUBLIC est le choix correct.
Tout consommateur qui lie contre my_project_core via target_link_libraries() recevra automatiquement -I${PROJECT_SOURCE_DIR}/include dans sa ligne de compilation.
target_include_directories(my_project_core
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/internal
)Le répertoire internal/ contient des headers d'implémentation que seuls les .cpp de my_project_core incluent. Aucun header public ne référence ces fichiers. Les consommateurs n'en ont pas besoin et ne doivent pas y accéder. PRIVATE garantit l'encapsulation.
target_include_directories(header_only_lib
INTERFACE
${PROJECT_SOURCE_DIR}/include
)Pour une bibliothèque INTERFACE (header-only), la cible ne compile rien elle-même. Les chemins d'inclusion sont exclusivement destinés aux consommateurs. INTERFACE est la seule visibilité valide dans ce cas.
Le pattern que vous utiliserez le plus souvent pour une bibliothèque classique combine les trois niveaux :
target_include_directories(my_project_core
PUBLIC
${PROJECT_SOURCE_DIR}/include # Headers publics (API)
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/internal # Headers d'implémentation
${CMAKE_CURRENT_BINARY_DIR} # Headers générés (configure_file)
)Nous avons introduit ces expressions en sections 26.1 et 26.2 sans les détailler complètement. Elles résolvent un problème fondamental : les chemins d'inclusion sont différents selon que la bibliothèque est utilisée depuis son arbre source (pendant le build) ou depuis son emplacement d'installation (après cmake --install).
Pendant le build, les headers publics se trouvent dans votre arborescence source :
/home/dev/my_project/include/my_project/core.h
Après installation dans /usr/local, ces mêmes headers se trouvent à :
/usr/local/include/my_project/core.h
Si vous déclarez un chemin absolu vers votre arborescence source, la bibliothèque installée cherchera ses headers au mauvais endroit. À l'inverse, si vous déclarez le chemin d'installation, le build depuis les sources échouera.
Les generator expressions $<BUILD_INTERFACE:...> et $<INSTALL_INTERFACE:...> permettent de spécifier les deux chemins. CMake utilise l'un ou l'autre selon le contexte :
target_include_directories(my_project_core
PUBLIC
$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
)-
$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>: utilisé quandmy_project_coreest compilée depuis ses sources, ou quand un autre projet l'intègre viaadd_subdirectory()ouFetchContent. Le chemin est absolu et pointe vers l'arbre source. -
$<INSTALL_INTERFACE:include>: utilisé quandmy_project_coreest consommée après installation, viafind_package(). Le chemin est relatif au préfixe d'installation (CMAKE_INSTALL_PREFIX). Si le préfixe est/usr/local, le chemin réel sera/usr/local/include.
La réponse est simple : dès que vous déclarez un include directory PUBLIC ou INTERFACE. Les chemins PRIVATE n'ont pas besoin de generator expressions car ils ne sont jamais vus par les consommateurs — ils ne servent que pendant le build de la cible elle-même.
target_include_directories(my_project_core
PUBLIC
# ⬇ Generator expressions obligatoires pour PUBLIC
$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
PRIVATE
# ⬇ Chemin simple suffisant pour PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/internal
${CMAKE_CURRENT_BINARY_DIR}
)Si votre bibliothèque est strictement interne au projet et ne sera jamais installée ni consommée via find_package(), les generator expressions sont techniquement superflues. Un chemin simple fonctionne :
# Bibliothèque purement interne — pas de support d'installation prévu
target_include_directories(my_project_core
PUBLIC ${PROJECT_SOURCE_DIR}/include
)Cependant, adopter systématiquement les generator expressions est une bonne habitude. Si un jour vous décidez d'installer votre bibliothèque ou de la publier, les CMakeLists.txt seront déjà prêts. Le surcoût en lisibilité est modeste et le gain en robustesse est réel.
Quand vous incluez des headers de bibliothèques tierces, le compilateur peut émettre des warnings sur du code que vous ne contrôlez pas. C'est frustrant et pollue la sortie de compilation. Le mot-clé SYSTEM résout ce problème en indiquant au compilateur de traiter les chemins comme des headers système, ce qui supprime les warnings :
target_include_directories(my_project_core SYSTEM
PUBLIC /opt/third_party/include
)CMake génère alors -isystem /opt/third_party/include au lieu de -I /opt/third_party/include. Le compilateur traitera les headers de ce chemin avec la même indulgence que les headers de /usr/include.
En pratique, vous n'avez que rarement besoin d'utiliser SYSTEM explicitement. Les cibles importées créées par find_package() marquent automatiquement leurs include directories comme SYSTEM. Quand vous écrivez :
target_link_libraries(my_project_core PRIVATE OpenSSL::SSL)Les headers d'OpenSSL sont automatiquement inclus avec -isystem. Vos propres headers sont inclus avec -I. Le compilateur émet des warnings sur votre code, pas sur celui d'OpenSSL. C'est un des nombreux avantages d'utiliser des cibles CMake plutôt que des chemins bruts.
Il arrive qu'on veuille désactiver le traitement SYSTEM pour une cible importée — typiquement quand vous maintenez un fork d'une bibliothèque tierce et voulez voir les warnings. La propriété IMPORTED_NO_SYSTEM permet cela :
find_package(SomeLib REQUIRED)
set_target_properties(SomeLib::SomeLib PROPERTIES IMPORTED_NO_SYSTEM TRUE)
# Les headers de SomeLib seront désormais inclus avec -I, pas -isystemUn piège subtil : ne déclarez jamais les headers de votre propre projet avec SYSTEM. Cela masquerait des warnings légitimes dans votre propre code :
# ❌ Dangereux — masque les warnings dans VOS headers
target_include_directories(my_project_core SYSTEM
PUBLIC ${PROJECT_SOURCE_DIR}/include
)
# ✅ Correct — warnings actifs sur votre code
target_include_directories(my_project_core
PUBLIC $<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>
)Par défaut, les chemins ajoutés par target_include_directories() sont ajoutés à la fin de la liste d'include directories de la cible (comportement AFTER). Cela signifie que les chemins système et les chemins déjà déclarés sont consultés en premier.
Le mot-clé BEFORE insère les chemins au début de la liste, leur donnant priorité :
# Ce chemin sera consulté EN PREMIER
target_include_directories(my_project_core BEFORE
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/overrides
)Le besoin le plus courant pour BEFORE est la substitution de headers : vous voulez qu'un header local prenne le pas sur un header système du même nom. Par exemple, vous maintenez une version patchée d'un header :
# overrides/openssl/ssl.h remplace le ssl.h système
target_include_directories(my_project_core BEFORE
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/overrides
)C'est une technique délicate à utiliser avec prudence — la substitution silencieuse de headers peut créer des bugs difficiles à diagnostiquer. En général, si vous avez besoin de modifier un header tiers, un fork explicite de la bibliothèque est une approche plus maintenable.
Certains headers sont générés pendant la configuration CMake — typiquement via configure_file() (section 26.4) ou par des outils comme Protocol Buffers. Ces fichiers se trouvent dans le répertoire de build (CMAKE_CURRENT_BINARY_DIR), pas dans l'arbre source.
# Génère un header de version à partir d'un template
configure_file(
${CMAKE_CURRENT_SOURCE_DIR}/config.h.in
${CMAKE_CURRENT_BINARY_DIR}/generated/my_project/config.h
)
target_include_directories(my_project_core
PUBLIC
$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>
$<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}/generated>
$<INSTALL_INTERFACE:include>
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/internal
)Le répertoire ${CMAKE_CURRENT_BINARY_DIR}/generated est ajouté avec BUILD_INTERFACE car les headers générés doivent aussi être accessibles aux consommateurs qui utilisent la bibliothèque comme sous-projet. Après installation, ces headers générés seront copiés dans le même répertoire include/ que les headers statiques — d'où le chemin INSTALL_INTERFACE unique.
Le code source peut alors inclure le header généré de manière transparente :
#include <my_project/config.h> // Fonctionne aussi bien depuis le build que depuis l'installL'ancienne commande globale include_directories() ajoute des chemins à toutes les cibles du répertoire courant et de ses sous-répertoires :
# ❌ Ancien style — pollution globale
include_directories(${PROJECT_SOURCE_DIR}/include)
include_directories(${OPENSSL_INCLUDE_DIR}) Ses défauts sont nombreux. Toutes les cibles du scope reçoivent ces chemins, même celles qui n'en ont pas besoin. Il n'y a pas de notion de visibilité — tout est implicitement « public ». L'ajout d'un chemin dans un CMakeLists.txt parent affecte silencieusement tous les enfants. Les dépendances entre cibles sont invisibles. Et la commande ne supporte pas les generator expressions.
En CMake moderne, include_directories() n'a plus aucune raison d'être utilisée. target_include_directories() la remplace dans tous les cas de figure :
| Besoin | Ancien style ❌ | Modern CMake ✅ |
|---|---|---|
| Headers publics | include_directories(include/) |
target_include_directories(lib PUBLIC include/) |
| Headers privés | include_directories(internal/) |
target_include_directories(lib PRIVATE internal/) |
| Headers tiers | include_directories(${LIB_INCLUDE}) |
target_link_libraries(lib PRIVATE Lib::Lib) |
Notez que pour les headers de bibliothèques tierces, la bonne pratique n'est même pas d'utiliser target_include_directories() : c'est de lier contre la cible importée via target_link_libraries(). La cible importée porte déjà ses include directories et les propage automatiquement.
Plusieurs variables CMake interviennent régulièrement dans la gestion des chemins d'inclusion :
| Variable | Signification | Exemple |
|---|---|---|
PROJECT_SOURCE_DIR |
Racine du projet (là où se trouve le project() le plus récent) |
/home/dev/my_project |
CMAKE_CURRENT_SOURCE_DIR |
Répertoire du CMakeLists.txt en cours de traitement |
/home/dev/my_project/src |
CMAKE_CURRENT_BINARY_DIR |
Répertoire de build correspondant au CMakeLists.txt courant |
/home/dev/my_project/build/src |
CMAKE_SOURCE_DIR |
Racine absolue du projet de plus haut niveau | /home/dev/my_project |
CMAKE_BINARY_DIR |
Racine absolue du répertoire de build | /home/dev/my_project/build |
La distinction entre PROJECT_SOURCE_DIR et CMAKE_SOURCE_DIR est subtile mais importante. Si votre projet est inclus comme sous-projet d'un autre via add_subdirectory(), CMAKE_SOURCE_DIR pointe vers la racine du projet parent, tandis que PROJECT_SOURCE_DIR pointe vers la racine de votre projet (le CMakeLists.txt contenant votre appel project()). Pour les chemins d'inclusion, utilisez toujours PROJECT_SOURCE_DIR ou CMAKE_CURRENT_SOURCE_DIR — jamais CMAKE_SOURCE_DIR, qui casse dès que votre projet est consommé comme sous-projet.
# ❌ Fragile — casse si le projet est inclus comme sous-projet
target_include_directories(my_lib PUBLIC ${CMAKE_SOURCE_DIR}/include)
# ✅ Robuste — fonctionne dans tous les contextes
target_include_directories(my_lib PUBLIC
$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>
)Quand un #include échoue et que vous soupçonnez un problème de chemins, plusieurs techniques de diagnostic sont disponibles.
# Dans un CMakeLists.txt, après la déclaration de la cible
get_target_property(INC_DIRS my_project_core INCLUDE_DIRECTORIES)
message(STATUS "Include dirs (build): ${INC_DIRS}")
get_target_property(IFACE_INC_DIRS my_project_core INTERFACE_INCLUDE_DIRECTORIES)
message(STATUS "Include dirs (interface/propagated): ${IFACE_INC_DIRS}") INCLUDE_DIRECTORIES contient les chemins utilisés pour compiler la cible (PRIVATE + PUBLIC). INTERFACE_INCLUDE_DIRECTORIES contient les chemins propagés aux consommateurs (PUBLIC + INTERFACE).
# Avec Ninja
cmake --build build -- -v
# Avec Make
cmake --build build -- VERBOSE=1La sortie verbose affiche la ligne de compilation complète de chaque fichier, incluant tous les flags -I et -isystem. Cherchez le fichier qui pose problème et vérifiez que le chemin attendu apparaît dans les flags.
Pour les projets complexes, la commande cmake --build build --target help liste toutes les cibles, et les fichiers compile_commands.json (générés automatiquement avec Ninja, ou avec -DCMAKE_EXPORT_COMPILE_COMMANDS=ON pour Make) contiennent la ligne de compilation exacte de chaque fichier source :
cmake -B build -G Ninja
# Le fichier build/compile_commands.json est généré automatiquement
cat build/compile_commands.json | python3 -m json.tool | grep "core.cpp" -A 5Ce fichier est aussi utilisé par clangd et les extensions IDE pour fournir l'autocomplétion et la navigation dans le code — raison supplémentaire de toujours générer avec Ninja ou d'activer CMAKE_EXPORT_COMPILE_COMMANDS.
Toujours utiliser target_include_directories(), jamais include_directories(). La commande globale est obsolète et produit des builds fragiles.
Appliquer la visibilité minimale. PUBLIC pour les headers que les consommateurs incluent, PRIVATE pour tout le reste. En cas de doute, commencez par PRIVATE.
Utiliser les generator expressions pour les chemins PUBLIC et INTERFACE. $<BUILD_INTERFACE:...> / $<INSTALL_INTERFACE:...> garantissent que la bibliothèque fonctionne correctement aussi bien en tant que sous-projet qu'après installation.
Préférer target_link_libraries() à target_include_directories() pour les bibliothèques tierces. Une cible importée (OpenSSL::SSL, GTest::gtest_main) porte déjà ses include directories. Les déclarer manuellement est redondant et source d'erreurs.
Ne jamais utiliser CMAKE_SOURCE_DIR dans les chemins d'inclusion. Préférer PROJECT_SOURCE_DIR ou CMAKE_CURRENT_SOURCE_DIR pour la compatibilité avec les sous-projets.
Ne pas marquer vos propres headers comme SYSTEM. Réserver SYSTEM aux headers tiers dont vous ne contrôlez pas la qualité.
Inclure CMAKE_CURRENT_BINARY_DIR quand vous utilisez configure_file(). Les headers générés vivent dans le répertoire de build, pas dans l'arbre source.
À suivre : La sous-section 26.2.4 synthétise et approfondit le système
PUBLIC,PRIVATE,INTERFACE— avec des exemples avancés de propagation en chaîne, les pièges de surexposition, et un arbre de décision pour choisir la bonne visibilité dans tous les cas.