
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à:
- Build;
- Esecuzione automatica dei test;
- Misurazione della copertura del codice;
- Installazione;
- Autodocumentazione;
- Generazione di un sandbox online;
- Analisi statica.
Chi ha dimestichezza con 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.cppEssenzialmente, parleremo di come organizzare gli script CMake, quindi verranno analizzati in dettaglio. Gli altri file possono essere consultati direttamente .
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. ).
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)
Prevediamo due opzioni.
La prima opzione — — 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 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)
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()
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 , 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 .
Vale la pena prestare attenzione anche ai cosiddetti .
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 i 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 ).
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. .
add_library(Mylib::mylib ALIAS mylib)
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)
Se i test sono disattivati esplicitamente tramite o il nostro progetto è un sottoprogetto, cioè è collegato a un altro progetto CMake tramite il comando , 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()
La documentazione non verrà generata nel caso di un sottoprogetto.
if(NOT IS_SUBPROJECT)
add_subdirectory(doc)
endif()
Allo stesso modo, non ci sarà una sandbox online per il sottoprogetto.
if(NOT IS_SUBPROJECT)
add_subdirectory(online)
endif()
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)
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 , 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()
.
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 ).
Dopodiché creiamo un obiettivo , 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 ()
Qui troviamo il terzo Python e creiamo un obiettivo , che genera una richiesta corrispondente all'API del servizio , 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()
Adesso vediamo come utilizzare tutto questo.
La costruzione di questo progetto, come quella di qualsiasi altro progetto nel sistema di build CMake, è composta da due fasi:
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 ...]
.
cmake --build percorso\/alla\/directory\/di\/build [--target target].
cmake -S ... -B ... -DMYLIB_COVERAGE=ON [altre opzioni ...]Attiva l'obiettivo , tramite la quale è possibile avviare la misurazione della copertura del codice tramite test.
cmake -S ... -B ... -DMYLIB_TESTING=OFF [altre opzioni ...]Fornisce la possibilità di disabilitare la compilazione dei test modulari e dell'obiettivo . Di conseguenza, la misurazione della copertura del codice tramite test viene disabilitata (vedi ).
La testazione viene anche disabilitata automaticamente se il progetto viene collegato a un altro progetto come sotto progetto utilizzando 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 è riportato su .
Per impostazione predefinita è abilitato il russo.
cmake --build path/to/build/directory
cmake --build path/to/build/directory --target allSe non è specificato alcun obiettivo (il che equivale 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/compilazione --target checkEsegue i test modulari compilati (compila se non lo sono già). Abilitato per impostazione predefinita.
Vedi anche .
cmake --build percorso/alla/directory/di/compilazione --target coverageAnalizza i test modulari eseguiti (esegue se non lo sono già) per la copertura del codice tramite il programma .
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 .
Vedi anche .
cmake --build percorso/alla/directory/di/compilazione --target docAvvia la generazione della documentazione del codice utilizzando il sistema .
cmake --build percorso/alla/directory/di/compilazione --target wandboxLa risposta dal servizio apparirà circa così:
{
"permlink" : "QElvxuMzHgL9fqci",
"status" : "0",
"url" : "https://wandbox.org/permlink/QElvxuMzHgL9fqci"
}A tal fine si utilizza il servizio . Non so quanto siano elastici i loro server, ma penso che non sia il caso di abusare di questa possibilità.
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 16Installazione 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 installCompilazione 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 4Generazione 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
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.
Libreria di test
I test possono essere disabilitati (vedi ).
Per cambiare la lingua in cui verrà generata la documentazione, è prevista un'opzione .
Interprete di linguaggio di programmazione
Per generazione automatica .
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 .
Per farlo, è necessario 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 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 è 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/buildQui, a differenza del caso con Cppcheck, è necessario avviare la build ogni volta tramite scan-build.
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.
→
Fonte: habr.com
