CMake et C++ sont frères pour toujours

CMake et C++ sont frères pour toujours

Pendant le développement, j'aime changer de compilateur, de mode de construction, de version de dépendance, effectuer une analyse statique, mesurer les performances, collecter la couverture, générer de la documentation, etc. Et j'aime vraiment CMake car il me permet de faire tout ce que je veux.

Beaucoup de gens critiquent CMake, et souvent à juste titre, mais si vous y regardez, tout n'est pas si mauvais, et récemment pas mal du tout, et la direction du développement est assez positive.

Dans cette note, je souhaite vous expliquer comment organiser simplement une bibliothèque d'en-têtes en C++ dans le système CMake pour obtenir les fonctionnalités suivantes :

  1. Assemblée;
  2. Tests d'exécution automatique ;
  3. Mesure de la couverture du code ;
  4. Installation;
  5. Auto-documentation ;
  6. Génération de bac à sable en ligne ;
  7. Analyse statique.

Quiconque comprend déjà les avantages et C-make peut simplement télécharger le modèle de projet et commencez à l'utiliser.


Teneur

  1. Projet de l'intérieur
    1. Structure du projet
    2. Fichier CMake principal (./CMakeLists.txt)
      1. Informations sur le projet
      2. Options du projet
      3. Options de compilation
      4. L'objectif principal
      5. Installation
      6. Tests
      7. Documentation
      8. Bac à sable en ligne
    3. Script de test (test/CMakeLists.txt)
      1. Test
      2. Покрытие
    4. Script pour la documentation (doc/CMakeLists.txt)
    5. Script pour sandbox en ligne (online/CMakeLists.txt)
  2. Projet à l'extérieur
    1. assemblage
      1. Génération
      2. assemblage
    2. Options de
      1. MYLIB_COVERAGE
      2. MYLIB_TESTING
      3. MYLIB_DOXYGEN_LANGUAGE
    3. Cibles d'assemblage
      1. Par défaut
      2. mylib-unit-tests
      3. vérifier
      4. couverture
      5. dock
      6. boîte à baguette
    4. Exemples
  3. Outils
  4. Analyse statique
  5. Postface

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 parlerons principalement de la façon d'organiser les scripts CMake, ils seront donc discutés en détail. Tout le monde peut voir le reste des fichiers directement sur la page du projet modèle.

Fichier CMake principal (./CMakeLists.txt)

Informations sur le projet

Tout d'abord, vous devez demander la version requise du système CMake. CMake évolue, les signatures de commandes et le comportement dans différentes conditions changent. Pour que CMake comprenne immédiatement ce que nous attendons de lui, nous devons immédiatement enregistrer nos exigences à ce sujet.

cmake_minimum_required(VERSION 3.13)

Ensuite nous désignerons notre projet, son nom, sa version, les langues utilisées, etc. (voir. команду project).

Dans ce cas nous indiquons la langue CXX (et cela signifie C++) afin que CMake ne se fatigue pas et ne recherche pas un compilateur de langage C (par défaut, CMake inclut deux langages : C et C++).

project(Mylib VERSION 1.0 LANGUAGES CXX)

Ici, vous pouvez vérifier immédiatement si notre projet est inclus dans un autre projet en tant que sous-projet. Cela aidera beaucoup à l’avenir.

get_directory_property(IS_SUBPROJECT PARENT_DIRECTORY)

Options du projet

Nous proposerons deux options.

La première option est MYLIB_TESTING — pour désactiver les tests unitaires. Cela peut être nécessaire si nous sommes sûrs que tout est en ordre avec les tests, mais que nous souhaitons uniquement, par exemple, installer ou packager notre projet. Soit 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. Vous ne testez pas les dépendances que vous utilisez, n'est-ce pas ?

option(MYLIB_TESTING "Включить модульное тестирование" ON)

