Skip to content

Latest commit

 

History

History
451 lines (315 loc) · 19.3 KB

File metadata and controls

451 lines (315 loc) · 19.3 KB

🔝 Retour au Sommaire

26.2.2 target_link_libraries

Objectif : Comprendre en profondeur la commande target_link_libraries() — le mécanisme central qui relie les cibles entre elles, déclenche la propagation transitive des propriétés, et construit le graphe de dépendances du projet.


Bien plus qu'un simple linkage

Le nom target_link_libraries est trompeur. À première vue, on pense à une commande qui passe des flags -l au linker. En réalité, c'est la commande la plus puissante de CMake moderne : elle établit une relation de dépendance entre deux cibles, et cette relation déclenche la propagation automatique de toutes les propriétés transitives — include directories, définitions de préprocesseur, flags de compilation, options de linkage, et bien sûr les bibliothèques elles-mêmes.

Quand vous écrivez :

target_link_libraries(my_app PRIVATE my_project::core)

Vous ne dites pas seulement « lie my_app contre my_project_core ». Vous dites : « my_app dépend de my_project_core — donne-lui tout ce dont il a besoin pour compiler et lier correctement ». CMake consulte alors les propriétés PUBLIC et INTERFACE de my_project_core (et récursivement de ses propres dépendances) et les applique à my_app.


Syntaxe

target_link_libraries(<cible>
    <PRIVATE|PUBLIC|INTERFACE> <dépendance1> [<dépendance2> ...]
    [<PRIVATE|PUBLIC|INTERFACE> <dépendance3> ...]
)

La commande accepte plusieurs blocs de visibilité dans un seul appel :

target_link_libraries(my_project_core
    PUBLIC
        my_project::utils
        Threads::Threads
    PRIVATE
        OpenSSL::SSL
        ZLIB::ZLIB
)

Chaque dépendance est associée au niveau de visibilité qui la précède. On peut aussi effectuer plusieurs appels séparés sur la même cible — les dépendances s'accumulent :

target_link_libraries(my_project_core PUBLIC my_project::utils)  
target_link_libraries(my_project_core PRIVATE OpenSSL::SSL)  

Le résultat est identique. Un seul appel regroupé est généralement plus lisible, mais les appels séparés sont utiles pour les dépendances conditionnelles.


Ce que target_link_libraries propage réellement

Pour comprendre la propagation, il faut distinguer deux faces de chaque cible : ses exigences de build (build requirements) et ses exigences d'usage (usage requirements).

Les exigences de build sont ce dont la cible a besoin pour se compiler elle-même : ses include directories PRIVATE, ses flags, ses définitions internes.

Les exigences d'usage sont ce que la cible exporte vers ses consommateurs : ses include directories PUBLIC et INTERFACE, ses dépendances transitives, ses contraintes de compilation exposées.

Quand my_app lie contre my_project_core, CMake récupère les exigences d'usage de my_project_core et les applique à my_app. C'est ce mécanisme qui rend la propagation transitive possible.

Concrètement, voici ce qui est propagé et ce qui ne l'est pas :

# Déclaration de my_project_core
target_include_directories(my_project_core
    PUBLIC  /path/to/public/headers    # ← Propagé aux consommateurs
    PRIVATE /path/to/internal/headers  # ← NON propagé
)

target_compile_definitions(my_project_core
    PUBLIC  WITH_NETWORKING=1          # ← Propagé
    PRIVATE INTERNAL_TRACE=1           # ← NON propagé
)

target_link_libraries(my_project_core
    PUBLIC  Threads::Threads           # ← Propagé (transitivement)
    PRIVATE OpenSSL::SSL               # ← NON propagé
)
# Consommation par my_app
target_link_libraries(my_app PRIVATE my_project_core)

