Retrouvez cet article dans : Linux Magazine 105
Il y a un an, nous avons découvert CMake au travers d’un article dans le numéro 92 [1]. Après cette initiation et quelques expérimentations, il est maintenant temps d’aborder certains thèmes récurrents du développement et de la distribution de Logiciels libres.
1. 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.
~ $ 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 :
01: // main.cpp
02:
03: #include <iostream>
04: #include <cstdlib>
05:
06: using namespace std;
07:
08: int main (void)
09: {
10: cout << "Hello World !" << endl;
11:
12: exit (EXIT_SUCCESS);
13: }
14:
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.
01: # top-level CMakeLists.txt
02:
03: cmake_minimum_required (VERSION 2.4.0 FATAL_ERROR)
04:
05: project (Hello)
06:
07: include (CheckIncludeFileCXX)
08:
09: set (Hello_RQ_HEADERS iostream cstdlib)
10: foreach (RQ_HDR ${Hello_RQ_HEADERS})
11: check_include_file_cxx (${RQ_HDR} RQ_HDR_RET)
12: if (NOT RQ_HDR_RET)
13: message (FATAL_ERROR "missing ${RQ_HDR} !")
14: endif (NOT RQ_HDR_RET)
15: endforeach (RQ_HDR ${Hello_RQ_HEADERS})
16:
17: add_subdirectory (src)
18:
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.
01: # src CMakeLists.txt
02:
03: include_directories (${Hello_SOURCE_DIR}/src)
04:
05: add_executable (hello main.cpp)
06:
07: install (TARGETS hello DESTINATION bin)
08:
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) :
~ $ mkdir hello_build ~ $ mkdir hello_install
Construisons et installons notre projet :
~ $ cd hello_build ~/hello_build $ cmake -DCMAKE_INSTALL_PREFIX=../hello_install ../hello ... ~/hello_build $ make ... ~/hello_build $ make install ... ~/hello_build $ ls -R ../hello_install ../hello_install: bin ../hello_install/bin: hello ~/hello_build $ ../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.
2. 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 :
01: // main.cpp ... 05: 06: #include “config.h” 07: ...
Et rajouter l’emplacement du config.h dans les directives de compilation. Ce fichier sera enregistré dans le répertoire de compilation du projet.
01: # src CMakeLists.txt
02:
03: include_directories (${Hello_SOURCE_DIR}/src ${Hello_BINARY_DIR}/src)
04:
05: add_executable (hello main.cpp)
06:
07: 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é.
01: // main.cpp
...
10: const int hcount = HELLO_COUNT;
11:
12: int main (void)
13: {
14: for (int i=hcount; i>0; i--)
15: 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.
01: // config.h.cmake 02: 03: #ifndef _CONFIG_H 04: #define _CONFIG_H 05: 06: #define HELLO_COUNT @WITH_HELLO_COUNT@ 07: 08: #endif /* _CONFIG_H */ 09:
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 :
01: # top-level CMakeLists.txt
...
06:
07: if (NOT WITH_HELLO_COUNT)
08: set (WITH_HELLO_COUNT 1 CACHE STRING "Repeating count")
09: endif (NOT WITH_HELLO_COUNT)
10:
...
20:
21: configure_file (${Hello_SOURCE_DIR}/src/config.h.cmake ${Hello_BINARY_DIR}/src/config.h)
22:
...
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 :
~/hello_build $ 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).
~/hello_build $ cmake -DCMAKE_INSTALL_PREFIX=../hello_install -DWITH_HELLO_COUNT=2 ../hello ... ~/hello_build $ make ... ~/hello_build $ make install ... ~/hello_build $ ../hello_install/bin/hello Hello World ! Hello World ! ~/hello_build $
3. 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 :
01: # Hello version number
02:
03: set (Hello_MAJOR 0)
04: set (Hello_MINOR 0)
05: set (Hello_PATCH 0)
06: set (Hello_VERSION ${Hello_MAJOR}.${Hello_MINOR}.${Hello_PATCH})
07:
08: # Hello release date
09:
10: set (Hello_RELEASE "2008-12-03")
11:
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 :
01:
top-level CmakeLists.txt
...
06:
07: include (version.cmake)
08:
09: message (STATUS "*** Building Hello ${Hello_VERSION} ***")
10:
Ç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 :
01: // config.h.cmake ... 06: #define HELLO_MAJOR @Hello_MAJOR@ 07: #define HELLO_MINOR @Hello_MINOR@ 08: #define HELLO_PATCH @Hello_PATCH@ 09: #define HELLO_RELEASE @Hello_RELEASE@ 10: ...
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.
4. Internationalisation
Vous souhaitez internationaliser votre code. Préparez votre fichier main.cpp :
01: //main.cpp ... 08:#ifdef HELLO_NLS_ENABLED09:#include <locale>10:#include <libintl.h>11:#define _(String) dgettext (HELLO_NLS_PACKAGE, String)12:#else /* HELLO_NLS_ENABLED */13:#define _(String) String14:#endif /* HELLO_NLS_ENABLED */... 21: { 22:#ifdef HELLO_NLS_ENABLED23:locale::global (locale (“”));24:bindtextdomain (HELLO_NLS_PACKAGE, HELLO_NLS_LOCALEDIR);25:#endif /* HELLO_NLS_ENABLED */... 27: 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 27.
Adaptez votre config.h.cmake en lui rajoutant les lignes suivantes :
... 12: 13: #cmakedefine HELLO_NLS_ENABLED 14: #cmakedefine HELLO_NLS_PACKAGE "@HELLO_NLS_PACKAGE@" 15: #cmakedefine HELLO_NLS_LOCALEDIR "@HELLO_NLS_LOCALEDIR@" 16: ...
L’implémentation étant prête dans votre programme, il faut générer le fichier po/hello.pot grâce à xgettext.
~/hello $ mkdir po ~/hello $ 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 :
...
14: 15: option (ENABLE_NLS "Native Language Support" ON) 16: ...
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 :
~/hello $ 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 :
...
26:
27: if (ENABLE_NLS)
28: set (HELLO_NLS_ENABLED TRUE)
29: set (Hello_I18N_HEADERS locale.h libintl.h)
30: foreach (HDR ${Hello_I18N_HEADERS})
31: check_include_file_cxx (${HDR} HDR_RET)
32: if (NOT HDR_RET)
33: message (STATUS "missing ${HDR} ! Internationalization aborted.")
34: set (HELLO_NLS_ENABLED FALSE)
35: endif (NOT HDR_RET)
36: endforeach (HDR ${Hello_I18N_HEADERS})
37: if (HELLO_NLS_ENABLED)
38: find_program (MSGFMT_EXECUTABLE msgfmt)
39: if (NOT MSGFMT_EXECUTABLE)
40: message (STATUS "msgfmt not found! Internationalization aborted.")
41: set (HELLO_NLS_ENABLED FALSE)
42: endif (NOT MSGFMT_EXECUTABLE)
43: endif (HELLO_NLS_ENABLED)
44: else (ENABLE_NLS)
45: set (HELLO_NLS_ENABLED FALSE)
46: endif (ENABLE_NLS)
47:
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 :
47:
48: if (HELLO_NLS_ENABLED)
49: set (HELLO_NLS_PACKAGE hello)
50: set (HELLO_NLS_LOCALEDIR ${CMAKE_INSTALL_PREFIX}/share/locale)
51: add_subdirectory (po)
52: message (STATUS "Native language support enabled.")
53: else (HELLO_NLS_ENABLED)
54: message (STATUS "Native language support disabled.")
55: endif (HELLO_NLS_ENABLED)
56:
...
À 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 :
01: # po CmakeLists.txt
02:
03: add_custom_target (i18n ALL COMMENT “Building i18n messages.”)
04: file (GLOB Hello_PO_FILES ${Hello_SOURCE_DIR}/po/*.po)
05: foreach (Hello_PO_INPUT ${Hello_PO_FILES})
06: get_filename_component (Hello_PO_INPUT_BASE ${Hello_PO_INPUT} NAME_WE)
07: set (Hello_MO_OUTPUT ${Hello_BINARY_DIR}/po/${Hello_PO_INPUT_BASE}.mo)
08: add_custom_command (TARGET i18n COMMAND ${MSGFMT_EXECUTABLE} -o ${Hello_MO_OUTPUT} ${Hello_PO_INPUT})
09: install (FILES ${Hello_MO_OUTPUT} DESTINATION share/locale/${Hello_PO_INPUT_BASE}/LC_MESSAGES RENAME ${HELLO_NLS_PACKAGE}.mo)
10: endforeach (Hello_PO_INPUT ${Hello_PO_FILES})
11:
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é :
~/hello_build $ cmake -DCMAKE_INSTALL_PREFIX=../hello_install ../hello
... -- Native language support enbaled. ... ~/hello_build $ make ... ~/hello_build $ make install ... -- Installing /home/patrice/hello_install/share/locale/LC_MESSAGES/fr/hello.mo ... ~/hello_build $ ../hello_install/bin/hello Bonjour Monde ! ~/hello_build $
" Oh ! Ça marche ! "
5. 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.
...
60:
61: set (CPACK_GENERATOR "TGZ")
62: set (CPACK_PACKAGE_VERSION_MAJOR ${Hello_MAJOR})
63: set (CPACK_PACKAGE_VERSION_MINOR ${Hello_MINOR})
64: set (CPACK_PACKAGE_VERSION_PATCH ${Hello_MAJOR})
65: set (CPACK_SOURCE_GENERATOR "TBZ2")
66: set (CPACK_SOURCE_PACKAGE_FILE_NAME Hello-${Hello_VERSION})
67: set (CPACK_SOURCE_IGNORE_FILES "~$" ".bz2$" ".gz$")
68:
69: include (CPack)
70:
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 :
~/hello_build $ cmake -DCMAKE_INSTALL_PREFIX=../hello_install ../hello ... ~/hello_build $ make package ... ~/hello_build $ make package_source ... ~/hello_build $ 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.
Retrouvez cet article dans : Linux Magazine 105