De plus, nous ferons une option distincte MYLIB_COVERAGE pour mesurer la couverture du code par les tests, mais cela nécessitera des outils supplémentaires, il devra donc être activé explicitement.

option(MYLIB_COVERAGE "Включить измерение покрытия кода тестами" OFF)

Options de compilation

Bien sûr, nous sommes des programmeurs sympas et nous voulons donc le niveau maximum de diagnostics au moment de la compilation de la part du compilateur. Pas une seule souris ne passera à travers.

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 désactiverons également les extensions afin de respecter pleinement le standard du langage C++. Ils sont activés par défaut dans CMake.

if(NOT CMAKE_CXX_EXTENSIONS)
    set(CMAKE_CXX_EXTENSIONS OFF)
endif()

L'objectif principal

Notre bibliothèque se compose uniquement de fichiers d'en-tête, ce qui signifie que nous n'avons aucun épuisement sous forme de bibliothèques statiques ou dynamiques. Par contre, pour pouvoir utiliser notre bibliothèque en externe, il faut qu'elle soit installée, qu'elle soit détectable dans le système et connectée à votre projet, et en même temps ces mêmes en-têtes, ainsi qu'éventuellement quelques supplémentaires, lui sont attachés des propriétés.

Pour cela, nous créons une bibliothèque d'interfaces.

add_library(mylib INTERFACE)

Nous lions les en-têtes à notre bibliothèque d'interface.

L'utilisation moderne, à la mode et par les jeunes de CMake implique que les en-têtes, les propriétés, etc. transmis via une seule cible. Alors il suffit de dire target_link_libraries(target PRIVATE dependency), et tous les en-têtes associés à la cible dependency, sera disponible pour les sources appartenant à la cible target. Et tu n'en as pas besoin [target_]include_directories. Cela sera démontré ci-dessous dans l’analyse Script CMake pour les tests unitaires.

Il convient également de prêter attention à ce qu'on appelle. выражения-генераторы: $<...>.

Cette commande associe les en-têtes dont nous avons besoin à notre bibliothèque d'interface, et si notre bibliothèque est connectée à une cible dans la même hiérarchie CMake, alors les en-têtes du répertoire lui seront associés. ${CMAKE_CURRENT_SOURCE_DIR}/include, et si notre bibliothèque est installée sur le système et connectée à un autre projet à l'aide de la commande find_package, alors les en-têtes du répertoire lui seront associés include par rapport au répertoire d'installation.

target_include_directories(mylib INTERFACE
    $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
    $<INSTALL_INTERFACE:include>
)

Établissons une norme linguistique. Bien sûr, le tout dernier. Dans le même temps, nous incluons non seulement la norme, mais nous l'étendons également à ceux qui utiliseront notre bibliothèque. Ceci est obtenu grâce au fait que la propriété définie a une catégorie INTERFACE (Voir. commande target_compile_features).

target_compile_features(mylib INTERFACE cxx_std_17)

Créons un alias pour notre bibliothèque. De plus, pour la beauté, ce sera dans un « espace de noms » spécial. Cela sera utile lorsque différents modules apparaîtront dans notre bibliothèque et que nous allons les connecter indépendamment les uns des autres. Comme à Busta par exemple.

add_library(Mylib::mylib ALIAS mylib)

Installation

Installation de nos en-têtes dans le système. Tout est simple ici. On dit que le dossier avec tous les en-têtes doit aller dans le répertoire include par rapport au lieu d'installation.

install(DIRECTORY include/mylib DESTINATION include)

Ensuite, nous informons le système de build que nous souhaitons pouvoir appeler la commande dans des projets tiers find_package(Mylib) et obtenir un objectif Mylib::mylib.

install(TARGETS mylib EXPORT MylibConfig)
install(EXPORT MylibConfig NAMESPACE Mylib:: DESTINATION share/Mylib/cmake)

