CMake Presets: enfin un moyen de partager les configurations de nos projets
Tags: cmake, cpp, programming, todayilearned
Published: 2022-01-20 08:45:30

Une des principales [nouveautés de CMake 3.19](https://www.kitware.com/cmake-3-19-0-available-for-download/) est l'apparition de [_presets_](https://cmake.org/cmake/help/latest/manual/cmake-presets.7.html). La [version 3.20](https://www.kitware.com/cmake-3-21-0-available-for-download/) 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](https://cmake.org/cmake/help/latest/manual/cmake-toolchains.7.html#cross-compiling) 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_](https://www.jetbrains.com/help/clion/cmake-profile.html), 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](https://www.qt.io/blog/new-features-in-cmake-3.19). 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` :

```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
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 :

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` :

```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](https://www.jetbrains.com/help/clion/cmake-presets.html) ou [VSCode](https://devblogs.microsoft.com/cppblog/cmake-presets-integration-in-visual-studio-and-visual-studio-code/).

Ici, je vais vous montrer comment faire depuis la ligne de commande. Il suffit de donner le nom du _preset_ [avec l'option `--preset`](https://cmake.org/cmake/help/v3.20/manual/cmake.1.html#cmake-options) :

```bash
$ 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 :

```bash
$ 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 :

```bash
$ .\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 :

```json
"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 !