CMake și C++ — frați pentru totdeauna

CMake și C++ — frați pentru totdeauna

În procesul de dezvoltare, îmi place să schimb compilatoarele, modurile de construire, versiunile dependențelor, să fac analize statice, să măsor performanța, să colectez acoperirea, să generez documentație și altele. Și îmi place foarte mult CMake, pentru că îmi permite să fac tot ceea ce vreau.

Mulți critică CMake, și adesea pe bună dreptate, dar dacă ne aprofundăm, nu totul este atât de rău, iar în ultima vreme chiar stă bine., iar direcția de dezvoltare este destul de pozitivă.

În această notă, vreau să explic cum este relativ simplu să organizezi o bibliotecă de anteturi în C++ în sistemul CMake, pentru a obține următoarea funcționalitate:

  1. Construire;
  2. Autotestare;
  3. Măsurarea acoperirii codului;
  4. Instalare;
  5. Autodocumentare;
  6. Generarea unui sandbox online;
  7. Analiză statică.

Cine se pricepe deja la C++ și CMake poate pur și simplu descărca template-ul de proiect și începe să-l folosească.


Cuprins

  1. Proiectul văzut din interior
    1. Structura proiectului
    2. Fișierul principal CMake (. /CMakeLists.txt)
      1. Informații despre proiect
      2. Opțiuni pentru proiect
      3. Opțiuni pentru compilare
      4. Obiectiv principal
      5. Instalare
      6. Teste
      7. Documentație
      8. Sandbox online
    3. Script pentru teste (test/CMakeLists.txt)
      1. Testare
      2. Acoperire
    4. Script pentru documentație (doc/CMakeLists.txt)
    5. Script pentru sandbox online (online/CMakeLists.txt)
  2. Proiectul văzut din exterior
    1. Compilare
      1. Generare
      2. Compilare
    2. Opțiuni
      1. MYLIB_COVERAGE
      2. MYLIB_TESTING
      3. MYLIB_DOXYGEN_LANGUAGE
    3. Obiective de construire
      1. Implicit
      2. mylib-unit-tests
      3. check
      4. coverage
      5. doc
      6. wandbox
    4. Exemple
  3. Instrumente
  4. Analiza statică
  5. Cuvânt înainte

Proiectul văzut din interior

Structura proiectului

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

În principal, voi discuta despre organizarea scripturilor CMake, așa că acestea vor fi analizate detaliat. Ceilalți pot vizualiza fiecare fișier direct pe pagina proiectului-template..

Fișierul principal CMake (. /CMakeLists.txt)

Informații despre proiect

În primul rând, trebuie să solicităm versiunea necesară a sistemului CMake. CMake evoluează, se modifică semnăturile comenzilor, comportamentul în diferite condiții. Pentru ca CMake să înțeleagă imediat ce vrem de la el, trebuie să ne fixăm din start cerințele.

cmake_minimum_required(VERSION 3.13)

Apoi, vom defini proiectul nostru, numele său, versiunea, limbajele utilizate și altele (vezi comanda project.).

În acest caz, indicăm limbajul CXX (iar asta înseamnă C++), pentru ca CMake să nu facă probleme și să nu caute compilatorul pentru limbajul C (în mod implicit, CMake include două limbaje: C și C++).

project(Mylib VERSION 1.0 LANGUAGES CXX)

Aici puteți verifica imediat dacă proiectul nostru este inclus în alt proiect ca subproiect. Acest lucru va ajuta considerabil în continuare.

get_directory_property(IS_SUBPROJECT PARENT_DIRECTORY)

Opțiuni pentru proiect

Să prevedem două opțiuni.

Prima opțiune — MYLIB_TESTING — pentru dezactivarea testelor modulare. Acest lucru poate fi necesar dacă suntem siguri că testele sunt în regulă și dorim, de exemplu, doar să instalăm sau să pachetam proiectul nostru. Sau proiectul nostru este inclus ca subproiect — în acest caz, utilizatorului proiectului nostru nu-i va păsa să ruleze testele noastre. Nu testați dependențele pe care le utilizați?

