CMake en C++ — broers voor altijd

CMake en C++ — broers voor altijd

Tijdens de ontwikkeling houd ik ervan om compilers, build-modi, versies van afhankelijkheden te veranderen, statische analyse uit te voeren, de prestaties te meten, de dekking te verzamelen, documentatie te genereren, enzovoort. En ik ben dol op CMake, omdat het me in staat stelt om alles te doen wat ik wil.

Veel mensen bekritiseren CMake, en vaak ten onrechte, maar als je het goed bekijkt, is het niet zo slecht en in de laatste tijd zelfs heel goed, en de ontwikkelingsrichting is behoorlijk positief.

In deze notitie wil ik uitleggen hoe je vrij eenvoudig een header-bibliotheek in C++ kunt organiseren in een CMake-systeem, zodat je de volgende functionaliteit kunt krijgen:

  1. Build;
  2. Automatisch testen;
  3. Code dekking meten;
  4. Installatie;
  5. Automatische documentatie;
  6. Generatie van een online sandbox;
  7. Statische analyse.

Wie al goed is in C++ en CMake kan gewoon de projecttemplate downloaden en ermee aan de slag gaan.


Inhoud

  1. Het project van binnenuit
    1. Projectstructuur
    2. Hoofd CMake-bestand (./CMakeLists.txt)
      1. Projectinformatie
      2. Projectopties
      3. Compilatieopties
      4. Hoofddoel
      5. Installatie
      6. Tests
      7. Documentation
      8. Online sandbox
    3. Script voor tests (test/CMakeLists.txt)
      1. Testen
      2. Dekking
    4. Script voor documentatie (doc/CMakeLists.txt)
    5. Script voor online sandbox (online/CMakeLists.txt)
  2. Het project van buitenaf
    1. Bouwen
      1. Generatie
      2. Bouwen
    2. Opties
      1. MYLIB_COVERAGE
      2. MYLIB_TESTING
      3. MYLIB_DOXYGEN_LANGUAGE
    3. Bouwdoelen
      1. Standaard
      2. mylib-unit-tests
      3. check
      4. coverage
      5. doc
      6. wandbox
    4. Voorbeelden
  3. Hulpmiddelen
  4. Statistische analyse
  5. Naschrift

Het project van binnenuit

Projectstructuur

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

De focus zal voornamelijk liggen op hoe CMake-scripts te organiseren, daarom zullen ze gedetailleerd worden besproken. De andere bestanden kan iedereen bekijken op de pagina van de projecttemplate.

Hoofd CMake-bestand (./CMakeLists.txt)

Projectinformatie

Ten eerste moet je de vereiste versie van het CMake-systeem declareren. CMake ontwikkelt zich, de commandosignaturen en het gedrag in verschillende omstandigheden veranderen. Om ervoor te zorgen dat CMake weet wat we van hem willen, moeten we onze vereisten direct vastleggen.

cmake_minimum_required(VERSION 3.13)

Vervolgens definiëren we ons project, de naam, versie, gebruikte talen en meer (zie de projectopdracht).

In dit geval geven we de taal aan CXX (en dat betekent C++), zodat CMake zich geen zorgen hoeft te maken en geen compiler voor de C-taal hoeft te zoeken (standaard zijn er in CMake twee talen: C en C++).

project(Mylib VERSION 1.0 LANGUAGES CXX)

Hier kun je onmiddellijk controleren of ons project in een ander project is opgenomen als subproject. Dit zal ons in de toekomst enorm helpen.

get_directory_property(IS_SUBPROJECT PARENT_DIRECTORY)

Projectopties

Laten we twee opties overwegen.

De eerste optie — MYLIB_TESTING — om de moduletests uit te schakelen. Dit kan nodig zijn als we zeker weten dat de tests in orde zijn, maar we alleen ons project willen installeren of inpakken. Of als ons project als subproject is opgenomen — in dat geval is het voor de gebruiker van ons project niet interessant om onze tests uit te voeren. Test je niet de afhankelijkheden die je gebruikt?

option(MYLIB_TESTING "Moduletesten inschakelen" ON)

