Salta al contenuto
Note per Studenti CMake - build di progetti C++

CMake - build di progetti C++

In questa pagina 7

Fin qui i programmi si compilavano con un comando come g++ main.cpp rational.cpp -o main. Con un progetto su più file, più librerie e più eseguibili (C++ - buona interfaccia e progetti su più fileUna classe C++ ben progettata decide esplicitamente le sette operazioni essenziali (costruttore con argomenti, di default, di copia, assegnamento di copia, costruttore e assegnamento di spostamento, distruttore): se serve il distruttore perché si possiede una risorsa, servono quasi sempre anche copia e spostamento; un costruttore a un argomento definisce una conversione implicita, che si blocca con explicit; per capire quando i costruttori e il distruttore vengono chiamati si traccia una struct che stampa ogni chiamata. Un progetto su più file separa interfaccia (header .h con definizioni di classe e dichiarazioni, protetto da include guard) e implementazione (.cpp, compilati uno per uno, mai inclusi); al compilatore si passano solo i .cpp, il main sta in un file a sé; gli errori di compilazione (sintassi, tipi, dichiarazioni mancanti) vengono dal compilatore, quelli di linking (definizione mancante, due main, nessun main) dal linker.C++ - buona interfaccia e progetti su più file →) il comando diventa lungo, va riscritto a ogni compilazione e ricompila tutto anche se è cambiato un file solo. Questa nota descrive lo strumento che risolve il problema: CMake.

Descrivere un progetto

Un progetto software può contenere uno o più eseguibili e una o più librerie; i progetti grandi si dividono in progetti più piccoli. I soli sorgenti non bastano a descriverlo: serve sapere come organizzare la compilazione. Per ciascun target di compilazione (un eseguibile o una libreria) bisogna dichiarare:

  • la lista dei .cpp da compilare insieme;
  • la lista delle librerie da linkare;
  • dove cercare gli header, lo standard del linguaggio (-std=...), le opzioni (-O2, -Wall).

Modi di descrivere un progetto, in ordine di generalità crescente:

modo problema
comando di compilazione scritto a mano poco pratico: si riscrive tutto a ogni compilazione, ricompilazione completa
progetto dell'IDE dipende dall'IDE in uso, poco portabile
sistema di build (Make, Ninja, ...) gestisce il processo di compilazione (compila solo ciò che è cambiato) ma spesso dipende dalla piattaforma
meta-sistema di build (CMake) un'unica descrizione, indipendente da compilatore e piattaforma

Definizione (CMake). CMake è un meta-sistema di build: non compila nulla da solo. Legge la descrizione del progetto in un file di testo CMakeLists.txt e genera i file del sistema di build nativo (i Makefile per Make, un progetto per Visual Studio o Codelite, ...), che poi esegue la compilazione.

CMakeLists.txt  --cmake-->  file nativi (Makefile, progetto IDE)  --make-->  eseguibili e librerie

Si usa in abbinamento al sistema nativo, è supportato da molti IDE (incluso Codelite) ed è utile per distribuire il codice con le indicazioni per compilarlo.

Lato utente: configurazione e compilazione

Chi scarica un progetto gestito con CMake riceve, oltre ai sorgenti, la descrizione del progetto, un sistema che prepara la compilazione e uno per compilare solo i moduli voluti. Esempio di progetto:

Rational/
├── include/
│   └── rational.h
├── CMakeLists.txt
└── src/
    ├── main.cpp
    └── rational.cpp

Le attività sono due, configurazione e compilazione, e si fanno in una cartella build creata a parte:

user@host:~/Rational$ mkdir build
user@host:~/Rational$ cd build
user@host:~/Rational/build$ cmake ..          # configurazione: legge ../CMakeLists.txt
user@host:~/Rational/build$ make              # compilazione (oppure: cmake --build .)

Il .. di cmake .. è la cartella che contiene CMakeLists.txt. Equivalente senza spostarsi: cmake -S . -B build per configurare e cmake --build build per compilare.

Perché una cartella build separata (build out-of-source): non si mescolano i file prodotti (oggetti, librerie, eseguibili, cache di CMake) con i sorgenti, e per tornare alla situazione iniziale basta cancellare la cartella. Il file CMakeLists.txt generato da un IDE, in generale, non va modificato a mano: può essere sovrascritto.