option(MYLIB_TESTING "Activați testarea modulară" ON)

În plus, vom crea o opțiune separată MYLIB_COVERAGE pentru măsurarea acoperirii codului de teste, dar aceasta va necesita instrumente suplimentare, așa că va trebui activată în mod explicit.

option(MYLIB_COVERAGE "Activați măsurarea acoperirii codului de teste" OFF)

Opțiuni pentru compilare

Desigur, suntem programatori C++ foarte pricepuți, așa că dorim un nivel maxim de diagnosticare în timpul compilării din partea compilatorului. Nicio greșeală nu va trece neobservată.

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
)

De asemenea, vom dezactiva extensiile pentru a respecta complet standardul limbajului C++. În mod implicit, acestea sunt activate în CMake.

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

Obiectiv principal

Biblioteca noastră constă doar din fișiere de antet, ceea ce înseamnă că nu avem ieșiri sub formă de biblioteci statice sau dinamice. Pe de altă parte, pentru a utiliza biblioteca noastră din exterior, trebuie instalată, trebuie să fie detectabilă în sistem și să fie legată de proiectul tău, iar împreună cu ea, să fie legate aceste antete, precum și, poate, câteva proprietăți suplimentare.

Pentru acest scop, creăm o bibliotecă de interfață.

add_library(mylib INTERFACE)

Legăm antetele la biblioteca noastră de interfață.

Utilizarea modernă, la modă, a CMake presupune că antetele, proprietățile etc. sunt transmise printr-o singură țintă. Astfel, este suficient să spunem target_link_libraries(target PRIVATE dependency), iar toate antetele asociate cu ținta dependency, vor fi disponibile pentru sursele care aparțin țintei destinația. Și nu este nevoie de niciun [target_]include_directoriesAcest lucru va fi demonstrat mai jos în analiza scriptului CMake pentru teste modulare.

De asemenea, este important să acordăm atenție așa-numitelor expresii-generator: $.

Această comandă asociază capsele de care avem nevoie cu biblioteca noastră de interfață, iar în cazul în care biblioteca noastră este conectată la un anumit obiect în cadrul aceleași ierarhii CMake, vor fi asociate capsele din directorul ${CMAKE_CURRENT_SOURCE_DIR}/include, iar dacă biblioteca noastră este instalată în sistem și conectată într-un alt proiect folosind comanda find_package, atunci vor fi asociate capsele din directorul include în raport cu directorul de instalare.

target_include_directories(mylib INTERFACE
    $
    $
)

Setăm standardul limbajului. Desigur, cel mai recent. În acest fel, nu doar includem standardul, ci și îl extindem asupra celor care vor folosi biblioteca noastră. Acest lucru se realizează prin faptul că proprietatea stabilită are categoria INTERFACE (vezi comanda target_compile_features).

target_compile_features(mylib INTERFACE cxx_std_17)

Creăm un alias pentru biblioteca noastră. De altfel, pentru frumusețe, va fi într-un „spațiu de nume” special. Acest lucru va fi util atunci când biblioteca noastră va avea diferite module, și le vom conecta independent unii de alții. Ca în Boost, de exemplu.

add_library(Mylib::mylib ALIAS mylib)

Instalare

Instalarea capselor noastre în sistem. Aici totul este simplu. Spunem că folderul cu toate capsele trebuie să ajungă în directorul include în raport cu locul de instalare.

install(DIRECTORY include/mylib DESTINATION include)

Apoi informăm sistemul de construire că dorim să avem posibilitatea de a apela comanda find_package(Mylib) și să obținem obiectul Mylib::mylib.

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

Următoarea comandă trebuie înțeleasă astfel. Când în proiecte externe vom apela comanda find_package(Mylib 1.2.3 REQUIRED), și în același timp versiunea reală a bibliotecii instalate se dovedește a fi incompatibilă cu versiunea 1.2.3, CMake va genera automat o eroare. Adică nu va fi nevoie să urmărim versiunile manual.

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)