Après résolution, my_app reçoit automatiquement :

  • -I/path/to/public/headers (include directory PUBLIC de my_project_core)
  • -DWITH_NETWORKING=1 (définition PUBLIC de my_project_core)
  • le linkage contre Threads::Threads (dépendance PUBLIC de my_project_core)
  • le linkage contre my_project_core elle-même

Et my_app ne reçoit pas :

  • /path/to/internal/headers (PRIVATE)
  • INTERNAL_TRACE=1 (PRIVATE)
  • OpenSSL::SSL (PRIVATE — my_app n'a pas besoin de connaître OpenSSL)

Propagation transitive en chaîne

La propagation ne s'arrête pas au premier niveau. Elle traverse l'ensemble du graphe de dépendances, en suivant les arcs PUBLIC. Reprenons notre chaîne à trois niveaux :

# Niveau 3 : bibliothèque de base
add_library(my_project_utils STATIC src/string_utils.cpp)  
target_include_directories(my_project_utils  
    PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include
)
target_compile_features(my_project_utils PUBLIC cxx_std_23)

# Niveau 2 : bibliothèque principale, dépend de utils
add_library(my_project_core STATIC core.cpp network.cpp)  
target_link_libraries(my_project_core  
    PUBLIC  my_project_utils    # ← utils est exposé transitivement
    PRIVATE OpenSSL::SSL
)

# Niveau 1 : exécutable, dépend de core
add_executable(my_app main.cpp)  
target_link_libraries(my_app PRIVATE my_project_core)  

Que reçoit my_app ? Déroulons la propagation :

  1. my_app lie contre my_project_core (PRIVATE) → récupère les exigences d'usage de my_project_core.
  2. Parmi ces exigences : my_project_utils est une dépendance PUBLIC de my_project_coremy_app récupère aussi les exigences d'usage de my_project_utils.
  3. Parmi les exigences d'usage de my_project_utils : le include directory et cxx_std_23.
  4. OpenSSL est PRIVATE dans my_project_coremy_app ne le voit pas.

La ligne de compilation finale de my_app inclura donc les headers de my_project_utils et le flag -std=c++23, sans qu'aucune ligne du CMakeLists.txt de apps/ ne les mentionne. Tout est automatique, piloté par le graphe de dépendances.

Visualiser la propagation

Pour vérifier ce que CMake propage réellement, vous pouvez inspecter les propriétés résolues d'une cible :

# Après configuration
cmake -B build -G Ninja

# Afficher les include directories résolues de my_app
cmake --build build --target my_project_app -- -n -v 2>&1 | grep -- "-I"

Ou de manière plus directe, dans le CMakeLists.txt :

# Debug : afficher les include directories propagées vers my_app
get_target_property(APP_INCLUDES my_project_app INCLUDE_DIRECTORIES)  
message(STATUS "my_app includes: ${APP_INCLUDES}")  

Visibilité de la dépendance elle-même

Le choix PUBLIC / PRIVATE / INTERFACE dans target_link_libraries() détermine si la dépendance est retransmise aux consommateurs de la cible courante. Revenons sur cette distinction avec un exemple concret et ses conséquences.

PRIVATE : dépendance d'implémentation

target_link_libraries(my_project_core PRIVATE OpenSSL::SSL)

my_project_core utilise OpenSSL dans ses fichiers .cpp, mais ses headers publics ne mentionnent aucun type d'OpenSSL. Un consommateur de my_project_core n'a pas besoin de savoir qu'OpenSSL existe. C'est une dépendance d'implémentation.

Le consommateur :

  • ne reçoit pas les include directories d'OpenSSL ;
  • ne reçoit pas les flags de compilation d'OpenSSL ;
  • ne lie pas contre OpenSSL directement.

Pourtant, le binaire final fonctionnera. Si my_project_core est une bibliothèque statique, les symboles OpenSSL nécessaires seront résolus au moment du linkage final de l'exécutable — CMake sait qu'il doit ajouter OpenSSL à la ligne de linkage de l'exécutable, même si le consommateur ne le « voit » pas au niveau des propriétés de compilation. C'est une subtilité importante : PRIVATE signifie « ne pas propager les exigences de compilation », mais les bibliothèques nécessaires au linkage final sont quand même transmises en interne.

PUBLIC : dépendance exposée dans l'API

target_link_libraries(my_project_core PUBLIC Threads::Threads)

Ici, les headers publics de my_project_core utilisent des types de la bibliothèque threads (par exemple, std::mutex nécessite -pthread sur certains systèmes). Le consommateur a besoin des mêmes flags pour compiler correctement le code qui inclut vos headers. PUBLIC s'impose.

Le consommateur :

  • reçoit les propriétés de compilation de Threads::Threads ;
  • lie contre Threads::Threads.

INTERFACE : dépendance pour les consommateurs uniquement

target_link_libraries(header_only_lib INTERFACE nlohmann_json::nlohmann_json)

Une bibliothèque INTERFACE ne compile rien elle-même. Toutes ses dépendances sont nécessairement en INTERFACE — elles sont destinées aux cibles qui consommeront cette bibliothèque.


Les différentes formes de dépendances

target_link_libraries() accepte plusieurs types d'arguments comme dépendances. Chacun est interprété différemment par CMake.

Cibles CMake (la forme recommandée)

target_link_libraries(my_app PRIVATE my_project::core)  
target_link_libraries(my_app PRIVATE OpenSSL::SSL)  
target_link_libraries(my_app PRIVATE GTest::gtest_main)  

C'est la forme à privilégier systématiquement. Quand la dépendance est une cible CMake (créée par add_library() dans votre projet ou importée par find_package()), CMake connaît toutes ses propriétés et peut propager correctement les includes, flags et dépendances transitives.

Les noms avec :: indiquent des cibles importées — si le nom est mal tapé, CMake signale une erreur immédiate au lieu de transmettre un flag incompréhensible au linker.

Noms de bibliothèques bruts

target_link_libraries(my_app PRIVATE pthread m dl)

Un nom sans :: et sans chemin est interprété comme un flag -l<nom> passé au linker. CMake cherchera libpthread.so, libm.so, libdl.so dans les chemins de recherche système. Cette forme fonctionne, mais elle est fragile : pas de propagation de propriétés, pas de détection de typo, pas de portabilité.

Pour pthread spécifiquement, préférez toujours la cible CMake standard :

find_package(Threads REQUIRED)  
target_link_libraries(my_app PRIVATE Threads::Threads)  

Threads::Threads gère automatiquement les différences entre plateformes (flag -pthread sur Linux, pas de flag nécessaire sur certains systèmes, etc.).

Chemins complets vers des bibliothèques

target_link_libraries(my_app PRIVATE /usr/local/lib/libcustom.a)

Un chemin absolu vers un fichier .a ou .so est passé tel quel au linker. C'est le dernier recours quand la bibliothèque n'a pas de fichier de configuration CMake et ne peut pas être trouvée par find_package(). Cette forme est non portable et à éviter dans tout code destiné à être partagé.

Flags de linker bruts

target_link_libraries(my_app PRIVATE -static-libstdc++ -Wl,--as-needed)

Les flags commençant par - sont passés directement au linker. Cette forme est à éviter en faveur de target_link_options(), qui est plus explicite :

# ✅ Plus clair et intentionnel
target_link_options(my_app PRIVATE -static-libstdc++)

Résumé des formes

Forme Exemple Propagation ? Recommandé ?
Cible CMake my_project::core ✅ Complète ✅ Toujours
Cible importée OpenSSL::SSL ✅ Complète ✅ Toujours
Nom de bibliothèque pthread ⚠️ Dernier recours
Chemin complet /usr/lib/libfoo.a ❌ Éviter
Flag de linker -lfoo ❌ Utiliser target_link_options

L'ancienne syntaxe sans visibilité

Vous rencontrerez dans du code plus ancien des appels sans mot-clé de visibilité :

# ⚠️ Ancien style — pas de visibilité explicite
target_link_libraries(my_app my_project_core)

Ce comportement dépend du contexte. Si la cible my_app n'a jamais eu d'appel à target_link_libraries() avec un mot-clé de visibilité, CMake traite toutes les dépendances en mode « plain » — un mode de compatibilité hérité qui se comporte essentiellement comme PUBLIC. Si la cible a déjà reçu un appel avec PRIVATE, PUBLIC ou INTERFACE, alors mélanger les deux styles provoque un avertissement et un comportement imprévisible.

La règle est simple : spécifiez toujours la visibilité explicitement. Il n'y a aucune raison valable d'utiliser la forme sans mot-clé en CMake moderne.

# ❌ Ancien style — ambigu
target_link_libraries(my_app my_project_core)

# ✅ Moderne — intention claire
target_link_libraries(my_app PRIVATE my_project_core)

Dépendances conditionnelles

Il est fréquent qu'une dépendance ne soit nécessaire que sous certaines conditions — une plateforme spécifique, une option de build activée, ou une bibliothèque optionnelle détectée sur le système.

Par plateforme

target_link_libraries(my_project_core
    PRIVATE
        $<$<PLATFORM_ID:Linux>:dl>
        $<$<PLATFORM_ID:Linux>:rt>
)

Les generator expressions $<$<PLATFORM_ID:Linux>:dl> signifient « inclure dl uniquement si la plateforme cible est Linux ». Cette forme est évaluée au moment de la génération, pas au moment de la configuration, ce qui la rend robuste face au cross-compilation (la plateforme cible peut différer de la plateforme hôte).

L'alternative avec if() fonctionne aussi, mais est évaluée à la configuration :

if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
    target_link_libraries(my_project_core PRIVATE dl rt)
endif()

Pour les builds natifs (pas de cross-compilation), les deux approches sont équivalentes. Pour la cross-compilation, les generator expressions sont plus correctes.

Par option de build

option(MY_PROJECT_USE_SSL "Activer le support SSL" ON)

if(MY_PROJECT_USE_SSL)
    find_package(OpenSSL REQUIRED)
    target_link_libraries(my_project_core PRIVATE OpenSSL::SSL OpenSSL::Crypto)
    target_compile_definitions(my_project_core PUBLIC MY_PROJECT_HAS_SSL=1)
endif()

Ici, la définition MY_PROJECT_HAS_SSL est en PUBLIC parce qu'elle conditionne du code dans les headers publics (par exemple, la disponibilité de certaines API réseau). Les consommateurs ont besoin de cette information pour compiler correctement contre my_project_core.

Par détection de bibliothèque optionnelle

find_package(ZLIB QUIET)

if(ZLIB_FOUND)
    target_link_libraries(my_project_core PRIVATE ZLIB::ZLIB)
    target_compile_definitions(my_project_core PRIVATE HAS_ZLIB=1)
    message(STATUS "Compression ZLIB activée")
else()
    message(STATUS "ZLIB non trouvé — compression désactivée")
endif()

Le mot-clé QUIET empêche find_package d'afficher un avertissement si la bibliothèque n'est pas trouvée. C'est approprié pour les dépendances véritablement optionnelles.


Ordre des dépendances et linkage

L'ordre dans lequel vous listez les dépendances dans target_link_libraries() peut avoir de l'importance, en particulier pour les bibliothèques statiques. Le linker GNU (ld) traite les bibliothèques de gauche à droite : quand il rencontre une bibliothèque, il extrait uniquement les symboles nécessaires pour résoudre les références pendantes à ce moment. Si une bibliothèque A dépend de symboles définis dans une bibliothèque B, alors A doit apparaître avant B dans la ligne de commande.

CMake gère cet ordonnancement automatiquement pour les dépendances déclarées entre cibles CMake — c'est l'un des avantages majeurs de passer par des cibles plutôt que par des noms bruts. Cependant, si vous mélangez des noms de bibliothèques bruts, l'ordre peut redevenir significatif :

# CMake ordonne correctement les cibles connues
target_link_libraries(my_app PRIVATE
    my_project::core      # ← CMake sait que core dépend de utils
    my_project::utils     # ← Sera correctement ordonné
)

# Pour les noms bruts, l'ordre compte
target_link_libraries(my_app PRIVATE
    custom_high_level     # ← Doit apparaître avant custom_low_level
    custom_low_level      #    si high_level utilise des symboles de low_level
)

En pratique, si vous utilisez exclusivement des cibles CMake (ce qui est recommandé), vous n'avez pas à vous soucier de l'ordre — CMake effectue un tri topologique du graphe de dépendances et produit la ligne de linkage correcte.


Dépendances circulaires

Les dépendances circulaires entre cibles sont en principe interdites dans un graphe de build. Si A dépend de B et B dépend de A, CMake signale une erreur :

# ❌ Dépendance circulaire — CMake refusera
target_link_libraries(lib_a PRIVATE lib_b)  
target_link_libraries(lib_b PRIVATE lib_a)  

Si vous vous retrouvez dans cette situation, c'est un signal que l'architecture de vos bibliothèques doit être repensée. Les solutions courantes sont l'extraction d'une bibliothèque commune, la fusion des deux bibliothèques, ou le découplage via une interface (cible INTERFACE définissant le contrat sans implémentation).

Dans de rares cas liés à des bibliothèques C legacy ayant de véritables dépendances mutuelles, le linker GNU peut résoudre le problème avec le flag --start-group / --end-group, mais c'est un contournement — pas une solution architecturale.


Exemple complet : toutes les pièces assemblées

Voici les CMakeLists.txt complets de notre projet exemple, en se concentrant sur les target_link_libraries() :

# libs/utils/CMakeLists.txt
add_library(my_project_utils src/string_utils.cpp)  
add_library(my_project::utils ALIAS my_project_utils)  
target_include_directories(my_project_utils  
    PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
)
target_compile_features(my_project_utils PUBLIC cxx_std_23)
# src/CMakeLists.txt
find_package(OpenSSL REQUIRED)  
find_package(Threads REQUIRED)  

add_library(my_project_core core.cpp network.cpp)  
add_library(my_project::core ALIAS my_project_core)  
target_include_directories(my_project_core  
    PUBLIC  $<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>
    PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/internal
)
target_link_libraries(my_project_core
    PUBLIC
        my_project::utils       # Types de utils visibles dans nos headers
        Threads::Threads        # std::mutex utilisé dans nos headers
    PRIVATE
        OpenSSL::SSL            # Utilisé uniquement dans les .cpp
        OpenSSL::Crypto
)
# apps/CMakeLists.txt
add_executable(my_project_app main.cpp)  
target_link_libraries(my_project_app PRIVATE my_project::core)  
# tests/CMakeLists.txt
find_package(GTest REQUIRED)

add_executable(my_project_test_core test_core.cpp)  
target_link_libraries(my_project_test_core  
    PRIVATE
        my_project::core
        GTest::gtest_main
)
add_test(NAME test_core COMMAND my_project_test_core)

La beauté de ce système apparaît dans apps/CMakeLists.txt et tests/CMakeLists.txt : une seule ligne de dépendance suffit. my_project_app reçoit automatiquement les headers de my_project_core, ceux de my_project_utils, les flags de threading, et le linkage contre toutes les bibliothèques nécessaires — le tout grâce à la propagation transitive orchestrée par les niveaux de visibilité déclarés dans chaque CMakeLists.txt.


À suivre : La sous-section 26.2.3 approfondit target_include_directories() — la gestion fine des chemins d'inclusion, les generator expressions BUILD_INTERFACE / INSTALL_INTERFACE, et les subtilités des includes système.

⏭️ target_include_directories