Lato sviluppatore: scrivere il CMakeLists.txt

Il file si costruisce riga per riga. Questo è l'esempio del corso, commentato dopo ogni aggiunta.

CMakeLists.txtcmake
cmake_minimum_required(VERSION 3.10)
set(CMAKE_CXX_STANDARD 11)
project(rational)

include_directories(include)

add_library(${PROJECT_NAME} SHARED
            src/rational.cpp)

add_executable(main src/main.cpp)

target_link_libraries(main ${PROJECT_NAME})

install(TARGETS ${PROJECT_NAME}
        LIBRARY DESTINATION lib
        INCLUDES DESTINATION include)
riga cosa fa
cmake_minimum_required(VERSION ...) versione minima di CMake richiesta dal file
set(CMAKE_CXX_STANDARD 11) compila in C++11; equivale al flag -std=c++11
project(rational) nome del progetto, riutilizzabile come ${PROJECT_NAME}
include_directories(include) cartella in cui cercare gli header (relativa alla posizione di CMakeLists.txt): così #include "rational.h" funziona
add_library(${PROJECT_NAME} SHARED src/rational.cpp) crea la libreria librational.so dal sorgente rational.cpp; SHARED = dinamica, STATIC = statica (librational.a)
add_executable(main src/main.cpp) crea l'eseguibile main dal sorgente main.cpp
target_link_libraries(main ${PROJECT_NAME}) linka l'eseguibile main alla libreria librational
install(TARGETS ...) installa libreria e header nel sistema (make install)

Due osservazioni sul testo delle slide.

  • Nelle slide compare cmake_minimum_required(VERSION 2.84): una versione 2.84 di CMake non esiste (probabilmente si intendeva la 2.8.4). Le versioni di CMake dalla 4.0 in poi rifiutano le richieste inferiori alla 3.5, quindi in un progetto nuovo si scrive una versione plausibile come 3.10.
  • Nelle slide appare anche find_package(Qt5 REQUIRED COMPONENTS Core): serve a cercare nel sistema una libreria esterna (qui Qt) e a collegarla. È solo un esempio: se Qt non è installato, la configurazione si interrompe con errore, perciò in un progetto che usa solo la libreria standard la riga va tolta.

Il file del corso per il laboratorio di Rational usa anche install(TARGETS main DESTINATION $ENV{HOME}/Local/bin), per mettere l'eseguibile nella cartella dell'utente senza permessi di amministratore.

Una versione più moderna

Lo stesso progetto, con i comandi che legano le proprietà ai singoli target invece che a tutto il progetto:

CMakeLists.txtcmake
cmake_minimum_required(VERSION 3.10)
project(rational LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

add_library(rational src/rational.cpp)                  # STATIC di default
target_include_directories(rational PUBLIC include)     # chi usa la libreria vede gli header
target_compile_options(rational PRIVATE -O2)

add_executable(main src/main.cpp)
target_link_libraries(main PRIVATE rational)
target_compile_options(main PRIVATE -O2)

Differenze rispetto al file del corso:

  • target_include_directories(rational PUBLIC include) dichiara la cartella degli header sul target: PUBLIC vuol dire che serve sia per compilare la libreria sia per compilare chi la usa, e target_link_libraries(main ... rational) la propaga a main da sola.
  • PRIVATE / PUBLIC / INTERFACE indicano a chi si applica una proprietà: solo al target (PRIVATE), al target e a chi lo linka (PUBLIC), solo a chi lo linka (INTERFACE).
  • CMAKE_CXX_STANDARD_REQUIRED ON impedisce che CMake ripieghi su uno standard più vecchio.
  • -O2 attiva le ottimizzazioni (richiesto dal progetto finale del corso). In alternativa si può configurare con cmake -DCMAKE_BUILD_TYPE=Release .. (Release usa -O3 -DNDEBUG), oppure scrivere set(CMAKE_BUILD_TYPE Release) nel file.

Le librerie di soli template

Una libreria fatta di soli template (come MyVector<T> dei laboratori, v. C++ - template e programmazione genericaun template permette di usare un tipo (o un intero noto in compilazione) come parametro di una classe o di una funzione; il compilatore genera il codice per ogni tipo per cui il template è specializzato (template instantiation, a tempo di compilazione): il codice è efficiente ma gli errori sono lunghi e oscuri, e il compilatore controlla sia il template sia il suo argomento; la notazione è template (o class T, equivalenti) davanti alla classe o alla funzione, e le funzioni membro fuori dalla classe si scrivono template tipo Classe::nome(...); nelle function template il compilatore deduce gli argomenti dai parametri della chiamata (myMax(i, j) invece di myMax(i, j)), ma due argomenti di tipo diverso per lo stesso T sono un errore; una specializzazione esplicita (template<> ...) fornisce il codice per un tipo particolare; i parametri interi (template<typename T, int N> struct array) danno array di dimensione fissa senza free store, più efficienti e che esprimono concetti come la tripletta RGB; poiché il compilatore deve vedere la definizione completa al punto d'uso, le definizioni si mettono nell'header (spesso in un file .hpp incluso in coda al .h).C++ - template e programmazione generica →) non genera nessuna libreria precompilata: il codice dei template viene istanziato nel file che li usa, quindi tutto deve stare negli header. Per questo motivo non c'è nessun .cpp da compilare. In CMake si usa una libreria di tipo INTERFACE, "composta di soli header":

CMakeLists.txtcmake
cmake_minimum_required(VERSION 3.10)
set(CMAKE_CXX_STANDARD 11)
project(myvector)

add_library(${PROJECT_NAME} INTERFACE)
target_include_directories(${PROJECT_NAME} INTERFACE
    $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
    $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/src>
)

add_executable(main src/main.cpp)
target_link_libraries(main ${PROJECT_NAME})

La libreria INTERFACE ha solo le cartelle in cui trovare i file .h e .hpp; chi la linka (main) eredita quelle cartelle. L'espressione $<BUILD_INTERFACE:...> significa "questo percorso vale quando si compila questo progetto"; ${CMAKE_CURRENT_SOURCE_DIR} è la cartella del CMakeLists.txt. Nel progetto del laboratorio 9 i file .hpp di MyVector stanno in src, per questo anche quella cartella è tra le directory di include; per il rapporto tra template e header vedi C++ - template e programmazione genericaun template permette di usare un tipo (o un intero noto in compilazione) come parametro di una classe o di una funzione; il compilatore genera il codice per ogni tipo per cui il template è specializzato (template instantiation, a tempo di compilazione): il codice è efficiente ma gli errori sono lunghi e oscuri, e il compilatore controlla sia il template sia il suo argomento; la notazione è template (o class T, equivalenti) davanti alla classe o alla funzione, e le funzioni membro fuori dalla classe si scrivono template tipo Classe::nome(...); nelle function template il compilatore deduce gli argomenti dai parametri della chiamata (myMax(i, j) invece di myMax(i, j)), ma due argomenti di tipo diverso per lo stesso T sono un errore; una specializzazione esplicita (template<> ...) fornisce il codice per un tipo particolare; i parametri interi (template<typename T, int N> struct array) danno array di dimensione fissa senza free store, più efficienti e che esprimono concetti come la tripletta RGB; poiché il compilatore deve vedere la definizione completa al punto d'uso, le definizioni si mettono nell'header (spesso in un file .hpp incluso in coda al .h).C++ - template e programmazione generica →.

Un progetto con più librerie

Una libreria può dipendere da un'altra: libinertialdriver usa libmyvector, e l'eseguibile usa libinertialdriver. Basta dichiarare le dipendenze con target_link_libraries; CMake ricava l'ordine di compilazione. Lo schema (sviluppato nell'esercizio Esercizio - Ristrutturare un progetto con CMake (laboratorio 9)):

CMakeLists.txtcmake
add_library(myvector INTERFACE)
target_include_directories(myvector INTERFACE include)

add_library(inertialdriver src/inertial_driver.cpp)
target_link_libraries(inertialdriver PUBLIC myvector)   # inertialdriver usa myvector

add_executable(main src/main.cpp)
target_link_libraries(main PRIVATE inertialdriver)      # main usa inertialdriver

