CMake och C++ — bröder för alltid

CMake och C++ — bröder för alltid

Under development, I like to change compilers, build modes, dependency versions, perform static analysis, measure performance, collect coverage, generate documentation, etc. And I really love CMake because it allows me to do everything I want.

Many criticize CMake, often justifiably, but if you look closely, it’s not all that bad, and recently it's actually quite good, and the development direction is quite positive.

In this note, I want to explain how easy it is to organize a header library in C++ in CMake to achieve the following functionality:

  1. Building;
  2. Automatic test startup;
  3. Measuring code coverage;
  4. Installation;
  5. Auto-documentation;
  6. Generating an online sandbox;
  7. Static analysis.

Anyone who understands C++ and CMake can simply download the project template and start using it.


InnehÄll

  1. The project from the inside
    1. Projektstruktur
    2. The main CMake file (./CMakeLists.txt)
      1. Information om projektet
      2. Project options
      3. Compilation options
      4. Main target
      5. Installation
      6. Tester
      7. Dokumentation
      8. Online sandbox
    3. Test script (test/CMakeLists.txt)
      1. Testning
      2. TĂ€ckning
    4. Documentation script (doc/CMakeLists.txt)
    5. Online sandbox script (online/CMakeLists.txt)
  2. The project from the outside
    1. Samling
      1. Generation
      2. Samling
    2. Alternativ
      1. MYLIB_COVERAGE
      2. MYLIB_TESTING
      3. MYLIB_DOXYGEN_LANGUAGE
    3. Build targets
      1. Som standard
      2. mylib-unit-tests
      3. check
      4. coverage
      5. dokument
      6. wandbox
    4. Exempel
  3. Verktyg
  4. Static analysis
  5. Epilog

The project from the inside

Projektstruktur

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

The main focus will be on how to organize CMake scripts, so they will be examined in detail. The other files can be viewed directly by anyone interested on the template project page.

The main CMake file (./CMakeLists.txt)

Information om projektet

First, we need to request the required version of the CMake system. CMake evolves, command signatures change, and behavior varies under different conditions. To ensure CMake understands our intentions immediately, we need to specify our requirements upfront.

cmake_minimum_required(VERSION 3.13)

Next, we define our project, its name, version, languages used, and other properties (see the project command).

In this case, we specify the language CXX (which means C++), so that CMake doesn't get confused and look for a C language compiler (by default, CMake includes two languages: C and C++).

project(Mylib VERSION 1.0 LANGUAGES CXX)

HÀr kan vi ocksÄ omedelbart kontrollera om vÄrt projekt ingÄr som ett delprojekt i ett annat projekt. Detta kommer att vara till stor hjÀlp framöver.

get_directory_property(IS_SUBPROJECT PARENT_DIRECTORY)

Project options

Vi kommer att övervÀga tvÄ alternativ.

Det första alternativet — MYLIB_TESTING — för att stĂ€nga av modultester. Detta kan vara nödvĂ€ndigt om vi Ă€r sĂ€kra pĂ„ att testerna fungerar bra, men vi vill till exempel bara installera eller paketera vĂ„rt projekt. Eller om vĂ„rt projekt ingĂ„r som ett delprojekt — i sĂ„ fall Ă€r det kanske inte relevant för anvĂ€ndaren att köra vĂ„ra tester. Du testar vĂ€l inte de beroenden du anvĂ€nder?

option(MYLIB_TESTING "Aktivera modultestning" ON)

Dessutom kommer vi att skapa ett separat alternativ MYLIB_COVERAGE för att mÀta testtÀckning, men det kommer att krÀva ytterligare verktyg, sÄ det mÄste aktiveras tydligt.

option(MYLIB_COVERAGE "Aktivera mÀtning av testtÀckning" OFF)

Compilation options

SjÀlvklart Àr vi fantastiska C++-programmerare, sÄ vi vill ha maximal kompileringsdiagnosnivÄ frÄn kompilatorn. Ingen mus ska smita förbi.

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
)