Teste

Dacă testele sunt dezactivate explicit prin opțiunea corespunzătoare sau proiectul nostru este un subproiect, adică este conectat la un alt proiect CMake prin comanda add_subdirectory, nu trecem mai departe în ierarhie, iar scriptul în care sunt descrise comenzile pentru generarea și rularea testelor pur și simplu nu se execută.

if(NOT MYLIB_TESTING)
    message(STATUS "Testarea proiectului Mylib este dezactivată")
elseif(IS_SUBPROJECT)
    message(STATUS "Mylib nu este testat în modul submodul")
else()
    add_subdirectory(test)
endif()

Documentație

Documentația nu va fi generată în cazul unui subproiect.

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

Sandbox online

În mod similar, nu va exista un sand box online pentru subproiect.

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

Script pentru teste (test/CMakeLists.txt)

Testare

În primul rând, găsim pachetul cu cadrul de testare dorit (înlocuiți cu preferatul vostru).

find_package(doctest 2.3.3 REQUIRED)

Creăm fișierul executabil cu teste. De obicei, adaug doar fișierul care va conține funcția main.

add_executable(mylib-unit-tests test_main.cpp)

Dar fișierele în care sunt descrise testele în sine le adaug mai târziu. Dar nu este obligatoriu să procedați astfel.

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

Conectăm dependențele. Rețineți că la binarul nostru am legat doar obiectivele CMake necesare, și nu am apelat comanda target_include_directories. Anteturile din cadrul de testare și din Mylib::mylib, precum și parametrii de compilare (în cazul nostru este standardul limbajului C++) au trecut împreună cu aceste obiective.

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

În cele din urmă, creăm un obiectiv fals, "compilarea" căruia este echivalentă cu rularea testelor, și adăugăm acest obiectiv la compilarea implicită (acest lucru este gestionat de atributul , sau). Aceasta înseamnă că compilarea implicită inițiază rularea testelor, deci nu le vom uita niciodată.

add_custom_target(check ALL COMMAND mylib-unit-tests)

Acoperire

Apoi, activăm măsurarea acoperirii codului, dacă este specificată opțiunea corespunzătoare. Nu voi intra în detalii, deoarece acestea se referă mai mult la instrumentul de măsurare a acoperirii decât la CMake. Este important doar să notăm că, în urma rezultatelor, va fi creat un obiectiv coverage, cu ajutorul căruia este convenabil să rulați măsurarea acoperirii.

find_program(GCOVR_EXECUTABLE gcovr)
if(MYLIB_COVERAGE AND GCOVR_EXECUTABLE)
    message(STATUS "Măsurarea acoperirii codului prin teste este activată")

    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 "Pentru măsurarea acoperirii codului prin teste este necesar programul gcovr")
endif()

Script pentru documentație (doc/CMakeLists.txt)

Am găsit Doxygen.

find_package(Doxygen)

Apoi verificăm dacă utilizatorul a setat variabila cu limba. Dacă da, nu o modificăm, dacă nu, atunci o setăm pe română. Apoi configurăm fișierele sistemului Doxygen. Toate variabilele necesare, inclusiv limba, ajung acolo în timpul configurării (vezi. comanda configure_file).

După aceea, creăm o țintă doc, care va lansa generarea documentației. Deoarece generarea documentației nu este o necesitate stringentă în procesul de dezvoltare, ținta nu va fi inclusă în mod implicit, aceasta va trebui să fie lansată explicit.

if (Doxygen_FOUND)
    if (NOT MYLIB_DOXYGEN_LANGUAGE)
        set(MYLIB_DOXYGEN_LANGUAGE Romanian)
    endif()
    message(STATUS "Documentația Doxygen va fi generată în ${MYLIB_DOXYGEN_LANGUAGE}")
    configure_file(Doxyfile.in Doxyfile)
    add_custom_target(doc COMMAND ${DOXYGEN_EXECUTABLE} ${CMAKE_CURRENT_BINARY_DIR}/Doxyfile)
