CMake e C++ — fratelli per sempre

CMake e C++ — fratelli per sempre

Durante lo sviluppo, mi piace cambiare compilatori, modalità di build, versioni delle dipendenze, effettuare analisi statica, misurare le prestazioni, raccogliere la copertura, generare documentazione, ecc. E adoro molto CMake perché mi permette di fare tutto ciò che voglio.

Molti criticano CMake, spesso a giusta ragione, ma se si scava a fondo, la situazione non è così tragica, e nell'ultimo periodo è stata anche piuttosto positiva, e la direzione dello sviluppo è completamente incoraggiante.

In questo articolo, voglio spiegare come sia relativamente semplice organizzare una libreria di intestazione in C++ utilizzando CMake, per ottenere la seguente funzionalità:

  1. Build;
  2. Esecuzione automatica dei test;
  3. Misurazione della copertura del codice;
  4. Installazione;
  5. Autodocumentazione;
  6. Generazione di un sandbox online;
  7. Analisi statica.

Chi ha dimestichezza con C++ e CMake può semplicemente scaricare un modello di progetto e iniziare a utilizzarlo.


Contenuto

  1. Il progetto dall'interno
    1. Struttura del progetto
    2. File CMake principale (. /CMakeLists.txt)
      1. Informazioni sul progetto
      2. Opzioni del progetto
      3. Opzioni di compilazione
      4. Obiettivo principale
      5. Installazione
      6. Test
      7. Documentazione
      8. Sandbox online
    3. Script per i test (test /CMakeLists.txt)
      1. Test
      2. Copertura
    4. Script per la documentazione (doc /CMakeLists.txt)
    5. Script per la sandbox online (online /CMakeLists.txt)
  2. Il progetto dall'esterno
    1. Compilazione
      1. Generazione
      2. Compilazione
    2. Opzioni
      1. MYLIB_COVERAGE
      2. MYLIB_TESTING
      3. MYLIB_DOXYGEN_LANGUAGE
    3. Obiettivi di build
      1. Per impostazione predefinita
      2. mylib-unit-tests
      3. check
      4. copertura
      5. doc
      6. wandbox
    4. Esempi
  3. Strumenti
  4. Analisi statica
  5. Postfazione

Il progetto dall'interno

Struttura del progetto

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

Essenzialmente, parleremo di come organizzare gli script CMake, quindi verranno analizzati in dettaglio. Gli altri file possono essere consultati direttamente sulla pagina del progetto modello.

File CMake principale (. /CMakeLists.txt)

Informazioni sul progetto

In primo luogo, è necessario richiedere la versione corretta di CMake. CMake si evolve, le firme dei comandi cambiano, e il comportamento in condizioni diverse. Affinché CMake comprenda immediatamente cosa ci aspettiamo, è necessario specificare subito i nostri requisiti.

cmake_minimum_required(VERSION 3.13)

Successivamente, definiamo il nostro progetto, il suo nome, la versione, i linguaggi utilizzati e altro (v. comando project).

In questo caso, specifichiamo il linguaggio CXX (il che significa C++), affinché CMake non si confonda e non cerchi un compilatore per il linguaggio C (per impostazione predefinita CMake include due linguaggi: C e C++).

project(Mylib VERSION 1.0 LANGUAGES CXX)

Qui puoi anche controllare se il nostro progetto è incluso in un altro progetto come sottoprogetto. Questo sarà molto utile in seguito.

get_directory_property(IS_SUBPROJECT PARENT_DIRECTORY)

Opzioni del progetto

Prevediamo due opzioni.

La prima opzione — MYLIB_TESTING — per disabilitare i test modulari. Questo può essere necessario se siamo certi che i test siano a posto e vogliamo, ad esempio, solo installare o pacchettizzare il nostro progetto. Oppure se il nostro progetto è incluso come sottoprogetto — in questo caso, all'utente del nostro progetto non interessa eseguire i nostri test. Del resto, non testi le dipendenze che utilizzi?

option(MYLIB_TESTING "Abilita i test modulari" ON)

Inoltre, creeremo un'opzione separata MYLIB_COVERAGE per misurare la copertura del codice con i test, ma essa richiederà strumenti aggiuntivi, quindi dovrà essere abilitata esplicitamente.

option(MYLIB_COVERAGE "Abilita la misurazione della copertura del codice con i test" OFF)

Opzioni di compilazione

Naturalmente, siamo programmatori C++ in gamba, quindi vogliamo il livello massimo di diagnosi di compilazione dal compilatore. Nessun errore deve sfuggire.

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
)

Disattiveremo anche le estensioni, per rispettare completamente lo standard del linguaggio C++. Di default, in CMake sono abilitate.

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

Obiettivo principale

