
Dans le processus de développement, j'aime changer de compilateurs, de modes de compilation, de versions de dépendances, effectuer une analyse statique, mesurer les performances, collecter la couverture, générer de la documentation, etc. Et j'adore CMake, car il me permet de faire tout ce que je veux.
Beaucoup critiquent CMake, souvent Ă juste titre, mais si l'on y regarde de plus prĂšs, ce n'est pas si mal, et ces derniers temps c'est mĂȘme plutĂŽt bon, et la direction du dĂ©veloppement est tout Ă fait positive.
Dans cet article, je souhaite expliquer comment organiser assez facilement une bibliothĂšque d'en-tĂȘtes en C++ dans un systĂšme CMake pour obtenir les fonctionnalitĂ©s suivantes :
- Compilation ;
- Exécution automatique des tests ;
- Mesure de la couverture du code ;
- Installation ;
- Auto-documentation ;
- Génération de sandbox en ligne ;
- Analyse statique.
Ceux qui s'y connaissent déjà dans C++ et CMake peuvent simplement et commencer à l'utiliser.
Contenu
.
âââ CMakeLists.txt
âââ README.en.md
âââ README.md
âââ doc
â âââ CMakeLists.txt
â âââ Doxyfile.in
âââ include
â âââ mylib
â âââ myfeature.hpp
âââ online
â âââ CMakeLists.txt
â âââ mylib-example.cpp
â âââ wandbox.py
âââ test
âââ CMakeLists.txt
âââ mylib
â âââ myfeature.cpp
âââ test_main.cppNous allons principalement parler de la façon d'organiser les scripts CMake, ils seront donc examinĂ©s en dĂ©tail. Les autres fichiers peuvent ĂȘtre consultĂ©s directement par quiconque le souhaite .
Tout d'abord, il est nécessaire de demander la version requise de CMake. CMake évolue, les signatures des commandes et le comportement dans différentes conditions changent. Pour que CMake comprenne immédiatement ce que nous attendons de lui, il faut immédiatement fixer nos exigences.
cmake_minimum_required(VERSION 3.13)Ensuite, indiquons notre projet, son nom, sa version, les langages utilisés, etc. (voir ).
Dans ce cas, nous spécifions le langage CXX (ce qui signifie C++), afin que CMake ne recherche pas le compilateur du langage C (par défaut, CMake inclut deux langages : C et C++).
project(Mylib VERSION 1.0 LANGUAGES CXX)Ici, vous pouvez également vérifier immédiatement si notre projet est inclus dans un autre projet en tant que sous-projet. Cela sera d'une grande aide par la suite.
get_directory_property(IS_SUBPROJECT PARENT_DIRECTORY)
Préparons deux options.
La premiĂšre option â â pour dĂ©sactiver les tests modulaires. Cela peut ĂȘtre nĂ©cessaire si nous sommes sĂ»rs que les tests sont corrects et que nous souhaitons, par exemple, simplement installer ou empaqueter notre projet. Ou notre projet est inclus en tant que sous-projet â dans ce cas, l'utilisateur de notre projet n'est pas intĂ©ressĂ© Ă exĂ©cuter nos tests. AprĂšs tout, vous ne testez pas les dĂ©pendances que vous utilisez ?
option(MYLIB_TESTING "Activer les tests modulaires" ON)De plus, nous créerons une option distincte pour mesurer la couverture de code par les tests, mais cela nécessitera des outils supplémentaires, donc il faudra l'activer explicitement.
option(MYLIB_COVERAGE "Activer la mesure de la couverture de code par les tests" OFF)
Bien sûr, nous sommes de supers programmeurs C++, donc nous voulons le plus haut niveau de diagnostic du compilateur au moment de la compilation. Aucun détail ne devrait échapper.
add_compile_options(
-Werror
-Wall
-Wextra
-Wpedantic
-Wcast-align
-Wcast-qual
-Wconversion
-Wctor-dtor-privacy
-Wenum-compare
-Wfloat-equal
-Wnon-virtual-dtor
-Wold-style-cast
-Woverloaded-virtual
-Wredundant-decls
-Wsign-conversion
-Wsign-promo
)Nous allons également désactiver les extensions pour respecter complÚtement la norme du langage C++. Par défaut, elles sont activées dans CMake.
if(NOT CMAKE_CXX_EXTENSIONS)
set(CMAKE_CXX_EXTENSIONS OFF)
endif()
Notre bibliothĂšque se compose uniquement de fichiers d'en-tĂȘte, ce qui signifie que nous n'avons aucun output sous forme de bibliothĂšques statiques ou dynamiques. D'autre part, pour utiliser notre bibliothĂšque de l'extĂ©rieur, elle doit ĂȘtre installĂ©e, afin qu'elle puisse ĂȘtre dĂ©tectĂ©e dans le systĂšme et liĂ©e Ă votre projet, tout en incluant les en-tĂȘtes ainsi que, peut-ĂȘtre, d'autres propriĂ©tĂ©s.
Pour cela, nous créons une bibliothÚque d'interface.
add_library(mylib INTERFACE)Nous associons les en-tĂȘtes Ă notre bibliothĂšque d'interface.
L'utilisation moderne et tendance de CMake prĂ©voit que les en-tĂȘtes, les propriĂ©tĂ©s, etc., sont transmis via un seul et unique objectif. Ainsi, il suffit de dire , et tous les en-tĂȘtes associĂ©s Ă l'objectif dependency, seront accessibles pour les sources appartenant Ă l'objectif cible. Et il n'est pas nĂ©cessaire d'utiliser des [target_]include_directories. Cela sera dĂ©montrĂ© ci-dessous lors de l'analyse .
Il convient Ă©galement de prĂȘter attention aux .
Cette commande associe les en-tĂȘtes nĂ©cessaires Ă notre bibliothĂšque d'interface, et si notre bibliothĂšque est liĂ©e Ă une certaine cible dans la mĂȘme hiĂ©rarchie CMake, alors elle sera associĂ©e aux en-tĂȘtes du rĂ©pertoire ${CMAKE_CURRENT_SOURCE_DIR}/include, et si notre bibliothĂšque est installĂ©e dans le systĂšme et liĂ©e Ă un autre projet avec la commande , alors elle sera associĂ©e aux en-tĂȘtes du rĂ©pertoire include par rapport au rĂ©pertoire d'installation.
target_include_directories(mylib INTERFACE
$
$
)Nous allons définir la norme du langage. Bien sûr, la plus récente. De plus, nous n'incluons pas seulement la norme, mais nous l'appliquons à ceux qui utiliseront notre bibliothÚque. Cela est réalisé grùce à l'attribut installé qui a la catégorie INTERFACE (voir ).
target_compile_features(mylib INTERFACE cxx_std_17)Créons un alias pour notre bibliothÚque. De maniÚre esthétique, il sera dans un « espace de noms » spécial. Cela sera utile lorsque notre bibliothÚque aura différents modules, et que nous souhaitons les connecter indépendamment les uns des autres. .
add_library(Mylib::mylib ALIAS mylib)
Installation de nos en-tĂȘtes dans le systĂšme. C'est assez simple. Nous disons que le dossier contenant tous les en-tĂȘtes doit se retrouver dans le rĂ©pertoire include par rapport Ă l'endroit d'installation.
install(DIRECTORY include/mylib DESTINATION include)Ensuite, nous informons le systĂšme de build que nous souhaitons pouvoir appeler la commande find_package(Mylib) et obtenir la cible Mylib::mylib.
install(TARGETS mylib EXPORT MylibConfig)
install(EXPORT MylibConfig NAMESPACE Mylib:: DESTINATION share/Mylib/cmake)Le sort suivant doit ĂȘtre compris ainsi. Lorsque dans un projet tiers nous appelons la commande find_package(Mylib 1.2.3 REQUIRED), et si la version rĂ©elle de la bibliothĂšque installĂ©e est incompatible avec la version 1.2.3, CMake gĂ©nĂ©rera automatiquement une erreur. Ainsi, il ne sera pas nĂ©cessaire de suivre manuellement les versions.
include(CMakePackageConfigHelpers)
write_basic_package_version_file("${PROJECT_BINARY_DIR}/MylibConfigVersion.cmake"
VERSION
${PROJECT_VERSION}
COMPATIBILITY
AnyNewerVersion
)
install(FILES "${PROJECT_BINARY_DIR}/MylibConfigVersion.cmake" DESTINATION share/Mylib/cmake)
Si les tests sont désactivés explicitement à l'aide de ou notre projet est un sous-projet, c'est-à -dire qu'il est intégré dans un autre projet CMake à l'aide de la commande , nous ne descendons pas plus bas dans la hiérarchie, et le script décrivant les commandes pour générer et exécuter les tests ne s'exécute tout simplement pas.
if(NOT MYLIB_TESTING)
message(STATUS "Les tests du projet Mylib sont désactivés")
elseif(IS_SUBPROJECT)
message(STATUS "Mylib n'est pas testé en mode sous-module")
else()
add_subdirectory(test)
endif()
La documentation ne sera pas générée non plus dans le cas d'un sous-projet.
if(NOT IS_SUBPROJECT)
add_subdirectory(doc)
endif()
De mĂȘme, il n'y aura pas de bac Ă sable en ligne pour le sous-projet.
if(NOT IS_SUBPROJECT)
add_subdirectory(online)
endif()
Tout d'abord, nous trouvons le paquet avec le framework de test requis (remplacez par votre préféré).
find_package(doctest 2.3.3 REQUIRED)Nous créons notre exécutable avec les tests. Habituellement, je n'ajoute que le fichier dans lequel la fonction sera définie directement dans l'exécutable binaire. main.
add_executable(mylib-unit-tests test_main.cpp)Les fichiers contenant les tests eux-mĂȘmes sont ajoutĂ©s plus tard. Mais cela n'est pas obligatoire.
target_sources(mylib-unit-tests PRIVATE mylib/myfeature.cpp)Nous connectons les dĂ©pendances. Notez que nous avons liĂ© Ă notre binaire uniquement les cibles CMake nĂ©cessaires, et nous n'avons pas appelĂ© la commande target_include_directories. Les en-tĂȘtes du framework de test et de notre Mylib::mylib, ainsi que les options de compilation (dans notre cas il s'agit de la norme du langage C++) ont Ă©tĂ© incluses avec ces cibles.
target_link_libraries(mylib-unit-tests
PRIVATE
Mylib::mylib
doctest::doctest
)Enfin, nous créons une cible fictive, dont la "construction" équivaut à l'exécution des tests, et nous ajoutons cette cible à la construction par défaut (ce qui est géré par l'attribut , ou). Cela signifie que la construction par défaut initie l'exécution des tests, donc nous n'oublierons jamais de les exécuter.
add_custom_target(check ALL COMMAND mylib-unit-tests)
Ensuite, nous activons la mesure de la couverture de code, si l'option correspondante est définie. Je ne vais pas entrer dans les détails, car ils concernent davantage l'outil de mesure de couverture que CMake. Il est simplement important de noter qu'une cible sera créée à partir des résultats, , avec laquelle il est facile de lancer la mesure de couverture.
find_program(GCOVR_EXECUTABLE gcovr)
if(MYLIB_COVERAGE AND GCOVR_EXECUTABLE)
message(STATUS "La mesure de la couverture du code par les tests est activée")
target_compile_options(mylib-unit-tests PRIVATE --coverage)
target_link_libraries(mylib-unit-tests PRIVATE gcov)
add_custom_target(coverage
COMMAND
${GCOVR_EXECUTABLE}
--root=${PROJECT_SOURCE_DIR}/include/
--object-directory=${CMAKE_CURRENT_BINARY_DIR}
DEPENDS
check
)
elseif(MYLIB_COVERAGE AND NOT GCOVR_EXECUTABLE)
set(MYLIB_COVERAGE OFF)
message(WARNING "Le programme gcovr est requis pour mesurer la couverture du code par les tests")
endif()
.
find_package(Doxygen)Ensuite, nous vérifions si l'utilisateur a défini une variable pour la langue. Si oui, nous ne la modifions pas, sinon nous prenons le russe. Ensuite, nous configurons les fichiers du systÚme Doxygen. Toutes les variables nécessaires, y compris la langue, sont incluses lors de la configuration (voir ).
AprĂšs cela, nous crĂ©ons une cible , qui exĂ©cutera la gĂ©nĂ©ration de documentation. Ătant donnĂ© que la gĂ©nĂ©ration de documentation n'est pas la plus grande nĂ©cessitĂ© dans le processus de dĂ©veloppement, la cible ne sera pas activĂ©e par dĂ©faut et devra ĂȘtre lancĂ©e explicitement.
if (Doxygen_FOUND)
if (NOT MYLIB_DOXYGEN_LANGUAGE)
set(MYLIB_DOXYGEN_LANGUAGE Russian)
endif()
message(STATUS "La documentation Doxygen sera générée en ${MYLIB_DOXYGEN_LANGUAGE}")
configure_file(Doxyfile.in Doxyfile)
add_custom_target(doc COMMAND ${DOXYGEN_EXECUTABLE} ${CMAKE_CURRENT_BINARY_DIR}/Doxyfile)
endif ()
Nous trouvons maintenant le troisiĂšme Python et crĂ©ons une cible , qui gĂ©nĂšre une requĂȘte correspondant Ă l'API du service , et l'envoie. Une fois le traitement terminĂ©, un lien vers le bac Ă sable prĂȘt est renvoyĂ©.
find_program(PYTHON3_EXECUTABLE python3)
if(PYTHON3_EXECUTABLE)
set(WANDBOX_URL "https://wandbox.org/api/compile.json")
add_custom_target(wandbox
COMMAND
${PYTHON3_EXECUTABLE} wandbox.py mylib-example.cpp "${PROJECT_SOURCE_DIR}" include |
curl -H "Content-type: application/json" -d @- ${WANDBOX_URL}
WORKING_DIRECTORY
${CMAKE_CURRENT_SOURCE_DIR}
DEPENDS
mylib-unit-tests
)
else()
message(WARNING "Un interpréteur Python de la version 3 est requis pour créer un bac à sable en ligne")
endif()
Voyons maintenant comment utiliser tout cela.
La construction de ce projet, comme celle de tout autre projet utilisant le systÚme de construction CMake, se compose de deux étapes :
cmake -S chemin/vers/les/sources -B chemin/vers/le/répertoire/de/construction [options ...]Si la commande ci-dessus ne fonctionne pas à cause d'une version obsolÚte de CMake, essayez de supprimer
-S:cmake chemin/vers/les/sources -B chemin/vers/le/répertoire/de/construction [options ...]
.
cmake --build chemin/vers/le/répertoire/de/construction [--target cible].
cmake -S ... -B ... -DMYLIB_COVERAGE=ON [autres options ...]Active la cible , avec lequel il est possible de lancer une mesure de couverture de code par des tests.
cmake -S ... -B ... -DMYLIB_TESTING=OFF [autres options ...]Permet de désactiver la compilation des tests unitaires et de la cible . Par conséquent, la mesure de couverture de code par des tests est désactivée (voir ).
Le test est également automatiquement désactivé si le projet est intégré dans un autre projet en tant que sous-projet avec la commande .
cmake -S ... -B ... -DMYLIB_DOXYGEN_LANGUAGE=English [autres options ...]Change la langue de la documentation générée par la cible à celle spécifiée. La liste des langues disponibles est consultable sur .
Par défaut, le russe est activé.
cmake --build chemin/vers/répertoire/de/build
cmake --build chemin/vers/rĂ©pertoire/de/build --target allSi la cible n'est pas spĂ©cifiĂ©e (ce qui est Ă©quivalent Ă la cible tout), compile tout ce qui peut l'ĂȘtre et appelle Ă©galement la cible .
cmake --build chemin/vers/répertoire/de/build --target mylib-unit-testsCompile les tests unitaires. Activé par défaut.
cmake --build chemin/vers/répertoire/de/build --target checkLance les tests unitaires compilés (les compile s'ils ne l'ont pas encore été). Activé par défaut.
Voir aussi .
cmake --build chemin/vers/répertoire/de/build --target coverageAnalyse les tests unitaires exécutés (les exécute s'ils ne l'ont pas encore été) pour la couverture de code par l'outil .
La sortie de couverture apparaĂźtra environ comme suit :
------------------------------------------------------------------------------
Rapport de couverture de code GCC
Répertoire : /chemin/vers/cmakecpptemplate/include/
------------------------------------------------------------------------------
Fichier Lignes Exécution Couverture Manquantes
------------------------------------------------------------------------------
mylib/myfeature.hpp 2 2 100%
------------------------------------------------------------------------------
TOTAL 2 2 100%
------------------------------------------------------------------------------La cible n'est disponible que si l'option est activée .
Voir aussi .
cmake --build chemin/vers/répertoire/de/build --target docLance la génération de documentation du code avec le systÚme .
cmake --build chemin/vers/répertoire/de/build --target wandboxLa réponse du service apparaßtra environ comme suit :
{
"permlink" : "QElvxuMzHgL9fqci",
"status" : "0",
"url" : "https://wandbox.org/permlink/QElvxuMzHgL9fqci"
}Pour cela, on utilise le service . Je ne sais pas combien leurs serveurs sont flexibles, mais je pense qu'il ne faut pas abuser de cette possibilité.
Compilation du projet en mode debug avec mesure de couverture
cmake -S chemin/vers/sources -B chemin/vers/répertoire/de/build -DCMAKE_BUILD_TYPE=Debug -DMYLIB_COVERAGE=ON
cmake --build chemin/vers/répertoire/de/build --target coverage --parallel 16Installation du projet sans compilation ni test préalables
cmake -S chemin/vers/source -B chemin/vers/répertoire/de/build -DMYLIB_TESTING=OFF -DCMAKE_INSTALL_PREFIX=chemin/vers/répertoire/d'installation
cmake --build chemin/vers/répertoire/de/build --target installCompilation en mode release avec le compilateur spécifié
cmake -S chemin/vers/source -B chemin/vers/répertoire/de/build -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_COMPILER=g++-8 -DCMAKE_PREFIX_PATH=chemin/vers/répertoire/ou/dépendances/sont/installées
cmake --build chemin/vers/répertoire/de/build --parallel 4Génération de la documentation en anglais
cmake -S chemin/vers/source -B chemin/vers/répertoire/de/build -DCMAKE_BUILD_TYPE=Release -DMYLIB_DOXYGEN_LANGUAGE=English
cmake --build chemin/vers/répertoire/de/build --target doc
3.13
En réalité, la version CMake 3.13 est nécessaire uniquement pour exécuter certaines commandes en console décrites dans cette documentation. Du point de vue de la syntaxe des scripts CMake, la version 3.8 est suffisante si la génération est appelée par d'autres moyens.
BibliothĂšque de test
Les tests peuvent ĂȘtre dĂ©sactivĂ©s (voir ).
Pour changer la langue dans laquelle la documentation sera générée, une option est prévue .
Interpréteur PNL
Pour une génération automatique .
Avec CMake et quelques bons outils, il est possible de garantir une analyse statique avec un minimum d'efforts.
Cppcheck
CMake intĂšgre le support d'un outil pour analyse statique .
Pour cela, il faut utiliser l'option :
cmake -S chemin/vers/source -B chemin/vers/répertoire/de/build -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_CPPCHECK="cppcheck;--enable=all;-Ichemin/vers/source/include"AprÚs cela, l'analyse statique sera automatiquement lancée chaque fois que le code sera compilé et recompilé. Il n'est pas nécessaire de faire des démarches supplémentaires.
Clang
Avec ce merveilleux outil il est également possible d'exécuter une analyse statique en deux temps :
scan-build cmake -S chemin/vers/source -B chemin/vers/répertoire/de/build -DCMAKE_BUILD_TYPE=Debug
scan-build cmake --build chemin/vers/répertoire/de/buildIci, contrairement au cas de Cppcheck, il est nécessaire de lancer la compilation par scan-build.
CMake est un systĂšme trĂšs puissant et flexible qui permet de rĂ©aliser des fonctionnalitĂ©s selon tous les goĂ»ts et couleurs. Et bien que la syntaxe laisse parfois Ă dĂ©sirer, il nâest pas si terrible que cela. Utilisez le systĂšme de construction CMake pour le bien de la sociĂ©tĂ© et pour votre santĂ©.
â
Source : habr.com
