🔝 Retour au Sommaire
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.
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.
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.
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 demy_project_core)-DWITH_NETWORKING=1(définition PUBLIC demy_project_core)- le linkage contre
Threads::Threads(dépendance PUBLIC demy_project_core) - le linkage contre
my_project_coreelle-même
Et my_app ne reçoit pas :
/path/to/internal/headers(PRIVATE)INTERNAL_TRACE=1(PRIVATE)OpenSSL::SSL(PRIVATE —my_appn'a pas besoin de connaître OpenSSL)
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 :
my_applie contremy_project_core(PRIVATE) → récupère les exigences d'usage demy_project_core.- Parmi ces exigences :
my_project_utilsest une dépendance PUBLIC demy_project_core→my_apprécupère aussi les exigences d'usage demy_project_utils. - Parmi les exigences d'usage de
my_project_utils: le include directory etcxx_std_23. - OpenSSL est PRIVATE dans
my_project_core→my_appne 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.
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}") 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.
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.
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.
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.
target_link_libraries() accepte plusieurs types d'arguments comme dépendances. Chacun est interprété différemment par CMake.
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.
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.).
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é.
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++)| 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 |
❌ | |
| Chemin complet | /usr/lib/libfoo.a |
❌ | ❌ Éviter |
| Flag de linker | -lfoo |
❌ | ❌ Utiliser target_link_options |
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)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.
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.
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.
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.
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.
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.
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 expressionsBUILD_INTERFACE/INSTALL_INTERFACE, et les subtilités des includes système.