La nostra libreria è composta solo da file header, perciò non abbiamo alcun output sotto forma di librerie statiche o dinamiche. D'altra parte, per utilizzare la nostra libreria dall'esterno, deve essere installata, deve poter essere trovata nel sistema e collegata al proprio progetto, e insieme ad essa devono essere collegati anche questi header, nonché, possibilmente, alcune proprietà aggiuntive.

A tal fine, creiamo una libreria d'interfaccia.

add_library(mylib INTERFACE)

Colleghiamo gli header alla nostra libreria d'interfaccia.

L'uso moderno e alla moda di CMake implica che gli header, le proprietà, ecc. siano trasferiti tramite un'unica destinazione. Quindi, è sufficiente dire target_link_libraries(target PRIVATE dependency), e tutti gli header associati alla destinazione dependency, saranno disponibili per il codice sorgente appartenente alla destinazione target. Non è necessaria alcuna [target_]include_directories. Questo sarà dimostrato di seguito nell'analisi del file CMake per i test modulari.

Vale la pena prestare attenzione anche ai cosiddetti generator expressions: $.

Questo comando associa i titoli di cui abbiamo bisogno alla nostra libreria di interfaccia, e se la nostra libreria viene collegata a un qualsiasi obiettivo all'interno di una stessa gerarchia CMake, i titoli provenienti dalla directory ${CMAKE_CURRENT_SOURCE_DIR}/includesaranno associati; se invece la nostra libreria è installata nel sistema e collegata in un altro progetto tramite il comando find_packagei titoli associati saranno quelli provenienti dalla directory include rispetto alla directory di installazione.

target_include_directories(mylib INTERFACE
    $
    $
)

Impostiamo lo standard del linguaggio. Naturalmente, l'ultimo disponibile. Non ci limitiamo a includere lo standard, ma lo rendiamo disponibile anche a chi utilizza la nostra libreria. Questo si ottiene impostando la proprietà nella categoria INTERFACE (vedi il comando target_compile_features).

target_compile_features(mylib INTERFACE cxx_std_17)

Creiamo un alias per la nostra libreria. Per motivi estetici sarà in uno "spazio dei nomi" speciale. Questo sarà utile quando nella nostra libreria compariranno diversi moduli, e potremo collegarli indipendentemente l'uno dall'altro. Come in Boost, ad esempio.

add_library(Mylib::mylib ALIAS mylib)

Installazione

Installazione dei nostri titoli nel sistema. Qui è tutto semplice. Indichiamo che la cartella contenente tutti i titoli deve essere collocata nella directory include rispetto al percorso di installazione.

install(DIRECTORY include/mylib DESTINATION include)

Successivamente, informiamo il sistema di build che vogliamo avere la possibilità di invocare il comando find_package(Mylib) e ottenere l'obiettivo Mylib::mylib.

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

Il prossimo comando deve essere interpretato così. Quando in un progetto esterno invochiamo il comando find_package(Mylib 1.2.3 REQUIRED), se la reale versione della libreria installata risulta incompatibile con la versione 1.2.3, CMake genererà automaticamente un errore. Non sarà quindi necessario controllare manualmente le versioni.

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)

Test

Se i test sono disattivati esplicitamente tramite l'apposita opzione o il nostro progetto è un sottoprogetto, cioè è collegato a un altro progetto CMake tramite il comando add_subdirectory, non procediamo oltre nella gerarchia, e lo script che descrive i comandi per la generazione e l'esecuzione dei test non viene semplicemente eseguito.

if(NOT MYLIB_TESTING)
    message(STATUS "Il testing del progetto Mylib è disattivato")
elseif(IS_SUBPROJECT)
    message(STATUS "Mylib non è testato in modalità sottoprogetto")
else()
    add_subdirectory(test)
endif()

Documentazione

La documentazione non verrà generata nel caso di un sottoprogetto.

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

Sandbox online

Allo stesso modo, non ci sarà una sandbox online per il sottoprogetto.

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

Script per i test (test /CMakeLists.txt)

Test

Per prima cosa troviamo il pacchetto con il framework di test richiesto (sostituiscilo con il tuo preferito).

find_package(doctest 2.3.3 REQUIRED)

Creiamo il nostro file eseguibile con i test. Di solito nel file binario eseguibile aggiungo solo il file che conterrà la funzione main.

add_executable(mylib-unit-tests test_main.cpp)

E i file in cui sono descritti i test stessi li aggiungo in seguito. Ma non è necessario farlo in questo modo.

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

Colleghiamo le dipendenze. Nota che al nostro binario abbiamo associato solo gli obiettivi CMake necessari e non abbiamo chiamato il comando target_include_directories. Gli header dal framework di test e dal nostro Mylib::mylib, così come i parametri di compilazione (nel nostro caso è lo standard del linguaggio C++) sono passati insieme a questi obiettivi.

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