Vi kommer ocksÄ att stÀnga av tillÀgg för att fullt ut följa C++-standarden. Som standard Àr de aktiverade i CMake.

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

Main target

VĂ„rt bibliotek bestĂ„r endast av header-filer, vilket innebĂ€r att vi inte har nĂ„got utslĂ€pp i form av statiska eller dynamiska bibliotek. Å andra sidan, för att anvĂ€nda vĂ„rt bibliotek utanför, mĂ„ste det installeras, det mĂ„ste kunna upptĂ€ckas i systemet och kopplas till sitt projekt, och samtidigt inkludera dessa header-filer samt kanske nĂ„gra ytterligare egenskaper.

För detta ÀndamÄl skapar vi ett grÀnssnittsbibliotek.

add_library(mylib INTERFACE)

Ansluter header-filerna till vÄrt grÀnssnittsbibliotek.

Modern, trendig, ungdomlig anvÀndning av CMake innebÀr att header-filer, egenskaper osv. överförs genom ett enda mÄl. SÄ det rÀcker att sÀga target_link_libraries(target PRIVATE dependency), och alla header-filer som Àr kopplade till mÄlet syntax, kommer att vara tillgÀngliga för kÀllkoden som tillhör mÄlet target. Det krÀvs inga [target_]include_directories. Det kommer att demonstreras nedan vid analysen av CMake-skriptet för modulÀra tester.

Det Àr ocksÄ vÀrt att uppmÀrksamma de sÄ kallade generatoruttrycken: $.

Detta kommando kopplar de nödvÀndiga rubrikerna med vÄrt grÀnsbibliotek, och om vÄrt bibliotek ansluts till nÄgot mÄl inom en och samma CMake-hierarki, kommer rubrikerna frÄn mappen ${CMAKE_CURRENT_SOURCE_DIR}/include, och om vÄrt bibliotek installeras i systemet och ansluts i ett annat projekt med kommandot find_package, kommer rubrikerna att kopplas frÄn mappen include relativt installationsmappen.

target_include_directories(mylib INTERFACE
    $
    $
)

Vi stÀller in sprÄkstandarden. SjÀlvklart, den senaste. Dessutom inkluderar vi inte bara standarden, utan sprider den till dem som kommer att anvÀnda vÄrt bibliotek. Detta uppnÄs genom att den instÀllda egenskapen har kategorin INTERFACE (se kommandot target_compile_features).

target_compile_features(mylib INTERFACE cxx_std_17)

Vi skapar ett alias för vÄrt bibliotek. Dessutom kommer det att vara i ett speciellt "namnrum" för att göra det snyggt. Detta kommer att vara anvÀndbart nÀr vÄrt bibliotek fÄr olika moduler och vi vill ansluta dem oberoende av varandra. Som i Boost exempelvis.

add_library(Mylib::mylib ALIAS mylib)

Installation

Installation av vÄra rubriker i systemet. HÀr Àr det enkelt. Vi sÀger att mappen som innehÄller alla rubriker ska hamna i katalogen include relativt installationsplatsen.

install(DIRECTORY include/mylib DESTINATION include)

DÀrefter informerar vi byggsystemet om att vi vill kunna kalla kommandot i externa projekt find_package(Mylib) och fÄ mÄlet Mylib::mylib.

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

Den följande kommandon ska förstÄs sÄ hÀr. NÀr vi i ett externt projekt anropar kommandot find_package(Mylib 1.2.3 REQUIRED), och om den faktiska versionen av det installerade biblioteket Àr inkompatibel med versionen 1.2.3, kommer CMake automatiskt att generera ett fel. Det betyder att vi inte behöver hÄlla reda pÄ versionerna manuellt.

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)

Tester

