CMAKE target_link_libraries & target_link_options

approche philosophque vs technique et bonnes pratiques

Le problème exposé dans ce sujet a été résolu.

Bonjour les agrumes :)

Contexte :

OS = OpenBSD

Librairie : curses & ncurses

Particularité :

  • L’implémentation de ncurses sous OpenBSD est la 5.7 pour des raisons de sécurité du code essentiellement et du maintien intégration de l’équipe OpenBSD ; le problème n’est pas ici
  • clang -lncurses -lform main.c compile sans soucis ; les librairies sont ici :
/usr/lib/libncurses.a
/usr/lib/libncurses.so.14.0
/usr/lib/libncurses.so.15.0
/usr/lib/libncurses_p.a
/usr/lib/libncursesw.a
/usr/lib/libncursesw.so.14.0
/usr/lib/libncursesw.so.15.0
/usr/lib/libncursesw_p.a
Cas de figure & interrogations

Cas 1 : build des configurations & génération du binaire OK 7936ko CMakeLists.txt

(...)                                                                                                             
add_executable(MyExec libs/src/02_add2.c)                                                                                                                                                                                                                                                                                                                                                                                                                     
target_link_options(MyExec PRIVATE "-lncurses")    

Cas 2 : build des configurations & génération du binaire OK 7936ko

(...)
add_executable(MyExec libs/src/02_add2.c)                                                                                                        
target_link_libraries(MyExec "/usr/lib/libncurses.so.15.0")

Cas 3 : build des configurations & génération du binaire OK 7968ko

(...)
#set(CURSES_NEED_NCURSES TRUE)
Find_package(Curses REQUIRED)

add_executable(MyExec libs/src/02_add2.c)
target_link_libraries(MyExec PRIVATE ${CURSES_LIBRARIES})

le cas 3 utilisant Find_package() à mon sens est la bonne approche philosophique. Il permet d’appliquer les IMPORTED et de "forcer" la libncurses via la property CURSES_NEED_NCURSES que l’on peut SET à TRUE

  • Pourquoi le binaire n’a-t-il pas la même taille avec ou sans Findpackage() ? (7936 ko vs 7968 ko) "target_link_libraries() Ma compréhension est qu’il fournit les indications pour linker LIBRARIES et INCLUDE. Dans le cas 3 si
target_link_libraries(MyExec PRIVATE ${CURSES_LIBRARIES}::${CURSES_INCLUDE_DIRS})

est utilisé la génération des configurations retourne l’erreur ci-dessous au build. |

— Configuring done (1.0s) CMake Error at CMakeLists.txt:34 (target_link_libraries): Target "02_add2c" links to:

/usr/lib/libform.so.7.0::/usr/include

but the target was not found. Possible reasons include:

* There is a typo in the target name.
* A find_package call is missing for an IMPORTED target.
* An ALIAS target is missing.

— Generating done (0.0s) CMake Generate step failed. Build files cannot be regenerated correctly. |

Je comprends qu’il faille seulement utiliser la property IMPORTED par Find_package() : CURSE_LIBRARIES et laisser le code source C gérer l’appel aux headers.

Merci pour vos retours :) |

+0 -0

Salut,

La bonne méthode est en effet de passer par find_package puis d’appeler target_link_librairies avec ${CURSES_LIBRARIES}. Cependant, il faut aussi passer ${CURSES_INCLUDE_DIRS} à target_include_directory, et ${CURSES_CFLAGS} à target_compile_options. Dans l’idéal, FindCurses implémenterait une IMPORTED target Curses::Curses, et à ce moment-là tu aurais juste besoin d’appeler target_link_librairies et les includes dirs et compile flags seraient propagés automatiquement… Malheureusement ça n’a pas l’air d’être le cas ici donc t’es obligé de te cogner tout ce cirque à la main.

