Compilation d'une bibliothèque

La commande est :

add_library(<name> [<source>...])

En général, il est préférable de laisser le choix entre bibliothèque statique et dynamique à l'utilisateur. CMake crée par défaut une bibliothèque statique.

Le nom est un nom de cible, comme pour la commande add_executable (voir plus haut). Le nom de la cible est utilisé pour former le nom de fichier contenant la bibliothèque compilée, selon les conventions du système d'exploitation. Ne pas mettre le préfixe lib dans le nom de la cible, ne pas mettre de suffixe .a ou .so.

La liste de fichiers sources doit contenir les fichiers visés par les lignes include.

Propriétés d'une cible

  • La cible n'est pas seulement un identificateur utilisé pour former un nom de fichier (bibliothèque ou exécutable). Une cible a des propriétés.

  • Propriétés d'interface ou de "non interface". On peut aussi appeler "privées" les propriétés de "non interface".

    • Les propriétés privées sont utilisées pour compiler la cible elle-même.
    • Les propriétés d'interface d'une cible A sont destinées aux cibles "consommatrices" de A : les cibles qui vont utiliser A.
  • Propriétés importantes (mais il en existe d'autres) :

    • INTERFACE_COMPILE_DEFINITIONS et COMPILE_DEFINITIONS : les macros pour le préprocesseur
    • INTERFACE_INCLUDE_DIRECTORIES et INCLUDE_DIRECTORIES : les répertoires dans lesquels chercher les "en-têtes", c'est-à-dire les fichiers inclus et les fichiers .mod

    • INTERFACE_LINK_LIBRARIES et LINK_LIBRARIES : les dépendances, c'est-à-dire typiquement des cibles qui désignent des bibliothèques

  • Celles qui n'ont pas le préfixe INTERFACE_ sont les propriétés privées.

Propriétés d'une cible

Définition des propriétés

Trois commandes :

  • target_compile_definitions pour INTERFACE_COMPILE_DEFINITIONS et COMPILE_DEFINITIONS
  • target_include_directories pour INTERFACE_INCLUDE_DIRECTORIES et INCLUDE_DIRECTORIES
  • target_link_libraries pour INTERFACE_LINK_LIBRARIES et LINK_LIBRARIES

Ces commandes prennent un argument PRIVATE, INTERFACE ou PUBLIC.

PUBLIC : la commande définit deux propriétés d'un coup, la propriété privée et la propriété interface. PUBLIC = PRIVATE + INTERFACE

Définition d'une macro du préprocesseur

target_compile_definitions(<nom de cible> <INTERFACE|PUBLIC|PRIVATE> <macro>)

ou

target_compile_definitions(<nom de cible> <INTERFACE|PUBLIC|PRIVATE> <macro>=<valeur>)

Pas de -D

Ajout d'un chemin d'en-tête

Ajout d'un répertoire à la liste des répertoires dans lesquels chercher un fichier inclus ou un fichier .mod :

target_include_directories(<nom de cible> <INTERFACE|PUBLIC|PRIVATE> répertoire)

Ajout d'une dépendance à une bibliothèque

target_link_libraries(<nom de cible A> <INTERFACE|PUBLIC|PRIVATE> <nom de cible B>)

Nota bene : cette commande est différente des deux précédentes en ce qu'elle met en relation deux cibles.

Elle indique en particulier que B est nécessaire à l'édition de liens de A.

Transitivité des contraintes

  • Les propriétés d'interface d'une cible C sont prises en compte non seulement pour la compilation de toute cible B consommant C mais aussi de toute cible A consommant B, si C est dans l'interface de B. La documentation de CMake parle de "propagation le long de la chaîne de consommation", ou encore de "transitivité des contraintes".

  • Si CMake crée des bibliothèques statiques et non dynamiques, toutes les bibliothèques dans la chaîne de dépendance d'une cible A sont bien prises en compte à l'édition de liens de A, même si elles ne sont pas en interface.

  • À l'édition de liens, CMake met automatiquement les bibliothèques dans le bon ordre en tenant compte des dépendances entre cibles.

Exemple de suivi des dépendances

Exemple de suivi des dépendances

Compilation de A :

  • def privées A + def interface B + def interface C

  • inc privés A + inc interface B + inc interface C

  • édition de liens :

    • bibliothèques dynamiques : lib B + lib C
    • bibliothèques statiques : lib B + lib C + lib D

Compilation de B :

  • def privées B + def interface C + def interface D
  • inc privés B + inc interface C + inc interface D
  • édition de liens, bibliothèques statiques ou dynamiques : lib C + lib D

Le choix de PUBLIC dans :

target_link_libraries(A PUBLIC B)

est le bon si par exemple les procédures de A prennent toujours au moins un argument qui est d'un type dérivé défini dans B. Alors toute cible consommatrice de A doit faire directement référence à B.

Manipulation 19

  • Écrire le fichier CMakeLists.txt pour NR_util. Ne pas oublier les cibles pour les programmes de test.
  • Tester.

nagfor et les fichiers inclus

Le compilateur NAG cherche automatiquement les fichiers visés par les lignes include dans le répertoire courant mais pas dans le répertoire contenant le fichier incluant, si le fichier incluant n'est pas dans le répertoire courant. Donc, si le code (bibliothèque ou exécutable) contient des fichiers inclus :

if(CMAKE_Fortran_COMPILER_ID MATCHES NAG)
    target_include_directories(<cible> PRIVATE ${CMAKE_CURRENT_LIST_DIR})
endif()

Variable normale ou de cache

Deux types de variables dans CMake : normales et de cache.

Variables normales : définies à l'intérieur de CMakeLists.txt, le temps de l'éxécution de cmake (ou cmake-gui), puis perdues. Syntaxe :

set(varName value)

Variables de cache : peuvent être modifiées à l'extérieur de CMakeLists.txt, au moment de la configuration, valeur conservée d'une exécution de cmake à l'autre, dans CMakeCache.txt.

L'idée de variable de cache est de donner une option à l'utilisateur sans qu'il ait besoin de modifier CMakeLists.txt.

Affectation à une variable de cache

On définit habituellement une variable de cache dans CMakeLists.txt, avec la syntaxe :

set(varName <valeur> CACHE <type> "docstring")

La valeur doit être comprise comme une valeur par défaut. Le type peut être : BOOL, FILEPATH, PATH ou STRING. Conseil : quand on définit ainsi une variable de cache, préfixer son nom avec le nom du projet. Idée d'éviter un conflit si le projet est inclus dans un autre projet.

La valeur peut être modifiée sur la ligne de commande avec l'option -D de cmake. Cette option définit toujours une variable de cache.

Si la variable est aussi définie dans CMakeLists.txt ou prédéfinie par CMake alors la donnée de la valeur suffit :

-D<var>=<value>

Par exemple : CMAKE_BUILD_TYPE est une variable de cache prédéfinie de CMake, donc on peut juste écrire :

-DCMAKE_BUILD_TYPE=Debug

Sinon, il est très conseillé de donner un type avec la syntaxe :

-D<var>:<type>=<value>

La valeur peut être modifiée dans cmake-gui, avec le bouton "Add Entry". La variable ainsi définie est toujours une variable de cache.

Manipulation 20

  • Pour la bibliothèque NR_util, ajouter la prise en compte du compilateur NAG.
  • Donner l'option à l'utilisateur de choisir la valeur de la macro CPP_WP à la configuration (sans qu'il ait besoin de modifier CMakeLists.txt).
  • Tester en changeant la valeur par défaut (par exemple à kind(0d0)).

Bibliothèque dynamique

Pour créer une bibliothèque dynamique :

cmake -DBUILD_SHARED_LIBS:BOOL=True ...

(Ou définir BUILD_SHARED_LIBS dans cmake-gui.)

Messages

Pour afficher des messages à la configuration :

message(<mode> <un message>)

où mode peut être, dans l'ordre du plus "haut niveau" au plus "bas niveau" : FATAL_ERROR, WARNING, STATUS, VERBOSE. FATAL_ERROR arrête l'exécution.

Choix par l'utilisateur du niveau de message à afficher :

  • avec l'option --log-level de cmake, seulement pour cette exécution de cmake (pas de création de variable de cache)
  • ou en définissant la variable de cache CMAKE_MESSAGE_LOG_LEVEL. Nota bene : cette variable n'est pas prédéfinie par CMake donc si vous la définissez sur la ligne de commande de cmake, donnez-lui son type, STRING. Exemple :
-DCMAKE_MESSAGE_LOG_LEVEL:STRING=VERBOSE

Les messages affichés sont ceux du niveau spécifié et ceux de niveau supérieur.

Par défaut, le niveau de log est STATUS.

Manipulation 21

À la configuration de la bibliothèque NR_util, ajouter un message annonçant la précision (valeur de CPP_WP) utilisée.

Modification des options de compilation

  • CMake ajoute tout seul des options correspondant aux choix : CMAKE_BUILD_TYPE=Release ou Debug
  • Par exemple avec gfortran : -O3 pour Release et -g pour Debug.

  • Les variables de cache prédéfinies CMAKE_Fortran_FLAGS, CMAKE_Fortran_FLAGS_DEBUG et CMAKE_Fortran_FLAGS_RELEASE contiennent les options de base et les options supplémentaires pour Debug et Release. On peut les modifier à la configuration. Nota bene : ces variables ne sont pas censées contenir d'option -I (recherche des fichiers inclus et des modules) ni -D (définition de macros).

  • Dans cmake-gui, cocher "Advanced" pour voir CMAKE_Fortran_FLAGS, CMAKE_Fortran_FLAGS_DEBUG et CMAKE_Fortran_FLAGS_RELEASE.

Toolchain

Pour ajouter d'un seul coup de nombreuses options (notamment pour le débogage), il est pratique de collecter ces options dans un fichier, destiné à être lu directement par CMake. Mais c'est un choix qui doit être laissé à l'utilisateur et non à l'auteur du fichier CMakeLists.txt. Ce n'est donc pas une bonne idée d'inclure le fichier d'options de compilation dans CMakeLists.txt. Il faut un autre mécanisme qui puisse prendre en compte un tel fichier à la configuration.

On utilise la variable de cache CMAKE_TOOLCHAIN_FILE, qui donne le chemin du fichier. Le chemin peut être absolu ou relatif à la racine du répertoire source ou du répertoire de compilation.

Définir CMAKE_TOOLCHAIN_FILE à la première exécution de cmake ou cmake-gui. Ne pas le modifier ensuite. Si vous utilisez cmake-gui, cocher l'option "Specify toolchain file for cross-compiling".

Les variables à renseigner (avec la syntaxe de CMake) dans ce fichier sont CMAKE_Fortran_FLAGS_INIT, CMAKE_Fortran_FLAGS_DEBUG_INIT ou CMAKE_Fortran_FLAGS_RELEASE_INIT. Ces variables sont utilisées pour initialiser CMAKE_Fortran_FLAGS, CMAKE_Fortran_FLAGS_DEBUG ou CMAKE_Fortran_FLAGS_RELEASE à la première exécution de cmake ou cmake-gui seulement.

On peut éventuellement encore modifier ensuite CMAKE_Fortran_FLAGS, CMAKE_Fortran_FLAGS_DEBUG ou CMAKE_Fortran_FLAGS_RELEASE à la configuration. Le plus pratique est alors de le faire via cmake-gui.

Nota bene : l'esprit du fichier toolchain est d'être spécifique à la machine mais indépendant des projets. Conseils :

  • ne pas mettre de logique spécifique à un projet dedans ;
  • ne pas le placer dans le répertoire source ;
  • ne pas le placer dans le répertoire de compilation.

Manipulation 22

  • Configurer NR_util pour une compilation de type Debug, en utilisant le fichier gfortran_7.cmake, qui contient (entre autres) des options de débogage.
  • Re-configurer en supprimant la sous-option invalid de -ffpe-trap.

Projet à plusieurs répertoires

Dans le répertoire racine, créer la cible (exécutable ou bibliothèque) en référençant seulement les fichiers sources du répertoire racine.

Pour chaque sous-répertoire :

add_subdirectory(source_dir)

Chaque sous-répertoire doit contenir un fichier CMakeLists.txt. La lecture du CMakeLists.txt parent est suspendue pour lire le CMakeLists.txt du sous-répertoire.

Dans chaque sous-répertoire :

target_sources(<target> PRIVATE liste de fichiers sources)

avec les fichiers qui sont à ce niveau de l'arborescence uniquement.