Participer au site avec un Tip
Rechercher
 

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 :

Vous êtes un professionnel et vous avez besoin d'une formation ? Programmation avec
Le langage C
Voir le programme détaillé

Construire un projet C avec CMake

Construire un projet C avec un Makefile


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 vocabulaire de base

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.

Un premier projet CMake

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
$>

Choisir le standard C

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
)

Ajouter des warnings

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()

Séparer une librairie et un exécutable

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.

Librairie statique ou partagée

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.

Options de configuration

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.

Debug et Release

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

Choisir le générateur

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.

Installer les fichiers produits

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

Ajouter un test simple

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

Trouver une librairie externe

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.

Un CMakeLists.txt récapitulatif

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
)

Makefile ou CMake ?

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.

Construire un projet C avec un Makefile




Vous êtes un professionnel et vous avez besoin d'une formation ? Programmation avec
Le langage C
Voir le programme détaillé