endif ()

Script pentru sandbox online (online/CMakeLists.txt)

Aici găsim al treilea Python și creăm o țintă wandbox, care generează o solicitare, conform API-ului serviciului Wandbox, și o trimite. Ca răspuns, primim un link către sandboxul generat.

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 "Pentru crearea unui sandbox online este necesar un interpretator Python versiunea 3")
endif()

Proiectul văzut din exterior

Acum să vedem cum putem folosi toate acestea.

Compilare

Compilarea acestui proiect, la fel ca orice alt proiect pe sistemul de compilare CMake, constă în două etape:

Generare

cmake -S cale/catre/surse -B cale/catre/directoria/de/compilare [opțiuni ...]

Dacă comanda de mai sus nu a funcționat din cauza unei versiuni vechi de CMake, încercați să omiteți -S:

cmake cale/catre/surse -B cale/catre/directoria/de/compilare [opțiuni ...]

Mai multe despre opțiuni.

Compilarea proiectului

cmake --build cale/catre/directoria/de/compilare [--target țintă]

Mai multe despre țintele de compilare.

Opțiuni

MYLIB_COVERAGE

cmake -S ... -B ... -DMYLIB_COVERAGE=ON [alte opțiuni ...]

Activează ținta coverage, cu ajutorul căruia se poate lansa măsurarea acoperirii codului cu teste.

MYLIB_TESTING

cmake -S ... -B ... -DMYLIB_TESTING=OFF [alte opțiuni ...]

Oferă posibilitatea de a dezactiva compilarea testelor modulare și a obiectivului check. În consecință, se dezactivează măsurarea acoperirii codului cu teste (vezi MYLIB_COVERAGE).

De asemenea, testarea este dezactivată automat în cazul în care proiectul este adăugat la alt proiect ca subproiect prin comanda add_subdirectory.

MYLIB_DOXYGEN_LANGUAGE

cmake -S ... -B ... -DMYLIB_DOXYGEN_LANGUAGE=English [alte opțiuni ...]

Schimbă limba documentației generate de obiectiv doc la cea specificată. Lista limbilor disponibile poate fi găsită pe site-ul sistemului Doxygen.

Implicit este activă limba rusă.

Obiective de construire

Implicit

cmake --build calea/spre/directorul/de/compilare
cmake --build calea/spre/directorul/de/compilare --target all

Dacă obiectivul nu este specificat (ceea ce este echivalent cu obiectivul all), compilează tot ce este posibil și, de asemenea, cheamă obiectivul check.

mylib-unit-tests

cmake --build calea/spre/directorul/de/compilare --target mylib-unit-tests

Compilarea testelor modulare. Activat în mod implicit.

check

cmake --build calea/spre/directorul/de/compilare --target check

Rulează testele modulare compilate (le compilează dacă nu sunt compilate). Activat în mod implicit.

Vezi de asemenea mylib-unit-tests.

coverage

cmake --build calea/spre/directorul/de/compilare --target coverage

Analizează testele modulare rulante (le rulează dacă nu sunt rulante) pentru acoperirea codului cu teste folosind programul gcovr.

Rezultatul acoperirii va arăta aproximativ astfel:

------------------------------------------------------------------------------
                           Raport de Acoperire a Codului GCC
Director: /calea/catre/cmakecpptemplate/include/
------------------------------------------------------------------------------
Fișier                                       Linii    Exec  Acoperire   Lipsă
------------------------------------------------------------------------------
mylib/myfeature.hpp                            2       2   100%   
------------------------------------------------------------------------------
TOTAL                                          2       2   100%
------------------------------------------------------------------------------

Obiectivul este disponibil doar atunci când opțiunea este activată MYLIB_COVERAGE.

Vezi de asemenea check.

doc

cmake --build calea/spre/directorul/de/compilare --target doc

Începe generarea documentației codului folosind sistemul Doxygen.

wandbox