Infine, creiamo un obiettivo fittizio, la cui "build" è equivalente all'esecuzione dei test, e aggiungiamo questo obiettivo alla build predefinita (questo è gestito dall'attributo ALL). Questo significa che la build predefinita avvia l'esecuzione dei test, quindi non dimenticheremo mai di eseguirli.

add_custom_target(check ALL COMMAND mylib-unit-tests)

Copertura

Poi attiviamo la misurazione della copertura del codice, se è stata specificata l'opzione corrispondente. Non entrerò nei dettagli, poiché riguardano più lo strumento per le misurazioni della copertura, piuttosto che CMake. È importante solo notare che, a seguito dei risultati, verrà creato un obiettivo copertura, che facilita l'esecuzione della misurazione della copertura.

find_program(GCOVR_EXECUTABLE gcovr)
if(MYLIB_COVERAGE AND GCOVR_EXECUTABLE)
    message(STATUS "La misurazione della copertura del codice dei test è attivata")

    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 "È richiesto il programma gcovr per misurare la copertura del codice dei test")
endif()

Script per la documentazione (doc /CMakeLists.txt)

Trovato Doxygen.

find_package(Doxygen)

Controlliamo se l'utente ha impostato la variabile della lingua. Se sì, non la tocchiamo, se no, prendiamo il russo. Configuriamo poi i file del sistema Doxygen. Tutte le variabili necessarie, compresa la lingua, vengono incluse in fase di configurazione (vedi il comando configure_file).

Dopodiché creiamo un obiettivo doc, che eseguirà la generazione della documentazione. Poiché la generazione della documentazione non è una necessità fondamentale nel processo di sviluppo, l'obiettivo non sarà attivato per impostazione predefinita e dovrà essere avviato esplicitamente.

if (Doxygen_FOUND)
    if (NOT MYLIB_DOXYGEN_LANGUAGE)
        set(MYLIB_DOXYGEN_LANGUAGE Russian)
    endif()
    message(STATUS "La documentazione Doxygen verrà generata in ${MYLIB_DOXYGEN_LANGUAGE}")
    configure_file(Doxyfile.in Doxyfile)
    add_custom_target(doc COMMAND ${DOXYGEN_EXECUTABLE} ${CMAKE_CURRENT_BINARY_DIR}\/Doxyfile)
endif ()

Script per la sandbox online (online /CMakeLists.txt)

Qui troviamo il terzo Python e creiamo un obiettivo wandbox, che genera una richiesta corrispondente all'API del servizio Wandbox, e la invia. In risposta arriva un link alla sandbox pronta.

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 "Per creare una sandbox online è richiesto un interprete Python di terza versione")
endif()

Il progetto dall'esterno

Adesso vediamo come utilizzare tutto questo.

Compilazione

La costruzione di questo progetto, come quella di qualsiasi altro progetto nel sistema di build CMake, è composta da due fasi:

Generazione

cmake -S percorso\/ai\/sorgenti -B percorso\/alla\/directory\/di\/build [opzioni ...]

Se il comando sopra non funziona a causa di una versione obsoleta di CMake, prova a omettere -S:

cmake percorso\/ai\/sorgenti -B percorso\/alla\/directory\/di\/build [opzioni ...]

Maggiori dettagli sulle opzioni.

Costruzione del progetto

cmake --build percorso\/alla\/directory\/di\/build [--target target]

Maggiori dettagli sugli obiettivi di build.

Opzioni

MYLIB_COVERAGE

cmake -S ... -B ... -DMYLIB_COVERAGE=ON [altre opzioni ...]

Attiva l'obiettivo copertura, tramite la quale è possibile avviare la misurazione della copertura del codice tramite test.

MYLIB_TESTING

cmake -S ... -B ... -DMYLIB_TESTING=OFF [altre opzioni ...]

Fornisce la possibilità di disabilitare la compilazione dei test modulari e dell'obiettivo check. Di conseguenza, la misurazione della copertura del codice tramite test viene disabilitata (vedi MYLIB_COVERAGE).

La testazione viene anche disabilitata automaticamente se il progetto viene collegato a un altro progetto come sotto progetto utilizzando il comando add_subdirectory.

MYLIB_DOXYGEN_LANGUAGE

cmake -S ... -B ... -DMYLIB_DOXYGEN_LANGUAGE=English [altre opzioni ...]

Cambia la lingua della documentazione generata dall'obiettivo doc alla lingua specificata. L'elenco delle lingue disponibili è riportato su sito del sistema Doxygen.

Per impostazione predefinita è abilitato il russo.

Obiettivi di build

Per impostazione predefinita

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