Le prochain sort doit être compris de cette façon. Lorsque dans un projet tiers nous appelons la commande find_package(Mylib 1.2.3 REQUIRED), et la version réelle de la bibliothèque installée sera incompatible avec la version 1.2.3CMake générera automatiquement une erreur. Autrement dit, vous n'aurez pas besoin de suivre les versions manuellement.

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 explicitement désactivés en utilisant option correspondante ou notre projet est un sous-projet, c'est-à-dire qu'il est connecté à un autre projet CMake à l'aide de la commande add_subdirectory, nous n'avançons pas plus loin dans la hiérarchie, et le script, qui décrit les commandes pour générer et exécuter des tests, ne s'exécute tout simplement pas.

if(NOT MYLIB_TESTING)
    message(STATUS "Тестирование проекта Mylib выключено")
elseif(IS_SUBPROJECT)
    message(STATUS "Mylib не тестируется в режиме подмодуля")
else()
    add_subdirectory(test)
endif()

Documentation

La documentation ne sera pas non plus générée dans le cas d'un sous-projet.

if(NOT IS_SUBPROJECT)
    add_subdirectory(doc)
endif()

Bac à sable en ligne

De même, le sous-projet n’aura pas non plus de bac à sable en ligne.

if(NOT IS_SUBPROJECT)
    add_subdirectory(online)
endif()

Script de test (test/CMakeLists.txt)

Test

Tout d'abord, nous trouvons un package avec le framework de test requis (remplacez par votre préféré).

find_package(doctest 2.3.3 REQUIRED)

Créons notre fichier exécutable avec des tests. Habituellement j'ajoute directement au binaire exécutable uniquement le fichier qui contiendra la fonction main.

add_executable(mylib-unit-tests test_main.cpp)

Et j'ajoute des fichiers dans lesquels les tests eux-mêmes sont décrits plus tard. Mais vous n’êtes pas obligé de faire ça.

target_sources(mylib-unit-tests PRIVATE mylib/myfeature.cpp)

Nous connectons les dépendances. Veuillez noter que nous avons lié uniquement les cibles CMake dont nous avions besoin à notre binaire et n'avons pas appelé la commande target_include_directories. Rubriques du framework de test et du nôtre Mylib::mylib, ainsi que les paramètres de construction (dans notre cas, il s'agit du standard du langage C++) sont venus accompagner ces objectifs.

target_link_libraries(mylib-unit-tests
    PRIVATE
        Mylib::mylib
        doctest::doctest
)

Enfin, nous créons une cible factice, dont la « build » équivaut à l'exécution de tests, et ajoutons cette cible à la build par défaut (l'attribut est responsable de cette ALL). Cela signifie que la version par défaut déclenche l'exécution des tests, ce qui signifie que 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 du code si l'option appropriée est spécifiée. Je n’entrerai pas dans les détails, car ils concernent davantage un outil de mesure de couverture que CMake. Il est seulement important de noter qu'un objectif sera créé sur la base des résultats. coverage, avec lequel il est pratique de commencer à mesurer la couverture.

find_program(GCOVR_EXECUTABLE gcovr)
if(MYLIB_COVERAGE AND GCOVR_EXECUTABLE)
    message(STATUS "Измерение покрытия кода тестами включено")

    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 "Для замеров покрытия кода тестами требуется программа gcovr")
endif()

Script pour la documentation (doc/CMakeLists.txt)

Doxygène trouvé.

find_package(Doxygen)

Ensuite, nous vérifions si l'utilisateur a défini la variable de langue. Si oui, alors nous n'y touchons pas, sinon nous prenons le russe. Ensuite, nous configurons les fichiers système de Doxygen. Toutes les variables nécessaires, y compris la langue, y sont placées lors du processus de configuration (voir. команду configure_file).

Ensuite, nous créons un objectif doc, qui commencera à générer de la documentation. Puisque la génération de documentation n'est pas le besoin le plus important dans le processus de développement, la cible ne sera pas activée par défaut ; elle devra être lancée explicitement.

