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 <span class="css-span">cmake -G "mon générateur"</span> pour obtenir un projet prêt à builder. Sauf que c'est pas toujours aussi simple...
Le <span class="css-span">CMakeLists.txt</span> 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 <span class="css-span">debug</span> et <span class="css-span">release</span>.
- 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 <span class="css-span">idea/cmake.xml.</span> 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.
<span class="css-span">main.cpp </span>:
<pre><code>#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");
}<code><pre>
<span class="css-span">CMakeLists.txt </span>:
<pre><code>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 ()<code><pre>
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 <span class="css-span">-DENABLE_LOGGING=TRUE,</span>
- sans les logs si on utilise l'option <span class="css-span">-DENABLE_LOGGING=FALSE</span> 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, <span class="css-span">CMakePresets.json</span> et <span class="css-span">CMakeUserPresets.json</span>, 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 :
<span class="css-span">CMakePresets.json</span> and <span class="css-span">CMakeUserPresets.json</span> 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 <span class="css-span">--preset</span> is specified.) <span class="css-span">CMakePresets.json</span> is meant to save project-wide builds, while <span class="css-span">CMakeUserPresets.json</span> is meant for developers to save their own local builds. <span class="css-span">CMakePresets.json</span> may be checked into a version control system, and <span class="css-span">CMakeUserPresets.json</span> should NOT be checked in. For example, if a project is using Git, <span class="css-span">CMakePresets.json</span> may be tracked, and <span class="css-span">CMakeUserPresets.json</span> should be added to the <span class="css-span">.gitignore</span>.
Pour l'exemple, on va écrire <span class="css-span">CMakePresets.json</span> pour pouvoir le partager avec les autres membre de l'équipe, mais cela fonctionne de la même manière avec <span class="css-span">CMakeUserPresets.json</span> :
<pre><code>{
"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"
}
]
}<code><pre>
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 :
<pre><code>$ 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<code><pre>
On peut ensuite builder et exécuter notre projet :
<pre><code>$ 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
$<code><pre>
On peut faire la même chose avec <span class="css-span">no-log</span> au lieu de <span class="css-span">with-logs</span>. On aura à la fin :
<pre><code>$ .\cmake-build-no-log\use-cmake-presets.exe
$<code><pre>
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 <span class="css-span">CMakePresets.json</span> des presets génériques mais non utilisables directement, mais dont on peut hériter dans son <span class="css-span">CMakeUserPresets.json</span> 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 :
<pre><code>"vendor": {
"jetbrains.com/clion": {
"toolchain": "Visual Studio"
}
}<code><pre>
Conclusion
Pour le projet sur lequel je travaille, on a mis en place un fichier <span class="css-span">CMakePresets.json</span> 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 !
Que la vie de Pierre, expert embarqué Younup, serait terne sans les variadic templates et les fold expressions de C++17. Heureusement pour lui, Python a tué l'éternel débat sur l’emplacement de l’accolade : "alors, à la fin de la ligne courante ou au début de la ligne suivante ?"
Homme de terrain, il est aussi à l’aise au guidon de son VTT à sillonner les chemins de forêt, dans une salle de concert de black metal ou les mains dans les soudures de sa carte électronique quand il doit déboguer du code (bon ça, il aime moins quand même !)
Son vœu pieux ? Il hésite encore... Faire disparaitre le C embarqué au profit du C++ embarqué ? Ou stopper la génération sans fin d'entropie de son bureau.