Se non è specificato alcun obiettivo (il che equivale all'obiettivo all), compila tutto ciò che è possibile, e chiama anche l'obiettivo check.

mylib-unit-tests

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

Compila i test modulari. Abilitato per impostazione predefinita.

check

cmake --build percorso/alla/directory/di/compilazione --target check

Esegue i test modulari compilati (compila se non lo sono già). Abilitato per impostazione predefinita.

Vedi anche mylib-unit-tests.

copertura

cmake --build percorso/alla/directory/di/compilazione --target coverage

Analizza i test modulari eseguiti (esegue se non lo sono già) per la copertura del codice tramite il programma gcovr.

L'output della copertura apparirà circa così:

------------------------------------------------------------------------------
                           Rapporto di copertura del codice GCC
Directory: /percorso/to/cmakecpptemplate/include/
------------------------------------------------------------------------------
File                                       Linee    Exec  Copertura   Mancante
------------------------------------------------------------------------------
mylib/myfeature.hpp                            2       2   100%   
------------------------------------------------------------------------------
TOTALE                                          2       2   100%
------------------------------------------------------------------------------

L'obiettivo è disponibile solo se l'opzione è abilitata MYLIB_COVERAGE.

Vedi anche check.

doc

cmake --build percorso/alla/directory/di/compilazione --target doc

Avvia la generazione della documentazione del codice utilizzando il sistema Doxygen.

wandbox

cmake --build percorso/alla/directory/di/compilazione --target wandbox

La risposta dal servizio apparirà circa così:

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

A tal fine si utilizza il servizio Wandbox. Non so quanto siano elastici i loro server, ma penso che non sia il caso di abusare di questa possibilità.

Esempi

Compilazione del progetto in modalità debug con misurazione della copertura

cmake -S percorso/ai/sorgenti -B percorso/alla/directory/di/compilazione -DCMAKE_BUILD_TYPE=Debug -DMYLIB_COVERAGE=ON
cmake --build percorso/alla/directory/di/compilazione --target coverage --parallel 16

Installazione del progetto senza assemblaggio e test preliminari

cmake -S percorso/ai/sorgenti -B percorso/alla/directory/di/build -DMYLIB_TESTING=OFF -DCMAKE_INSTALL_PREFIX=percorso/alla/directory/di/installazione
cmake --build percorso/alla/directory/di/build --target install

Compilazione in modalità rilascio con il compilatore specificato

cmake -S percorso/ai/sorgenti -B percorso/alla/directory/di/build -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_COMPILER=g++-8 -DCMAKE_PREFIX_PATH=percorso/alla/directory/dove/sono/installate/le/dipendenze
cmake --build percorso/alla/directory/di/build --parallel 4

Generazione della documentazione in inglese

cmake -S percorso/ai/sorgenti -B percorso/alla/directory/di/build -DCMAKE_BUILD_TYPE=Release -DMYLIB_DOXYGEN_LANGUAGE=English
cmake --build percorso/alla/directory/di/build --target doc

Strumenti

  1. CMake 3.13

    In realtà, la versione di CMake 3.13 è necessaria solo per eseguire alcuni comandi console descritti in questa guida. Dal punto di vista della sintassi degli script CMake, va bene anche la versione 3.8 se la generazione viene invocata in altri modi.

  2. Libreria di test doctest

    I test possono essere disabilitati (vedi opzione MYLIB_TESTING).

  3. Doxygen

    Per cambiare la lingua in cui verrà generata la documentazione, è prevista un'opzione MYLIB_DOXYGEN_LANGUAGE.

  4. Interprete di linguaggio di programmazione Python 3

    Per generazione automatica sandbox online.

Analisi statica

Con CMake e un paio di buoni strumenti è possibile garantire un'analisi statica con il minimo sforzo.

Cppcheck

CMake include il supporto per uno strumento di analisi statica Cppcheck.

Per farlo, è necessario utilizzare l'opzione CMAKE_CXX_CPPCHECK:

cmake -S percorso/ai/sorgenti -B percorso/alla/directory/di/build -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_CPPCHECK="cppcheck;--enable=all;-Ipercorso/ai/sorgenti/include"

Dopo questo, l'analisi statica verrà eseguita automaticamente ogni volta che si compila e si ricompila il sorgente. Non è necessaria alcuna operazione aggiuntiva.

Clang

Con il meraviglioso strumento scan-build è possibile eseguire l'analisi statica in un batter d'occhio:

scan-build cmake -S percorso/ai/sorgenti -B percorso/alla/directory/di/build -DCMAKE_BUILD_TYPE=Debug
scan-build cmake --build percorso/alla/directory/di/build

Qui, a differenza del caso con Cppcheck, è necessario avviare la build ogni volta tramite scan-build.

Postfazione

CMake è un sistema molto potente e flessibile, che consente di implementare funzionalità di ogni genere. E, sebbene la sintassi a volte possa lasciare a desiderare, non è così spaventosa come la si fa sembrare. Utilizzate il sistema di build CMake a beneficio della società e per il vostro benessere.

Scarica il modello di progetto

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server 🔥 Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server | ProHoster