if (Doxygen_FOUND)
    if (NOT MYLIB_DOXYGEN_LANGUAGE)
        set(MYLIB_DOXYGEN_LANGUAGE Russian)
    endif()
    message(STATUS "Doxygen documentation will be generated in ${MYLIB_DOXYGEN_LANGUAGE}")
    configure_file(Doxyfile.in Doxyfile)
    add_custom_target(doc COMMAND ${DOXYGEN_EXECUTABLE} ${CMAKE_CURRENT_BINARY_DIR}/Doxyfile)
endif ()

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

Ici nous trouvons le troisième Python et créons une cible wandbox, qui génère une requête correspondant à l'API du service Boîte à baguettes, et le renvoie. La réponse est accompagnée d'un lien vers le bac à sable terminé.

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 "Для создания онлайн-песочницы требуется интерпретатор ЯП python 3-й версии")
endif()

Projet à l'extérieur

Voyons maintenant comment utiliser tout cela.

assemblage

La construction de ce projet, comme tout autre projet sur le système de build CMake, se compose de deux étapes :

Génération

cmake -S путь/к/исходникам -B путь/к/сборочной/директории [опции ...]

Si la commande ci-dessus n'a pas fonctionné à cause d'une ancienne version de CMake, essayez d'omettre -S:

cmake путь/к/исходникам -B путь/к/сборочной/директории [опции ...]

En savoir plus sur les options.

Construire le projet

cmake --build путь/к/сборочной/директории [--target target]

En savoir plus sur les objectifs d'assemblage.

Options de

MYLIB_COVERAGE

cmake -S ... -B ... -DMYLIB_COVERAGE=ON [прочие опции ...]

Comprend la cible coverage, avec lequel vous pouvez commencer à mesurer la couverture du code par des tests.

MYLIB_TESTING

cmake -S ... -B ... -DMYLIB_TESTING=OFF [прочие опции ...]

Offre la possibilité de désactiver la construction et la cible des tests unitaires check. En conséquence, la mesure de la couverture du code par les tests est désactivée (voir. MYLIB_COVERAGE).

Les tests sont également automatiquement désactivés si le projet est connecté à un autre projet en tant que sous-projet à l'aide de la commande add_subdirectory.

MYLIB_DOXYGEN_LANGUAGE

cmake -S ... -B ... -DMYLIB_DOXYGEN_LANGUAGE=English [прочие опции ...]

Change la langue de la documentation générée par la cible doc à celui donné. Pour une liste des langues disponibles, voir Site Web du système Doxygen.

Le russe est activé par défaut.

Cibles d'assemblage

Par défaut

cmake --build path/to/build/directory
cmake --build path/to/build/directory --target all

Si la cible n'est pas précisée (ce qui équivaut à la cible all), collecte tout ce qu'il peut et appelle également la cible check.

mylib-unit-tests

cmake --build path/to/build/directory --target mylib-unit-tests

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

vérifier

cmake --build путь/к/сборочной/директории --target check

Exécute les tests unitaires collectés (collectés, si ce n’est déjà fait). Activé par défaut.

См. также mylib-unit-tests.

couverture

cmake --build путь/к/сборочной/директории --target coverage

Analyses en cours d'exécution (exécute, si ce n'est déjà fait) les tests unitaires pour la couverture du code par les tests utilisant le programme gcovr.

L'échappement du revêtement ressemblera à ceci :

------------------------------------------------------------------------------
                           GCC Code Coverage Report
Directory: /path/to/cmakecpptemplate/include/
------------------------------------------------------------------------------
File                                       Lines    Exec  Cover   Missing
------------------------------------------------------------------------------
mylib/myfeature.hpp                            2       2   100%   
------------------------------------------------------------------------------
TOTAL                                          2       2   100%
------------------------------------------------------------------------------

La cible n'est disponible que lorsque l'option est activée MYLIB_COVERAGE.

