Initiation à CMake

Florian Goujeon

Ce document est un cours d'initiation à CMake, un système de construction logicielle libre, multilangage et multiplateforme.

Ce cours vous enseignera les bases nécessaires au bon usage de cet outil. Une fois ces informations assimilées, vous serez capable d'utiliser CMake dans les cas les plus simples. Vous pourrez alors poursuivre vers une utilisation avancée avec l'unique aide de la documentation officielle.

- Titre : Initiation à CMake

- Auteur : Florian Goujeon

- Parution : 6 janvier 2009

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2013 Florian Goujeon. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Présentation

- I-A. Problématique

- I-B. Les systèmes de construction logicielle

- I-C. CMake

- II. Installation

- III. Compilation d'un projet simple

- III-A. Écriture du fichier CMakeLists.txt

- III-A-1. Syntaxe

- III-A-2. Mise en pratique

- III-B. Lancement de CMake

- III-C. Compilation

- III-D. Améliorations

- III-D-1. Génération automatique de la liste des fichiers sources

- III-D-2. Modification du chemin de sortie de l'exécutable

- III-D-3. Édition du fichier CMakeCache.txt

- IV. Compilation d'un projet utilisant une bibliothèque tierce

- IV-A. Méthode manuelle

- IV-B. Méthode assistée

- V. Compilation d'une bibliothèque

- V-A. Présentation de la bibliothèque

- V-B. Écriture du fichier CMakeLists.txt

- V-C. Test de la bibliothèque

- VI. Conclusion

- VII. Remerciements

I - Présentation

I-A - Problématique

Qu'un logiciel soit libre ou propriétaire, son code source n'a que peu de chances de rester exclusivement entre les mains de son auteur d'origine. Un projet propriétaire développé en entreprise pourra être maintenu par une multitude de personnes tout au long de son existence. Un logiciel libre, quant à lui, peut compter plusieurs dizaines voire plusieurs centaines de développeurs éparpillés à travers le monde, sans compter les utilisateurs souhaitant compiler le code source eux-mêmes.

Pour compiler le code source, toutes ces personnes ont besoin d'un ensemble de fichiers contenant les paramètres de compilation. En effet, construire un programme à partir de son unique code source relève de la rétro-ingénierie. Or, au vu des conditions précitées, il est tout à fait impensable de fournir avec le code source un tel fichier dont le format serait lié à un EDI particulier, à un compilateur particulier ou encore à un système d'exploitation particulier. De plus, les paramètres de compilation listés dans ce fichier ne doivent évidemment pas dépendre d'une configuration spécifique de la machine effectuant la compilation (telle que le chemin d'installation d'une bibliothèque tierce).

En conséquence, il devient évident que le partage du fichier de projet de l'EDI ou encore du ou des fichiers makefile n'est pas une solution acceptable.

I-B - Les systèmes de construction logicielle

En réponse à ce besoin est née une nouvelle catégorie d'outils : les systèmes de construction logicielle (ou build systems, en anglais). Comme son nom l'indique, un tel système a pour but de permettre, directement ou indirectement, la compilation d'un code source.

Tous les systèmes de construction ont leurs spécificités, mais leur fonctionnement est globalement le même. Dans un premier temps, le développeur écrit dans un fichier la description du projet (incluant la liste des fichiers sources, les bibliothèques à lier au binaire final, etc.) de façon totalement indépendante de la configuration logicielle de la machine (qui inclut le compilateur utilisé, le chemin vers les fichiers des bibliothèques à lier, etc.). Ensuite, les personnes souhaitant compiler le projet appellent le système de construction en lui passant en paramètre le fichier écrit par le développeur. Le système analyse alors le fichier d'une part et tente de récupérer des informations sur la configuration logicielle de la machine l'exécutant d'autre part. Une fois ces opérations accomplies, il sera enfin en mesure de produire un script de compilation de bas niveau (tel qu'un fichier Makefile, comprenant des appels explicites au compilateur, dont les paramètres seront propres à la configuration logicielle) :

Certains systèmes effectueront plutôt des appels directs au compilateur afin de produire le binaire final, mais l'idée générale est la même.