Om testerna Àr avstÀngda uttryckligen med hjÀlp av den motsvarande alternativet eller om vÄrt projekt Àr ett delprojekt, det vill sÀga om det Àr kopplat till ett annat CMake-projekt med kommandot add_subdirectory, vi gÄr inte lÀngre ner i hierarkin, och skriptet som beskriver kommandona för att generera och köra testerna kommer helt enkelt inte att köras.

if(NOT MYLIB_TESTING)
    message(STATUS "Testning av projektet Mylib avstÀngd")
elseif(IS_SUBPROJECT)
    message(STATUS "Mylib testas inte i delmoduls lÀge")
else()
    add_subdirectory(test)
endif()

Dokumentation

Dokumentationen kommer inte heller att genereras i fallet med ett delprojekt.

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

Online sandbox

PÄ samma sÀtt kommer det inte att finnas nÄgra online-sandboxar för delprojektet.

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

Test script (test/CMakeLists.txt)

Testning

Först letar vi efter paketet med det nödvÀndiga test-ramverket (byt ut mot ditt favoritrÀmsystem).

find_package(doctest 2.3.3 REQUIRED)

Vi skapar vÄr körbara fil med tester. Vanligtvis lÀgger jag direkt i den körbara binÀren bara filen dÀr funktionen kommer att finnas main.

add_executable(mylib-unit-tests test_main.cpp)

Men filer som beskriver sjÀlva testerna lÀgger jag till senare. Men det Àr inte nödvÀndigt att göra sÄ.

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

Vi kopplar in beroenden. Observera att vi kopplat endast de nödvÀndiga CMake-mÄlen till vÄr binÀrfil och inte anropat kommandot target_include_directories. Rubriker frÄn test-ramverket och frÄn vÄr Mylib::mylib, sÄvÀl som byggparams (i vÄrt fall Àr detta standardsprÄket C++) har gÄtt ihop med dessa mÄl.

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

Till slut skapar vi ett falskt mÄl, "bygget" av vilket Àr ekvivalent med att köra tester, och lÀgger till detta mÄl i standardbygget (detta ansvaras av attributet ALL). Detta innebÀr att standardbygget initierar körningen av testerna, det vill sÀga att vi aldrig glömmer att köra dem.

add_custom_target(check ALL COMMAND mylib-unit-tests)

TĂ€ckning

DÀrefter aktiverar vi kodtÀckning mÀtning, om en motsvarande option har angetts. Jag gÄr inte in pÄ detaljerna eftersom de mer handlar om verktyget för tÀckningsmÀtning Àn CMake. Det Àr viktigt att notera att en mÄlinriktning kommer att skapas utifrÄn resultaten coverage, som gör det bekvÀmt att köra kodtÀckning.

find_program(GCOVR_EXECUTABLE gcovr)
if(MYLIB_COVERAGE AND GCOVR_EXECUTABLE)
    message(STATUS "MÀtning av kodtÀckning med tester Àr aktiverad")

    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 "För att mÀta kodtÀckning med tester behövs programmet gcovr")
endif()

Documentation script (doc/CMakeLists.txt)

Hittade Doxygen.

find_package(Doxygen)

DÀrefter kontrollerar vi om anvÀndaren har stÀllt in en sprÄkvariabel. Om ja, lÄt den vara, om inte, tar vi ryska. Sedan konfigurerar vi Doxygen-systemfilerna. Alla nödvÀndiga variabler, inklusive sprÄk, hamnar dÀr under konfigurationen (se kommandot configure_file).

DÀrefter skapar vi ett mÄl dokument, som kommer att köra genereringen av dokumentationen. Eftersom genereringen av dokumentation inte Àr den största nödvÀndigheten i utvecklingsprocessen, kommer mÄlet som standard inte att vara aktiverat, utan mÄste startas explicit.