Con PUBLIC la dipendenza da myvector viene propagata a main, che infatti la vede transitivamente e può includere gli header di myvector.

Errori frequenti

  • Compilare nella cartella dei sorgenti invece che in build: i file generati si mescolano ai sorgenti.
  • Cambiare il CMakeLists.txt e dimenticare di riconfigurare: make di solito riesegue cmake da solo, ma se la configurazione resta incoerente si cancella la cartella build e si riparte.
  • Dimenticare un .cpp in add_library / add_executable: il compilatore non si accorge di niente, il linker dà undefined reference.
  • Una libreria di template dichiarata come STATIC / SHARED senza sorgenti: CMake segnala che non c'è nessun file sorgente. Va INTERFACE.
  • Lasciare find_package di librerie che non servono: la configurazione fallisce su un'altra macchina.
  • Più di un CMakeLists.txt: il progetto finale ne richiede uno solo, nella cartella principale.

Il codice di questa nota non è stato eseguito con cmake, perché lo strumento non è installato nell'ambiente di verifica: i file CMake seguono la documentazione ufficiale, e le stesse compilazioni sono state provate a mano con i comandi equivalenti (vedi l'esercizio citato).

Domande d'esame

1. Cos'è CMake e quale problema risolve? Che differenza c'è tra configurazione e compilazione? Traccia. Meta-sistema di build: descrive target (eseguibili e librerie), sorgenti, librerie da linkare in CMakeLists.txt, indipendentemente da compilatore e piattaforma. La configurazione (cmake ..) legge la descrizione e genera i file del sistema nativo; la compilazione (make) esegue il sistema nativo e produce gli eseguibili. Si lavora in build per non mescolare i file generati ai sorgenti.

2. Scrivere il CMakeLists.txt di un progetto con include/rational.h, src/rational.cpp, src/main.cpp: una libreria e un eseguibile che la usa. Traccia. cmake_minimum_required, project, add_library(rational src/rational.cpp), target_include_directories(rational PUBLIC include), add_executable(main src/main.cpp), target_link_libraries(main PRIVATE rational).

3. Perché una libreria di soli template si dichiara INTERFACE? Traccia. I template sono istanziati nel file che li usa, quindi non esiste codice precompilato da mettere in una libreria; resta solo da indicare agli utenti le cartelle degli header.

Versione ripasso

CMakeLists.txtcmake
cmake_minimum_required(VERSION 3.10)
set(CMAKE_CXX_STANDARD 11)
project(rational)

include_directories(include)

add_library(${PROJECT_NAME} SHARED
            src/rational.cpp)

add_executable(main src/main.cpp)

target_link_libraries(main ${PROJECT_NAME})

install(TARGETS ${PROJECT_NAME}
        LIBRARY DESTINATION lib
        INCLUDES DESTINATION include)
  • Codice (CMakeLists.txt):
CMakeLists.txtcmake
cmake_minimum_required(VERSION 3.10)
project(rational LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

add_library(rational src/rational.cpp)                  # STATIC di default
target_include_directories(rational PUBLIC include)     # chi usa la libreria vede gli header
target_compile_options(rational PRIVATE -O2)

add_executable(main src/main.cpp)
target_link_libraries(main PRIVATE rational)
target_compile_options(main PRIVATE -O2)
  • Codice (CMakeLists.txt):
CMakeLists.txtcmake
cmake_minimum_required(VERSION 3.10)
set(CMAKE_CXX_STANDARD 11)
project(myvector)

add_library(${PROJECT_NAME} INTERFACE)
target_include_directories(${PROJECT_NAME} INTERFACE
    $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
    $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/src>
)

add_executable(main src/main.cpp)
target_link_libraries(main ${PROJECT_NAME})
  • Codice (CMakeLists.txt):
CMakeLists.txtcmake
add_library(myvector INTERFACE)
target_include_directories(myvector INTERFACE include)

add_library(inertialdriver src/inertial_driver.cpp)
target_link_libraries(inertialdriver PUBLIC myvector)   # inertialdriver usa myvector

add_executable(main src/main.cpp)
target_link_libraries(main PRIVATE inertialdriver)      # main usa inertialdriver

Esercizi su questo argomento

Teoria collegata