Bovendien maken we een aparte optie MYLIB_COVERAGE voor het meten van de testcode-dekking, maar dit vereist extra tools, dus het moet expliciet ingeschakeld worden.

option(MYLIB_COVERAGE "Meten van testcode-dekking inschakelen" OFF)

Compilatieopties

Natuurlijk zijn we geweldige C++-programmers, dus willen we de hoogste diagnostische niveaus van de compiler tijdens de compilatietijd. Geen enkele muis zal ontsnappen.

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
)

We schakelen ook de extensies uit om volledig te voldoen aan de C++-taalstandaard. Standaard zijn deze ingeschakeld in CMake.

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

Hoofddoel

Onze bibliotheek bestaat alleen uit headerbestanden, wat betekent dat we geen uitgangen hebben in de vorm van statische of dynamische bibliotheken. Aan de andere kant, om onze bibliotheek extern te gebruiken, moet deze geïnstalleerd worden, zodat deze in het systeem kan worden gedetecteerd en aan je project kan worden gekoppeld, evenals deze headers, en mogelijk enkele aanvullende eigenschappen.

Voor dit doel maken we een interfacebibliotheek.

add_library(mylib INTERFACE)

We koppelen de headers aan onze interfacebibliotheek.

Modern, hip gebruik van CMake houdt in dat headers, eigenschappen, enz. via één enkele doelstelling worden doorgegeven. Het is dus voldoende om te zeggen target_link_libraries(target PRIVATE dependency), en alle headers die zijn geassocieerd met de doelstelling dependency, zullen beschikbaar zijn voor de bronnen die toebehoren aan de doelstelling target. En er zijn geen vereisten voor [target_]include_directoriesDit zal hieronder worden geïllustreerd bij de analyse CMake-script voor modultests.

Het is ook belangrijk om aandacht te besteden aan de zogenaamde expressie-generatoren: $.

Dit commando associeert de benodigde headers met onze interfacebibliotheek, en als onze bibliotheek aan een doel binnen dezelfde CMake-hiërarchie wordt gekoppeld, zullen de headers uit de directory ${CMAKE_CURRENT_SOURCE_DIR}/include, en als onze bibliotheek in het systeem is geïnstalleerd en in een ander project is aangesloten met behulp van het commando find_package, dan zullen de headers uit de directory worden geassocieerd include ten opzichte van de installatiedirectory.

target_include_directories(mylib INTERFACE
    $
    $
)

Stel de taalstandaard in. Uiteraard de meest recente. We schakelen niet alleen de standaard in, maar maken deze ook beschikbaar voor degenen die onze bibliotheek gebruiken. Dit wordt bereikt doordat de ingestelde eigenschap de categorie heeft INTERFACE (zie het commando target_compile_features).

target_compile_features(mylib INTERFACE cxx_std_17)

We maken een alias voor onze bibliotheek. Voor de esthetiek zal dit in een speciaal "namespace" gebeuren. Dit is nuttig wanneer er verschillende modules in onze bibliotheek verschijnen, en we deze onafhankelijk van elkaar willen aansluiten. Zoals in Boost, bijvoorbeeld.

add_library(Mylib::mylib ALIAS mylib)

Installatie

Instellingen van onze headers in het systeem. Dit is eenvoudig. We geven aan dat de map met alle headers in de directory moet komen include ten opzichte van de installatielocatie.

install(DIRECTORY include/mylib DESTINATION include)

Vervolgens informeren we het build-systeem dat we in externe projecten het commando willen kunnen aanroepen find_package(Mylib) en het doel ontvangen Mylib::mylib.

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

De volgende instructie moet als volgt worden begrepen. Wanneer we in een extern project het commando aanroepen find_package(Mylib 1.2.3 REQUIRED), en de echte versie van de geïnstalleerde bibliotheek niet compatibel blijkt te zijn met de versie 1.2.3, zal CMake automatisch een foutmelding genereren. Dit betekent dat we niet handmatig de versies hoeven te controleren.

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