if (Doxygen_FOUND)
    if (NOT MYLIB_DOXYGEN_LANGUAGE)
        set(MYLIB_DOXYGEN_LANGUAGE Russian)
    endif()
    message(STATUS "Doxygen-dokumentation kommer att genereras pÄ ${MYLIB_DOXYGEN_LANGUAGE}")
    configure_file(Doxyfile.in Doxyfile)
    add_custom_target(doc COMMAND ${DOXYGEN_EXECUTABLE} ${CMAKE_CURRENT_BINARY_DIR}/Doxyfile)
endif ()

Online sandbox script (online/CMakeLists.txt)

HÀr hittar vi den tredje Python och skapar ett mÄl wandbox, som genererar en begÀran som överensstÀmmer med API:t för tjÀnsten Wandbox, och skickar den. Svar kommer med en lÀnk till den fÀrdiga sandlÄdan.

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 "För att skapa en online-sandlÄda krÀvs en tolk för sprÄk Python version 3")
endif()

The project from the outside

Nu ska vi se hur man anvÀnder allt detta.

Samling

Bygget av detta projekt, precis som vilket annat projekt pÄ CMake-byggsystemet, bestÄr av tvÄ steg:

Generation

cmake -S sökvÀg/till/kÀllor -B sökvÀg/till/byggochkatalog [alternativ ...]

Om kommandot ovan inte fungerar pÄ grund av en gammal version av CMake, försök att ta bort -S:

cmake sökvÀg/till/kÀllor -B sökvÀg/till/byggochkatalog [alternativ ...]

Mer om alternativen.

Bygg projektet

cmake --build sökvÀg/till/byggochkatalog [--target target]

Mer om byggmÄl.

Alternativ

MYLIB_COVERAGE

cmake -S ... -B ... -DMYLIB_COVERAGE=ON [övriga alternativ ...]

Aktivera mÄlet coverage, med vilket man kan starta mÀtning av kodtÀckning med tester.

MYLIB_TESTING

cmake -S ... -B ... -DMYLIB_TESTING=OFF [övriga alternativ ...]

Möjliggör att stÀnga av byggandet av modulÀra tester och mÄlet check. Som en följd stÀngs mÀtningen av kodtÀckning med tester av (se. MYLIB_COVERAGE).

Testning stÀngs ocksÄ automatiskt av om projektet inkluderas i ett annat projekt som en submodul med hjÀlp av kommandot add_subdirectory.

MYLIB_DOXYGEN_LANGUAGE

cmake -S ... -B ... -DMYLIB_DOXYGEN_LANGUAGE=English [övriga alternativ ...]

VÀxlar sprÄket för dokumentationen som mÄlet genererar dokument till det angivna. Lista över tillgÀngliga sprÄk finns pÄ Doxygen-systemets webbplats.

Som standard Àr ryska aktiverat.

Build targets

Som standard

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

Om mÄlet inte anges (vilket Àr ekvivalent med mÄlet all), bygger allt som gÄr att bygga och anropar Àven mÄlet check.

mylib-unit-tests

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

Kommer att kompilera modulÀra tester. Detta Àr aktiverat som standard.

check

cmake --build path/k/samlings/directory --target check

Kör de samlade (eller bygger om de inte redan Àr det) modulÀra testerna. Aktiverat som standard.

See also mylib-unit-tests.

coverage

cmake --build path/k/samlings/directory --target coverage

Analyserar de körda (kör om de inte redan Àr det) modulÀra testerna för kodtÀckning med hjÀlp av programmet gcovr.

Utdata av tÀckningen kommer att se ut ungefÀr sÄ hÀr:

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

MÄlet Àr endast tillgÀngligt nÀr alternativet Àr aktiverat MYLIB_COVERAGE.

See also check.

dokument

cmake --build path/k/samlings/directory --target doc

Startar genereringen av dokumentation för koden med hjÀlp av systemet Doxygen.

wandbox

cmake --build path/k/samlings/directory --target wandbox

Svaret frÄn tjÀnsten ser ungefÀr ut sÄ hÀr:

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

För detta anvÀnds tjÀnsten Wandbox. Jag vet inte hur hÄllbara deras servrar Àr, men jag tror inte att man bör missbruka denna möjlighet.

