Lazygeek Lazygeek
Title

đź’ˇ Excellent talk de Daniel Pfeifer lors de C++Now de 2017

đź’ˇ Bonne pratiques CMake

Commandes CMake

  • cmake -B build -S . -G Ninja
    • -B : dossier dans lequel sera buildĂ© le projet
    • -S : dossier dans lequel se trouvent les sources (et donc le fichier CMakeLists.txt de plus haut niveau)
    • -G : utilisation d’un gĂ©nĂ©rateur (tel Ninja ou Unix Makefiles)
  • cmake --build build --config Release --target all
  • cmake --build build --target install : installe le programme
  • cmake --install build : installe le programme pour CMake 3.15+

Modern CMake

Une syntaxe moderne de CMake consiste à ne plus manipuler des variables mais de déclarer des cibles et propriétés, puis de gérer les dépendances enre les différentes cibles.

Ainsi, plutôt que de manipuler difficilement les include_directories pour accéder aux headers d’une autre lib, on utilisera avantageusement l’interface de la lib et on créera une dépendance. Idem pour les options et autres.

Plutôt que de modifier la variable CMAKE_CXX_FLAGS pour y ajouter le flag -std=c++11, préférons la commande target_compile_features(myTarget PRIVATE cxx_std_11)

Private, Interface et Public

  • PRIVATE : Les propriĂ©tĂ©s privĂ©es sont des propriĂ©tĂ©s applicables uniquement pour la cible elle-mĂŞme, typiquement lorsqu’on va la compiler.
    • Des rĂ©pertoires d’include qui ne sont utilisĂ©s que la cible elle-mĂŞme (des headers qu’on ne veut surtout pas exposer aux utilisateurs de la lib)
    • des options de compil utilisĂ©es uniquement pour compiler la cible (on pour vouloir compiler la lib avec -Werror, mais sans vouloir l’imposer aux utilisateurs de la lib)
  • INTERFACE : Les propriĂ©tĂ©s interface sont aplicables uniquement pour les utilisateurs de la lib. Un rĂ©pertoire d’include, une feature requise, …
  • PUBLIC : les propriĂ©tĂ©s publiques sont propagĂ©es Ă  la fois pour la lib elle-mĂŞme, mais aussi pour les utilisateurs de la lib.
    • PUBLIC = PRIVATE + INTERFACE

Transitivité des dépendances

Grâce à la transitivités des dépendances, il est possible qu’une lib remonte ses propres dépendances à un utilisateur de la lib.

Exemple

  • Une lib A utilise 2 autres libs: lib B et lib C
  • lib B est utilisĂ©e uniquement en interne de la lib A
    • on crĂ©era une dĂ©pendance PRIVEE → target_link_library(libA PRIVATE libB)
  • lib C est utilisĂ©e par la lib A mais sur son interface public (ie une mĂ©thode de l’interface publique retourne ou utilise un objet d’un type dĂ©clarĂ© dans la lib C)
    • Tout utilisateur de la lib A devra Ă©galement connaitre la lib C (puisqu’il devra gĂ©rer un objet d’un type dĂ©clarĂ© dans lib C)
    • on crĂ©era une dĂ©pendance PUBLIC → target_link_library(libA PUBLIC libC)
    • tout utilisateur de lib A, sera donc aussi utilisateur de lib C

Conclusion:

si mon appli utilise la lib A

  • l’appli sera linkĂ©e avec lib A
  • l’appli ne sera pas linkĂ©e avec lib B
  • l’appli sera linkĂ©e avec lib C (mĂŞme sans le spĂ©cifier explicitement, juste parce que lib C est en lien public avec lib B)

👉 Voir ce site qui explique très bien le principe de transitivité des dépendances.

Projet type

# Configuration projet
cmake_minimum_required(VERSION 3.24) # Eviter de prendre une version trop vieille. <3.5 déconseillé, >3.15 conseillé
project(modernCpp
        VERSION 1.0.0.0
        DESCRIPTION "My amazing project"
        HOMEPAGE project-url
        LANGUAGES CXX)