Il y a plusieurs raisons au fait qu’utiliser find_package est la bonne méthode :

  • avec target_link_options, la sémantique n’est pas la bonne. On n’est pas en train de passer des options au linker, on est en train de se lier à une bibliothèque. Par ailleurs, ça ne marche que parce que ncurses est dans un chemin standard, ta config CMake ne marchera pas forcément si ncurses est dans un endroit plus baroque/selon l’environnement.
  • avec un chemin en dur /usr/lib/libncurses.so.15.0, tu as à nouveau le problème que ta config CMake n’est pas portable du tout, en pire qu’avec juste -lncurses.
  • en passant par find_package, tu as accès à toute la machinerie relativement standard de CMake pour trouver la bibliothèque. Tu peux utiliser les options particulières à la lib comme tu le soulignes, mais aussi être sûr d’avoir bien tous les headers, et les flags pour les compilos (ici avec ${CURSES_INCLUDE_DIRS} et ${CURSES_CFLAGS}). Ta config est aussi beaucoup plus souple, c’est facile de pointer CMake vers une installation arbitraire de la lib avec la variable CURSES_ROOT.

Pourquoi le binaire n’a-t-il pas la même taille avec ou sans find_package() ? (7936 ko vs 7968 ko) "target_link_libraries() Ma compréhension est qu’il fournit les indications pour linker LIBRARIES et INCLUDE.

Il faudrait voir la ligne de compilation produite, il y a peut être une lib en plus qui est inclue par CMake et dont tu n’as pas besoin par chance.

target_link_libraries(MyExec PRIVATE ${CURSES_LIBRARIES}::${CURSES_INCLUDE_DIRS})

Pas sûr de comprendre ce que tu essaies de faire avec ça, mais c’est complètement invalide et n’a aucune raison de fonctionner.

Je comprends qu’il faille seulement utiliser la property IMPORTED par Find_package() : CURSE_LIBRARIES et laisser le code source C gérer l’appel aux headers.

Comme dit plus haut, malheureusement, FindCurses ne définit pas d'IMPORTED target. Donc t’es obligé de te cogner les appels en plus à la main.

+2 -0

Adri1, merci pour ton retour.

La bonne méthode est en effet de passer par find_package puis d’appeler target_link_librairies avec ${CURSES_LIBRARIES}. Cependant, il faut aussi passer ${CURSES_INCLUDE_DIRS} à target_include_directory, et ${CURSES_CFLAGS} à target_compile_options. Dans l’idéal, FindCurses implémenterait une IMPORTED target Curses::Curses, et à ce moment-là tu aurais juste besoin d’appeler target_link_librairies et les includes dirs et compile flags seraient propagés automatiquement… Malheureusement ça n’a pas l’air d’être le cas ici donc t’es obligé de te cogner tout ce cirque à la main.

  • en passant par find_package, tu as accès à toute la machinerie relativement standard de CMake pour trouver la bibliothèque. Tu peux utiliser les options particulières à la lib comme tu le soulignes, mais aussi être sûr d’avoir bien tous les headers, et les flags pour les compilos (ici avec ${CURSES_INCLUDE_DIRS} et ${CURSES_CFLAGS}). Ta config est aussi beaucoup plus souple, c’est facile de pointer CMake vers une installation arbitraire de la lib avec la variable CURSES_ROOT.

Merci, c’est effectivement ce que je cherchais à comprendre :D

Pourquoi le binaire n’a-t-il pas la même taille avec ou sans find_package() ? (7936 ko vs 7968 ko) "target_link_libraries() Ma compréhension est qu’il fournit les indications pour linker LIBRARIES et INCLUDE.

Il faudrait voir la ligne de compilation produite, il y a peut être une lib en plus qui est inclue par CMake et dont tu n’as pas besoin par chance.

Je ferai des tests plus verbeux et chercherai à identifier de mon coté, les paramètres de la compilation.

target_link_libraries(MyExec PRIVATE ${CURSES_LIBRARIES}::${CURSES_INCLUDE_DIRS})

Pas sûr de comprendre ce que tu essaies de faire avec ça, mais c’est complètement invalide et n’a aucune raison de fonctionner.

target_link_libraries(MyExec Curses) ou Curses::curses) comme pour intégrer les headers de boost Boost::boost

cf. plus bas, je dois mieux appréhender target_link_libraries avec les COMPONENTS IMPORTED de Find_Package() :(

Je comprends qu’il faille seulement utiliser la property IMPORTED par Find_package() : CURSE_LIBRARIES et laisser le code source C gérer l’appel aux headers.

Comme dit plus haut, malheureusement, FindCurses ne définit pas d'IMPORTED target. Donc t’es obligé de te cogner les appels en plus à la main.

adri1

Ohhh, j’avais lu la doc de FindBoost et FindCurses

Effectivement je cherchais au départ à reproduire la syntaxe : Curses::Curses mais il n’y a pas de properties IMPORTED (j’essaye ici de m’imprégner du vocabulaire…)

Merci pour les explications très pertinentes :D

mon CMakeLists.txt complet avec tout que je viens d’ingérer :)