Exempel

Byggandet av projektet i felsökningslÀge med mÀtning av tÀckning

cmake -S vÀg/till/kÀllkod -B vÀg/till/samlings/directory -DCMAKE_BUILD_TYPE=Debug -DMYLIB_COVERAGE=ON
cmake --build vÀg/till/samlings/directory --target coverage --parallel 16

Installation av projektet utan föregÄende bygg- och testning

cmake -S sökvÀg/till/kÀllor -B sökvÀg/till/byggkatalog -DMYLIB_TESTING=OFF -DCMAKE_INSTALL_PREFIX=sökvÀg/till/installationskatalog
cmake --build sökvÀg/till/byggkatalog --target install

Bygg i produktionslÀge med angiven kompilator

cmake -S sökvÀg/till/kÀllor -B sökvÀg/till/byggkatalog -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_COMPILER=g++-8 -DCMAKE_PREFIX_PATH=sökvÀg/till/katalog/dÀr/beroenden/Àr/installernade
cmake --build sökvÀg/till/byggkatalog --parallel 4

Generering av dokumentation pÄ engelska

cmake -S sökvÀg/till/kÀllor -B sökvÀg/till/byggkatalog -DCMAKE_BUILD_TYPE=Release -DMYLIB_DOXYGEN_LANGUAGE=English
cmake --build sökvÀg/till/byggkatalog --target doc

Verktyg

  1. CMake 3.13

    Faktiskt krÀvs CMake-version 3.13 bara för att köra vissa konsolkommandon som beskrivs i denna dokumentation. NÀr det gÀller syntax av CMake-skript Àr version 3.8 tillrÀcklig om generationen anropas pÄ andra sÀtt.

  2. Testbibliotek doctest

    Testning kan stÀngas av (se alternativet MYLIB_TESTING).

  3. Doxygen

    För att vÀxla sprÄk som dokumentationen ska genereras pÄ, finns det ett alternativ MYLIB_DOXYGEN_LANGUAGE.

  4. Tolk för programmeringssprÄk Python 3

    För automatisk generation online sandlÄdor.

Static analysis

Med CMake och ett par bra verktyg kan man sÀkerstÀlla statisk analys med minimala insatser.

Cppcheck

CMake har inbyggt stöd för verktyget för statisk analys Cppcheck.

För detta behövs alternativet CMAKE_CXX_CPPCHECK:

cmake -S sökvÀg/till/kÀllor -B sökvÀg/till/byggkatalog -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_CPPCHECK="cppcheck;--enable=all;-I sökvÀg/till/kÀllor/include"

Efter detta kommer den statiska analysen automatiskt att startas varje gÄng kÀllkoden kompileras och omkompileras. Inga ytterligare ÄtgÀrder behövs.

Clang

Med det fantastiska verktyget scan-build kan man ocksÄ köra statisk analys snabbt:

scan-build cmake -S sökvÀg/till/kÀllor -B sökvÀg/till/byggkatalog -DCMAKE_BUILD_TYPE=Debug
scan-build cmake --build sökvÀg/till/byggkatalog

HÀr, till skillnad frÄn med Cppcheck, mÄste man alltid köra byggprocessen via scan-build.

Epilog

CMake Àr ett mycket kraftfullt och flexibelt system som möjliggör implementation av funktionalitet pÄ alla sÀtt och vis. Och Àven om syntaktiska aspekter ibland lÀmnar mycket att önska, Àr det inte sÄ illa som det sÀgs. AnvÀnd CMake-byggsystemet till nytta för samhÀllet och för er hÀlsa.

→ Ladda ner projektmall

KĂ€lla: habr.com

Köp pĂ„litlig webbhotell med DDoS-skydd, VPS VDS-servrar đŸ”„ Köp pĂ„litlig webbhotell med DDoS-skydd, VPS VDS-servrar | ProHoster