# Config propre Ă  C++
set(CMAKE_CXX_STANDARD 20)            # Declare which C++ version to use. Can be 98, 11, 14, 17 (CMake >3.8), 20 (CMake >3.12)
set(CMAKE_CXX_STANDARD_REQUIRED ON)   # Requires C++ standard to be applied. CMake doesn't downgrade if no compatible compiler is found. (Default is OFF)
set(CMAKE_CXX_EXTENSIONS OFF)

# Ajout d'un module
add_subdirectory(path/to/dir/that/contains/another/CMakelists/file)

# Inclusion d'un fichier cmake pour déclarer certaines fonctions/librairies externes
include(path/to/file.cmake)

# Déclaration des binaires (exe & lib)
add_executable(ProgramName source1 source2)
add_library(LibName source1 source2)

# link entre exe et lib
target_link_libraries(ProgramName PUBLIC LibName)

# Déclaration des dossier d'include
target_include_directories(ProgramName PUBLIC path/to/include/files)

Configuration du projet

Indique la version minimum requise pour CMake

cmake_minimum_required(VERSION 3.5)

Configurer le projet

project(modernCpp
        VERSION 1.0.0.0
        DESCRIPTION "My amazing project"
        HOMEPAGE project-url
        LANGUAGES CXX)
  • Nom du projet
  • Version du projet (optionel) - peut Ă©galement ĂŞtre dĂ©fini indĂ©pendamment avec la variable PROJECT_VERSION ou <project-name>_VERSION
  • Description du projet (optionel) - peut Ă©galement ĂŞtre dĂ©fini indĂ©pendamment avec la variable PROJECT_DESCRIPTION ou <project-name>_DESCRIPTION
  • Lien vers le site web du projet (optionel) - peut Ă©galement ĂŞtre dĂ©fini indĂ©pendamment avec la variable PROJECT_HOMEPAGE_URL ou <project-name>_HOMEPAGE_URL
  • Langage de dĂ©veloppement (optionel) - C, CXX, CSharp, ASM, ASM_NASM, etc.

Options propres au langage C++

  • set(CMAKE_CXX_STANDARD 20): DĂ©clare la version de C++ Ă  utiliser: 98, 11, 14, 17 (Ă  partir de CMake 3.8), 20 (Ă  partir de CMake 3.12)
  • set(CMAKE_CXX_STANDARD_REQUIRED ON): Requiert l’application du standard C++ indiquĂ©. CMake ne choisira pas tout seul une version antĂ©rieure si aucun compilateur comaptible n’est dĂ©tectĂ©. (OFF par dĂ©faut)
  • set(CMAKE_CXX_EXTENSIONS OFF)

