CMake e C++ — fratelli per sempre

CMake e C++ — fratelli per sempre

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

Molti criticano CMake, spesso a ragione, ma se ci si ferma a riflettere, non è tutto nero, e ultimamente è davvero molto buono, e la direzione dello sviluppo è piuttosto positiva.

In questo articolo, voglio spiegare come sia piuttosto semplice organizzare una libreria di intestazione in C++ all'interno di un sistema CMake, per ottenere la seguente funzionalità:

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

Chi è già esperto di C++ e CMake può semplicemente scaricare il modello del progetto e iniziare a utilizzarlo.


Contenuto

  1. Struttura interna del progetto
    1. Struttura del progetto
    2. File principale di CMake (./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 sandbox online (online/CMakeLists.txt)
  2. Progetto esterno
    1. Build
      1. Generazione
      2. Build
    2. Opzioni
      1. MYLIB_COVERAGE
      2. MYLIB_TESTING
      3. MYLIB_DOXYGEN_LANGUAGE
    3. Obiettivi di build
      1. Di default
      2. mylib-unit-tests
      3. controlla
      4. coverage
      5. doc
      6. wandbox
    4. Esempi
  3. Strumenti
  4. Analisi statica
  5. Epifania

Struttura interna del progetto

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

L'argomento principale sarà come organizzare gli script CMake, quindi saranno trattati in dettaglio. Gli altri file possono essere visionati direttamente da chiunque sulla pagina del progetto modello.

File principale di CMake (./CMakeLists.txt)

Informazioni sul progetto

Per prima cosa è necessario richiedere la versione corretta del sistema CMake. CMake è in continua evoluzione, le firme dei comandi e il comportamento in diverse condizioni cambiano. Per far sì che CMake comprenda subito le nostre richieste, è necessario fissare subito le nostre necessità.

cmake_minimum_required(VERSION 3.13)

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

In questo caso indichiamo il linguaggio CXX (e questo significa C++), in modo che CMake non si preoccupi e non cerchi il compilatore del linguaggio C (per impostazione predefinita, CMake include due linguaggi: C e C++).

project(Mylib VERSION 1.0 LANGUAGES CXX)

Qui possiamo anche controllare subito se il nostro progetto è incluso in un altro progetto come sotto-progetto. 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 disattivare i test modulari. Questo potrebbe essere necessario se siamo certi che i test funzionino correttamente e vogliamo, per esempio, semplicemente installare o impacchettare il nostro progetto. Oppure, se il nostro progetto è incluso come sotto-progetto — in questo caso l'utente del nostro progetto non è interessato a eseguire i nostri test. Non testate le dipendenze che utilizzate, vero?

option(MYLIB_TESTING "Abilita il testing modulare" ON)

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

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

Opzioni di compilazione

Naturalmente, siamo programmatori C++ esperti, quindi vogliamo dal compilatore il massimo livello di diagnostica durante la compilazione. Nessun errore può 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
)

Disabiliteremo anche le estensioni per aderire completamente allo standard del linguaggio C++. Per impostazione predefinita, sono attivate in CMake.

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

Obiettivo principale

La nostra libreria è composta esclusivamente da file header, quindi non abbiamo alcun output in forma di librerie statiche o dinamiche. D'altra parte, per utilizzare la nostra libreria esternamente, deve essere installata, affinché possa essere trovata nel sistema e collegata al proprio progetto, e insieme ad essa devono essere legati gli stessi header, oltre a eventuali altre proprietà.

Per questo scopo, creiamo una libreria interfaccia.

add_library(mylib INTERFACE)

Colleghiamo gli header alla nostra libreria interfaccia.

L'uso moderno, alla moda e giovanile di CMake implica che intestazioni, proprietà, ecc. vengano passate tramite un'unica destinazione. Così, basta dire target_link_libraries(target PRIVATE dependency), e tutte le intestazioni associate alla destinazione dependency, saranno disponibili per i sorgenti appartenenti alla destinazione target. Non sono necessarie ulteriori [target_]include_directories. Questo sarà dimostrato di seguito esaminando uno script CMake per test modulari.

Vale anche la pena notare i cosiddetti generator expressions: $.

Questo comando associa le intestazioni necessarie alla nostra libreria di interfaccia; se la nostra libreria è collegata a qualche obiettivo all'interno della stessa gerarchia CMake, le intestazioni dalla directory ${CMAKE_CURRENT_SOURCE_DIR}/include, saranno associate, mentre se la nostra libreria è installata nel sistema e collegata a un altro progetto tramite il comando find_package, le intestazioni saranno associate dalla directory include rispetto alla directory di installazione.

target_include_directories(mylib INTERFACE
    $
    $
)

Imposteremo lo standard della lingua. Ovviamente, l'ultima versione disponibile. Non ci limitiamo a includere lo standard, ma lo estendiamo anche a coloro che utilizzeranno la nostra libreria. Questo si ottiene attraverso una proprietà impostata che ha una categoria INTERFACE (vedi il comando target_compile_features).

target_compile_features(mylib INTERFACE cxx_std_17)

Creiamo un alias per la nostra libreria. E per avere un tocco estetico, sarà in uno speciale "namespace". Questo sarà utile quando la nostra libreria avrà diversi moduli e potremo collegarli in modo indipendente l'uno dall'altro. Proprio come in Boost, ad esempio.

add_library(Mylib::mylib ALIAS mylib)

Installazione

Posizioniamo i nostri header nel sistema. Qui è tutto semplice. Diciamo che la cartella contenente tutti gli header deve andare nella directory include in relazione al posto di installazione.

install(DIRECTORY include/mylib DESTINATION include)

Successivamente informiamo il sistema di build che vogliamo avere la possibilità di chiamare il comando in progetti esterni find_package(Mylib) e ricevere il target Mylib::mylib.

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

La prossima istruzione deve essere compresa in questo modo. Quando in un progetto esterno chiameremo il comando find_package(Mylib 1.2.3 REQUIRED), e la versione reale della libreria installata risulterà incompatibile con la versione 1.2.3, CMake genererà automaticamente un errore. Quindi non sarà necessario tenere traccia delle versioni manualmente.

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 disabilitati esplicitamente con l'apposita opzione o il nostro progetto è un sottoprogetto, cioè è incluso in un altro progetto CMake tramite il comando add_subdirectory, non proseguiamo oltre nella gerarchia, e lo script che descrive i comandi per generare ed eseguire i test non verrà semplicemente eseguito.

if(NOT MYLIB_TESTING)
    message(STATUS "Test del progetto Mylib disabilitato")
elseif(IS_SUBPROJECT)
    message(STATUS "Mylib non viene testato in modalità sottoprodotto")
else()
    add_subdirectory(test)
endif()

Documentazione

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

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

Sandbox online

Allo stesso modo, non ci sarà un ambiente di prova 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 necessario (sostituisci con il tuo preferito).

find_package(doctest 2.3.3 REQUIRED)

Creiamo il nostro file eseguibile con i test. Di solito, aggiungo direttamente solo il file in cui sarà la funzione. main.

add_executable(mylib-unit-tests test_main.cpp)

I file in cui sono descritti i test 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 abbiamo associato al nostro file eseguibile solo gli obiettivi CMake necessari e non abbiamo chiamato il comando target_include_directories. Le intestazioni del framework di test e delle nostre Mylib::mylib, insieme ai parametri di compilazione (nel nostro caso è lo standard del linguaggio C++) sono state incluse 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

Attiviamo la misurazione della copertura del codice, se è stata impostata l'apposita opzione. Non entrerò nei dettagli, poiché riguardano più uno strumento per la misurazione della copertura che CMake. È importante solo notare che in base ai risultati verrà creata un'obiettivo. coverage, utile per avviare comodamente la misurazione della copertura.

find_program(GCOVR_EXECUTABLE gcovr)
if(MYLIB_COVERAGE AND GCOVR_EXECUTABLE)
    message(STATUS "La misurazione della copertura del codice tramite i test è abilitata")

    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 "Per la misurazione della copertura del codice tramite i test è necessario il programma gcovr")
endif()

Script per la documentazione (doc/CMakeLists.txt)

Trovato Doxygen.

find_package(Doxygen)

Verifichiamo quindi se l'utente ha impostato una variabile per la lingua. Se sì, non facciamo nulla; se no, prendiamo il russo. Quindi configuriamo i file del sistema Doxygen. Tutte le variabili necessarie, compresa la lingua, vengono aggiunte durante il processo di configurazione (vedi il comando configure_file).

Dopo di che creiamo un obiettivo doc, che avvierà la generazione della documentazione. Poiché la generazione della documentazione non è la priorità principale del processo di sviluppo, l'obiettivo non sarà incluso per impostazione predefinita e dovrà essere avviato esplicitamente.

if (Doxygen_FOUND)
    if (NOT MYLIB_DOXYGEN_LANGUAGE)
        set(MYLIB_DOXYGEN_LANGUAGE Italian)
    endif()
    message(STATUS "La documentazione di 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 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 si riceve 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()

Progetto esterno

Ora vediamo come utilizzare tutto questo.

Build

La costruzione di questo progetto, come di qualsiasi altro progetto nel sistema di build CMake, si compone di due fasi:

Generazione

cmake -S percorso/verso/i/sorgenti -B percorso/verso/la/directory/di/build [opzioni ...]

Se il comando precedente non ha funzionato a causa di una versione obsoleta di CMake, prova a omettere -S:

cmake percorso/verso/i/sorgenti -B percorso/verso/la/directory/di/build [opzioni ...]

Ulteriori informazioni sulle opzioni.

Compilazione del progetto

cmake --build percorso/verso/la/directory/di/build [--target target]

Ulteriori informazioni sugli obiettivi di build.

Opzioni

MYLIB_COVERAGE

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

Include l'obiettivo coverage, che consente di eseguire la misurazione della copertura del codice con i test.

MYLIB_TESTING

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

Fornisce la possibilità di disabilitare la compilazione dei test di unità e l'obiettivo controlla. Di conseguenza, viene disabilitata la misurazione della copertura del codice con i test (vedi MYLIB_COVERAGE).

Inoltre, il testing è automaticamente disabilitato se il progetto è incluso in un altro progetto come sottoprogetto tramite 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 è disponibile su sito del sistema Doxygen.

Per impostazione predefinita è abilitato il russo.

Obiettivi di build

Di default

cmake --build percorso/verso/la/directory/di/build
cmake --build percorso/verso/la/directory/di/build --target all

Se l'obiettivo non è specificato (ciò che è equivalente all'obiettivo all), compila tutto ciò che è possibile e chiama anche l'obiettivo controlla.

mylib-unit-tests

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

Compila i test modulari. Abilitato per impostazione predefinita.

controlla

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

Esegue i test modulari già compilati (li compila se non sono ancora stati eseguiti). Abilitato per impostazione predefinita.

Vedi anche mylib-unit-tests.

coverage

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

Analizza i test modulari eseguiti (li esegue se non sono stati ancora avviati) per la copertura del codice utilizzo del programma gcovr.

L'output della copertura sarà simile a questo:

------------------------------------------------------------------------------
                           Rapporto di Copertura del Codice GCC
Directory: /path/to/cmakecpptemplate/include/
------------------------------------------------------------------------------
File                                       Lines    Exec  Cover   Missing
------------------------------------------------------------------------------
mylib/myfeature.hpp                            2       2   100%   
------------------------------------------------------------------------------
TOTAL                                          2       2   100%
------------------------------------------------------------------------------

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

Vedi anche controlla.

doc

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

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

wandbox

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

La risposta dal servizio appare più o meno così:

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

Questo servizio viene utilizzato per Wandbox. Non so quanto siano flessibili i loro server, ma credo che non valga la pena abusare di questa possibilità.

Esempi

Compilazione del progetto in modalità debug con misurazione della copertura

cmake -S percorso/del/sorgente -B percorso/della/directory/di/compilazione -DCMAKE_BUILD_TYPE=Debug -DMYLIB_COVERAGE=ON
cmake --build percorso/della/directory/di/compilazione --target coverage --parallel 16

Installazione del progetto senza compilazione e test preliminari

cmake -S percorso/del/sorgente -B percorso/della/directory/di/compilazione -DMYLIB_TESTING=OFF -DCMAKE_INSTALL_PREFIX=percorso/della/directory/di/installazione
cmake --build percorso/della/directory/di/compilazione --target install

Compilazione in modalità release con il compilatore specificato

cmake -S percorso/del/sorgente -B percorso/della/directory/di/compilazione -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_COMPILER=g++-8 -DCMAKE_PREFIX_PATH=percorso/della/directory/delle/dependenze
cmake --build percorso/della/directory/di/compilazione --parallel 4

Generazione della documentazione in inglese

cmake -S percorso/del/sorgente -B percorso/della/directory/di/compilazione -DCMAKE_BUILD_TYPE=Release -DMYLIB_DOXYGEN_LANGUAGE=English
cmake --build percorso/della/directory/di/compilazione --target doc

Strumenti

  1. CMake 3.13

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

  2. Libreria di test doctest

    Il testing può essere disattivato (vedi l'opzione MYLIB_TESTING).

  3. Doxygen

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

  4. Interprete di linguaggi di programmazione Python 3

    Per la 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 ha il supporto integrato per lo strumento di analisi statica Cppcheck.

Per questo bisogna 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 di ciò, l'analisi statica verrà eseguita automaticamente ogni volta che si compila e ricompila il codice sorgente. Non è necessario fare altro.

Clang

Con l'incantevole strumento scan-build è possibile eseguire l'analisi statica in un attimo:

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 di Cppcheck, è necessario avviare la build ogni volta attraverso scan-build.

Epifania

CMake è un sistema molto potente e flessibile che consente di implementare funzionalità di ogni tipo. E, sebbene la sintassi a volte lasci a desiderare, non è così terribile come si dipinge. Utilizzate CMake per il bene della comunità e a beneficio della salute.

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