cmake --build calea/spre/directorul/de/compilare --target wandbox

Răspunsul de la serviciu arată aproximativ astfel:

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

Pentru aceasta se folosește serviciul Wandbox. Nu știu cât de rezistente sunt serverele lor, dar cred că nu ar trebui să abuzezi de această oportunitate.

Exemple

Compilarea proiectului în modul de depanare cu măsurarea acoperirii

cmake -S calea/spre/surse -B calea/spre/directorul/de/compilare -DCMAKE_BUILD_TYPE=Debug -DMYLIB_COVERAGE=ON
cmake --build calea/spre/directorul/de/compilare --target coverage --parallel 16

Instalarea proiectului fără o construcție și testare prealabilă

cmake -S calea/spre/surse -B calea/spre/directoria/constructiei -DMYLIB_TESTING=OFF -DCMAKE_INSTALL_PREFIX=calea/spre/directoria/de/installare
cmake --build calea/spre/directoria/constructiei --target install

Construirea în modul de eliberare cu compilatorul specificat

cmake -S calea/spre/surse -B calea/spre/directoria/constructiei -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_COMPILER=g++-8 -DCMAKE_PREFIX_PATH=calea/spre/directoria/unde/sunt/instalate/dependințele
cmake --build calea/spre/directoria/constructiei --parallel 4

Generarea documentației în limba engleză

cmake -S calea/spre/surse -B calea/spre/directoria/constructiei -DCMAKE_BUILD_TYPE=Release -DMYLIB_DOXYGEN_LANGUAGE=English
cmake --build calea/spre/directoria/constructiei --target doc

Instrumente

  1. CMake 3.13

    De fapt, versiunea CMake 3.13 este necesară doar pentru a rula anumite comenzi de consolă, descrise în această documentație. Din punct de vedere al sintaxei scripturilor CMake, versiunea 3.8 este suficientă dacă generarea este apelată prin alte metode.

  2. Biblioteca de testare doctest

    Testarea poate fi dezactivată (vedeți opțiunea MYLIB_TESTING).

  3. Doxygen

    Pentru a schimba limba în care va fi generată documentația, există opțiunea MYLIB_DOXYGEN_LANGUAGE.

  4. Interpretul limbajului de programare Python 3

    Pentru generarea automată sandbox-ului online.

Analiza statică

Cu ajutorul CMake și a câtorva instrumente bune, se poate asigura analiza statică cu un minim de eforturi.

Cppcheck

CMake include suport pentru un instrument de analiză statică Cppcheck.

Pentru aceasta, trebuie utilizată opțiunea CMAKE_CXX_CPPCHECK:

cmake -S calea/spre/surse -B calea/spre/directoria/constructiei -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_CPPCHECK="cppcheck;--enable=all;-Icalea/spre/surse/include"

După aceasta, analiza statică va fi rulată automat de fiecare dată în timpul compilării și recompilării surselor. Nu este nevoie să faceți nimic în plus.

Clang

Cu ajutorul minunatului instrument scan-build se poate de asemenea să se ruleze analiza statică rapid:

scan-build cmake -S calea/spre/surse -B calea/spre/directoria/constructiei -DCMAKE_BUILD_TYPE=Debug
scan-build cmake --build calea/spre/directoria/constructiei

Aici, spre deosebire de cazul cu Cppcheck, este necesar să se ruleze construcția de fiecare dată prin scan-build.

Cuvânt înainte

CMake este un sistem foarte puternic și flexibil, care permite implementarea funcționalității în orice stil și culoare. Și, deși sintaxa uneori lasă de dorit, nu este atât de înfricoșătoare pe cât pare. Folosiți sistemul de construire CMake pentru binele societății și în beneficiul sănătății.

→ Descărcați șablonul proiectului

Sursa: habr.com

Cumpără un hosting fiabil pentru site-uri cu protecție DDoS, servere VPS VDS 🔥 Cumpără un hosting fiabil pentru site-uri cu protecție DDoS, servere VPS VDS | ProHoster