Quelques exemples d’utilisation de CMake

From Deimos.fr / Bloc Notes Informatique
Jump to: navigation, search

1 Introduction

CMake est un « moteur de production » multiplate-forme. Il est comparable au programme Make dans le sens où le processus de construction logicielle est entièrement contrôlé par des fichiers de configuration, appelés CMakeLists.txt dans le cas de CMake. Mais CMake ne produit pas directement le logiciel final, il s'occupe de la génération de fichiers de construction standards : makefiles sous Unix, et fichiers de projet Visual Studio sous Windows. Cela permet aux développeurs d'utiliser leur environnement de développement préféré comme à leur habitude. C'est cette utilisation des outils habituels de développement qui distingue CMake des autres systèmes de production comme SCons ou les Autotools.

Le nom "CMake" est l'abréviation de "cross platform make". Malgré l'utilisation de "make" dans son nom, CMake est une application séparée et de plus haut niveau que l'outil make.

2 Préambule

En guise de résumé de l’article précédent, je vous propose de bâtir un petit projet de base qui servira de support à la suite de l’article. J’utilise ici la version 2.4 de CMake.

Considérons un projet ultra classique et pas original du tout que nous appellerons Hello. Tiens?! Ça sent le "?Hello World?!?". J’aurais pu faire un "?Goodbye Everybody?!?", mais j’ai eu peur que vous ne partiez avant la fin?;-). Redevenons sérieux?: tous les fichiers concernant ce projet seront stockés dans le répertoire hello. Ce répertoire contient les fichiers AUTHORS, COPYING, INSTALL, README, ChangeLog et le sous-répertoire src qui, lui, contient les fichiers source.

Command
~ $ ls -R hello
hello:
AUTHORS     CMakeLists.txt   src   ChangeLog   README
COPYING     INSTALL
hello/src:
CMakeLists.txt main.cpp

Nous débutons avec un code simplissime ne servant que de support?:

// main.cpp
 
#include <iostream>
#include <cstdlib>
 
using namespace std;
 
int main (void)
{
    cout << "Hello World !" << endl;
 
    exit (EXIT_SUCCESS);
}

L’intégration de CMake est réalisée par l’ajout des 2?fichiers CMakeLists.txt?: un dans le répertoire principal et un dans le répertoire src.

Le fichier hello/CMakeLists.txt est le premier à être lu par CMake lors de son exécution. C’est dans ce fichier que nous faisons les tests nécessaires pour s’assurer de la disponibilité des dépendances du projet. Nous y placerons également le code lisant et utilisant les options de compilation et la génération d’un paquet pour notre projet.

# top-level CMakeLists.txt
 
cmake_minimum_required (VERSION 2.4.0 FATAL_ERROR)
 
project (Hello)
 
include (CheckIncludeFileCXX)
 
set (Hello_RQ_HEADERS iostream cstdlib)
foreach (RQ_HDR ${Hello_RQ_HEADERS})
    check_include_file_cxx (${RQ_HDR} RQ_HDR_RET)
    if (NOT RQ_HDR_RET)
        message (FATAL_ERROR "missing ${RQ_HDR} !")
    endif (NOT RQ_HDR_RET)
endforeach (RQ_HDR ${Hello_RQ_HEADERS})
 
add_subdirectory (src)

Pour le moment, nous avons juste une boucle qui teste la présence sur le système des fichiers d’en-tête indispensables à la compilation du projet. S’ils ne sont pas présents, nous interrompons le processus. Ensuite, nous appelons le fichier CMakeLists.txt du sous-répertoire src.

Le fichier hello/src/CMakeLists.txt contient la définition des cibles de compilation du projet et leur configuration.

# src CMakeLists.txt
 
include_directories (${Hello_SOURCE_DIR}/src)
 
add_executable (hello main.cpp)
 
install (TARGETS hello DESTINATION bin)

Créons un répertoire de construction pour notre projet et un répertoire pour son installation (ceci nous permettra de vérifier les fichiers installés)?:

Command mkdir
mkdir hello_build
mkdir hello_install