Als de tests expliciet zijn uitgeschakeld door middel van de bijbehorende optie of ons project een subproject is, dat wil zeggen dat het is aangesloten op een ander CMake-project met behulp van de opdracht add_subdirectory, we gaan niet verder in de hiërarchie, en het script waarin de opdrachten voor het genereren en uitvoeren van tests zijn beschreven, wordt gewoonweg niet uitgevoerd.

if(NOT MYLIB_TESTING)
    message(STATUS "Het testen van het Mylib-project is uitgeschakeld")
elseif(IS_SUBPROJECT)
    message(STATUS "Mylib wordt niet getest in submodule-modus")
else()
    add_subdirectory(test)
endif()

Documentation

De documentatie wordt ook niet gegenereerd in het geval van een subproject.

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

Online sandbox

Evenzo zal er geen online sandbox voor het subproject zijn.

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

Script voor tests (test/CMakeLists.txt)

Testen

Als eerste vinden we het pakket met het benodigde testframework (vervang het door jouw favoriete).

find_package(doctest 2.3.3 REQUIRED)

We maken ons uitvoerbare bestand met tests. Gewoonlijk voeg ik alleen het bestand toe waarin de functie zit aan de uitvoerbare binaire bestand. main.

add_executable(mylib-unit-tests test_main.cpp)

De bestanden waarin de tests zelf zijn beschreven, voeg ik later toe. Maar dat is niet per se noodzakelijk.

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

We voegen afhankelijkheden toe. Let op dat we aan onze binaire bestand alleen de benodigde CMake-doelen hebben gekoppeld, en de opdracht niet hebben aangeroepen target_include_directories. De headers van het testframework en van onze Mylib::mylib, evenals de bouwparameters (in ons geval is dit de standaard van de programmeertaal C++) zijn samen met deze doelen doorgekomen.

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

Ten slotte maken we een fictief doel, waarvan de 'bouw' gelijkwaardig is aan het uitvoeren van tests, en voegen dit doel toe aan de standaardbouw (dit wordt geregeld door de atributen ALL). Dit betekent dat de standaardbouw de uitvoering van de tests initieert, zodat we ze nooit vergeten uit te voeren.

add_custom_target(check ALL COMMAND mylib-unit-tests)

Dekking

Vervolgens voegen we codecoverage meting toe, als de overeenkomstige optie is opgegeven. Ik ga niet in detail treden, omdat deze meer betrekking hebben op het hulpmiddel voor de meting van de dekking dan op CMake. Het is alleen belangrijk op te merken dat op basis van de resultaten een doel zal worden gecreëerd coverage, waarmee je de meting van de dekking gemakkelijk kunt uitvoeren.

find_program(GCOVR_EXECUTABLE gcovr)
if(MYLIB_COVERAGE AND GCOVR_EXECUTABLE)
    message(STATUS "Code coverage measuring is enabled")

    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 "The gcovr program is required for code coverage measurements")
endif()

Script voor documentatie (doc/CMakeLists.txt)

Doxygen gevonden.

find_package(Doxygen)

Vervolgens controleren we of de gebruiker een taalvariabele heeft ingesteld. Als dat zo is, laten we het zoals het is; als dat niet zo is, gebruiken we Russisch. Daarna configureren we de Doxygen-systeembestanden. Alle benodigde variabelen, inclusief de taal, worden tijdens de configuratie toegevoegd (zie de configure_file-opdracht).

Daarna creëren we een doel doc, dat de documentgeneratie uitvoert. Aangezien documentgeneratie niet de grootste noodzaak is in het ontwikkelingsproces, zal het doel standaard niet ingeschakeld zijn; men zal het expliciet moeten uitvoeren.

if (Doxygen_FOUND)
    if (NOT MYLIB_DOXYGEN_LANGUAGE)
        set(MYLIB_DOXYGEN_LANGUAGE Russian)
    endif()
    message(STATUS "Doxygen-documentatie zal gegenereerd worden in ${MYLIB_DOXYGEN_LANGUAGE}")
    configure_file(Doxyfile.in Doxyfile)
    add_custom_target(doc COMMAND ${DOXYGEN_EXECUTABLE} ${CMAKE_CURRENT_BINARY_DIR}/Doxyfile)