См. также check.

dock

cmake --build путь/к/сборочной/директории --target doc

Démarre la génération de la documentation du code à l'aide du système doxygen.

boîte à baguette

cmake --build путь/к/сборочной/директории --target wandbox

La réponse du service ressemble à ceci :

{
    "permlink" :    "QElvxuMzHgL9fqci",
    "status" :  "0",
    "url" : "https://wandbox.org/permlink/QElvxuMzHgL9fqci"
}

Le service est utilisé pour cela Boîte à baguettes. Je ne sais pas à quel point leurs serveurs sont flexibles, mais je pense qu’il ne faut pas abuser de cette opportunité.

Exemples

Construire le projet en mode débogage avec mesure de couverture

cmake -S путь/к/исходникам -B путь/к/сборочной/директории -DCMAKE_BUILD_TYPE=Debug -DMYLIB_COVERAGE=ON
cmake --build путь/к/сборочной/директории --target coverage --parallel 16

Installer un projet sans assemblage ni test préalables

cmake -S путь/к/исходникам -B путь/к/сборочной/директории -DMYLIB_TESTING=OFF -DCMAKE_INSTALL_PREFIX=путь/к/установойной/директории
cmake --build путь/к/сборочной/директории --target install

Construire en mode release avec un compilateur donné

cmake -S путь/к/исходникам -B путь/к/сборочной/директории -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_COMPILER=g++-8 -DCMAKE_PREFIX_PATH=путь/к/директории/куда/установлены/зависимости
cmake --build путь/к/сборочной/директории --parallel 4

Générer de la documentation en anglais

cmake -S путь/к/исходникам -B путь/к/сборочной/директории -DCMAKE_BUILD_TYPE=Release -DMYLIB_DOXYGEN_LANGUAGE=English
cmake --build путь/к/сборочной/директории --target doc

Outils

  1. Cfaire 3.13

    En fait, CMake version 3.13 n'est requis que pour exécuter certaines des commandes de console décrites dans cette aide. Du point de vue de la syntaxe des scripts CMake, la version 3.8 est suffisante si la génération est appelée autrement.

  2. Bibliothèque de tests docteur

    Les tests peuvent être désactivés (voir опцию MYLIB_TESTING).

  3. doxygen

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

  4. Interprète linguistique Python 3

    Pour la génération automatique bacs à sable en ligne.

Analyse statique

Avec CMake et quelques bons outils, vous pouvez fournir une analyse statique avec un minimum d'effort.

Cppcheck

CMake prend en charge un outil d'analyse statique Cppcheck.

Pour ce faire, vous devez utiliser l'option CMAKE_CXX_CPPCHECK:

cmake -S путь/к/исходникам -B путь/к/сборочной/директории -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_CPPCHECK="cppcheck;--enable=all;-Iпуть/к/исходникам/include"

Après cela, l'analyse statique sera automatiquement lancée à chaque fois que la source sera compilée et recompilée. Il n'est pas nécessaire de faire quoi que ce soit de plus.

Bruit

Avec l'aide d'un merveilleux outil scan-build Vous pouvez également exécuter une analyse statique en un rien de temps :

scan-build cmake -S путь/к/исходникам -B путь/к/сборочной/директории -DCMAKE_BUILD_TYPE=Debug
scan-build cmake --build путь/к/сборочной/директории

Ici, contrairement au cas avec Cppcheck, vous devez exécuter la build à chaque fois. scan-build.

Postface

CMake est un système très puissant et flexible qui vous permet de mettre en œuvre des fonctionnalités pour tous les goûts et toutes les couleurs. Et, même si la syntaxe laisse parfois beaucoup à désirer, le diable n'est toujours pas aussi terrible qu'on le peint. Utilisez le système de construction CMake au profit de la société et de la santé.

Télécharger le modèle de projet

Source: habr.com

Ajouter un commentaire