Compilation

  • add_executable(ProgramName source1 source2): CrĂ©e un exĂ©cutable en compilant et linkant tous les fichiers source indiquĂ©s
  • add_library(LibName source1 source2): Idem mais pour crĂ©er une lib
  • file(GLOB <list_name> path/to/files/*.cpp): Permet de lister tous les fichiers avec l’extension .cpp dans le dossier indiquĂ©. Utile pour rĂ©cupĂ©rer la liste exhaustive des fichiers prĂ©sent de manière automatique, sans besoin de tous les lister explcitement
    • GLOB_RECURSE: idem mais avec une recherche rĂ©cursive dans les sous-dossiers du rĂ©pertoire indiquĂ©
    • ⚠️ Directive non recommandĂ©e: la liste est uniquement Ă©tablie Ă  l’étape de configuration. Si un fichier est ajoutĂ© a posteriori, il ne sera pas dĂ©tectĂ© et ne sera pas compilĂ© sans passer une nouvelle fois par la configuration (certains IDE relance une configuration de manière systĂ©matique Ă  chaque build mais ce n’est pas toujours le cas)
  • target_link_libraries(ProgramName PUBLIC LibName): Lie la lib avec l’exĂ©cutable
  • target_include_directories(ProgramName PUBLIC path/to/include/files): Inclut le dossier indiquĂ© pour chercher les headers (directive applicable uniquement Ă  la cible indiquĂ©e)
    • Voir aussi include_directories
  • target_include_directories(LibName INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}) : Permet de dĂ©clarer le dossier indiquĂ© comme contenant les includes d’une lib. Le dossier sera alors automatiquement inclus pour tous les exĂ©cutables qui sont linkĂ©s avec cette lib.
  • target_compile_definitions(ProgramNale PRIVATE CUSTOM_DEFINE): Permet de crĂ©er une definition qui sera utilisable dans le code (#ifdef)
    • Voir aussi add_compile_definitions
  • target_compile_feature(<target_name> INTERFACE <requirement>): Exige que le compilateur possède une certaine fonctionalitĂ©
  • target_compile_options(<target_name> INTERFACE <options>): Ajoute des options de compilation Ă  la target
    • On peut crĂ©er une cible virtuelle qui possède ces paramètres et ensuite lier cette cible virtuelle Ă  n’importe quelle autre cible rĂ©elle (le link se fait alors de la mĂŞme manière que pour lier une lib)
    • On peut Ă©galement spĂ©cifier les options pour le build de l’appli uniquement (cf. generator expression et BUILD_INTERFACE). Dans ce cas, les options seront appliquĂ©es pour le build de la lib mais pas pour le dĂ©ploiement.
  • add_subdirectory(path/to/dir/that/contains/another/CMakelists/file): Ajoute un sous-dossier au projet. Le sous-dossier est analysĂ© et le fichier CMakeLists.txt s’y trouvant est interprĂ©tĂ© immĂ©diatement.
  • include(external/file.cmake): Permet d’inclure un autre fichier cmake contenant des cibles ou des options connexes. Ce fichier porte gĂ©nĂ©ralement l’extension .cmake
  • add_custom_command(OUTPUT <generated_output> COMMAND <commande> DEPENDS <dependance>): Permet d’exĂ©cuter une commande personnalisĂ©e afin de gĂ©nĂ©rer des fichiers. Ces fichiers peuvent ensuite ĂŞtre utilisĂ©s comme dĂ©pendances d’autres cibles
    • OUTPUT: les fichiers gĂ©nĂ©rĂ©s par cette commande. On peut ensuite utiliser ces fichiers comme dĂ©pendance pour d’autres cibles. Au moment de builder ces autres cibles, la commande personnalisĂ©e sera alors exĂ©cutĂ©e prĂ©alablement. Les fichiers gĂ©nĂ©rĂ©s doivent ĂŞtre ajoutĂ©s comme dĂ©pendance des autres cibles mĂŞme si les fichiers gĂ©nĂ©rĂ©s sont des headers.
    • COMMAND: la commande Ă  exĂ©cuter. Eventuellement suivi des arguments nĂ©cessaires
    • DEPENDS: les dĂ©pendances de cette commande. Si la dĂ©pendance est une cible, la cible sera rebuildĂ©e avant d’exĂ©cuter la commande. Si la dĂ©pendance est un fichier, la commande sera automatiquement re-exĂ©cutĂ©e si le fichier est modifiĂ©.
  • add_custom_target(<target_name> COMMAND <commande>): DĂ©clare une cible virtuelle. Cette cible ne produit pas un fichier nommĂ© comme la cible mais exĂ©cute une commande.
    • Il est possible de crĂ©er des dĂ©pendance sur cette cible
    • DiffĂ©rence avec add_custom_command:
      • add_custom_command: crĂ©e des fichiers. La dĂ©pendance pourra se faire sur ces fichiers
      • add_custom_target: DĂ©clare une cible. La dĂ©pendance pourra se faire sur la cible complète (qu’elle gĂ©nère des fichiers ou non)
  • execute_process(COMMAND <commande> WORKING_DIRECTORY <path/to/directory>): ExĂ©cute la commande indiquĂ© au moment de la configuration du projet

Cross compilation

👉 Voir Cross compilation for embedded

Installation

  • install(TARGETS <targetName> DESTINATION <dir/to/install/files/corresponding/to/target>): Copie les fichiers correspondant Ă  la cible dans le dossier indiquĂ© (Habituellement /usr/bin ou /usr/lib)
    • A cette Ă©tape, la target peut ĂŞtre une liste de plusieurs cibles (ex: une collection de libs).
    • La liste peut Ă©galement contenir des cibles virtuelles dĂ©finies avec add_library(<target_name> INTERFACE). Cette possibilitĂ© est surtout intĂ©ressant si on a dĂ©fini des options de compil activĂ©es uniquement en config build associĂ©es Ă  cette cible virtuelle.
  • install(FILES <files> DESTINATION <dir/where/to/copy/files>): Copie les fichiers indiquĂ©s dans le dossier choisi. On s’en sert notamment pour copier les headers d’une lib dans le dossier /usr/include

Test

CMake intègre le module CTest qui permet de lancer des tests. Les commandes ci-dessous permettent de coder les tests directement dans CMakeLists.txt, ce qui n’est pas la meilleure solution.

CMake offre également une bonne compatibilité avec d’autres suites de tests, comme GoogleTest par exemple.

enable_testing()
add_test(NAME <test case name> COMMAND <command to launch>)
set_tests_properties(<test case name> PROPERTIES PASS_REGULAR_EXPRESSION "regexp to match for test to pass" )
  • la commande enable_testing permet seulement d’activer la fonction test de CMake
  • la commande add_test crĂ©e un test qui va lancer la commande indiquĂ©e. Cette commande seule ne vĂ©rifie rien
  • la commande set_tests_properties permet d’ajouter une vĂ©rification au test

Hint

Si plusieurs tests sont similaires, on peut créer une fonction qui va créer/exécuter le même test plusieurs fois avec des paramètres différents

function(<nom_fonction> <args...>)
  add_test(NAME <test_name_${arg}> COMMAND <commande> <args...> ${arg})
  set_tests_properties(<test_name_${arg}> PROPERTIES PASS_REGULAR_EXPRESSION "regex to match" )
endfunction()

nom_fonction(param1 param2 ...)
nom_fonction(param3 param4 ...)
...

Gestion des dépendances externes

Find_package

Variables prédéfinies utiles

  • PROJECT_BINARY_DIR: Dossier de build
  • PROJECT_SOURCE_DIR: Dossier parent du projet. Dossier oĂą se trouve le fichier CMakeLists.txt parent de tout le projet
  • CMAKE_CURRENT_SOURCE_DIR: Dossier courant pour les fichiers en cours de traitement. Sous dossier dans lequel se trouve un CMakeLists.txt enfant en cours de traitement. Pour un CMakeLists.txt appelĂ© depuis un autre par la directive include(path/CMakeLists.txt), cette variable rĂ©fère toujours au fichier appelant
  • CMAKE_CURRENT_LIST_DIR: Dossier courant dans lequel se trouve le fichier CMakeLists.txt qui utilise cette variable. Valable pour les fichiers parcourus soit via include, soit via add_subdirectory
  • CMAKE_CURRENT_BINARY_DIR: Fait rĂ©fĂ©rence au dossier de build correspondant au CMakeLists.txt courant. i.e. le dossier de build dans lequel sera gĂ©nĂ©rĂ© le code compilĂ© par ce CMakeLists.txt

Options avancées

Copie et modification de fichiers

Cette commande permet de copier des fichiers en les modifiant pour y insérer des variables issues de la config CMake.

configure_file(<input file> <output file>)

Copie le fichier input et le renomme en output. Lors de la copie, CMake remplace toutes les variables identifiées @VAR@, ${VAR} ou encore $ENV{VAR} par leur valeur qui doit avoir préalablement été définie dans le fichier CMkakeLists.txt.

💡 L’option @ONLY permet de ne remplacer que les variables du type @VAR@, ceci afin d’éviter de remplacer d’éventuelles variables avec la syntaxe ${VAR} qui est normalement propre aux scripts bash.

💡 Les fichiers seront copiés dans le dossier de build. Il ne faudra pas oublier d’ajouter ce dossier à la liste des répertoires à inclure

Création d’une option

set(MyVar "Value" CACHE STRING "Description")
option(MyOption "Description" ON)

Permet de définir une variable qui pourra être configurée différement à l’appel de la commande cmake. Cette variable peut ensuite être réutilisée dans le fichier pour des traitements conditionnels par exemple.

  • La commande set permet de dĂ©finir des variables de type BOOL, STRING, FILEPATH, PATH ou INTERNAL.
  • la commande option ne permet de gĂ©rer uniquement des BOOL

📝 Le type INTERNAL impose une variable de type STRING et possède l’effet supplémentaire que la variable reste interne et n’est pas proposée dans les outils de configuration graphiques

La valeur de la variables est conservée dans le cache.

On peut passer une valeur dans la commande cmake: cmake . -DMY_VAR=ON

On peut aussi utiliser des outils comme ccmake

📝 CMake possède également des options internes permettant de modifier le comportement par défaut de certaines commandes. Ces options se gèrent de la même façon. Seul le nom de l’option devra correspondre à l’option retenue.

Traitement conditionnel

if(<test>)
    ...
else()
    ...
endif()

Le test peut être de nature variée: test d’un booléen,

Lib interface

add_library(target_compiler_flags INTERFACE)
target_compile_features(target_compiler_flags INTERFACE cxx_std_11)
target_compile_options(target_compiler_flags INTERFACE -Wall)

Crée une cible virtuelle qui ne génèrera aucun artefact sur le disque. Néanmoins on peut affecter des requirements ou des options à cette cible afin de la réutiliser en la liant aux autres cibles afin d’appliquer les mêmes options.

Generator expressions

Les expressions génératrices permettent de produire des informations spécifiques en fonction de l’environnement et de sa configuration au moment du build. Elles permettent par exemple de définir des options de compilations particulière en fonction du compilateur utilisé.

Une telle expression est de la forme: $<...>.

💡 Ces expressions peuvent être imbriquées

Forme élémentaire

Sa forme la plus élementaire est la suivante:

$<condition:true_string>

L’expression retourne “true_string” si la condition est vérifiée. Sinon, elle renvoie une chaine vide. La condition peut être soit une autre expression imbriquée, soit directement une variable. Dans ce cas, on n’oublera pas de rensigner la variable sous la forme ${variable}.

Forme avancée

On retrouvera plus souvent ces expressions avec un opérateur en première position

$<OPERATOR:parameters>

CMake propose une liste variée d’opérateurs permettant de vérifier de nombreux paramètres et de les combiner entre eux.

Liste non exhaustive des opérations possibles:

  • IF: Permet de retourner une valeur spĂ©cifique selon que la condition soit vĂ©rifiĂ©e ou non
  • AND: RĂ©aliser un ET logique entre plusieurs conditions
  • OR: RĂ©aliser un OU logique entre plusieurs conditions
  • EQUAL: Compare 2 valeurs numĂ©riques
  • STREQUAL: Compare 2 chaines de caractères
  • VERSION_LESS/VERSION_GREATER: Compare 2 numĂ©ros de version
  • LOWER_CASE/UPPER_CASE: Transforme une chaine de caractères
  • IN_LIST: Indique si un Ă©lĂ©ment est prĂ©sent dans une liste
  • LIST:LENGTH: Indique le nombre d’élĂ©ments dans une liste
  • LIST:GET: Renvoie un Ă©lĂ©ment d’une liste Ă  une position donnĂ©e
  • LIST:APPEND: Ajoute des Ă©lĂ©ments Ă  une liste
  • PATH:xxx: De nombreuses expressions permettant de manipuler des chemins (cf. doc pour plus d’infos)
  • C_COMPILER_VERSION/CXX_COMPILER_VERSION: Revoie la version du compilateur utilisĂ©
  • COMPILE_LANG_AND_ID: Renvoie 1 si le compilateur utilisĂ© pour le language indiquĂ© figure dans la liste des compilateurs attendus dans l’expression
  • BUILD_INTERFACE: Renvoie les paramètres indiquĂ©s uniquement lorsqu’on build la cible. Ne renvoie rien si on installe la cible
  • INSTALL_INTERFACE: Renvoie les paramètres indiquĂ©s uniquement lorsqu’on installe la cible. Ne renvoie rien lorsqu’on build la cible

👉 Voir la doc officielle pour plus de détails

Imported target

Les cibles importées sont des cibles qui représentent des dépendances pré-existantes, généralement une lib présente dans l’espace de travail avec laquelle il suffit de s’interfacer.

add_library(myLib STATIC|SHARED IMPORTED GLOBAL)

set_property(TARGET myLib
    PROPERTY IMPORTED_LOCATION path/to/find/myLib.a)
target_include_directories(myLib INTERFACE path/to/include/)
  • par dĂ©faut, la cible n’est visible que dans le fichier oĂą elle est dĂ©clarĂ©e. Le mot clĂ© GLOBAL permet de rendre cette cible visible partout
  • la propriĂ©tĂ© IMPORTED_LOCATION indique le path pour accĂ©der au fichier de la cible. Cette propriĂ©tĂ© doit mentionner explicitement le nom du fichier (et pas uniquement le dossier)
  • on dĂ©finit les dossier d’include et autres propriĂ©tĂ©s comme pour une cible classique.

FetchContent

Permet de récupérer du contenu tiers depuis une source externe. La source peut être une URL, une repo git, un repo SVN, etc.

include(FetchContent)

FetchContent_Declare(
  googletest_url
  URL https://github.com/google/googletest/archive/03597a01ee50ed33e9dfd640b249b4be3799d395.zip
)

FetchContent_Declare(
  googletest_git
  GIT_REPOSITORY https://github.com/google/googletest.git
  GIT_TAG        703bd9caab50b139428cea1aaff9974ebee5742e # release-1.10.0
)

FetchContent_MakeAvailable(googletest_url)
FetchContent_MakeAvailable(googletest_git)

👉 Voir la doc officielle

Introspection

L’introspection permet à CMake d’effectuer des tests sur le système afin de déterminer si tous les prérequis pour construire notre application sont présents. On peut ainsi adapter la configuration en fonction du système.

Le principe de base est de fournir un petit bout de code (au moins une fonction main()) dans le fichier CMakeLists.txt. Idéalement, cet extrait de code met en oeuvre la fonction ou la lib qu’on veut tester. CMake va alors essayer de compiler ce morceau. Si la compilation réussi, cela signifie que le composant requis est présent et utilisable sur le système. Une variable booléenne est alors positionnée à 1.

include(CheckCXXSourceCompiles)      # Inclus le module qui permet de tester du code C++. Exite pour d'autres languages

check_cxx_source_compiles("
#include ...
int main() {
   ... Le code qu'on veut tester
}
" <result var>)

On peut ensuite utiliser cette variable booléenne comme toute autre variable. Les usages peuvent être les suivants:

  • dĂ©claration d’un DEFINE qui sera utilisĂ© comme compilation conditonner directement dans les fichiers source
  • utilisation dans un bloc if() pour activer la compilation et l’installation du module manquant
  • etc.

⚠️ Une fois le test effectué, le résultat est gardé en cache et le test n’est plus jamais ré-exécuté, même si le bout de code à tester est modifier. Il faut alors supprimer la variable directement dans le cache ou tout nettoyer.

📝 Le comportement de ce test peut être modifié en positionnant l’une ou l’autre des variables suivantes avant d’effectuer le test: CMAKE_REQUIRED_FLAGS, CMAKE_REQUIRED_DEFINITIONS, CMAKE_REQUIRED_LINK_OPTIONS, CMAKE_REQUIRED_LIBRARIES, etc.