Accès rapide :
Le vocabulaire de base
Un premier projet CMake
Choisir le standard C
Ajouter des warnings
Séparer une librairie et un exécutable
Librairie statique ou partagée
Options de configuration
Debug et Release
Choisir le générateur
Installer les fichiers produits
Ajouter un test simple
Trouver une librairie externe
Un CMakeLists.txt récapitulatif
Makefile ou CMake ?
Un Makefile décrit directement des règles de construction. CMake travaille à un niveau un peu plus haut : on décrit le projet, les cibles à construire, les fichiers sources, les options et les dépendances. CMake génère ensuite les fichiers nécessaires pour l'outil de build choisi : Makefiles, Ninja, projet Visual Studio, projet Xcode, etc.
CMake est donc particulièrement utile quand un projet doit être compilé sur plusieurs plateformes. Il ne remplace pas le compilateur C et il ne remplace pas non plus l'outil de build : il orchestre leur utilisation.
Le fichier principal d'un projet CMake s'appelle CMakeLists.txt. On y déclare le projet et les cibles à produire.
Une cible peut être un exécutable, une librairie statique ou une librairie partagée. CMake distingue aussi deux grandes étapes :
la configuration et la construction.
Le tableau suivant résume ces deux étapes.
| Etape | Rôle |
|---|---|
| Configuration | CMake lit les CMakeLists.txt, détecte le compilateur et génère les fichiers de build. |
| Construction | L'outil de build compile les fichiers et produit les exécutables ou librairies. |
Les deux commandes suivantes correspondent au cycle de base d'un projet CMake.
$> cmake -S . -B build $> cmake --build build
L'option -S indique le dossier contenant les sources. L'option -B indique le dossier de build. Cette
séparation est importante : elle évite de mélanger les fichiers générés par CMake avec les fichiers sources du projet.
Reprenons le petit projet du chapitre précédent. Le fichier CMakeLists.txt minimal est très court.
Nous allons repartir de l'arborescence utilisée dans le chapitre sur les Makefiles.
SampleProject/
|-- CMakeLists.txt
|-- include/
| `-- calculator.h
`-- src/
|-- calculator.c
`-- main.c
Voici le fichier CMakeLists.txt minimal pour construire l'exécutable.
cmake_minimum_required(VERSION 3.20)
project(SampleProject LANGUAGES C)
add_executable(app
src/main.c
src/calculator.c
)
target_include_directories(app PRIVATE include)
La commande cmake_minimum_required fixe la version minimale de CMake attendue. La commande project
déclare le projet et les langages utilisés. Enfin, add_executable crée une cible exécutable nommée app.
La séquence suivante montre la configuration, la construction puis l'exécution du programme.
$> cmake -S . -B build -- The C compiler identification is GNU -- Configuring done -- Generating done -- Build files have been written to: .../SampleProject/build $> cmake --build build [ 33%] Building C object CMakeFiles/app.dir/src/main.c.o [ 66%] Building C object CMakeFiles/app.dir/src/calculator.c.o [100%] Linking C executable app [100%] Built target app $> ./build/app add( 40, 2 ) == 42 sub( 40, 2 ) == 38 $>
Comme avec gcc -std=c17, il est important d'indiquer le niveau de langage attendu. On peut le faire cible par cible,
ce qui est généralement plus propre qu'une variable globale.
L'extrait suivant demande explicitement les fonctionnalités du standard C17 pour la cible.
add_executable(app
src/main.c
src/calculator.c
)
target_compile_features(app PRIVATE c_std_17)
Cette écriture ne se contente pas d'ajouter une option au hasard : CMake sait que la cible a besoin des fonctionnalités de C17 et choisit l'option adaptée au compilateur utilisé. Si vous devez absolument interdire les extensions du compilateur, vous pouvez également préciser les propriétés suivantes.
On peut aussi fixer les propriétés de standard C de manière plus stricte.
set_target_properties(app PROPERTIES
C_STANDARD 17
C_STANDARD_REQUIRED YES
C_EXTENSIONS NO
)
Les options de warning dépendent du compilateur. -Wall et -Wextra sont très utiles avec GCC et Clang,
mais elles ne sont pas comprises de la même manière par tous les compilateurs. Pour un premier projet GCC, on peut rester simple.
Pour GCC ou Clang, on peut ajouter les options de warning directement sur la cible.
target_compile_options(app PRIVATE
-Wall
-Wextra
-Wconversion
-Werror
)
Pour un projet multi-compilateur, il est préférable de tester le compilateur avant d'ajouter des options spécifiques.
L'extrait suivant protège ces options en vérifiant d'abord le compilateur utilisé.
if (CMAKE_C_COMPILER_ID MATCHES "GNU|Clang")
target_compile_options(app PRIVATE -Wall -Wextra -Wconversion -Werror)
endif()
Dans un projet un peu plus propre, on sépare le code réutilisable et le programme principal. CMake encourage cette approche avec
des cibles. On peut créer une librairie calculator, puis lier l'exécutable app avec cette librairie.
Le fichier suivant sépare la librairie calculator de l'exécutable app.
cmake_minimum_required(VERSION 3.20)
project(SampleProject LANGUAGES C)
add_library(calculator STATIC
src/calculator.c
)
target_include_directories(calculator PUBLIC include)
target_compile_features(calculator PUBLIC c_std_17)
add_executable(app
src/main.c
)
target_link_libraries(app PRIVATE calculator)
Le mot clé PUBLIC signifie que le dossier include est nécessaire pour compiler la librairie, mais aussi
pour compiler les cibles qui l'utilisent. Le mot clé PRIVATE signifie que l'information ne concerne que la cible courante.
La commande add_library peut produire une librairie statique ou une librairie partagée. Le choix dépend de vos besoins
de déploiement, de vos contraintes de versionnement et de votre plateforme.
L'exemple suivant montre explicitement les deux formes possibles de librairie.
add_library(calculator_static STATIC src/calculator.c) add_library(calculator_shared SHARED src/calculator.c) target_include_directories(calculator_static PUBLIC include) target_include_directories(calculator_shared PUBLIC include)
Une librairie statique est intégrée à l'exécutable au moment de l'édition des liens. Une librairie partagée reste un fichier séparé, chargé à l'exécution. Les deux choix sont utiles, mais ils n'impliquent pas les mêmes contraintes de distribution.
CMake permet de proposer des options activables depuis la ligne de commande. Par exemple, on peut décider de compiler un petit programme de démonstration uniquement si l'utilisateur le demande.
Le bloc suivant ajoute une option de configuration pour activer ou non le programme de démonstration.
option(BUILD_DEMO "Build demo executable" ON)
add_library(calculator STATIC src/calculator.c)
target_include_directories(calculator PUBLIC include)
if (BUILD_DEMO)
add_executable(app src/main.c)
target_link_libraries(app PRIVATE calculator)
endif()
Voici comment désactiver cette option depuis la ligne de commande.
$> cmake -S . -B build -DBUILD_DEMO=OFF $> cmake --build build
Les options de configuration sont stockées dans le cache CMake du dossier de build. Si vous changez beaucoup d'options pendant vos
essais, n'hésitez pas à supprimer le dossier build pour repartir d'un état propre.
Avec les générateurs de type Makefiles ou Ninja, on choisit généralement le type de build au moment de la configuration. Les deux
valeurs les plus courantes sont Debug et Release.
On peut maintenir deux dossiers de build séparés pour comparer un build debug et un build release.
$> cmake -S . -B build-debug -DCMAKE_BUILD_TYPE=Debug $> cmake --build build-debug $> cmake -S . -B build-release -DCMAKE_BUILD_TYPE=Release $> cmake --build build-release
Sous Visual Studio ou Xcode, le générateur peut gérer plusieurs configurations dans le même dossier de build. Dans ce cas, la configuration se précise plutôt pendant la construction.
Avec un générateur multi-configuration, le choix se fait plutôt comme ceci.
$> cmake --build build --config Release
Par défaut, CMake choisit un générateur adapté à votre environnement. Vous pouvez toutefois le préciser explicitement. Par exemple, Ninja est souvent apprécié pour sa rapidité et la lisibilité de ses sorties.
La commande suivante force l'utilisation du générateur Ninja.
$> cmake -S . -B build -G Ninja $> cmake --build build
Le même projet CMake peut donc générer des Makefiles sur Linux, un projet Visual Studio sur Windows ou des fichiers Ninja sur différentes plateformes. C'est l'une des raisons de son succès.
Pour décrire l'installation, on utilise la commande install. Elle peut installer des exécutables, des librairies et
des fichiers d'entête.
L'extrait suivant déclare les règles d'installation pour l'exécutable, la librairie et les entêtes.
add_library(calculator STATIC src/calculator.c)
target_include_directories(calculator PUBLIC include)
add_executable(app src/main.c)
target_link_libraries(app PRIVATE calculator)
install(TARGETS app calculator
RUNTIME DESTINATION bin
ARCHIVE DESTINATION lib
LIBRARY DESTINATION lib
)
install(DIRECTORY include/
DESTINATION include
)
Les commandes suivantes construisent le projet puis installent les fichiers dans un préfixe temporaire.
$> cmake -S . -B build -DCMAKE_INSTALL_PREFIX=/tmp/sample $> cmake --build build $> cmake --install build
CMake s'intègre avec CTest, l'outil de lancement de tests fourni avec CMake. Pour un premier exemple, on peut exécuter le programme et vérifier simplement qu'il se termine correctement.
Le bloc suivant active les tests et déclare un premier test d'exécution.
enable_testing()
add_test(NAME run_app
COMMAND app
)
On peut ensuite construire le projet et lancer les tests avec ctest.
$> cmake -S . -B build
$> cmake --build build
$> ctest --test-dir build
Test project .../SampleProject/build
Start 1: run_app
1/1 Test #1: run_app .......................... Passed
100% tests passed, 0 tests failed out of 1
CMake fournit des modules de recherche pour de nombreuses librairies. Quand un module existe, find_package permet de
récupérer une cible importée, que l'on peut ensuite lier proprement à son programme. L'exemple suivant montre l'idée avec ZLIB.
L'extrait suivant recherche ZLIB et lie la cible importée correspondante.
find_package(ZLIB REQUIRED) add_executable(app src/main.c) target_link_libraries(app PRIVATE ZLIB::ZLIB)
Cette approche est plus robuste que l'ajout manuel de -I, -L et -lz, parce que CMake garde
l'information attachée à une cible. Les dossiers d'entêtes, les options de compilation et les librairies nécessaires sont propagés
correctement.
Voici un fichier plus complet, adapté à un petit projet. Il reste volontairement raisonnable, mais il montre déjà les idées à conserver : travailler avec des cibles, éviter les variables globales inutiles, séparer les options privées et publiques, et garder le dossier de build hors des sources.
Pour terminer, voici un CMakeLists.txt récapitulatif pour ce petit projet.
cmake_minimum_required(VERSION 3.20)
project(SampleProject VERSION 1.0.0 LANGUAGES C)
option(BUILD_DEMO "Build demo executable" ON)
add_library(calculator STATIC
src/calculator.c
)
target_compile_features(calculator PUBLIC c_std_17)
target_include_directories(calculator PUBLIC include)
if (CMAKE_C_COMPILER_ID MATCHES "GNU|Clang")
target_compile_options(calculator PRIVATE -Wall -Wextra -Wconversion -Werror)
endif()
if (BUILD_DEMO)
add_executable(app src/main.c)
target_link_libraries(app PRIVATE calculator)
endif()
enable_testing()
if (BUILD_DEMO)
add_test(NAME run_app COMMAND app)
endif()
install(TARGETS calculator
ARCHIVE DESTINATION lib
LIBRARY DESTINATION lib
)
install(DIRECTORY include/
DESTINATION include
)
Pour un petit projet personnel, un Makefile bien écrit reste simple, lisible et efficace. Pour un projet multi-plateformes, ou un projet qui doit être intégré dans des environnements très différents, CMake devient vite plus confortable. Le point important est de bien comprendre que CMake ne dispense pas de connaître le compilateur : il vous aide à organiser la construction, mais les concepts de compilation, d'édition des liens, d'options et de librairies restent les mêmes.
Améliorations / Corrections
Vous avez des améliorations (ou des corrections) à proposer pour ce document : je vous remerçie par avance de m'en faire part, cela m'aide à améliorer le site.
Emplacement :
Description des améliorations :