CMake et C++ — des frùres pour toujours

CMake et C++ — des frùres pour toujours

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 :

  1. Compilation ;
  2. Exécution automatique des tests ;
  3. Mesure de la couverture du code ;
  4. Installation ;
  5. Auto-documentation ;
  6. Génération de sandbox en ligne ;
  7. Analyse statique.

Ceux qui s'y connaissent déjà dans C++ et CMake peuvent simplement télécharger le modÚle de projet et commencer à l'utiliser.


Contenu

  1. Le projet de l'intérieur
    1. Structure du projet
    2. Le fichier principal de CMake (. /CMakeLists.txt)
      1. Informations sur le projet
      2. Options du projet
      3. Options de compilation
      4. Objectif principal
      5. Installation
      6. Tests
      7. Documentation
      8. Sandbox en ligne
    3. Script pour les tests (test/CMakeLists.txt)
      1. Test
      2. Couverture
    4. Script pour la documentation (doc/CMakeLists.txt)
    5. Script pour la sandbox en ligne (online/CMakeLists.txt)
  2. Le projet de l'extérieur
    1. Assemblage
      1. Génération
      2. Assemblage
    2. Options
      1. MYLIB_COVERAGE
      2. MYLIB_TESTING
      3. MYLIB_DOXYGEN_LANGUAGE
    3. Objectifs de construction
      1. Par défaut
      2. mylib-unit-tests
      3. check
      4. couverture
      5. doc
      6. wandbox
    4. Exemples
  3. Outils
  4. Analyse statique
  5. Postface

Le projet de l'intérieur

Structure du projet

.
├── 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.cpp

Nous 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 sur la page du projet modĂšle.

Le fichier principal de CMake (. /CMakeLists.txt)

Informations sur le projet

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 la commande project).

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)

Options du projet

Préparons deux options.

La premiĂšre option — MYLIB_TESTING — 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 MYLIB_COVERAGE 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)

Options de compilation

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()

Objectif principal

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 target_link_libraries(target PRIVATE dependency), 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 du script CMake pour les tests unitaires.

Il convient Ă©galement de prĂȘter attention aux expressions gĂ©nĂ©ratrices : $.

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 find_package, 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 la commande target_compile_features).

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. Comme avec Boost, par exemple.

add_library(Mylib::mylib ALIAS mylib)

Installation

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)

Tests

Si les tests sont désactivés explicitement à l'aide de l'option correspondante 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 add_subdirectory, 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()

Documentation

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()

Sandbox en ligne

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()

Script pour les tests (test/CMakeLists.txt)

Test

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)

Couverture

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, couverture, 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()

Script pour la documentation (doc/CMakeLists.txt)

Doxygen trouvé.

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 la commande configure_file).

AprĂšs cela, nous crĂ©ons une cible doc, 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 ()

Script pour la sandbox en ligne (online/CMakeLists.txt)

Nous trouvons maintenant le troisiĂšme Python et crĂ©ons une cible wandbox, qui gĂ©nĂšre une requĂȘte correspondant Ă  l'API du service Wandbox, 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()

Le projet de l'extérieur

Voyons maintenant comment utiliser tout cela.

Assemblage

La construction de ce projet, comme celle de tout autre projet utilisant le systÚme de construction CMake, se compose de deux étapes :

Génération

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 ...]

Pour plus de détails sur les options.

Construction du projet

cmake --build chemin/vers/le/répertoire/de/construction [--target cible]

Pour plus de détails sur les cibles de construction.

Options

MYLIB_COVERAGE

cmake -S ... -B ... -DMYLIB_COVERAGE=ON [autres options ...]

Active la cible couverture, avec lequel il est possible de lancer une mesure de couverture de code par des tests.

MYLIB_TESTING

cmake -S ... -B ... -DMYLIB_TESTING=OFF [autres options ...]

Permet de désactiver la compilation des tests unitaires et de la cible check. Par conséquent, la mesure de couverture de code par des tests est désactivée (voir MYLIB_COVERAGE).

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 add_subdirectory.

MYLIB_DOXYGEN_LANGUAGE

cmake -S ... -B ... -DMYLIB_DOXYGEN_LANGUAGE=English [autres options ...]

Change la langue de la documentation générée par la cible doc à celle spécifiée. La liste des langues disponibles est consultable sur le site de Doxygen.

Par défaut, le russe est activé.

Objectifs de construction

Par défaut

cmake --build chemin/vers/répertoire/de/build
cmake --build chemin/vers/répertoire/de/build --target all

Si 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 check.

mylib-unit-tests

cmake --build chemin/vers/répertoire/de/build --target mylib-unit-tests

Compile les tests unitaires. Activé par défaut.

check

cmake --build chemin/vers/répertoire/de/build --target check

Lance les tests unitaires compilés (les compile s'ils ne l'ont pas encore été). Activé par défaut.

Voir aussi mylib-unit-tests.

couverture

cmake --build chemin/vers/répertoire/de/build --target coverage

Analyse 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 gcovr.

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 MYLIB_COVERAGE.

Voir aussi check.

doc

cmake --build chemin/vers/répertoire/de/build --target doc

Lance la génération de documentation du code avec le systÚme Doxygen.

wandbox

cmake --build chemin/vers/répertoire/de/build --target wandbox

La 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 Wandbox. Je ne sais pas combien leurs serveurs sont flexibles, mais je pense qu'il ne faut pas abuser de cette possibilité.

Exemples

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 16

Installation 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 install

Compilation 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 4

Gé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

Outils

  1. CMake 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.

  2. BibliothĂšque de test doctest

    Les tests peuvent ĂȘtre dĂ©sactivĂ©s (voir l'option MYLIB_TESTING).

  3. Doxygen

    Pour changer la langue dans laquelle la documentation sera générée, une option est prévue MYLIB_DOXYGEN_LANGUAGE.

  4. Interpréteur PNL Python 3

    Pour une génération automatique bac à sable en ligne.

Analyse statique

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 Cppcheck.

Pour cela, il faut utiliser l'option CMAKE_CXX_CPPCHECK:

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 scan-build 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/build

Ici, contrairement au cas de Cppcheck, il est nécessaire de lancer la compilation par scan-build.

Postface

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Ă©.

→ TĂ©lĂ©charger le modĂšle de projet

Source : habr.com

Acheter un hĂ©bergement fiable pour les sites avec protection DDoS, serveurs VPS VDS đŸ”„ Acheter un hĂ©bergement fiable pour les sites avec protection DDoS, serveurs VPS VDS | ProHoster