I. CMake▲
I-A. Introduction▲
CMake est un outil open source et gratuit permettant de gérer la compilation d'un projet.
Si nous prenons le modèle classique de compilation, nous avons :
Les fichiers source peuvent être des fichiers en C, C++, Java ou tout autre langage. Ceux-ci seront traités par le script de compilation qui appellera le compilateur (ou tout autre outil) pour créer l'exécutable final.
Le script de compilation peut avoir plusieurs formes suivant le projet et l'éditeur utilisé :
- un Makefile ;
- un fichier de projet Code::Blocks ;
- un ficher de projet Microsoft Visual Studio ;
- un fichier de projet Eclipse ;
- …
Toutefois, ce modèle possède quelques limites. En effet, si vous démarrez avec un projet Microsoft Visual Studio et que vous devez compiler sous Linux, vous serez obligé de recommencer la configuration du projet afin qu'il soit compilable sur le nouveau système. Cela peut aussi se produire, si vous commencez le projet sous Linux et que vous voulez faire un exécutable Windows, ou Mac OS X. Votre script de compilation dépend de votre système et/ou de votre chaîne de compilation.
Ce problème existe pour les projets communautaires. En effet, les projets open source seront sûrement compilés par de nombreuses personnes qui utilisent des logiciels différents. Par exemple, le premier veut compiler sous Linux avec GCC, le second sous Windows avec Microsoft Visual Studio et un troisième avec une autre configuration. Il est évident que gérer toutes les configurations serait une perte de temps et apporterait des complications inutiles.
C'est pourquoi CMake a été créé. En effet, CMake va résoudre cette problématique en créant les scripts de compilation à l'aide d'un fichier de configuration générique. Voici le nouveau modèle de compilation :
Cette fois, CMake, grâce au fichier CMakeLists.txt, va produire le script de compilation permettant la création de l'exécutable.
Le fichier CMakeLists.txt est indépendant de la plateforme. Il décrit comment compiler le projet à l'aide d'informations comme : le langage utilisé, les fichiers à compiler, les dépendances (externes ou comme sous-projet). Ainsi CMake va pouvoir produire le script de compilation adéquat pour votre machine et votre projet.
Bien entendu, pour résoudre cette problématique, il existe différents logiciels similaires à CMake : autotools, scons, Jam…
I-B. Installation▲
CMake s'installe simplement : par le gestionnaire de paquets pour les distributions Linux, par un installateur pour Windows, ou par un fichier dmg pour MAC OS X téléchargeables directement sur le site officiel.
Pour Windows, je conseille vivement de rajouter le chemin d'installation dans la variable d'environnement PATH. Cela est proposé durant l'installation de CMake.
Bien entendu, pour compiler un projet, il ne suffit pas d'avoir CMake, il faut aussi un compilateur ou un EDI.
I-C. Fonctionnement▲
Avant de compiler un projet, vous devez générer le script de compilation à l'aide de CMake. Pour cela, vous devez lancer CMake dans le répertoire où se trouve le fichier CMakeLists.txt.
Pour générer le script de compilation, CMake utilise un générateur. Donc pour pouvoir générer le script de compilation que nous souhaitons avoir, nous devons indiquer le générateur à utiliser.
Les générateurs pour les compilateurs et IDE les plus courants sont fournis avec CMake.
I-D. Compiler un projet utilisant CMake▲
Il arrive souvent que l'on tombe sur des projets open source que nous devons compiler nous-mêmes afin de pouvoir les utiliser. Les projets CMake se reconnaissent grâce à la présence du fichier CMakeLists.txt (généralement à la racine du projet).
Voyons comment compiler ces projets.
I-D-1. Utilisation en ligne de commande▲
CMake peut être utilisé à travers l'invite de commande (ou terminal). Il suffit de taper :
cmake -G générateur
« générateur » correspond à l'un des générateurs proposés par CMake. Vous devez prendre celui correspondant à l'environnement avec lequel vous voulez compiler.
Vous pouvez obtenir la liste des générateurs avec la commande :
cmake --help
Si tout se passe correctement, les fichiers pour compiler le projet ont été générés. Sinon, lisez ce que CMake a affiché pour voir où il a bloqué et tentez de corriger suivant les messages.
I-D-2. Utilisation par l'interface graphique▲
CMake peut aussi être utilisé à travers une interface graphique.
La première étape est de remplir les chemins « Where is the code source » (Où se trouve le source code) et « Where to build the binaries » (Où compiler les binaires). Pour le premier, vous devez indiquer le dossier contenant le CMakeList.txt, le second doit indiquer où les fichiers générés seront placés. Ce répertoire sera l'emplacement de travail de votre compilateur ou EDI.
Ensuite, vous pouvez cliquer sur le bouton « Configure » pour lancer la configuration de CMake. Immédiatement (et aussi, car c'est votre premier lancement), il vous demandera le générateur à utiliser.
En validant la fenêtre de configuration, CMake va lancer la configuration du projet. Certains projets nécessitent de spécifier des chemins vers les bibliothèques, ou encore, des options de construction. Autant que possible, le CMakeLists.txt devrait être construit pour que CMake soit indépendant. Lorsque la configuration ne peut se finir sans un apport supplémentaire de la part de l'utilisateur, le bouton « Generate » restera grisé et l'interface permettra l'édition des variables utilisées dans le fichier CMakeLists.txt.
La couleur rouge pour les variables n'indique pas nécessairement que la configuration est fausse. Elle indique simplement que les variables sont nouvelles ou ont été modifiées lors de la dernière configuration.
Finalement, une fois la configuration finie, il suffit de cliquer sur le bouton « Generate » pour générer les fichiers permettant la compilation ou l'ouverture du projet dans un EDI.
I-E. Création du projet▲
Maintenant, nous allons voir comment créer un fichier CMakeLists.txt afin que CMake puisse générer les fichiers appropriés pour compiler notre projet.
I-E-1. CMakeLists.txt▲
Comme nous l'avons vu, le fichier CMakeLists.txt est la pièce maîtresse pour compiler notre projet. Ce fichier décrit comment compiler le projet et permet donc à CMake de générer les scripts de compilation pour construire le projet.
I-E-1-a. Syntaxe▲
Le fichier CMakeLists.txt possède sa propre syntaxe. Celle-ci possède deux particularités :
- les commentaires sont spécifiés avec un croisillon '#' ;
- tout est fonction.
Il y a certes des tests conditionnels (if()/else()/elseif()), mais ils s'utilisent comme des fonctions. De même pour les variables, elles existent, mais vous devez les définir à travers la fonction set().
Le nom des fonctions n'est pas sensible à la casse (toutefois, on conseillera de toujours utiliser la même écriture tout au long du fichier). Par contre, le nom des variables l'est.
Toutes les variables sont de type chaîne de caractères. Il est aussi possible d'avoir des listes, mais celles-ci ne sont que des chaînes de caractères avec un format spécifique (plusieurs valeurs séparées par des points virgule ';').
Pour une description complète du langage, libre à vous de lire la documentation officielle.
I-E-1-b. Composition▲
Le CMakeLists.txt doit décrire votre projet. Pour cela, il faudra lui indiquer les éléments suivants :
- la version minimale pour lire le fichier CMakeLists.txt (cmake_minimum_required()) ;
- le nom du projet (project()) ;
- les dépendances à ajouter (target_include_directories()/target_link_libraries()) ;
- le binaire à produire (add_executable()/add_library()).
Les fichiers à compiler sont donnés lors de l'appel à la fonction add_executable()/add_library().
I-E-1-c. Exemple sur un projet simple▲
Pour nous entraîner, prenons un projet simple. C'est un projet en C, n'ayant aucune dépendance, juste trois fichiers source. Voici notre arborescence :
.
├── CMakeLists.txt
├── hello.c
├── hello.h
└── main.c
Vous pouvez télécharger le projet ici.
Voici le CMakeLists.txt correspondant :
# Nous voulons un cmake "récent" pour utiliser les dernières fonctionnalités
cmake_minimum_required
(
VERSION 3
.0
)
# Notre projet est étiqueté hello
project
(
hello)
# Crée des variables avec les fichiers à compiler
set
(
SRCS
main.c
hello.c
)
set
(
HEADERS
hello.h
)
# On indique que l'on veut un exécutable "hello" compilé à partir des fichiers décrits par les variables SRCS et HEADERS
add_executable
(
hello ${SRCS}
${HEADERS}
)
Et c'est tout ! Ce projet peut facilement compiler sous Linux, Windows, MAC OS et ce, peu importe le compilateur ou l'EDI que vous utilisez.
I-E-1-d. Exemple sur un projet avec une bibliothèque en sous-projet▲
Ce second projet, toujours en C, crée d'abord une bibliothèque « libhello », puis un programme hello utilisant la fonction définie dans la bibliothèque. Voici l'arborescence du projet :
.
├── CMakeLists.txt
├── libhello
│ ├── CMakeLists.txt
│ ├── hello.c
│ └── hello.h
└── main.c
Notre projet est décomposé en deux modules :
- la bibliothèque libhello ;
- le programme principal.
Vous pouvez télécharger le projet ici.
I-E-1-d-i. La bibliothèque▲
Le CMakeLists.txt de la bibliothèque reprend tous les éléments que nous avons déjà étudiés, mais au lieu de générer un exécutable, c'est une bibliothèque que nous voulons. Pour l'indiquer, nous remplaçons add_executable() par add_library() :
# Nous voulons un cmake "récent" pour utiliser les dernières fonctionnalités
cmake_minimum_required
(
VERSION 3
.0
)
# Notre projet est étiqueté libhello
project
(
libhello)
# Crée des variables avec les fichiers à compiler
set
(
SRCS
hello.c
)
set
(
HEADERS
hello.h
)
add_library
(
hello ${SRCS}
${HEADERS}
)
Le nom du binaire indiqué à add_library() est « hello », car selon le système, CMake rajoutera « lib » comme préfixe afin de suivre les conventions du système cible.
I-E-1-d-ii. Le programme principal▲
Les particularités de ce nouveau CMakeLists.txt sont :
- l'inclusion d'un sous-projet pour compiler la bibliothèque (add_subdirectory) ;
- la liaison de la bibliothèque avec le programme (target_link_libraries).
# Nous voulons un cmake "récent" pour utiliser les dernières fonctionnalités
cmake_minimum_required
(
VERSION 3
.0
)
# Notre projet est étiqueté hello
project
(
hello)
# On inclut notre bibliothèque dans le processus de CMake
add_subdirectory
(
libhello)
# Crée des variables avec les fichiers à compiler
set
(
SRCS
main.c
)
# Notre exécutable
add_executable
(
main ${SRCS}
)
# Et pour que l'exécutable fonctionne, il faut lui indiquer la bibliothèque dont il dépend
target_link_libraries
(
main hello)
I-E-1-d-iii. Remarques▲
Si vous souhaitez simplement écrire #include "hello.h" dans le fichier main.c, vous pouvez rajouter target_include_directories(libhello PUBLIC .) à la fin du fichier CMakeLists.txt du dossier libhello.
I-E-1-e. Exemple sur un projet avec une bibliothèque externe▲
Ce second projet, toujours en C, utilise la bibliothèque libpng. Voici l'architecture de notre projet :
.
├── CMakeLists.txt
└── main.c
Vous pouvez télécharger le projet ici.
Dans le fichier CMakeLists.txt nous devons indiquer la dépendance à la bibliothèque libpng. Pour cela, CMake doit scanner différents dossiers (qui dépendent de la plateforme) pour savoir si les fichiers nécessaires sont présents ou non.
Heureusement, nous n'avons pas à le faire manuellement. Il suffit d'utiliser la fonction find_package(). La commande n'est pas magique. Elle va chercher dans les dossiers d'installation de CMake, un fichier FindNOM_DE_LA_BIBLIOTHÈQUE.cmake et le lire. Dans celui-ci, de simples fonctions et tests cherchant à savoir si la bibliothèque est installée et où.
À la fin, plusieurs variables seront définies :
- NOM_DE_LA_BIBLIOTHÈQUE_FOUND : indique si oui ou non la bibliothèque a été trouvée ;
- NON_DE_LA_BIBLIOTHÈQUE_INCLUDE_DIR/NOM_DE_LA_BIBLIOTHÈQUE_INCLUDE_DIRS : le répertoire à inclure pour trouver les fichiers d'entêtes ;
- NOM_DE_LA_BIBLIOTHÈQUE_LIBRARY/NOM_DE_LA_BIBLIOTHÈQUE_LIBRARIES : les bibliothèques à ajouter à l'édition de liens.
Certains modules définissent des variables supplémentaires. Il est donc conseillé de lire les commentaires du début du fichier du module.
Voici donc le CMakeFiles.txt de ce projet :
# Nous voulons un cmake "récent" pour utiliser les dernières fonctionnalités
cmake_minimum_required
(
VERSION 3
.0
)
# Notre projet est étiqueté hello
project
(
helloSDL)
# Crée des variables avec les fichiers à compiler
set
(
SRCS
main.c
)
# Notre exécutable
add_executable
(
main ${SRCS}
)
# Recherche la dépendance externe
find_package
(
PNG)
if
(
PNG_FOUND)
# Une fois la dépendance trouvée, nous l'incluons au projet
target_include_directories
(
main PUBLIC ${PNG_INCLUDE_DIR}
)
target_link_libraries
(
main ${PNG_LIBRARY}
)
else
()
# Sinon, nous affichons un message
message
(
FATAL_ERROR "libpng not found"
)
endif
()
Il est possible de rechercher une version spécifique avec la syntaxe : find_package(Boost 1.57.0).
I-E-1-f. En vrac▲
I-E-1-f-i. Activation du C++11/C++14▲
Sur certains compilateurs, le support du C++11/C++14/… doit être activé en ajoutant une option supplémentaire lors de la compilation. La fonction appropriée pour ce faire est : target_compile_features().
Vous pouvez l'utiliser ainsi, notamment, pour vérifier si le compilateur supporte les nullptr :
# Cette ligne doit être placée après les add_executable/add_library
target_compile_features
(
hello PUBLIC cxx_nullptr)
Vous pouvez trouver la liste des fonctionnalités du C++ vérifiables à travers la fonction target_compile_features() dans la documentation.
I-E-1-f-ii. Copie de fichiers▲
La fonction install() permet de copier des fichiers :
install
(
TARGETS hello RUNTIME DESTINATION .)
Il est aussi possible de copier des fichiers quelconques (données du projet) :
install
(
FILES fichier DESTINATION .)
ou encore un dossier :
install
(
DIRECTORY source DESTINATION .)
Chaque fichier peut nécessiter des actions particulières, il est important de lire la documentation pour trouver les arguments les plus appropriés.
I-E-1-f-iii. Passer des variables de CMake à votre code source▲
Par exemple, vous souhaitez afficher dans votre programme la version de celui-ci, ou encore, des informations de configuration du projet, déterminées par CMake lors de sa configuration. Pour cela, vous devez préparer un fichier qui servira de motif :
#define VERSION_MAJOR "@HELLO_VERSION_MAJOR@"
Ensuite, dans votre CMakeLists.txt vous indiquez à CMake qu'il faut lire ce fichier et le transformer :
project
(
hello VERSION 1
.0
.1
)
configure_file
(
${CMAKE_CURRENT_SOURCE_DIR}
/src/version.h.in ${CMAKE_CURRENT_SOURCE_DIR}
/version.h)
Ici, nous utilisons les arguments de la fonction project() afin de définir la version de notre projet. À partir de cette information, CMake va définir les variables : <NOM-DU-PROJET>_VERSION_MAJOR, <NOM-DU-PROJET>_VERSION_MINOR, <NOM-DU-PROJET>_VERSION_PATCH (entre autres).
La fonction configure_file() lira le fichier indiqué en premier argument, cherchera les éléments englobés par des arobases et les remplacera par le contenu de la variable correspondant au nom trouvé entre les arobases.
Le résultat (le fichier transformé) sera écrit dans le fichier indiqué en second argument.
Ce dernier pourra être inclus et utilisé dans votre code source.
I-F. Projet avec Qt▲
Bien que les projets Qt soient souvent compilés avec qmake, la solution de construction de projet associée à Qt, il est tout à fait possible de compiler son projet avec CMake.
I-F-1. Vérifier la présence de Qt▲
CMake vous permet de vérifier la présence de la bibliothèque et d'obtenir les fichiers à lier à votre projet avec la fonction find_package() :
find_package
(
Qt5 COMPONENTS Widgets OpenGL)
Dans cet exemple, nous souhaitons obtenir les modules « Widgets » et « OpenGL ». Par la suite, vous devrez utiliser « Qt5::Widgets » et « Qt5::OpenGL » pour lier les bibliothèques de Qt à votre projet.
Une seconde syntaxe permet d'écrire :
find_package
(
Qt5Widgets)
Elle ne présente pas de différence avec la première.
I-F-2. Exécution du moc▲
Une des particularités de la compilation d'un projet Qt est son utilisation du moc. CMake propose un mécanisme très simple pour définir lorsqu'un projet nécessite l'utilisation du moc :
# Indique d'utiliser moc lorsque nécessaire.
set
(
CMAKE_AUTOMOC ON)
# Indique de chercher les entêtes dans les répertoires de compilation.
# Cela est nécessaire, car les fichiers du moc seront générés dans ces dossiers.
set
(
CMAKE_INCLUDE_CURRENT_DIR ON)
I-F-3. Utiliser des fichiers d'interface utilisateur (.ui)▲
Les fichiers d'interface utilisateur (.ui) de Qt doivent être transformés avant de pouvoir les inclure dans le binaire. Pour ce faire il faut utiliser la fonction qt5_wrap_ui() :
set
(
UIS interface1.ui interface2.ui)
qt5_wrap_ui
(
UIS_HDRS ${UIS}
)
Finalement, la variable UIS_HDRS doit être passée en argument de la fonction add_executable() lors de la création du binaire.
I-F-4. Utiliser des fichiers de ressources (.qrc)▲
De la même manière que les fichiers d'interface utilisateur, les fichiers de ressources doivent être traités avant de pouvoir être intégrés. La fonction est qt5_add_resources :
set
(
RESSOURCE hello-qt.qrc)
qt5_add_resources
(
RESSOURCE_HDR RESSOURCE)
Finalement, la variable RESSOURCE_HDR devra être passée en argument de la fonction add_executable() lors de la création du binaire.
I-F-5. Création du binaire▲
La création du binaire se fait comme dans tout autre projet. Il ne faut simplement pas oublier les fichiers générés par le traitement des fichiers d'interface utilisateur (.ui) ou de ressources (.qrc) :
add_executable
(
hello-qt ${SRCS}
${HDRS}
${UIS_HDRS}
${RESSOURCE_HDR}
)
target_link_libraries
(
hello-qt Qt::Widgets Qt::OpenGL)
I-G. Conseils▲
I-G-1. Version minimale▲
Il est conseillé d'indiquer une version minimale 3.X avec la fonction cmake_minimum_required(). Il peut encore être toléré de voir une version préalable 2.8, mais en règle générale, pour un nouveau projet, il vaut mieux utiliser la version la plus récente.
Il est surtout déconseillé d'indiquer les versions 2.4 et 2.6, qui, elles, sont vraiment trop anciennes.
I-G-1-a. Version minimale pour Qt▲
Il est fortement conseillé d'utiliser au minimum une version 2.8.12 lors d'une utilisation conjointe avec Qt. En effet, les fonctions ont été simplifiées rendant l'écriture du fichier CMakeLists.txt pour un projet Qt bien plus aisée.
I-G-2. Sous-projets▲
Lors de la création du CMakeLists.txt ne faites pas la supposition que votre projet sera toujours à la racine. Il est possible que le projet soit inclus à son tour dans un autre projet.
Pour la même raison, ne modifiez pas les variables globales.
I-H. Empaquetage du projet▲
CMake, à travers CPack permet d'empaqueter le projet. L'empaquetage est le processus de réalisation d'un paquet (pour Linux), d'un installateur (pour Windows) ou simplement d'une archive.
Aucune installation supplémentaire n'est à faire. CPack est inclus à CMake.
I-H-1. Configuration du projet pour Cpack▲
Tout comme pour la compilation, nous allons utiliser le fichier CMakeLists.txt pour décrire à CPack comment empaqueter le projet.
Nous reprenons le deuxième projet et lui rajoutons la configuration nécessaire pour en produire un paquet (ou installateur pour Windows).
Le fichier CMakeLists.txt de la bibliothèque :
# Nous voulons un cmake "récent" pour utiliser les dernières fonctionnalités
cmake_minimum_required
(
VERSION 3
.0
)
# Notre projet est étiqueté libhello
project
(
libhello)
# Crée des variables avec les fichiers à compiler
set
(
SRCS
hello.c
)
set
(
HEADERS
hello.h
)
add_library
(
hello ${SRCS}
${HEADERS}
)
# La bibliothèque peut être soit une bibliothèque statique, soit dynamique, donc on précise où installer pour les deux cas
install
(
TARGETS hello LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib)
Et le CMakeLists.txt pour le projet principal :
# Nous voulons un cmake "récent" pour utiliser les dernières fonctionnalités
cmake_minimum_required
(
VERSION 3
.0
)
# Notre projet est étiqueté hello
project
(
hello)
# On inclut notre bibliothèque dans le processus de CMake
add_subdirectory
(
libhello)
# Crée des variables avec les fichiers à compiler
set
(
SRCS
main.c
)
# Notre exécutable
add_executable
(
main ${SRCS}
)
# Et pour que l'exécutable fonctionne, il faut lui indiquer la bibliothèque dont il dépend
target_link_libraries
(
main hello)
# Indique que l'on installe l'exécutable dans un dossier bin
install
(
TARGETS main DESTINATION bin)
# Section CPack
include
(
CPack)
Pour faire notre paquet, nous avons simplement indiqué comment installer notre programme avec la fonction install(). Ensuite, pour pouvoir empaqueter le projet, nous avons invoqué CPack.
CPack utilise les appels à la fonction install() pour savoir quels sont les fichiers à inclure dans le paquet.
Pour obtenir un installateur sous Windows, il vous faudra installer le logiciel NSIS.
Pour améliorer la configuration de votre installateur, vous pouvez utiliser de nombreuses variables décrites dans la documentation officielle. Voici un court exemple d'utilisation :
SET
(
CPACK_PACKAGE_DESCRIPTION_SUMMARY "CMake demo"
)
SET
(
CPACK_PACKAGE_VENDOR "LittleWhite"
)
SET
(
CPACK_PACKAGE_EXECUTABLES "Hello"
"Hello"
)
L'empaquetage n'est pas réalisé par CMake. Il faut exécuter une phase de compilation supplémentaire pour créer le package. Par exemple, si vous générez des Makefiles, cela peut se faire avec la commande make package.
Vous pouvez récupérer le projet ici.
II. Conclusion▲
Nous avons vu comment configurer et compiler notre projet avec CMake afin de facilement produire les binaires, et ce, quels que soient le système ou les outils de compilation utilisés. De plus, pour facilement redistribuer notre projet, nous avons ajouté la configuration de Cpack.
III. Remerciements▲
Merci à Sebastien Gougeaud pour ses judicieuses remarques sur l'article.
Je remercie aussi ClaudeLELOUP pour sa relecture orthographique.