1. Cosa sono i Preset e dove vivono
Se avete mai sentito la frase «sul mio computer funziona», capite già intuitivamente perché sono nati i CMake Presets. Il problema non è che qualcuno sia un cattivo programmatore, ma che le impostazioni di build spesso risiedono in decine di posti: nell'IDE, nelle variabili d'ambiente, nella cache di CMake, in diverse cartelle di build e nelle teste dei membri del team (il deposito più instabile di tutti).
Immaginiamo la situazione. Su CLion avete selezionato Debug, sull'IDE del vostro collega per qualche motivo è Release, e una terza persona compila da terminale e una volta ha dimenticato di specificare lo standard C++23. Il codice sorgente è lo stesso, ma il comportamento «salta»: da qualche parte assert si è attivato, da qualche altra no; in qualche caso l'ottimizzazione ha riordinato il codice così che il debugger mostra «variabile non disponibile»; e in altri casi il linking ha improvvisamente preso una versione diversa di una libreria.
CMake Presets sono un modo per dire: «Ecco gli scenari ufficiali di build del progetto. Sono descritti in un file, sono versionati nel repository e tutti usiamo le stesse impostazioni». Non «a parole uguali», ma davvero uguali: stesse variabili chiave di configurazione, stesse directory di build, stessi comandi.
Cosa è un preset: un piccolo JSON che salva grandi nervi
Un preset è uno scenario nominato per CMake. Il più delle volte lo scenario è diviso in più fasi: prima configuriamo il progetto (CMake genera i file di build nella cartella di build), poi costruiamo, e a volte anche eseguiamo i test. I CMake Presets permettono di descrivere queste fasi in modo che voi (o il vostro docente, o la CI) possiate dire: «Costruisci il preset debug» — e non dover ricordare quali flag, percorsi e parametri servono.
Tenete a mente un modello semplice:
flowchart LR
A["Sorgenti (sourceDir)"] --> B["Configure preset
cmake --preset ..."]
B --> C["Directory di build (binaryDir)
build-debug/ o build-release/"]
C --> D["Build preset
cmake --build --preset ..."]
C --> E["Test preset
ctest --preset ... (se necessario)"]
Qui il punto importante è che il configure preset decide come viene creata la cartella di build e cosa finisce nel suo CMakeCache.txt. Il build preset si occupa della compilazione della directory già configurata. Il test preset è per l'esecuzione dei test (se presenti).
Sì, è esattamente ciò che di solito faceva l'IDE «con un click». La differenza è che ora non è «configurato da qualche parte nella vostra interfaccia», ma è descritto nel repository e ripetibile su qualsiasi macchina.
CMakePresets.json: struttura del file senza misteri
I CMake Presets sono descritti in un file JSON chiamato CMakePresets.json nella radice del progetto (vicino a CMakeLists.txt). È proprio JSON, quindi rigoroso: le virgolette sono obbligatorie, le virgole sono importanti, i commenti in stile // non sono permessi (e sì, è una scocciatura perché i commenti sarebbero comodi, ma JSON è severo).
La struttura minima del file è questa:
{
"version": 6,
"configurePresets": [],
"buildPresets": [],
"testPresets": []
}
Il campo version è la versione del formato dei preset (non la versione di CMake e non la versione del vostro progetto). In pratica vedrete versioni tipo 4/5/6: l'importante è che la vostra versione di CMake sappia leggerla. Se il vostro CMake è vecchio e improvvisamente «non capisce» il formato, apparirà un errore frustrante ancora prima della build.
In questo file di solito descrivete i preset «ufficiali» del progetto: debug, release, possibilmente asan (ma i sanitizer li vedremo in un'altra lezione), a volte anche ci. L'idea è di non creare 20 varianti «per ogni evenienza», ma di fissare 2–4 scenari di lavoro che tutti usano.
3. Configure preset: cartella di build e variabili chiave
Il configure preset è, in sostanza, la risposta alla domanda: «Dove e con quali parametri configuriamo il progetto?». Specifica il binaryDir, può specificare il generator, e la cosa più pratica è che definisce cacheVariables, cioè le variabili che finiranno nel CMakeCache.txt e influenzeranno la configurazione.
Per cominciare creiamo un preset base da cui ereditare Debug e Release. Non è per bellezza: l'ereditarietà riduce il copia-incolla e diminuisce la probabilità che in un preset dimentichiate di impostare lo standard C++23 e nell'altro no.
Esempio:
{
"version": 6,
"configurePresets": [
{
"name": "base",
"hidden": true,
"cacheVariables": {
"CMAKE_CXX_STANDARD": "23"
}
},
{
"name": "debug",
"inherits": "base",
"binaryDir": "${sourceDir}/build-debug",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug"
}
},
{
"name": "release",
"inherits": "base",
"binaryDir": "${sourceDir}/build-release",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release"
}
}
]
}
Qui succedono diverse cose importanti.
binaryDir fissa in modo netto dove vivrà la build. È una disciplina: Debug e Release non si pestano i piedi, e non avrete la situazione «ho cambiato il tipo di build e non ha effetto». In realtà ha effetto — solo che nella cache è rimasta la vecchia configurazione, e voi vivete nel passato. I preset aiutano a non vivere nel passato.
${sourceDir} è una variabile dei CMake Presets che indica «la directory dei sorgenti». È meglio che scrivere percorsi assoluti come C:\Users\Alice\Projects\..., perché i percorsi assoluti rendono il preset non portabile: su un'altra macchina si trasformerebbe in una zucca.
cacheVariables sono esattamente ciò che finirà nella cache. L'esempio più popolare è CMAKE_BUILD_TYPE, che imposta Debug/Release (nei generatori a configurazione singola). Un altro esempio sensato è CMAKE_CXX_STANDARD, in modo che il progetto venga compilato in C++23 «per contratto», e non «perché lo avete impostato nell'IDE».
Per collegare questo al tema Debug/Release potete aggiungere nel progetto un piccolo controllo nel codice e vedere cosa viene effettivamente compilato:
#include <iostream>
int main() {
#ifdef NDEBUG
std::cout << "Release-like build (NDEBUG is defined)\n";
#else
std::cout << "Debug-like build (NDEBUG is NOT defined)\n";
#endif
}
Il senso non è di scrivere così «in prodotto», ma di provare praticamente che il preset cambia la configurazione, la configurazione influenza i macro (per esempio, NDEBUG), e i macro influenzano la compilazione.
4. Build e Test presets: compilazione e test come fasi ripetibili
Build preset: la compilazione è un passo separato
Il build preset è responsabile di «come compilare ciò che è già stato configurato». Può sembrare noioso, finché non vi accorgete di aver lanciato la compilazione 15 volte da posti diversi: dall'IDE, dal terminale o per sbaglio da un'altra cartella di build perché la scheda del terminale non era quella giusta.
Il build preset di solito fa riferimento al configure preset tramite il nome. Esempio:
{
"version": 6,
"configurePresets": [
{
"name": "debug",
"binaryDir": "${sourceDir}/build-debug",
"cacheVariables": { "CMAKE_BUILD_TYPE": "Debug" }
}
],
"buildPresets": [
{
"name": "debug",
"configurePreset": "debug"
}
]
}
Il senso è questo: il comando di build «sa» quale directory di build usare, e voi non dovete ricordare dove si trova. Questo è particolarmente comodo quando ci sono più cartelle di build e volete che siano indipendenti.
Nella pratica il build preset può contenere parametri aggiuntivi: ad esempio la configurazione (per generator multi-config), il grado di parallelismo (quante job compilare contemporaneamente), la verbosità. Ma per la nostra lezione è sufficiente capire: il configure preset crea e configura la cartella di build, il build preset la compila.
Test preset: anche l'esecuzione dei test può diventare «accordo»
Il test preset è un modo per fissare anche l'esecuzione dei test. Importante non confondere: il test preset non è il «framework di test» né il modo di scrivere i test. È semplicemente uno scenario per eseguire ctest, che funziona se il vostro progetto ha aggiunto i test in CMake (ad esempio con enable_testing() e add_test(...)).
Sì, i test unitari veri li vedremo molto più avanti. Ma proprio ora è utile cogliere l'idea: se un progetto può essere testato, anche l'esecuzione dei test deve essere riproducibile.
Un esempio minimale di test preset potrebbe essere:
{
"version": 6,
"configurePresets": [
{
"name": "debug",
"binaryDir": "${sourceDir}/build-debug",
"cacheVariables": { "CMAKE_BUILD_TYPE": "Debug" }
}
],
"testPresets": [
{
"name": "debug",
"configurePreset": "debug"
}
]
}
Questo preset dice: «Esegui i test dalla directory di build corrispondente al preset debug». Nella pratica elimina molta confusione, per esempio quando qualcuno esegue ctest dalla build-release ma si aspetta risultati come se fosse la build-debug. In programmazione ci sono già abbastanza misteri; non serve aggiungerne un altro.
5. Uso nella vita reale: comandi e separazione file comune/locale
Tre comandi brevi invece di «cosa ho selezionato io»
I preset si rivelano particolarmente utili quando li usate ovunque in modo coerente: sulla vostra macchina, dal docente, dal compagno di corso e nella CI. L'idea è semplice: non dovete più tenere a mente le «comandi giusti», ma i soli nomi dei preset.
Il ciclo tipico è:
cmake --preset debug
cmake --build --preset debug
ctest --preset debug
A questo punto il principiante spesso domanda: «Perché non farlo con un solo comando?» Si potrebbe, ma non è necessario. La separazione in configure/build/test non è burocrazia, ma riflette la realtà: la configurazione crea il progetto di build e memorizza i parametri, la compilazione genera i binari e i test eseguono le verifiche.
Se avete modificato CMakePresets.json, ricordate che la directory di build potrebbe essere già stata configurata con valori vecchi. Allora è necessario rieseguire il passo di configure (con il primo comando) affinché le modifiche finiscano nella cache e nella generazione. I preset non annullano l'esistenza di CMakeCache.txt; semplicemente ne rendono la creazione gestita.
CMakePresets.json e CMakeUserPresets.json: cosa committo e cosa lascio locale
Storia molto comune: volete che i preset siano condivisi, ma ogni sviluppatore potrebbe avere esigenze locali. Ad esempio, qualcuno vuole log molto dettagliati della build, qualcuno specifica il percorso di un compilatore non standard, qualcun altro compila in una cartella su un altro disco perché lo spazio sul disco di sistema è poco.
Per questo in CMake esiste l'idea di due file:
- CMakePresets.json — comune, solitamente committato nel repository. Sono i preset «ufficiali» del progetto.
- CMakeUserPresets.json — utente, solitamente non committato. Sono le vostre comodità locali, che non dovrebbero rompere la riproducibilità per gli altri.
Anche se per ora lavorate da soli, è utile prendere questa disciplina: «gli scenari comuni nel file comune, le preferenze personali nel file utente». Altrimenti dopo un mese scoprirete che il progetto «si costruisce sulla vostra macchina solo perché avete aggiunto manualmente una configurazione nell'IDE» e nessun altro sa quale sia.
6. Errori tipici lavorando con i Preset
Su questo argomento gli errori non sono «complessi», ma insidiosamente quotidiani: sembra che stiate facendo tutto giusto, ma un piccolo dettaglio trasforma la build in una lotteria. La buona notizia è che questi problemi sono prevedibili, e una volta che li riconoscete li riconoscerete subito in futuro.
Errore n.1: la stessa binaryDir per ogni situazione.
Se due preset (per esempio Debug e Release) usano la stessa cartella di build, tornate nel mondo del «perché le impostazioni non si applicano». Lo scopo dei preset è anche la disciplina delle directory di build separate, affinché le configurazioni non si sovrascrivano nella cache. Usate almeno build-debug/ e build-release/, e la vita sarà molto più tranquilla.
Errore n.2: aspettarsi che il build preset «applichi da solo le nuove cacheVariables».
Il build preset non «configura» nulla, semplicemente avvia la build nella directory di build già esistente. Se avete cambiato cacheVariables (per esempio avete aggiunto lo standard C++23 o cambiato il tipo di build) ma non avete rieseguito il passo di configure, la build continuerà a usare il vecchio CMakeCache.txt. Non c'è magia: è solo stato mantenuto lo stato precedente.
Errore n.3: percorsi assoluti rigidi nei preset.
I percorsi assoluti rendono i preset «vincolati alla vostra macchina». Oggi siete contenti di aver messo rapidamente C:\..., domani non riuscirete a costruire il progetto su un'altra macchina, in CI o anche sul vostro PC dopo aver spostato la cartella. Usate ${sourceDir} e altre variabili per mantenere i preset portabili.
Errore n.4: tentare di trasformare i preset in sostituti dell'input utente.
I preset riguardano la build, non i «parametri di esecuzione» dell'applicazione. Non è corretto usare i preset per passare cose come «nome utente» o «percorso del file di input» come se fossero impostazioni di runtime. Certo, tecnicamente si può definire un macro e ricompilare per cambiare un valore, ma fine fine otterrete un progetto che si ricompila solo per cambiare un parametro. I preset definiscono come compilare, non come usare il programma.
Errore n.5: ignorare che CMAKE_BUILD_TYPE non sempre si comporta come ci si aspetta.
In alcuni ambienti CMake usa generatori dove Debug/Release non vengono scelti tramite CMAKE_BUILD_TYPE al momento della configurazione, ma in altri modi (questo dipende dal generator). Per la nostra lezione il principio pratico è: se vedete che il preset «Debug» si comporta come Release, non discutete con i fatti — controllate quale generator sta usando il vostro ambiente e se CMAKE_BUILD_TYPE viene realmente considerata.
GO TO FULL VERSION