endif ()

Script voor online sandbox (online/CMakeLists.txt)

Hier vinden we de derde Python en creëren we een doel wandbox, dat een verzoek genereert dat overeenkomt met de API van de service Wandbox, en verstuurt het. In antwoord ontvangen we een link naar de kant-en-klare sandbox.

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 "Een Python 3-interpreter is vereist voor het creëren van een online sandbox")
endif()

Het project van buitenaf

Laten we nu eens kijken hoe we al deze dingen kunnen gebruiken.

Bouwen

De bouw van dit project, net als elk ander project in het CMake-bouwsysteem, bestaat uit twee fasen:

Generatie

cmake -S pad/naar/gegevens -B pad/naar/bouwdirectory [opties ...]

Als de bovenstaande opdracht niet werkte vanwege een oude versie van CMake, probeer dan -S:

cmake pad/naar/gegevens -B pad/naar/bouwdirectory [opties ...]

Meer over de opties.

Project bouwen

cmake --build pad/naar/bouwdirectory [--target doel]

Meer over bouwdoelen.

Opties

MYLIB_COVERAGE

cmake -S ... -B ... -DMYLIB_COVERAGE=ON [andere opties ...]

Schakelt doel in coverage, waarmee de codecoverage testen kan worden uitgevoerd.

MYLIB_TESTING

cmake -S ... -B ... -DMYLIB_TESTING=OFF [andere opties ...]

Geeft de mogelijkheid om de bouw van module tests en de doelstelling uit te schakelen. check. Als gevolg hiervan wordt de codecoverage metingen uitgeschakeld (zie. MYLIB_COVERAGE).

Daarnaast wordt het testen automatisch uitgeschakeld als het project als subproject wordt toegevoegd aan een ander project met behulp van de command. add_subdirectory.

MYLIB_DOXYGEN_LANGUAGE

cmake -S ... -B ... -DMYLIB_DOXYGEN_LANGUAGE=English [andere opties ...]

Schakelt de documentatie-taal in die door de doelstelling wordt gegenereerd. doc naar de opgegeven. De lijst met beschikbare talen vindt u op. de website van het Doxygen-systeem..

Standaard is Russisch ingeschakeld.

Bouwdoelen

Standaard

cmake --build pad/naar/bouwdirectory
cmake --build pad/naar/bouwdirectory --target all

Als er geen doel is opgegeven (wat gelijk staat aan het doel. all), bouwt alles wat kan worden gebouwd en roept tevens het doel aan. check.

mylib-unit-tests

cmake --build pad/naar/bouwdirectory --target mylib-unit-tests

Compliceert module tests. Standaard ingeschakeld.

check

cmake --build pad/naar/bouwdirectory --target check

Voert de samengevoegde (bouwt, als deze nog niet is) module tests uit. Standaard ingeschakeld.

Zie ook mylib-unit-tests.

coverage

cmake --build pad/naar/bouwdirectory --target coverage

Analyseert de uitgevoerde (voert uit, als deze nog niet is) module tests voor codecoverage met behulp van de toepassing. gcovr.

De output van de coverage zal er ongeveer zo uitzien:

------------------------------------------------------------------------------
                           GCC Code Coverage Rapport
Directory: /pad/naar/cmakecpptemplate/include/
------------------------------------------------------------------------------
Bestand                                 Regels    Exec  Cover   Missend
------------------------------------------------------------------------------
mylib/myfeature.hpp                          2       2   100%   
------------------------------------------------------------------------------
TOTAAL                                      2       2   100%
------------------------------------------------------------------------------

De doelstelling is alleen beschikbaar wanneer de optie is ingeschakeld. MYLIB_COVERAGE.

Zie ook check.

doc

cmake --build pad/naar/bouwdirectory --target doc

Start de documentatiegeneratie van de code met behulp van het systeem. Doxygen.

wandbox

cmake --build pad/naar/bouwdirectory --target wandbox