Le fichier décrivant le projet, accompagné du code source (et éventuellement de la documentation et d'autres ressources diverses), est donc tout ce qui constitue le paquetage partagé par les différents contributeurs du projet (et par les utilisateurs si le projet est libre). Étant donné que ce fichier est utilisable par tous quelle que soit leur configuration logicielle, les systèmes de construction constituent donc une réponse viable à la problématique exposée ci-dessus.

Il existe un grand nombre de systèmes de construction logicielle différents, tels que les fameux GNU Autotools, SCons, Jam ou encore qmake. Le système qui nous intéresse dans ce document est le non moins connu CMake.

I-C - CMake

CMake est un système de construction logicielle. C'est un logiciel libre (licence BSD), multilangage et multiplateforme.

À partir du fichier descriptif du projet (nommé CMakeLists.txt) et des informations sur la configuration logicielle de la machine effectuant la compilation (listées dans le fichier CMakeCache.txt, prérempli lors de la phase d'analyse de la configuration), CMake est capable de générer les types de fichiers suivants :

- un fichier Makefile ;

- un fichier de projet d'EDI ;

- ou une combinaison des deux (pour les EDI supportant les fichiers Makefile).

C'est le générateur qui permet de définir le type de fichier à générer. Il existe plusieurs générateurs pour les makefiles, ainsi qu'un générateur par EDI supporté :

Alors que d'autres systèmes de construction ne sont utilisables que sous un ensemble restreint de systèmes d'exploitation, CMake est disponible aussi bien sous GNU/Linux que sous Windows et MacOS, pour ne citer qu'eux.

À l'heure où cet article est publié, CMake gère déjà les langages de programmation les plus populaires, tels que C, C++ et Java.

De plus, comme vous pourrez vous-même en juger dans la suite de ce cours, CMake est très facile à utiliser (ce qui est loin d'être le cas de tous les outils de cette catégorie) et la syntaxe du fichier CMakeLists.txt est à la fois simple et efficace.

Le paquetage livré avec CMake contient une interface graphique pour assister son fonctionnement. Cependant, cette interface n'est à l'heure actuelle pas disponible sous tous les systèmes d'exploitation. De plus, son utilisation n'est pas indispensable, voire nuisible d'un point de vue pédagogique ; il est en effet toujours intéressant de savoir ce qui se passe derrière ce type d'interfaces. Par conséquent, l'utilisation des interfaces graphiques de CMake ne sera pas traitée dans ce cours.

Son indépendance vis-à-vis du système d'exploitation, du compilateur et des outils de développement, sa simplicité d'utilisation, ses performances et ses nombreuses fonctionnalités font de CMake le système de prédilection de plus en plus d'utilisateurs. Pour plus d'informations sur CMake, dirigez-vous vers le site officiel.

II - Installation

Le site officiel de CMake propose des installateurs pour un vaste ensemble de systèmes d'exploitation. Vous pouvez également télécharger son code source si vous souhaitez le compiler vous-même. Dans tous les cas, rendez-vous sur la page du site officiel consacrée à l'installation de CMake.

CMake est également disponible dans les dépôts de nombreuses distributions GNU/Linux, telles que Debian, Fedora et Gentoo.

III - Compilation d'un projet simple

Passons directement à la pratique avec un projet simple. Par projet simple, comprenez un exécutable dont le code est constitué de quelques classes et n'utilisant pas de bibliothèque tierce.

Nous considérerons tout au long du chapitre que le projet est organisé de cette façon :

Le contenu des fichiers sources importe peu, pourvu que le code ne comporte pas d'erreur.

III-A - Écriture du fichier CMakeLists.txt

Une fois le code source écrit, occupons-nous de notre fichier CMakeLists.txt.

III-A-1 - Syntaxe

La syntaxe du fichier CMakeLists.txt est on ne peut plus simple. Elle est exclusivement constituée d'appels de commandes. Les arguments à passer aux commandes sont placés entre parenthèses et séparés par des espaces ou des sauts de ligne. Les différents appels de commandes sont séparés de la même façon :

nom_de_la_commande(argument1 argument2 argument3)

nom_d_une_autre_commande(
argument1
argument2
)

À noter qu'aucun saut de ligne n'est toléré entre le nom de la commande et la parenthèse ouvrante.

Si un argument doit lui-même contenir des espaces, il pourra être entouré de guillemets doubles :

commande(argument "argument avec des espaces")

Enfin, les éternels commentaires, quant à eux, sont marqués par un # en début de ligne :

# ceci est un commentaire
commande(argument1 argument2)

III-A-2 - Mise en pratique

Écrivez le code suivant dans le fichier CMakeLists.txt du projet :

# Déclaration du projet
project(MyProject)

# Déclaration de l'exécutable
add_executable(
my_executable
src/bird.h
src/duck.cpp
src/duck.h
src/main.cpp
src/sparrow.cpp
src/sparrow.h
)

La commande project() est utilisée pour déclarer un projet.

- Le premier paramètre est le nom que vous voulez donner à votre projet. Il s'agit entre autres du nom que portera le projet dans votre EDI.

- Cette commande prend également un second paramètre optionnel. Il s'agit du langage dans lequel le code du programme à compiler est écrit. Dans la majorité des cas, vous pouvez simplement ignorer cet argument, CMake étant capable de reconnaître lui-même le langage en analysant les sources.

Nous devons ensuite définir nos cibles, c'est-à-dire l'ensemble des exécutables et des bibliothèques que nous voulons générer à partir du code source. Pour le moment, nous ne souhaitons créer qu'un unique exécutable à partir de l'ensemble de notre code source. Pour ce faire, nous écrivons la commande add_executable().

- Le premier paramètre est le nom de l'exécutable. Inutile de spécifier une éventuelle extension de fichier (telle que .exe) : rappelez-vous que CMake est conçu pour être utilisé sous n'importe quel système d'exploitation. En outre, lorsqu'arrivera le moment de générer le fichier exécutable, il décidera lui-même des caractères à ajouter au nom que vous lui avez passé (en l'occurrence .exe sous Windows).

- Vous l'avez très certainement deviné : les arguments suivants constituent la liste des fichiers sources qui seront passés au compilateur. Remarquez toutefois la présence apparemment inopportune des fichiers d'en-tête (*.h) au sein de cette liste. En effet, on ne passe jamais directement un fichier d'en-tête à un compilateur. Ces fichiers ne sont lus qu'au moment de la résolution des directives d'inclusion (#include) par le préprocesseur. Par bonheur, CMake traite spécifiquement ces fichiers de façon à ce qu'ils n'apparaissent jamais dans une commande d'appel au compilateur. Vous n'avez donc pas à vous soucier de ce qui se passe sous le capot à ce niveau-là.
Dans ce cas, me direz-vous, pourquoi prendre la peine de lister les fichiers d'en-tête s'ils ne sont même pas passés au compilateur ? La raison est simple : vous voudrez certainement qu'ils soient présents parmi les fichiers sources du projet dans votre EDI, afin de pouvoir les ouvrir et les éditer rapidement. Par ailleurs, même si vous n'utilisez pas d'EDI pouvant gérer des projets, imaginez que cela pourrait être le cas de la personne qui reprendra votre code dans le futur.
En bref, ne vous posez pas de question : listez tous les fichiers sources sans faire de distinction.

Quel que soit votre système d'exploitation, c'est la notation UNIX qui est utilisée pour l'écriture de l'adresse d'un fichier. Il faudra donc écrire dossier/fichier et non dossier\fichier.

III-B - Lancement de CMake

Notre fichier CMakeLists.txt est maintenant complet ; lançons CMake sans plus attendre. Placez-vous dans le dossier racine de votre projet et entrez une commande cmake de cette forme :

cmake . -G"nom_du_générateur_de_mon_choix"

Le premier paramètre désigne le répertoire où se situe le fichier CMakeLists.txt du projet. Dans notre cas, il s'agit du dossier courant.

L'option -G permet de définir le générateur. Du générateur dépend le type de fichier(s) que CMake va générer (fichier Makefile, fichier de projet d'EDI, etc.), comme il a été évoqué dans la . Remplacez nom_du_générateur_de_mon_choix par l'un des générateurs de CMake. Pour obtenir la liste des générateurs disponibles sur votre plate-forme, affichez l'aide de la commande cmake :

cmake --help

Cela ne semble pas forcément évident au premier abord, mais il faut écrire en toutes lettres le nom du générateur dans la commande cmake. Par exemple, si vous voulez générer un fichier de projet pour Code : :Blocks et que votre compilateur est MinGW, entrez la commande suivante :

cmake . -G"CodeBlocks - MinGW Makefiles"

Si tout se passe bien, la commande devrait retourner quelque chose comme ceci :

-- The C compiler identification is nom_de_votre_compilateur_C
-- The CXX compiler identification is nom_de_votre_compilateur_C++
-- Check for working C compiler : chemin_vers_votre_compilateur_C
-- Check for working C compiler : chemin_vers_votre_compilateur_C -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working CXX compiler : chemin_vers_votre_compilateur_C++
-- Check for working CXX compiler : chemin_vers_votre_compilateur_C++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Configuring done
-- Generating done
-- Build files have been written to : adresse_du_dossier_de_votre_projet

CMake devrait avoir généré les fichiers suivants :

- CMakeFiles : dossier contenant les fichiers intermédiaires de compilation, des fichiers temporaires et autres fichiers de configuration qui ne regardent que CMake ;

- CMakeCache.txt : fichier définissant un ensemble de variables de configuration et d'informations sur votre système (nous nous pencherons davantage sur ce fichier plus loin dans le cours) ;

- cmake_install.cmake : script CMake d'installation de votre programme (dont nous ne parlerons pas dans ce cours) ;

- MyProject.xxx : fichier de projet de votre EDI (selon le générateur choisi) ;

- Makefile (selon le générateur choisi).

Remarquez que votre dossier src reste inchangé. Aucun fichier d'aucune sorte n'y a été créé. C'est un des aspects qui explique pourquoi CMake est autant apprécié chez les maniaques du rangement tels que moi : il ne s'introduit pas là où on ne lui a pas dit d'entrer et se contente de son dossier personnel (CMakeFiles) pour l'écriture de fichiers temporaires et de configuration interne.

III-C - Compilation

Vous avez maintenant tout ce qu'il faut pour lancer une compilation. Procédez comme vous le faîtes habituellement : ouvrez votre projet avec votre EDI préféré et cliquez sur le bouton Construire, ou bien lancez make depuis votre terminal. Si tout va bien, votre compilateur aura créé un exécutable dans le dossier racine de votre projet, aux côtés du fichier CMakeLists.txt. Lancez-le pour vérifier qu'il n'y a pas de problème.

III-D - Améliorations

III-D-1 - Génération automatique de la liste des fichiers sources

Au fur et à mesure que votre projet évolue, de nouvelles classes, de nouveaux fichiers sources apparaissent, tandis que d'autres disparaissent. Chaque modification de l'arborescence vous contraint à modifier le fichier CMakeLists.txt encore et encore.

CMake possède toute la panoplie de commandes nécessaires à la résolution de ce type de désagréments. Pour ce cas précis, utilisons la commande file() :

cmake_minimum_required(VERSION 2.6)

# Déclaration du projet
project(MyProject)

# Génération de la liste des fichiers sources
file(
GLOB_RECURSE
source_files
src/*
)

# Déclaration de l'exécutable
add_executable(
my_exe
${source_files}
)

file() est une commande pouvant effectuer un vaste nombre d'opérations différentes.

Le premier paramètre est le type d'opération à exécuter. L'opération GLOB récupère une liste de fichiers à partir d'une expression. La variante GLOB_RECURSE cherche dans les sous-dossiers des dossiers spécifiés.

Le troisième paramètre est une expression à laquelle le chemin de chaque fichier doit correspondre pour que celui-ci soit ajouté à la liste. src/* désigne tous les fichiers situés dans le dossier src, ainsi que tous les fichiers situés dans chaque sous-dossier de src (spécificité de la variante GLOB_RECURSE).

Le deuxième paramètre est la variable (ici source_files) dans laquelle la liste de fichiers est écrite.

La suite est évidente : parmi les arguments de la commande add_executable(), en lieu et place de la liste de fichiers sources se trouve désormais la variable contenant la liste des fichiers récupérés par la commande file(). Comme vous le voyez, pour accéder à la valeur d'une variable, il faut entourer son nom d'accolades et faire précéder le tout par le symbole $.

L'utilisation de la commande file() (ainsi que beaucoup d'autres) nécessite de spécifier une version minimale de CMake, ce qui explique la ligne ajoutée au début du script. Si vous l'omettez, pas de panique : un message d'erreur vous le rappellera explicitement.

Il est nécessaire de relancer cmake chaque fois que la liste des fichiers change !
En effet, même si le fichier CMakeLists.txt n'est pas modifié, le script de compilation (situé dans le fichier projet de votre EDI ou dans le Makefile) généré par CMake ne contient qu'une liste statique de fichiers sources ayant besoin d'être mise à jour explicitement.

III-D-2 - Modification du chemin de sortie de l'exécutable

Définissez le dossier où vos exécutables doivent être produits via la variable EXECUTABLE_OUTPUT_PATH. Optons pour un nouveau dossier nommé bin :

set(EXECUTABLE_OUTPUT_PATH bin)

Mieux encore : placez vos exécutables dans des dossiers portant le nom du type de compilation (Release, Debug...). Ce dernier est défini par la variable CMAKE_BUILD_TYPE (qui n'a d'effet seulement si le générateur choisi est basé sur Make) :

cmake_minimum_required(VERSION 2.6)

# Configuration du projet
project(MyProject)
set(EXECUTABLE_OUTPUT_PATH bin/${CMAKE_BUILD_TYPE})

# Génération de la liste des fichiers sources
file(
GLOB_RECURSE
source_files
src/*
)

# Déclaration de l'exécutable
add_executable(
my_exe
${source_files}
)

Ne vous inquiétez pas si aucun sous-dossier n'est créé dans bin : la variable CMAKE_BUILD_TYPE doit être définie par vous-même. Par défaut, cette variable est vide ; dans ce cas, CMake crée un script de compilation en mode Debug.

Avant de faire l'erreur de la définir directement dans le fichier CMakeLists.txt, lisez plutôt ce qui suit.

III-D-3 - Édition du fichier CMakeCache.txt

Les fichiers sources et la documentation mis à part, CMakeLists.txt est le seul fichier que vous partagerez avec les autres contributeurs du projet (et éventuellement avec les utilisateurs finaux qui souhaitent compiler les sources eux-mêmes). Par conséquent, ce fichier doit décrire la construction du projet de façon totalement indépendante du système et du cas d'utilisation.

En effet, les contributeurs n'utiliseront pas tous le même système avec les mêmes paramètres ; votre système lui-même pourrait très bien évoluer au cours du projet. De plus, vous ne compilerez pas toujours pour les mêmes raisons (pour tester ou déboguer votre code, pour publier une version de votre logiciel, etc.).

En bref, ne définissez rien dans CMakeLists.txt qui puisse varier d'un ordinateur à l'autre ou d'un cas d'utilisation à l'autre. Voici quelques exemples d'informations qui ne doivent pas figurer dans le fichier :

- le chemin vers votre compilateur ou tout autre utilitaire participant à la compilation ;

- le chemin vers une bibliothèque tierce ;

- les options de compilation propres au système (optimisation pour une architecture particulière) ou au cas d'utilisation (niveau d'optimisation, présence des symboles de débogage ;

- le niveau de verbosité des scripts de compilation générés ;

- etc.

Tous ces paramètres personnels doivent être définis ailleurs : dans le fichier CMakeCache.txt.

Exerçons-nous avec un exemple typique ; nous allons modifier le mode de compilation, caractérisé par la variable CMAKE_BUILD_TYPE. Ouvrez le fichier CMakeCache.txt et trouvez la ligne définissant cette variable. La ligne en question devrait être dans cet état :

CMAKE_BUILD_TYPE:STRING=

Toutes les définitions de variable ont cette forme :

NOM_DE_LA_VARIABLE:TYPE=valeur

Le type de la variable n'a d'autre intérêt que de permettre à l'interface graphique de CMake d'afficher le contrôle adéquat (case à cocher pour BOOL, champ de texte pour STRING, boite de dialogue d'ouverture de fichier pour FILEPATH, etc.). Toutefois, ne modifiez en aucun cas le type des variables ; CMake s'y perdrait.

Optons pour une compilation en mode Release (la majuscule est prise en compte) :

CMAKE_BUILD_TYPE:STRING=Release

Relancez la commande cmake et compilez. Si le niveau d'optimisation est élevé et que la macro NDEBUG est définie, alors l'opération est un succès.

Remarquez que vous pouvez également modifier les variables définies par CMakeCache.txt directement via la commande cmake grâce à l'option -D. Voici la commande correspondant à la modification que nous venons d'effectuer (l'absence d'espace entre le -D et la définition de la variable est requise) :

cmake . -G"nom_du_générateur_de_mon_choix" -DCMAKE_BUILD_TYPE :STRING=Release

Cela peut en effet être beaucoup plus commode que d'éditer le fichier manuellement si la variable en question est souvent modifiée. On pourrait par exemple imaginer un script cmake_debug.xxx et un autre cmake_release.xxx (extension selon le système d'exploitation) ; un simple double-clic sur le script (ou un appel depuis le terminal) permettrait de modifier rapidement le mode de compilation.

IV - Compilation d'un projet utilisant une bibliothèque tierce

Nous allons maintenant configurer la compilation d'un nouvel exécutable faisant usage d'une bibliothèque. Dans cet exemple, nous aimerions inclure l'une des célèbres bibliothèques de Boost dans notre programme : Boost.Signals. Si vous voulez inclure une autre bibliothèque, l'idée générale est bien entendu la même.

Le projet est organisé comme ceci :

L'unique fichier source est main.cpp, visible ci-dessous. Une fois ce code correctement compilé, l'exécutable résultant doit afficher Hello, World! sur la sortie standard.

#include <iostream>

#include <boost/signals.hpp>

struct HelloWorld
{
void operator()() const
{
std::cout << "Hello, World!" << std::endl;
}
};

int main(int argc, char** argv)
{
boost::signal0<void> sig;

HelloWorld hello;
sig.connect(hello);

sig();

return 0;
}

Nous débutons avec le fichier CMakeLists.txt suivant :

cmake_minimum_required(VERSION 2.6)

# Configuration du projet
project(MyProject)
set(EXECUTABLE_OUTPUT_PATH bin/${CMAKE_BUILD_TYPE})

# Configuration de l'exécutable
file(
GLOB_RECURSE
source_files
src/*
)

add_executable(
my_exe
${source_files}
)

Il y a deux méthodes pour inclure une bibliothèque avec CMake : la méthode manuelle et la méthode assistée. La méthode assistée est celle qui nécessite le moins de travail de votre part. Toutefois, il n'est possible de l'utiliser qu'avec un ensemble restreint de bibliothèques. C'est pourquoi nous allons passer en revue les deux méthodes.

IV-A - Méthode manuelle

De la même façon que lors d'une compilation en ligne de commande ou via un EDI, nous devons :

1. ajouter le répertoire des fichiers d'en-tête de Boost à la liste des répertoires parcourus par le préprocesseur lors de la recherche d'un fichier d'en-tête externe ;

2. ajouter le répertoire des fichiers de bibliothèques partagées de Boost à la liste des répertoires parcourus par l'éditeur de liens lors de la recherche d'une bibliothèque ;

3. lier le fichier de bibliothèque partagée de Boost.Signals à notre exécutable.

Pour remplir la première condition, nous utiliserons la commande include_directories() :

include_directories(/chemin/vers/boost_x_xx_x)

La commande link_directories() nous permettra de répondre à la deuxième condition :

link_directories(/chemin/vers/boost_x_xx_x/stage/lib)

Enfin, la commande target_link_libraries(), qui sert à spécifier les différentes bibliothèques à lier à une cible (indiquée en premier argument), remplira la dernière :

target_link_libraries(
my_exe
boost_signals-mt
)

Sous Windows, les adresses absolues doivent être notées sous cette forme : C:/chemin/vers/boost_x_xx_x.

Remarquez qu'il n'est pas nécessaire (même si cela reste possible) de spécifier le nom exact du fichier de bibliothèque. Par exemple, dans le cas d'un fichier sous GNU/Linux portant le nom libboost_signals-mt.so, CMake saura qu'il faudra ajouter le préfixe lib et l'extension .so.

Voici le contenu du fichier CMakeLists.txt après ces modifications :

cmake_minimum_required(VERSION 2.6)

# Configuration du projet
project(MyProject)
set(EXECUTABLE_OUTPUT_PATH bin/${CMAKE_BUILD_TYPE})

# Inclusion de Boost
include_directories(/chemin/vers/boost_x_xx_x)
link_directories(/chemin/vers/boost_x_xx_x/stage/lib)

# Configuration de l'exécutable
file(
GLOB_RECURSE
source_files
src/*
)
add_executable(
my_exe
${source_files}
)

#Configuration de l'édition de liens
target_link_libraries(
my_exe
boost_signals-mt
)

Appelez la commande cmake, compilez et testez le programme. Tout fonctionne à merveille ? Alors, ajoutons la touche finale à notre fichier CMakeLists.txt.

Comme expliqué dans le chapitre précédent, le fichier CMakeLists.txt ne devrait pas comporter d'informations relatives à votre système. Utilisons donc des variables à la place des chemins et des noms de fichiers spécifiés :

cmake_minimum_required(VERSION 2.6)

# Configuration du projet
project(MyProject)
set(EXECUTABLE_OUTPUT_PATH bin/${CMAKE_BUILD_TYPE})

# Inclusion de Boost
include_directories(${boost_include_dir})
link_directories(${boost_lib_dir})

# Configuration de l'exécutable
file(
GLOB_RECURSE
source_files
src/*
)
add_executable(
my_exe
${source_files}
)

# Configuration de l'édition de liens
target_link_libraries(
my_exe
${boost_signals_lib_name}
)

Encore une fois, définissez la valeur de ces variables dans le fichier CMakeCache.txt. Ajoutez-y les lignes suivantes :

//Chemin vers le dossier des fichiers d'en-tête de Boost
boost_include_dir:FILEPATH=/chemin/vers/boost_x_xx_x

//Chemin vers le dossier des fichiers bibliothèques de Boost
boost_lib_dir:FILEPATH=/chemin/vers/boost_x_xx_x/stage/lib

//Nom du fichier de la bibliothèque Boost.Signals
boost_signals_lib_name:STRING=boost_signals-mt

Effectuez un ultime test. Si celui-ci est toujours concluant, alors votre fichier CMakeLists.txt est prêt à être distribué aux autres contributeurs du projet.

IV-B - Méthode assistée

CMake est livré avec un ensemble de modules dont certains sont destinés à vous assister pour l'inclusion de bibliothèques dans votre projet. Les bibliothèques Boost font partie des quelques paquetages à bénéficier d'un tel module.

Tout d'abord, incluez le module correspondant (nommé FindBoost, dans notre cas) dans CMakeLists.txt :

include(FindBoost)

Utilisez ensuite la commande find_package(), désormais utilisable de cette manière pour Boost, grâce à l'inclusion du module FindBoost :

find_package(
Boost
1.36.0
REQUIRED signals
)

- Boost, le premier argument, est le nom du paquetage à trouver.

- 1.36.0, le deuxième argument définit la version du paquetage à trouver.

- REQUIRED, le troisième argument, indique que le ou les éléments de la bibliothèque qui vont suivre sont requis.

- signals, le quatrième et dernier argument est l'un de ces éléments.

Enfin, il est possible de spécifier des critères de recherche plus précis, tels que l'utilisation des versions statiques ou dynamique des bibliothèques ou encore de leurs versions optimisées multithread ou non. Ces variables sont à définir avant l'appel de la commande find_package() :

set(Boost_USE_STATIC_LIBS ON)
set(Boost_USE_MULTITHREAD OFF)

Si la commande find_package() parvient à trouver les paquetages spécifiés, elle définit un ensemble de variables comprenant, entre autres :

- Boost_LIBRARY_DIRS, répertoire contenant les fichiers d'en-tête de Boost ;

- Boost_INCLUDE_DIRS, répertoire contenant les fichiers de bibliothèques de Boost ;

- Boost_LIBRARIES, chemin des fichiers des bibliothèques recherchées (ici, Boost.Signals uniquement).

Ces variables sont à utiliser comme arguments des commandes respectives include_directories(), link_directories() et target_link_libraries(). Leurs définitions se situent dans la partie consacrée à l'inclusion manuelle d'une bibliothèque.

Vous aimeriez être certain qu'une variable soit définie comme vous le souhaitez ? Vous pouvez utiliser la commande message(), qui vous permettra d'afficher la valeur des variables de votre choix lors de l'exécution de la commande cmake :

message(
STATUS
${Boost_LIBRARIES}
)

Le fichier CMakeLists.txt complet est le suivant :

cmake_minimum_required(VERSION 2.6)

# Configuration du projet
project(MyProject)
set(EXECUTABLE_OUTPUT_PATH bin/${CMAKE_BUILD_TYPE})

# Inclusion de Boost
include(FindBoost)
find_package(
Boost
1.36.0
REQUIRED signals
)
link_directories(${Boost_LIBRARY_DIRS})
include_directories(${Boost_INCLUDE_DIRS})

# Configuration de l'exécutable
file(
GLOB_RECURSE
source_files
src/*
)
add_executable(
my_exe
${source_files}
)

# Configuration de l'édition de liens
target_link_libraries(
my_exe
${Boost_LIBRARIES}
)

Dans un environnement de type Unix où l'organisation des fichiers est standard, la recherche devrait être concluante sans avoir à fournir plus d'informations à CMake. Toutefois, dans le cas d'une installation exotique ou de l'utilisation d'un autre système d'exploitation, il peut s'avérer nécessaire de spécifier dans quel dossier chercher.

Dans un tel cas de figure, vous devez ajouter dans la section EXTERNAL cache entries du fichier CMakeCache.txt un ensemble de définitions de variables. Pour Boost, les variables à définir sont BOOST_ROOT, BOOST_INCLUDEDIR et BOOST_LIBRARYDIR, définissant respectivement le dossier racine de votre installation de Boost, le dossier contenant les fichiers d'en-tête et le dossier contenant les fichiers de bibliothèques :

//Chemin vers le dossier racine de Boost
BOOST_ROOT:FILEPATH=/chemin/vers/boost_x_xx_x

//Chemin vers le dossier des fichiers d'en-tête de Boost
BOOST_INCLUDEDIR:FILEPATH=/chemin/vers/boost_x_xx_x

//Chemin vers le dossier des fichiers bibliothèques de Boost
BOOST_LIBRARYDIR:FILEPATH=/chemin/vers/boost_x_xx_x/stage/lib
Lancez la commande cmake. Celle-ci devrait renvoyer la sortie suivante :
-- Boost version: x.xx.x
-- Found the following Boost libraries:
--   signals
-- Configuring done
-- Generating done
-- Build files have been written to : [adresse_du_dossier_de_votre_projet]

Compilez votre programme et vérifiez qu'il affiche bien Hello, World!.

Pour une description détaillée de l'ensemble des modules livrés avec CMake, rendez-vous sur la documentation officielle.

V - Compilation d'une bibliothèque

Poursuivons notre mise en pratique avec la compilation d'une bibliothèque.

V-A - Présentation de la bibliothèque

Restons dans la simplicité et imaginons une bibliothèque permettant de dire bonjour dans la langue de notre choix.

L'organisation d'un projet de bibliothèque est quelque peu différente de celle d'un exécutable :

Le dossier include contient les en-têtes publics dans un sous-dossier portant le nom de la bibliothèque. Ainsi, les programmes utilisant ladite bibliothèque incluront les en-têtes nécessaires de cette façon :

#include <iostream>
#include <libhello/french.h>

int main(int argc, char** argv)
{
std::cout << "In France, people say hello that way : ";
std::cout << libhello : :say_hello_in_french();
std::cout << std::endl;

return 0;
}

Le dossier src, quant à lui, contient le reste des fichiers sources. Si l'un de ces fichiers doit inclure l'un des en-têtes publics, la directive d'inclusion sera similaire à celle de l'exemple ci-dessus. Par exemple, le contenu du fichier src/french.cpp est le suivant :

#include <iostream>
#include <libhello/french.h>

namespace libhello
{

const std::string say_hello_in_french()
{
return "Bonjour, tout le monde !";
}

} //namespace libhello

L'en-tête correspondant, include/libhello/french.h, est la suivante :

#ifndef LIBHELLO_FRENCH_H
#define LIBHELLO_FRENCH_H

namespace libhello
{

const std::string say_hello_in_french();

} //namespace libhello

#endif

Les fonctions correspondant aux autres langues sont déclarées et définies de la même façon.

V-B - Écriture du fichier CMakeLists.txt

Comme d'habitude, on commence par déclarer le projet :

project(Hello)

On définit ensuite le dossier dans lequel le fichier de bibliothèque sera généré. Puisqu'il s'agit d'une bibliothèque, optons pour lib :

set(LIBRARY_OUTPUT_PATH lib/${CMAKE_BUILD_TYPE})

Les fichiers sources de la bibliothèque incluent les en-têtes publics avec une directive d'inclusion du type #include<>. Le dossier include doit donc être ajouté à la liste des dossiers parcourus par le préprocesseur lors de l'inclusion d'un en-tête externe. On retrouve la commande que l'on a utilisée pour l'inclusion d'une bibliothèque :

include_directories(include)

Comme toujours suit la génération de la liste des fichiers sources. On liste les fichiers du dossier src ainsi que ceux de include :

file(
GLOB_RECURSE
source_files
src/*
include/*
)

Enfin, nous déclarons la bibliothèque elle-même, avec la commande add_library() :

add_library(
hello
SHARED
${source_files}
)

Tout comme pour la commande add_executable(), le premier paramètre est le nom de la bibliothèque et le dernier la liste des fichiers sources. Le second paramètre, optionnel, indique le type de bibliothèque à générer :

- STATIC, pour générer une bibliothèque à lier statiquement ;

- SHARED, pour générer une bibliothèque à lier dynamiquement.

Par défaut, ce paramètre est défini à STATIC.

Voici le contenu du fichier CMakeLists.txt au complet :

cmake_minimum_required(VERSION 2.6)

# Configuration du projet
project(Hello)
set(LIBRARY_OUTPUT_PATH lib/${CMAKE_BUILD_TYPE})

# Inclusion des en-têtes publics
include_directories(include)

# Configuration de la bibliothèque
file(
GLOB_RECURSE
source_files
src/*
include/*
)
add_library(
hello
SHARED
${source_files}
)

Générez le script de compilation et compilez. Votre compilateur devrait avoir généré un fichier de bibliothèque partagée dans le dossier lib (ou dans l'un de ses sous-dossiers).

V-C - Test de la bibliothèque

Il est maintenant temps de tester notre bibliothèque. Pour ce faire, nous allons coder un exécutable effectuant l'ensemble des tests nécessaires. Commençons par mettre en place le projet CMake correspondant à cet exécutable dans un nouveau dossier (que nous nommerons test) de notre hiérarchie :

Le fichier main.cpp se contente de vérifier que les fonctions de la bibliothèque renvoient bien les chaînes de caractères attendues :

#include <cassert>
#include <iostream>
#include <libhello/english.h>
#include <libhello/french.h>

int main(int argc, char** argv)
{
assert(libhello::say_hello_in_english() == "Hello everyone!");
assert(libhello : :say_hello_in_french() == "Bonjour, tout le monde !");

std::cout << "Test OK." << std::endl;

return 0;
}

Le fichier CMakeLists.txt ne comporte aucune nouvelle notion. En fait, il s'agit du même fichier que celui de la , à l'exception que les variables boost_include_dir, boost_lib_dir et boost_signals_lib_name sont directement remplacées par les paramètres correspondant à notre bibliothèque :

cmake_minimum_required(VERSION 2.6)

# Configuration du projet
project(TestHello)
set(EXECUTABLE_OUTPUT_PATH bin/${CMAKE_BUILD_TYPE})

# Inclusion de la bibliothèque Hello
include_directories(../include)
link_directories(../lib/${CMAKE_BUILD_TYPE})

# Configuration de l'exécutable
file(
GLOB_RECURSE
source_files
src/*
)
add_executable(
test_hello
${source_files}
)

# Configuration de l'édition de liens
target_link_libraries(
test_hello
hello
)

Prêtez attention à la commande link_directories(). Si vous spécifiez le type de compilation (CMAKE_BUILD_TYPE) à Debug (par exemple) pour cet exécutable de test, le fichier de la bibliothèque Hello sera recherché dans ../lib/Debug. Veillez donc à ce que la bibliothèque ait été préalablement compilée en Debug avant de tenter une compilation de l'exécutable de test de ce type. Bien évidemment, cette remarque importe quel que soit le type de compilation.

Lancez CMake, compilez et vérifiez que le test se déroule sans échec d'assertion.

VI - Conclusion

Vous voilà fin prêt à utiliser CMake pour la gestion de vos propres projets logiciels. Bien entendu, il y a nombre de commandes que nous n'avons pas passées en revue dans ce cours, mais les exemples étudiés couvrent l'essentiel des cas que vous pourrez rencontrer. Quoi qu'il en soit, la documentation officielle reste le compagnon idéal pour l'initié que vous êtes désormais.

VII - Remerciements

Merci à Alp Mestan, Matthieu Brucher, Come David, Philippe Dunski et surtout 3DArchi pour leurs encouragements, leurs conseils et leur participation à la relecture de ce document. Merci également à ram-0000 pour ses corrections d'ordre grammatical. Si vous souhaitez vous aussi me faire part de vos suggestions, n'hésitez pas à me contacter.