1. Introduzione
Quando scrivete programmi didattici di 1–2 file, può sembrare che le librerie esterne siano roba per "sviluppatori grandi e seri" che vivono in grattacieli e bevono caffè dalle pipeline CI/CD. Ma la realtà è più semplice: appena il progetto diventa un po' più utile, quasi inevitabilmente vorrete prendere un mattoncino già pronto — formattazione, parser, logging, libreria per test, gestione delle configurazioni — invece di scrivere tutto da zero e poi passare tre settimane a debuggarlo eroicamente.
Il problema è che "prendere una libreria" in C++ non è un singolo pulsante Install, come spesso accade in altre ecosystem. Nel mondo C++ storicamente c'è molta varietà: le librerie possono essere header‑only, con binari, "CMake‑friendly", oppure possono avere un Makefile che è "configurato per il mio gatto e Ubuntu 14.04". Perciò ci serve almeno un modo ordinato, chiaro e riproducibile per includere dipendenze — e FetchContent è proprio questo.
Tre modi per includere dipendenze
Se non avete mai incluso librerie esterne, di solito emergono tre opzioni istintive. La prima — "copio i sorgenti della libreria nella cartella third_party/ e me ne dimentico". La seconda — "la libreria è installata nel sistema e io me la vado a cercare". La terza — "CMake, fallo bello per favore".
Mettiamole a confronto in una piccola tabella — non come "verità", ma come orientamento per il principiante:
| Approccio | Come appare | Pro | Contro |
|---|---|---|---|
| "Copia e incolla nel progetto" | mettiamo il codice della dipendenza nel repository | semplice, funziona offline | aggiornare è doloroso, facile creare confusione tra licenze/versioni |
| "Installazione di sistema" | la libreria è installata nel SO, il progetto la usa | veloce per lo sviluppo su una singola macchina | su un'altra macchina "non si compila", le versioni fluttuano |
|
CMake scarica i sorgenti della dipendenza nella build‑dir e li integra nella build | riproducibile, comodo, orientato ai target | richiede rete (di solito), importante fissare le versioni |
E qui nasce l'idea chiave della lezione: FetchContent è il modo per far sì che il vostro progetto "porti con sé" i sorgenti necessari (di solito durante la configurazione CMake), invece di affidarsi alla magia dell'ambiente.
Che cos'è FetchContent
Immaginate il vostro progetto come una cucina, e CMake come lo chef che deve preparare il piatto "compilare l'applicazione". Avete la ricetta (il vostro CMakeLists.txt), ma alcuni ingredienti non sono nel frigorifero del progetto: per esempio, serve "la spezia fmt" (la libreria di formattazione). FetchContent è il meccanismo che dice allo chef: "Se la spezia non c'è, vai al negozio a questo indirizzo, prendi questo pacchetto specifico (versione), portalo e usalo nella preparazione".
Tecnicamente la documentazione di CMake descrive FetchContent come un modulo che sa "popolare" il contenuto (scaricare/preparare i sorgenti della dipendenza) e poi (nello scenario tipico) aggiungere quel progetto alla build principale in modo che i suoi target diventino disponibili per il linking. Questo è proprio quello che fa il comando FetchContent_MakeAvailable().
Vale la pena ricordare una cosa: FetchContent non "installa la libreria nel sistema" né la rende globale. La rende parte di una build specifica nella build‑directory. Perciò le build out‑of‑source e i preset (che studiate oggi) si integrano naturalmente con FetchContent: in ogni cartella di build avrete il vostro stato, comprese le dipendenze scaricate.
2. Pattern base di FetchContent
Template minimo: Declare → MakeAvailable
Ora arriva il pezzo più importante: lo "scheletro" base per includere una dipendenza. In CMake moderno (da 3.14) c'è il comodo comando FetchContent_MakeAvailable(), e la documentazione consiglia, dove possibile, di usarlo invece di popolare manualmente e chiamare add_subdirectory.
Il pattern minimo è il seguente:
include(FetchContent)
FetchContent_Declare(
someLib
GIT_REPOSITORY https://example.com/someLib.git
GIT_TAG v1.2.3
)
FetchContent_MakeAvailable(someLib)
Il senso, passo dopo passo, è questo.
- Prima include(FetchContent) importa il modulo e "apre" i comandi FetchContent_Declare, FetchContent_MakeAvailable e (opzionalmente) comandi più low‑level.
- Poi FetchContent_Declare(...) registra la "ricetta", da dove prendere la dipendenza e quale versione. Importante: Declare di per sé non scarica nulla: fissa solo i parametri.
- Infine FetchContent_MakeAvailable(name) garantisce che la dipendenza venga preparata (populated) e, se possibile, aggiunta alla build in modo che i target della libreria siano disponibili per target_link_libraries.
La documentazione nota inoltre che FetchContent_MakeAvailable() è "New in version 3.14". È un buon riferimento: se il vostro cmake_minimum_required() è inferiore, dovrete usare un pattern più vecchio (e più verboso).
Disciplina: prima Declare, poi MakeAvailable
Qui molti principianti cadono in trappola, perché CMake sembra "solo uno script": scrivi una riga e viene eseguita. Ma con FetchContent c'è una sottigliezza importante: in grandi alberi di progetto le dipendenze possono essere dipendenze reciproche.
La documentazione avverte chiaramente: il progetto dovrebbe dichiarare i dettagli di tutte le dipendenze prima di chiamare FetchContent_MakeAvailable() per qualcuna di esse — in modo che il progetto principale mantenga il controllo sulle versioni effettivamente usate. Viene mostrato un esempio "WRONG / CORRECT" dove una dichiarazione tardiva FetchContent_Declare(other ...) può essere ignorata, perché un altro progetto ha già dichiarato other prima.
In parole semplici: se volete essere "l'adulto responsabile" nella build, non chiamate MakeAvailable subito dopo ogni Declare. Idealmente prima fate il blocco delle dichiarazioni, poi in un punto unico "rendete disponibili" le dipendenze.
Anche in un progetto piccolo questa regola aiuta: con un colpo d'occhio vedete l'elenco delle dipendenze e le loro versioni.
5. Esempio: includere fmt nell'applicazione
Per rendere l'esempio concreto, proseguiamo con la nostra piccola applicazione console. La chiamiamo BudgetBook — un semplice tracker di spese in console: legge comandi, stampa il bilancio, aggiunge transazioni. Funzionalmente è semplice, il che è comodo: non cambiamo architettura, mostriamo solo come integrare una dipendenza.
Supponiamo di voler stampare messaggi un po' più curati rispetto a std::cout, e decidiamo (come esempio) di usare la libreria di formattazione fmt. L'idea non è "avete urgente bisogno di fmt", ma che sia un tipico progetto CMake‑friendly che espone target e si presta bene al linking.
CMakeLists.txt: includere fmt tramite FetchContent
cmake_minimum_required(VERSION 3.14)
project(BudgetBook LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
add_executable(budgetbook
src/main.cpp
)
include(FetchContent)
FetchContent_Declare(
fmt
GIT_REPOSITORY https://github.com/fmtlib/fmt.git
# IMPORTANTE: in un progetto reale fissate la versione (ne parliamo nella sezione successiva)
GIT_TAG 9.1.0
)
FetchContent_MakeAvailable(fmt)
target_link_libraries(budgetbook PRIVATE fmt::fmt)
Nota il punto chiave "stile target": non aggiungiamo include directories globali né scriviamo manualmente i percorsi degli header di fmt. Colleghiamo semplicemente il target budgetbook con il target fmt::fmt. Il concetto di "target" qui funziona come i LEGO: se la libreria fmt si è descritta correttamente in CMake, porterà con sé gli include e i flag necessari.
FetchContent_MakeAvailable() è proprio il meccanismo che rende visibili i target della dipendenza nella build principale.
main.cpp: esempio minimo di utilizzo della libreria
Facciamo un piccolo esempio. Non stiamo studiando fmt come libreria — mostriamo solo che la dipendenza è stata effettivamente integrata e funziona.
// src/main.cpp
#include <string>
#include <vector>
// Libreria esterna (fornita tramite FetchContent)
#include <fmt/core.h>
int main() {
const std::string appName = "BudgetBook";
const int year = 2026;
fmt::print("{}: avviato, anno {}\n", appName, year); // BudgetBook: avviato, anno 2026
const std::vector<int> expenses = {10, 25, 7};
fmt::print("Operazioni: {}\n", expenses.size()); // Operazioni: 3
}
Se il progetto si compila e stampa le righe, significa che la "connessione" tra FetchContent e il vostro main.cpp è corretta: CMake ha scaricato la dipendenza, l'ha aggiunta alla build e il linker ha visto la libreria necessaria.
6. Fissare la versione: perché il ramo master è una lotteria
La regola più importante di questa lezione suona noiosa ma salva il sangue: le dipendenze vanno fissate per versione.
Quando scrivete:
GIT_TAG master
o
GIT_TAG main
state, di fatto, dicendo: "prendi quello che c'è attualmente alla punta del ramo". Oggi quel codice si compila, domani l'autore rilascia qualcosa, dopodomani cambia l'API, e dopo una settimana ricevete un errore di compilazione pur non avendo cambiato nulla. Non siete voi a modificare, è il mondo esterno. E il mondo esterno ama le sorprese.
La documentazione CMake raccomanda: se il contenuto viene scaricato da un server che non controllate, è meglio usare lo hash del commit per GIT_TAG, non il nome del ramo o del tag. Viene detto chiaramente che l'hash del commit è più sicuro e aiuta a garantire che si stia scaricando ciò che ci si aspetta.
Tag di rilascio o hash del commit
Ci sono due approcci comuni per fissare la versione.
Se vi fidate della semantica dei release tag e volete leggibilità:
GIT_TAG 9.1.0
Se invece vi interessa la "riproducibilità assoluta" (specialmente per progetti didattici o di team), usate l'hash del commit:
GIT_TAG 703bd9caab50b139428cea1aaff9974ebee5742e
Nella documentazione di FetchContent gli esempi con commit hash compaiono direttamente nel testo.
La differenza pratica è semplice: un tag può teoricamente essere ri‑puntato (raramente, ma possibile), mentre l'hash del commit identifica in modo univoco lo stato del repository. Quindi "ieri, oggi e tra un anno" otterrete lo stesso codice della dipendenza.
Archivio: URL + URL_HASH
A volte una libreria è distribuita non via git ma tramite un archivio. In quel caso FetchContent_Declare supporta le opzioni URL e il checksum dell'archivio.
La documentazione fornisce esempi del tipo:
FetchContent_Declare(
myCompanyIcons
URL https://.../iconset_1.12.tar.gz
URL_HASH MD5=...
)
Il senso di URL_HASH è lo stesso dell'hash del commit: fissate "questo esatto archivio byte‑per‑byte" e non "ciò che viene servito ora al link".
7. Cosa succede durante la configurazione
Per non considerare FetchContent come magia, è utile tenere a mente la sequenza degli eventi. A livello semplificato è così:
flowchart TD
A[Avviate la configurazione CMake] --> B[FetchContent_Declare: registrate le regole]
B --> C[FetchContent_MakeAvailable: verifichiamo se i sorgenti sono presenti]
C --> D{La dipendenza è già scaricata nella build‑dir?}
D -->|sì| E[Usiamo il contenuto in cache]
D -->|no| F[Scarichiamo/scompattiamo la dipendenza]
E --> G["Aggiungiamo la dipendenza alla build (come sotto‑progetto)"]
F --> G
G --> H[I target della dipendenza sono disponibili per target_link_libraries]
Questo modello spiega bene due osservazioni pratiche.
Prima: perché la build out‑of‑source è importante. Le dipendenze e il loro stato "scaricato" vivono nella build‑directory, non inondano i sorgenti.
Seconda: perché cambiare la build‑directory (o il preset) spesso causa il riscaricamento delle dipendenze. Per CMake quello è davvero uno stato diverso del mondo.
Collisioni di nomi e la regola "first to record wins"
C'è un altro dettaglio che sembra irrilevante finché non lo incontrate. In FetchContent il nome della dipendenza non è solo un commento. CMake usa quel nome per decidere se la dipendenza è già stata dichiarata e se evitarne la ridichiarazione.
La documentazione afferma chiaramente che FetchContent_Declare() adotta l'approccio "first to record, wins": se i dettagli sono già stati registrati prima nel progetto (anche in sottoprogetti), le dichiarazioni ripetute vengono ignorate.
Morale: nominate le dipendenze in modo sensato e prevedibile (di solito il nome ufficiale del progetto), altrimenti potreste scaricare la stessa cosa due volte con nomi diversi. E soprattutto: non tentate di "ridefinire" una dipendenza troppo tardi — probabilmente è già troppo tardi e la vostra dichiarazione verrà ignorata.
8. Errori tipici
Errore №1: usare GIT_TAG main/master e sorprendersi che "ieri funzionava, oggi no".
È la classica trappola del principiante: sembra che "non avete cambiato nulla", ma la dipendenza è stata aggiornata in uno stato incompatibile. Di conseguenza la build non è riproducibile. Meglio fissare la versione con un tag di rilascio o con l'hash del commit; la documentazione di FetchContent sottolinea che l'hash è più sicuro quando il server non è sotto vostro controllo.
Errore №2: pensare che FetchContent_Declare() abbia già scaricato la libreria.
Declare registra i parametri, non esegue il download immediato. Il download e l'integrazione avvengono quando chiamate FetchContent_MakeAvailable() (o fate manualmente il "populate"). Quindi se dichiari la dipendenza ma non la rendi disponibile, i target della libreria non compariranno in target_link_libraries.
Errore №3: chiamare FetchContent_MakeAvailable() troppo presto e poi tentare di "sovrascrivere" la dipendenza.
In grandi progetti le dipendenze possono essere dichiarate in sottoprogetti. La documentazione raccomanda di dichiarare prima tutti i dettagli e poi chiamare MakeAvailable, altrimenti una dichiarazione tardiva FetchContent_Declare(other ...) potrebbe essere ignorata. Anche in progetti piccoli questa disciplina è utile: blocco "tutte le dipendenze qui" e poi "le rendiamo disponibili".
Errore №4: provare a includere la libreria "tramite include directories" ignorando i target.
Dopo FetchContent_MakeAvailable() lo stile corretto è collegarsi al target della dipendenza (es. fmt::fmt), non scrivere manualmente include directories e indovinare i flag necessari. Altrimenti vi ricreereste il lavoro di CMake: invece di usare il modello "dipendenza come target", tornereste alla compilazione manuale in stile 2005.
Errore №5: aspettarsi che la dipendenza diventi "installata di sistema" e disponibile per tutti i progetti.
FetchContent opera a livello della singola build: pone i sorgenti e gli artefatti dove si sta costruendo il progetto. Non è apt, non è brew e non è un'installazione globale. Non contate che un altro progetto sulla stessa macchina veda automaticamente queste librerie: avrà la sua build‑directory e il suo stato.
GO TO FULL VERSION