De respons van de service zal er ongeveer zo uitzien:

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

Dit wordt gedaan met behulp van de service. Wandbox. Ik weet niet hoe robuust hun servers zijn, maar ik denk niet dat het raadzaam is om deze mogelijkheid te misbruiken.

Voorbeelden

Bouw het project in debugmodus met codecoverage.

cmake -S pad/naar/bronnen -B pad/naar/bouwdirectory -DCMAKE_BUILD_TYPE=Debug -DMYLIB_COVERAGE=ON
cmake --build pad/naar/bouwdirectory --target coverage --parallel 16

Project installatie zonder voorafgaande compilatie en testen

cmake -S pad/naar/bronbestanden -B pad/naar/bouwwerkdirectory -DMYLIB_TESTING=OFF -DCMAKE_INSTALL_PREFIX=pad/naar/installatiedirectory
cmake --build pad/naar/bouwwerkdirectory --target install

Bouwen in release modus met de opgegeven compiler

cmake -S pad/naar/bronbestanden -B pad/naar/bouwwerkdirectory -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_COMPILER=g++-8 -DCMAKE_PREFIX_PATH=pad/naar/directory/waar/de/afhankelijkheden/zijn/geïnstalleerd
cmake --build pad/naar/bouwwerkdirectory --parallel 4

Genereren van documentatie in het Engels

cmake -S pad/naar/bronbestanden -B pad/naar/bouwwerkdirectory -DCMAKE_BUILD_TYPE=Release -DMYLIB_DOXYGEN_LANGUAGE=English
cmake --build pad/naar/bouwwerkdirectory --target doc

Hulpmiddelen

  1. CMake 3.13

    In feite is versie CMake 3.13 alleen vereist voor het uitvoeren van bepaalde consoleopdrachten die in deze handleiding zijn beschreven. Wat betreft de syntaxis van CMake-scripts is versie 3.8 voldoende als de generatie op andere manieren wordt aangesproken.

  2. Testbibliotheek doctest

    Tests kunnen worden uitgeschakeld (zie de optie MYLIB_TESTING).

  3. Doxygen

    Om de taal te veranderen waarin de documentatie wordt gegenereerd, is er een optie beschikbaar MYLIB_DOXYGEN_LANGUAGE.

  4. Interpreter van programmeertaal Python 3

    Voor automatische generatie online sandbox.

Statistische analyse

Met CMake en een paar goede tools kan statische analyse met minimale inspanning worden gerealiseerd.

Cppcheck

CMake heeft ingebouwde ondersteuning voor een tool voor statische analyse Cppcheck.

Hiervoor moet je de optie gebruiken CMAKE_CXX_CPPCHECK:

cmake -S pad/naar/bronbestanden -B pad/naar/bouwwerkdirectory -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_CPPCHECK="cppcheck;--enable=all;-Ipad/naar/bronbestanden/include"

Daarna zal de statische analyse automatisch worden uitgevoerd telkens wanneer de bronnen worden gecompileerd en opnieuw gecompileerd. Je hoeft verder niets extra te doen.

Clang

Met het geweldige hulpmiddel scan-build kan je ook statische analyse eenvoudig uitvoeren:

scan-build cmake -S pad/naar/bronbestanden -B pad/naar/bouwwerkdirectory -DCMAKE_BUILD_TYPE=Debug
scan-build cmake --build pad/naar/bouwwerkdirectory

Hier, in tegenstelling tot het geval met Cppcheck, moet je elke keer de build uitvoeren via scan-build.

Naschrift

CMake is een zeer krachtige en flexibele systeem waarmee functionaliteit naar wens kan worden gerealiseerd. En hoewel de syntaxis soms te wensen overlaat, is het monster niet zo eng als het lijkt. Gebruik het CMake-buildsysteem ten goede en voor je gezondheid.

→ Download het projecttemplate

Bron: habr.com

Koop betrouwbare webhosting met bescherming tegen DDoS, VPS VDS servers 🔥 Koop betrouwbare webhosting met bescherming tegen DDoS, VPS VDS servers | ProHoster