cmake_minimum_required(VERSION 3.7...3.27)
if(${CMAKE_VERSION} VERSION_LESS 3.12)
    cmake_policy(VERSION ${CMAKE_MAJOR_VERSION}.${CMAKE_MINOR_VERSION})
endif()

project(
    yoda VERSION 1.0
    DESCRIPTION "ncurses training"
    LANGUAGES C
)

#set(CURSES_NEED_NCURSES TRUE)
Find_package(Curses REQUIRED)

add_executable(${PROJECT_NAME} libs/src/${PROJECT_NAME}.c)

target_compile_options(
    ${PROJECT_NAME} PRIVATE
    ${CURSES_CFLAGS}
    -Wall -Wextra -pedantic -fno-common -fno-builtin
)

target_compile_features(
    ${PROJECT_NAME} PRIVATE
    c_std_17
)
target_include_directories(${PROJECT_NAME} PRIVATE ${CURSES_INCLUDE_DIRS})
target_link_libraries(${PROJECT_NAME} PRIVATE ${CURSES_LIBRARIES})

Je ferai des tests plus verbeux et chercherai à identifier de mon coté, les paramètres de la compilation.

Selon le générateur que tu utilises (Make, Ninja, IDE, …), tu peux en général lui demander d’afficher les commandes exécutées. Par exemple, VERBOSE=1 make ou bien ninja -v. Si tu utilises un IDE, je sais pas mais c’est une bonne idée de chercher maintenant comment faire parce que c’est très utile pour vérifier que CMake fait bien ce que tu penses.

Effectivement je cherchais au départ à reproduire la syntaxe : Curses::Curses mais il n’y a pas de properties IMPORTED (j’essaye ici de m’imprégner du vocabulaire…)

Il n’y a pas de target IMPORTED. Si on prend un exemple beaucoup plus simple où CMake définit une IMPORTED target, il y a par exemple FindPNG. Les deux codes suivants sont équivalents :

find_package(PNG REQUIRED)
target_link_libraries(mytarget PNG::PNG)

et

find_package(PNG REQUIRED)
target_link_libraries(mytarget ${PNG_LIBRARIES})
target_include_directories(mytarget ${PNG_INCLUDE_DIRS})
target_compile_definitions(mytarget ${PNG_DEFINITIONS})

On voit vite l’intérêt des IMPORTED targets. Malheureusement, c’est un mécanisme relativement "récent" de CMake et certains modules Find* ne les définissent toujours pas. Personnellement, je serais tenté d’en définir une pour centraliser tout ce bazar :

find_package(Curses REQUIRED)
add_library(yoda::curses INTERFACE IMPORTED)
target_compile_options(yoda::curses INTERFACE ${CURSES_CFLAGS})
target_include_directories(yoda::curses INTERFACE ${CURSES_INCLUDE_DIRS})
target_link_libraries(yoda::curses INTERFACE ${CURSES_LIBRARIES})

add_executable(yoda ...)
target_link_libraries(yoda PRIVATE yoda::curses)

C’est un peu verbeux, mais au moins toute la gestion de Curses est centralisée, plus facile à débugger, à virer, à mettre à jour, etc.

cmake_minimum_required(VERSION 3.7...3.27)
if(${CMAKE_VERSION} VERSION_LESS 3.12)
    cmake_policy(VERSION ${CMAKE_MAJOR_VERSION}.${CMAKE_MINOR_VERSION})
endif()

De manière générale, si tu n’es pas 100% sûr d’avoir besoin d’autre chose que la syntaxe simple cmake_minimum_required(VERSION 3.15), c’est que tu n’en a pas besoin et que tu es en train de te tirer une balle dans le pied. Les policies sont le moyen qu’utilise CMake pour maintenir la rétro-compatibilité, donc là tu dis essentiellement que tu supportes toutes les versions de CMake de 3.7 à 3.27, avec toutes les changements de policies. C’est impossible, et d’ailleurs tu ne tiens pas ce contrat. Si tu écris un truc du genre cmake_minimum_required(VERSION 3.7), tu dis que tu supportes CMake 3.7, et toutes les versions futures sont en mode compatibilité avec cette version. C’est beaucoup moins casse-gueule. Bon en plus, sauf cas très particuliers où t’as pas le choix, ça sert à rien de supporter une aussi vieille version de CMake. Là de toute façon, tu utilises c_std_17 qui a été ajouté dans CMake 3.21, donc autant pas se casser la tête et supporter cmake_minimum_required(VERSION 3.21). Tu verras bien si tu as vraiment besoin de supporter des versions plus anciennes plus tard, mais de toute façon j’en doute très fortement.

