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
andCMakeUserPresets.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
etrelease
. - 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");
}
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 ()
Lors de la génération du projet, il est possible d'activer ou pas les logs. Il existe donc 2 variantes du projet :
- avec les logs si on utilise l'option
-DENABLE_LOGGING=TRUE
, - 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
andCMakeUserPresets.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, whileCMakeUserPresets.json
is meant for developers to save their own local builds.CMakePresets.json
may be checked into a version control system, andCMakeUserPresets.json
should NOT be checked in. For example, if a project is using Git,CMakePresets.json
may be tracked, andCMakeUserPresets.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"
}
]
}
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
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
$
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
$
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"
}
}
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 !