CMake Presets: enfin un moyen de partager les configurations de nos projets

Pierre Gradot - Jan 20 '22 - - Dev Community

Une des principales nouveautés de CMake 3.19 est l'apparition de presets. La version 3.20 a déjà amélioré cette nouvelle feature.

La première phrase de la documentation résume très bien à quoi servent ces presets :

One problem that CMake users often face is sharing settings with other people for common ways to configure a project. This may be done to support CI builds, or for users who frequently use the same build. CMake supports two files, CMakePresets.json and CMakeUserPresets.json, that allow users to specify common configure options and share them with others.

Et oui ! On était nombreux·ses à attendre une telle feature, on va enfin pouvoir partager facilement ses configurations. Je vous explique comment faire dans cet article.

Pourquoi a t-on besoin de partager des configurations ?

En général, on imagine qu'il suffit de faire cmake -G "mon générateur" pour obtenir un projet prêt à builder. Sauf que c'est pas toujours aussi simple...

Le CMakeLists.txt du projet sur lequel je travaille a de nombreux paramètres et certains ne sont pas optionnels. Il faut passer pas mal de choses à CMake sur la ligne de commandes pour obtenir la variante exacte du projet dont on a besoin à un instant T. Vous vous demandez ce que je peux bien avoir comme paramètres ? Oh ben je peux vous faire un rapide aperçu du bazar !

Certains paramètres sont obligatoires :

  • C'est un projet C++ embarqué, mais il y a aussi des tests unitaires et un simulateur qui s'exécutent sur PC. Il faut donc choisir le générateur qui va bien pour choisir l'une de ces 3 cibles.
  • Pour cross-compiler la cible embarquée, il faut indiquer le chemin vers le fichier de toolchain.
  • Il faut choisir entre debug et release.
  • Il faut choisir la version du matériel ciblé, pour générer un firmware adapté.

En plus de ces paramètres obligatoires, il y a des paramètres optionnels pour activer des features supplémentaires, comme des traces de debug pour les dev ou des instrumentations pour faciliter le travail des équipes de validation.

Au final, il y a un nombre conséquent de variantes. Certaines sont utiles tous les jours, beaucoup de temps en temps, d'autres exceptionnellement.

Comment partager des configurations ?

Quand on clone le dépôt et qu'on veut builder directement, ou quand on veut utiliser une variante un peu exotique, on n'a pas envie de passer 5 minutes à deviner et à taper une ligne de commande à rallonge.

On peut être tenté de fournir une liste de commandes toutes prêtes dans un fichier texte, ou de les regrouper dans un script. Hélas, ce n'est pas toujours adapté aux différents environnements, ça peut générer des variantes dont on n'a pas besoin, ce n'est pas forcément portable, chaque projet utilise des techniques différentes, etc.

Certains IDE ont développé des solutions custom. Par exemple, CLion de JetBrains utilise les CMake profiles, qui sont exportables via le fichier idea/cmake.xml. Cela impose d'utiliser CLion, et Jenkins, pour ne citer que lui, ne l'utilise pas...

L'arrivée des presets directement dans CMake a clairement attiré mon attention, mais visiblement aussi celle de gens de chez Qt. Ca pourrait être la solution magique qu'on a envie de tester immédiatement.

Petit projet d'exemple pour cet article

Parce que c'est toujours plus facile avec un exemple concret, voici un projet C++ très simple.

main.cpp :

#ifdef ENABLE_LOGGING
#include <iostream>

static void log(const char* message) {
    std::cout << message << '\n';
}

#else
#define log(x)
#endif

int main() {
    log("hello, world");
}
Enter fullscreen mode Exit fullscreen mode

CMakeLists.txt :

cmake_minimum_required(VERSION 3.20)
project(use-cmake-presets)

add_executable(${PROJECT_NAME} main.cpp)

if (${ENABLE_LOGGING})
    message("Enable logging")
    target_compile_definitions(${PROJECT_NAME} PRIVATE ENABLE_LOGGING)
endif ()
Enter fullscreen mode Exit fullscreen mode

Lors de la génération du projet, il est possible d'activer ou pas les logs. Il existe donc 2 variantes du projet :

  1. avec les logs si on utilise l'option -DENABLE_LOGGING=TRUE,
  2. sans les logs si on utilise l'option -DENABLE_LOGGING=FALSE ou si on n'utilise pas d'option.

Nous allons maintenant créer des presets pour générer facilement ces 2 variantes.

Ecrire ses presets

Comme évoqué dans l'introduction, CMake reconnait désormais 2 fichiers magiques, CMakePresets.json et CMakeUserPresets.json, dans lesquels on peut décrire nos variantes pour ensuite les partager.

