Skip to content

Latest commit

 

History

History
383 lines (260 loc) · 18.3 KB

File metadata and controls

383 lines (260 loc) · 18.3 KB

🔝 Retour au Sommaire

26.2.3 target_include_directories

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.


Le rôle des include directories

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.o

La commande target_include_directories() est le mécanisme CMake pour déclarer ces chemins de manière propre, ciblée et propageable.


Syntaxe

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).


Visibilité appliquée aux includes

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 ?

PUBLIC : headers nécessaires à la compilation ET à l'utilisation

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.

PRIVATE : headers internes à l'implémentation

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.

INTERFACE : headers pour les consommateurs uniquement

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.

Pattern complet typique

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)
)

Generator expressions : BUILD_INTERFACE et INSTALL_INTERFACE

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).

Le problème

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.

La solution

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é quand my_project_core est compilée depuis ses sources, ou quand un autre projet l'intègre via add_subdirectory() ou FetchContent. Le chemin est absolu et pointe vers l'arbre source.

  • $<INSTALL_INTERFACE:include> : utilisé quand my_project_core est consommée après installation, via find_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.

Quand les utiliser ?

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}
)

Quand ne PAS les utiliser ?

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.


Includes système : le mot-clé SYSTEM

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.

Propagation SYSTEM via les cibles importées

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.

Contrôle fin avec IMPORTED_NO_SYSTEM

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 -isystem

SYSTEM et les dépendances de votre propre projet

Un 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>
)

Ordre de recherche des includes : BEFORE et AFTER

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
)

Cas d'usage de BEFORE

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.


Le cas du répertoire de build : headers générés

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'install

L'ancêtre obsolète : include_directories()

L'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.


Variables CMake utiles pour les chemins

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>
)

Diagnostic : vérifier les include directories d'une cible

Quand un #include échoue et que vous soupçonnez un problème de chemins, plusieurs techniques de diagnostic sont disponibles.

Inspecter les propriétés CMake

# 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).

Compilation en mode verbose

# Avec Ninja
cmake --build build -- -v

# Avec Make
cmake --build build -- VERBOSE=1

La 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.

CMake file API (avancé)

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 5

Ce 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.


Récapitulatif des bonnes pratiques

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.

⏭️ PUBLIC, PRIVATE, INTERFACE