add_executable(${PROJECT_NAME} libs/src/${PROJECT_NAME}.c)

target_compile_options(
    ${PROJECT_NAME} PRIVATE
    ${CURSES_CFLAGS}
    -Wall -Wextra -pedantic -fno-common -fno-builtin
)

À ta place, j’éviterais de tout lier au ${PROJECT_NAME} comme ça. En principe, il n’y pas de raison pour que tous ces noms soient forcément les mêmes et que changer l’un doive se propager aux autres.

+1 -0

Merci, pour les explications et les corrections. Je prends note de tout cela :)

Actuellement, je m’entraine sur ncurse, raison pour laquelle j’utilise massivement ${PROJECT_NAME} n’ayant qu’un seul source sans headers, je passe de tests en tests et change simplement le nom du projet dans le CMakeLists.txt

J’ai bien noté le test sur cmake_minimun_required surtout que la version sur mon système est 3.27 et que je débute… (enfin je débute depuis longtemps avec de fortes pauses entre deux débuts :D

Dans mes autres programmes ou a terme, avec plusieurs modules je reviendrai aux bonnes pratiques

Je ferai des tests plus verbeux et chercherai à identifier de mon coté, les paramètres de la compilation.

Selon le générateur que tu utilises (Make, Ninja, IDE, …), tu peux en général lui demander d’afficher les commandes exécutées. Par exemple, VERBOSE=1 make ou bien ninja -v. Si tu utilises un IDE, je sais pas mais c’est une bonne idée de chercher maintenant comment faire parce que c’est très utile pour vérifier que CMake fait bien ce que tu penses.

J’ai effectué le test et propose la réponse à ma question :D

Je n’utilise pas d’IDE, uniquement l’éditeur VIM.je construit avec cmake où j’ai dans CMakeLists.txt positionné CMAKE_VERBOSE_MAKEFILE à TRUE

cmake -B . -S ..
cmake --build

Entre les cas où je ne l’utilisais et quand je l’utilise, find_package link en plus la libraire form :

(…)

[100%] Linking C executable sushi

/usr/local/bin/cmake -E cmake_link_script CMakeFiles/sushi.dir/link.txt —verbose=1

/usr/bin/cc CMakeFiles/sushi.dir~/Coding/Lang_C/curses/02/libs/src/sushi.c.o -o sushi /usr/lib/libcurses.so.15.0 /usr/lib/libform.so.7.0 -Wl,-rpath-link,/usr/X11R6/lib:/usr/local/lib

[100%] Built target sushi |

/usr/local/bin/cmake -E cmake_progress_start ~/Coding/Lang_C/curses/03/build/CMakeFiles 0

Je n’utilise pas d’IDE, uniquement l’éditeur VIM.je construit avec cmake où j’ai dans CMakeLists.txt positionné CMAKE_VERBOSE_MAKEFILE à TRUE

cmake -B . -S ..
cmake --build

Ce genre de variables qui influencent le comportement du générateur (ici make) sont destinées à être changées par l’utilisateur plutôt que directement dans CMakeLists.txt. Le but de CMakeLists.txt est de spécifier comment construire les différentes targets de ton projet d’une manière agnostique du générateur choisi (en pratique il y a parfois des détails qui se faufilent pour contourner des bugs, mais bon…). La bonne façon d’utiliser CMAKE_VERBOSE_MAKEFILE est de lui attribuer la valeur désirée sur le répertoire de build (comme indiqué dans la doc) :

$ cmake -B build -DCMAKE_VERBOSE_MAKEFILE=ON
$ cmake --build build

Comme tu utilises make, tu peux de toute façon te contenter d’appeler make de façon verbeuse :

$ cmake -B build
$ VERBOSE=1 cmake --build build
+1 -0
Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte