
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à:
- Compilazione;
- Invio automatico dei test;
- Misurazione della copertura del codice;
- Installazione;
- Autodocumentazione;
- Generazione di un sandbox online;
- Analisi statica.
Chi è già esperto di C++ e CMake può semplicemente e iniziare a utilizzarlo.
Contenuto
.
├── 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.cppL'argomento principale sarà come organizzare gli script CMake, quindi saranno trattati in dettaglio. Gli altri file possono essere visionati direttamente da chiunque .
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 ).
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)
Prevediamo due opzioni.
La prima opzione — — 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 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)
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()
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 , 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 .
Vale anche la pena notare i cosiddetti .
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 , 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 ).
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. .
add_library(Mylib::mylib ALIAS mylib)
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)
Se i test sono disabilitati esplicitamente con o il nostro progetto è un sottoprogetto, cioè è incluso in un altro progetto CMake tramite il comando , 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()
La documentazione non verrà generata nemmeno nel caso di un sottoprogetto.
if(NOT IS_SUBPROJECT)
add_subdirectory(doc)
endif()
Allo stesso modo, non ci sarà un ambiente di prova online per il sottoprogetto.
if(NOT IS_SUBPROJECT)
add_subdirectory(online)
endif()
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)
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. , 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()
.
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 ).
Dopo di che creiamo un obiettivo , 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 ()
Qui troviamo il terzo Python e creiamo un obiettivo , che genera una richiesta corrispondente all'API del servizio , 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()
Ora vediamo come utilizzare tutto questo.
La costruzione di questo progetto, come di qualsiasi altro progetto nel sistema di build CMake, si compone di due fasi:
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 ...]
.
cmake --build percorso/verso/la/directory/di/build [--target target].
cmake -S ... -B ... -DMYLIB_COVERAGE=ON [altre opzioni ...]Include l'obiettivo , che consente di eseguire la misurazione della copertura del codice con i test.
cmake -S ... -B ... -DMYLIB_TESTING=OFF [altre opzioni ...]Fornisce la possibilità di disabilitare la compilazione dei test di unità e l'obiettivo . Di conseguenza, viene disabilitata la misurazione della copertura del codice con i test (vedi ).
Inoltre, il testing è automaticamente disabilitato se il progetto è incluso in un altro progetto come sottoprogetto tramite il comando .
cmake -S ... -B ... -DMYLIB_DOXYGEN_LANGUAGE=English [altre opzioni ...]Cambia la lingua della documentazione generata dall'obiettivo alla lingua specificata. L'elenco delle lingue disponibili è disponibile su .
Per impostazione predefinita è abilitato il russo.
cmake --build percorso/verso/la/directory/di/build
cmake --build percorso/verso/la/directory/di/build --target allSe l'obiettivo non è specificato (ciò che è equivalente all'obiettivo all), compila tutto ciò che è possibile e chiama anche l'obiettivo .
cmake --build path/to/build/directory --target mylib-unit-testsCompila i test modulari. Abilitato per impostazione predefinita.
cmake --build percorso/alla/directory/di/build --target checkEsegue i test modulari già compilati (li compila se non sono ancora stati eseguiti). Abilitato per impostazione predefinita.
Vedi anche .
cmake --build percorso/alla/directory/di/build --target coverageAnalizza i test modulari eseguiti (li esegue se non sono stati ancora avviati) per la copertura del codice utilizzo del programma .
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 .
Vedi anche .
cmake --build percorso/alla/directory/di/build --target docAvvia la generazione della documentazione del codice tramite il sistema .
cmake --build percorso/alla/directory/di/build --target wandboxLa risposta dal servizio appare più o meno così:
{
"permlink" : "QElvxuMzHgL9fqci",
"status" : "0",
"url" : "https://wandbox.org/permlink/QElvxuMzHgL9fqci"
}Questo servizio viene utilizzato per . Non so quanto siano flessibili i loro server, ma credo che non valga la pena abusare di questa possibilità.
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 16Installazione 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 installCompilazione 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 4Generazione 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
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.
Libreria di test
Il testing può essere disattivato (vedi ).
Per cambiare la lingua in cui verrà generata la documentazione, è prevista l'opzione .
Interprete di linguaggi di programmazione
Per la generazione automatica .
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 .
Per questo bisogna utilizzare l'opzione :
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 è 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/buildQui, a differenza del caso di Cppcheck, è necessario avviare la build ogni volta attraverso scan-build.
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.
→
Fonte: habr.com