Quelle différence entre ces 2 fichiers ? La documentation l'explique très bien :

CMakePresets.json and CMakeUserPresets.json live in the project's root directory. They both have exactly the same format, and both are optional (though at least one must be present if --preset is specified.) CMakePresets.json is meant to save project-wide builds, while CMakeUserPresets.json is meant for developers to save their own local builds. CMakePresets.json may be checked into a version control system, and CMakeUserPresets.json should NOT be checked in. For example, if a project is using Git, CMakePresets.json may be tracked, and CMakeUserPresets.json should be added to the .gitignore.

Pour l'exemple, on va écrire CMakePresets.json pour pouvoir le partager avec les autres membre de l'équipe, mais cela fonctionne de la même manière avec CMakeUserPresets.json :

{
  "version": 2,
  "configurePresets": [
    {
      "name": "with-logs",
      "binaryDir": "cmake-build-with-logs",
      "generator": "MinGW Makefiles",
      "cacheVariables": {
        "ENABLE_LOGGING": "TRUE"
      }
    },
    {
      "name": "no-log",
      "binaryDir": "cmake-build-no-log",
      "generator": "MinGW Makefiles",
      "cacheVariables": {
        "ENABLE_LOGGING": "FALSE"
      }
    }
  ],
  "buildPresets": [
    {
      "name": "with-logs",
      "configurePreset": "with-logs"
    },
    {
      "name": "no-log",
      "configurePreset": "no-log"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Comment charger un preset ?

Certains IDE supportent déjà très bien les presets, comme CLion ou VSCode.

Ici, je vais vous montrer comment faire depuis la ligne de commande. Il suffit de donner le nom du preset avec l'option --preset :

$ ls
CMakeLists.txt  CMakePresets.json  main.cpp

$ cmake --list-presets=all .
Available configure presets:

  "with-logs"
  "no-log"

Available build presets:

  "with-logs"
  "no-log"


$ cmake --preset=with-logs
Preset CMake variables:

  ENABLE_LOGGING="TRUE"

-- The C compiler identification is GNU 10.2.0
-- The CXX compiler identification is GNU 10.2.0
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working C compiler: C:/msys64/mingw64/bin/gcc.exe - skipped
-- Detecting C compile features
-- Detecting C compile features - done
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: C:/msys64/mingw64/bin/g++.exe - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
Enable logging
-- Configuring done
-- Generating done
-- Build files have been written to: C:/presets/cmake-build-with-logs


$ ls
cmake-build-with-logs/  CMakeLists.txt  CMakePresets.json  main.cpp
Enter fullscreen mode Exit fullscreen mode

On peut ensuite builder et exécuter notre projet :

$ cmake --build --preset=with-logs
[ 50%] Building CXX object CMakeFiles/use-cmake-presets.dir/main.cpp.obj
[100%] Linking CXX executable use-cmake-presets.exe
[100%] Built target use-cmake-presets

$ .\cmake-build-with-logs\use-cmake-presets.exe
hello, world

$
Enter fullscreen mode Exit fullscreen mode

On peut faire la même chose avec no-log au lieu de with-logs. On aura à la fin :

$ .\cmake-build-no-log\use-cmake-presets.exe

$
Enter fullscreen mode Exit fullscreen mode

C'est parfait, non ?

Et bien... pas tout à fait.

On voit par exemple dans le fichier ci-dessus qu'on fige le générateur et le dossier de sortie par exemple. On peut améliorer les choses, par exemple en fournissant avec CMakePresets.json des presets génériques mais non utilisables directement, mais dont on peut hériter dans son CMakeUserPresets.json pour y ajouter le dossier binaire et le générateur. Car oui : un preset peut hériter d'un preset pour venir le compléter.

Par ailleurs, il est parfois obligatoire d'utiliser des informations spécifiques à un IDE pour écrire son preset, par exemple avec CLion pour utiliser une toolchain particulière :

"vendor": {
    "jetbrains.com/clion": {
        "toolchain": "Visual Studio"
    }
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

Pour le projet sur lequel je travaille, on a mis en place un fichier CMakePresets.json avec les 6 presets qu'on utilise quotidiennement et on réfléchit à ajouter d'autres presets qui servent régulièrement. Ca nous a vraiment simplifié la vie et ça a répondu à un problème qu'on avait depuis le début du projet : comment partager facilement les différentes configurations.

Je suis vraiment très emballé par cette nouvelle fonctionnalité Les presets comblent un manque que je ressentais depuis que mes débuts avec CMake... en 2016 !

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Terabox Video Player