Construisons et installons notre projet?:

Command
cd hello_build
cmake -DCMAKE_INSTALL_PREFIX=../hello_install ../hello
make
make install
ls -R ../hello_install
../hello_install:
bin
../hello_install/bin:
hello
Hello World !

Le projet est compilé et installé dans le répertoire voulu, l’arborescence nécessaire est créée. Vous pouvez l’exécuter.

Au cours de l’article, nous allons amender les divers fichiers du projet et en ajouter d’autres. Les numéros de ligne précisés dans les extraits de code de la suite indiquent à chaque fois où insérer le nouveau code dans le fichier concerné. Vous trouverez le paquet Hello-0.0.0.tar.bz2 contenant l’intégralité de cet exemple dans son état final sur le site http://www.arvernux.fr dans la section Téléchargements.

3 Gérer un fichier de configuration

Pour gérer la configuration du projet, par exemple pour rajouter des capacités optionnelles, nous allons passer par un fichier d’en-tête config.h généré par CMake à partir d’un fichier d’entrée config.h.cmake. Il faut donc l’inclure dans les sources du projet?:

// main.cpp
#include “config.h”
...

Et rajouter l’emplacement du config.h dans les directives de compilation. Ce fichier sera enregistré dans le répertoire de compilation du projet.

src CMakeLists.txt
 
include_directories (${Hello_SOURCE_DIR}/src ${Hello_BINARY_DIR}/src)
 
add_executable (hello main.cpp)
 
install (TARGETS hello DESTINATION bin)

Cette technique peut servir à traiter deux types d’options?: celles du genre enable/disable et celles du genre with_truc=bla. Nous verrons le premier genre dans la partie concernant l’internationalisation. Ici, nous nous concentrons sur le deuxième genre... Nous allons nous en servir pour répéter le message qui est affiché.

// main.cpp
...
const int hcount = HELLO_COUNT;
 
int main (void)
{
    for (int i=hcount; i>0; i--)
        cout << “Hello World !<< endl;
...

La valeur de la définition HELLO_COUNT sera importée depuis config.h. Créons le fichier modèle du config.h, c’est-à-dire le fichier config.h.cmake dans le sous-répertoire src.

// config.h.cmake
 
#ifndef _CONFIG_H
#define _CONFIG_H
 
#define HELLO_COUNT @WITH_HELLO_COUNT@
 
#endif /* _CONFIG_H */

Lors de la génération du config.h, CMake remplacera la chaîne @WITH_HELLO_COUNT@ par sa valeur qui, elle, est configurée par le fichier CMakeLists.txt du dossier racine du projet?:

# top-level CMakeLists.txt
 
if (NOT WITH_HELLO_COUNT)
    set (WITH_HELLO_COUNT 1 CACHE STRING "Repeating count")
endif (NOT WITH_HELLO_COUNT)
 
...
 
configure_file (${Hello_SOURCE_DIR}/src/config.h.cmake ${Hello_BINARY_DIR}/src/config.h)
 
...

Le test de la ligne 7 à 9 sert à assurer une valeur initiale si l’utilisateur ne la définit pas. De plus, nous utilisons cette définition de la ligne 8 pour fournir une description de l’option à l’utilisateur. Les options sont consultables ainsi?:

Command
$ cmake -LH ../hello
...
// Repeating count
WITH_HELLO_COUNT:STRING=1
...

Revenons sur les paramètres de la définition de variable de la ligne 8. D’abord, le nom de la variable, puis sa valeur (celle que nous souhaitons qu’elle ait par défaut). Le paramètre CACHE est obligatoire, sinon l’option n’apparaît pas. STRING correspond au type de l’option, cela sert à choisir le bon "?widget?" d’édition pour ccmake (la version ncurses et interactive de CMake). Enfin, la chaîne de documentation.

Et comment l’utilisateur peut-il la définir?? En ajoutant cette option -DWITH_HELLO_COUNT=2 à l’appel de CMake (ou en utilisant ccmake).

Command
 cmake -DCMAKE_INSTALL_PREFIX=../hello_install -DWITH_HELLO_COUNT=2 ../hello
 make
 make install
 ../hello_install/bin/hello
 Hello World !
 Hello World !

4 Gestion centralisée d’un numéro de version

Nous pouvons très bien utiliser la technique précédente pour gérer un numéro de version de manière centralisée. Créons un fichier version.cmake dans le répertoire hello avec le contenu suivant?:

# Hello version number
 
set (Hello_MAJOR 0)
set (Hello_MINOR 0)
set (Hello_PATCH 0)
set (Hello_VERSION ${Hello_MAJOR}.${Hello_MINOR}.${Hello_PATCH})
 
# Hello release date
 
set (Hello_RELEASE "2008-12-03")

Voilà?! Donc# nous ne modifierons le numéro de version que dans ce fichier et nous allons utiliser CMake pour diffuser ce numéro partout où nous le voudrons?; exemple?:

top-level CmakeLists.txt
...
 
include (version.cmake)
message (STATUS "*** Building Hello ${Hello_VERSION} ***")

Ça, c’est pour la cosmétique... Si, si, il en faut?! Mais on peut aussi s’en servir dans le programme en lui-même?:

// config.h.cmake
...
#define HELLO_MAJOR @Hello_MAJOR@
#define HELLO_MINOR @Hello_MINOR@
#define HELLO_PATCH @Hello_PATCH@
#define HELLO_RELEASE @Hello_RELEASE@
...

Ainsi, après configuration, nous aurons dans notre fichier config.h les numéros de version définis et nous pourrons les utiliser dans le code source.

Cette gestion du numéro de version nous servira également plus tard dans la section concernant les paquets et nous pouvons également l’utiliser dans la génération de documentation.

5 Internationalisation

Vous souhaitez internationaliser votre code. Préparez votre fichier main.cpp?:

//main.cpp
...
#ifdef HELLO_NLS_ENABLED
#include <locale>
#include <libintl.h>
#define _(String) dgettext (HELLO_NLS_PACKAGE, String)
#else /* HELLO_NLS_ENABLED */
#define _(String) String
#endif /* HELLO_NLS_ENABLED */
...
{
#ifdef HELLO_NLS_ENABLED
    locale::global (locale (“”));
    bindtextdomain (HELLO_NLS_PACKAGE, HELLO_NLS_LOCALEDIR);
#endif /* HELLO_NLS_ENABLED */
...
       cout << _(“Hello World !) << endl;
...

Les définitions HELLO_NLS_ENBALED, HELLO_NLS_LOCALEDIR et HELLO_NLS_PACKAGE dépendent du contenu du fichier config.h et leur valeur sera configurée par CMake. N’oubliez pas de marquer la chaîne à traduire en ligne 17.

Adaptez votre config.h.cmake en lui rajoutant les lignes suivantes?:

...
#cmakedefine HELLO_NLS_ENABLED
#cmakedefine HELLO_NLS_PACKAGE "@HELLO_NLS_PACKAGE@"
#cmakedefine HELLO_NLS_LOCALEDIR "@HELLO_NLS_LOCALEDIR@"
...

L’implémentation étant prête dans votre programme, il faut générer le fichier po/hello.pot grâce à xgettext.

Command
mkdir po
xgettext -k_ -o po/hello.pot -D src --package-name=hello main.cpp

Je vous renvoie aux pages de manuels et différents how-to pour l’implémentation de l’internationalisation et l’utilisation des outils la réalisant?: ce n’est pas l’objet de notre étude.

Nous créons la traduction pour le français (par exemple)?: po/fr.po.

Comment configurer CMake pour compiler les fichiers MO et les installer??

Dans le fichier hello/CMakeLists.txt, nous commençons par rendre l’activation optionnelle?:

...
option (ENABLE_NLS "Native Language Support" ON)
...

L’option est activée par défaut, mais l’utilisateur peut la désactiver en passant -DENABLE_NLS=OFF à CMake lors de son appel. La chaîne qui décrit l’option sera affichée si l’utilisateur exécute CMake avec les options -LH?:

Command
cmake -LH ../hello
...
// Native Language Support
ENABLE_NLS:BOOL=ON
...

Ensuite, dans un premier temps, nous réalisons la détection des fichiers et des utilitaires nécessaires?:

...
if (ENABLE_NLS)
    set (HELLO_NLS_ENABLED TRUE)
    set (Hello_I18N_HEADERS locale.h libintl.h)
    foreach (HDR ${Hello_I18N_HEADERS})
        check_include_file_cxx (${HDR} HDR_RET)
        if (NOT HDR_RET)
            message (STATUS "missing ${HDR} ! Internationalization aborted.")
            set (HELLO_NLS_ENABLED FALSE)
        endif (NOT HDR_RET)
    endforeach (HDR ${Hello_I18N_HEADERS})
 
    if (HELLO_NLS_ENABLED)
        find_program (MSGFMT_EXECUTABLE msgfmt)
        if (NOT MSGFMT_EXECUTABLE)
            message (STATUS "msgfmt not found! Internationalization aborted.")
            set (HELLO_NLS_ENABLED FALSE)
        endif (NOT MSGFMT_EXECUTABLE)
    endif (HELLO_NLS_ENABLED)
else (ENABLE_NLS)
    set (HELLO_NLS_ENABLED FALSE)
endif (ENABLE_NLS)

Si l’option est activée, nous vérifions que les headers locale.h et libintl.h sont bien présents sur le système. S’ils ne sont pas là, nous affichons un message d’avertissement et nous désactivons le support de l’internationalisation. Là, j’ai fait le choix de construire le projet quand même... Si vous estimez qu’il faut arrêter la construction et exiger les headers, il suffit de changer la directive STATUS de la ligne 33 en FATAL_ERROR. La section suivante recherche l’utilitaire msgfmt qui sert à compiler les fichiers PO en MO. Je fais la même remarque que précédemment concernant la possibilité d’arrêter le processus plutôt que de désactiver l’option en cas d’absence de cet utilitaire.

Note: Dans la version 2.5 de CMak apparaîtra une commande break permettant de sortir d’une boucle foreach. Ceci permettra de simplifier grandement l’implémentation de la boucle précédente.

Dans un deuxième temps, nous configurons les variables nécessaires?:

if (HELLO_NLS_ENABLED)
    set (HELLO_NLS_PACKAGE hello)
    set (HELLO_NLS_LOCALEDIR ${CMAKE_INSTALL_PREFIX}/share/locale)
    add_subdirectory (po)
    message (STATUS "Native language support enabled.")
else (HELLO_NLS_ENABLED)
    message (STATUS "Native language support disabled.")
endif (HELLO_NLS_ENABLED)
...

À la ligne 51, nous incluons le fichier CMakeLists.txt du sous-répertoire po. C’est dans ce dernier que nous allons rajouter la cible permettant de lancer la compilation des messages traduits?:

# po CmakeLists.txt
 
add_custom_target (i18n ALL COMMENT “Building i18n messages.”)
file (GLOB Hello_PO_FILES ${Hello_SOURCE_DIR}/po/*.po)
foreach (Hello_PO_INPUT ${Hello_PO_FILES})
    get_filename_component (Hello_PO_INPUT_BASE ${Hello_PO_INPUT} NAME_WE)
    set (Hello_MO_OUTPUT ${Hello_BINARY_DIR}/po/${Hello_PO_INPUT_BASE}.mo)
    add_custom_command (TARGET i18n COMMAND ${MSGFMT_EXECUTABLE} -o ${Hello_MO_OUTPUT} ${Hello_PO_INPUT})
 
    install (FILES ${Hello_MO_OUTPUT} DESTINATION share/locale/${Hello_PO_INPUT_BASE}/LC_MESSAGES RENAME ${HELLO_NLS_PACKAGE}.mo)
endforeach (Hello_PO_INPUT ${Hello_PO_FILES})

Détaillons un peu le code?:

D’abord, nous créons une nouvelle cible i18n liée à la cible all. Ensuite, nous récupérons la listes de tous les .po à compiler dans la variable Hello_PO_FILES. S’ensuit une boucle qui est répétée pour chacun de ces fichiers (Hello_PO_INPUT). Dans cette boucle, nous commençons par extraire le nom du fichier sans l’extension (Hello_PO_INPUT_BASE), c’est-à-dire la langue pour laquelle ledit fichier fournit la traduction. Puis, nous générons le nom et le chemin complet du fichier .mo où sera compilée la traduction (Hello_MO_OUTPUT). Ensuite, nous ajoutons à la cible i18n la commande permettant cette compilation (appel à msgfmt). Enfin, nous ajoutons la commande qui installera le fichier obtenu dans la bonne arborescence et avec le bon nom lors du make install.

Voyons si nous avons bien travaillé?:

Command
cmake -DCMAKE_INSTALL_PREFIX=../hello_install ../hello
...
-- Native language support enabled.
...
make
...
make install
...
-- Installing /home/patrice/hello_install/share/locale/LC_MESSAGES/fr/hello.mo
...
../hello_install/bin/hello
Bonjour Monde !

"?Oh?! Ça marche?!?"

6 Générer des paquets

Il est beau votre projet?! Très beau?! Vous aimeriez bien le distribuer. Il faut donc générer des paquets... Qu’à cela ne tienne?! CPack la composante "?gestion de paquets?" de CMake est là pour nous servir.

Générons un paquet contenant les binaires et un paquet contenant les sources de notre projet. Pour cela, il suffit de placer le code suivant à la fin du fichier CMakeLists.txt dans le répertoire racine de notre projet.

...
set (CPACK_GENERATOR "TGZ")
set (CPACK_PACKAGE_VERSION_MAJOR ${Hello_MAJOR})
set (CPACK_PACKAGE_VERSION_MINOR ${Hello_MINOR})
set (CPACK_PACKAGE_VERSION_PATCH ${Hello_MAJOR})
set (CPACK_SOURCE_GENERATOR "TBZ2")
set (CPACK_SOURCE_PACKAGE_FILE_NAME Hello-${Hello_VERSION})
set (CPACK_SOURCE_IGNORE_FILES "~$" ".bz2$" ".gz$")
include (CPack)

Les variables CPACK_GENERATOR et CPACK_SOURCE_GENERATOR définissent les formats des paquets générés. Ici, le paquet binaire sera une archive tar compressée par gzip et le paquet source sera une archive tar compressée par bzip2. La définition des numéros de version permet de configurer le nom du paquet. Concernant le paquet source, la variable CPACK_SOURCE_IGNORE_FILES permet de spécifier les fichiers et les répertoires qui ne doivent pas faire partie du paquet. Cette variable accepte les expressions régulières. Ainsi, dans "~$", le signe $ indique la fin du nom du fichier. Tous les fichiers se terminant par ~ seront ignorés.

Générons les paquets?:

Command
cmake -DCMAKE_INSTALL_PREFIX=../hello_install ../hello
...
make package
...
make package_source
...
ls *.tar*
Hello-0.0.0-Linux.tar.gz  Hello-0.0.0.tar.bz2

Note:

CPack, quand il génère le paquet binaire, ne prend en compte que les fichiers installés par une commande install à une condition?: la destination doit être un chemin relatif au contenu de la variable CMAKE_INSTALL_PREFIX. Les fichiers dont le chemin d’installation est absolu seront ignorés. C’est une limitation actuelle de CPack, peut-être disparaîtra-t-elle??

Voilà, nous sommes à la fin de cet article. Nous avons abordé quelques méthodes permettant d’obtenir de la part de CMake ce que nous savions faire avec les autotools (voire un peu plus). Cependant, il nous reste à voir quelques techniques

CPack, quand il génère lerticulières concernant la distribution de bibliothèques. L’occasion pour nous de nous retrouver une prochaine fois.

7 Ressources

http://www.unixgarden.com/index.php/programmation/quelques-exemples-d%E2%80%99utilisation-de-cmake
CMake : la releve dans la construction de projets