Architectural Note: Using CMake FetchContent for External Dependencies
Adopt CMake’s FetchContent to embed external libraries directly into your build tree—no pre‑installed packages, simplified CI, and automatic transitive linking. This guide covers the minimal design, trust boundaries, operational checks, failure modes, and when to pivot to another strategy.
08 Aug 2025, 13:56 UTC

Problem & Takeaway
Managing external libraries in CMake projects often requires juggling system‑wide packages, vendor binaries, or custom scripts. FetchContent solves this by downloading and building the dependency during the configure phase, keeping the build self‑contained and CI‑friendly. The key takeaway: use FetchContent for small to medium libraries when you want a single, reproducible build tree without external package managers.
Minimal Design with FetchContent
Only a handful of lines in the top‑level CMakeLists.txt are needed. The pattern is:
cmake_minimum_required(VERSION 3.11)
project(MyProject)
include(FetchContent)
FetchContent_Declare(
fmt
GIT_REPOSITORY https://github.com/fmtlib/fmt.git
GIT_TAG 8.1.1
)
FetchContent_MakeAvailable(fmt)
add_executable(my_app src/main.cpp)
target_link_libraries(my_app PRIVATE fmt::fmt)
Key points:
FetchContent_Declaredefines the source location and revision.FetchContent_MakeAvailablepulls the code into<build_dir>/_depsand registers the target.- Targets inherit interface properties, so downstream projects can use
fmt::fmtjust like any native target.
Trust & Data Boundaries
The dependency is fetched into the build tree, not the source tree. This keeps the repository clean and ensures that the same source is used for every build. However, the source is untrusted until it is built. Therefore:
- Verify the commit hash or tag to avoid accidental upgrades.
- Run
git ls-remote <repo> <ref>manually to confirm the expected revision. - Consider adding a
CHECKSUMfile if the provider supplies one, and verify it after download.
Operational Checks
After configuration, validate the following:
- Source extraction: Confirm that
<build_dir>/_deps/fmt-srcexists and containsCMakeLists.txt.ls <build_dir>/_deps/fmt-src - Target creation: Verify that
fmt::fmtis listed in the build graph.cmake --graphviz=deps.dot -S . -B <build_dir> - Linking: Build the final executable and check that the binary includes the dependency symbols.
cmake --build <build_dir> --target my_app nm <build_dir>/my_app | grep fmt
Failure Modes & Mitigations
- Network unavailability: Configure will fail with a clear error if the source cannot be fetched. Mitigation: cache the
_depsdirectory or use a local mirror. - Long configure time: Each configure rebuilds the dependency. Use an out‑of‑source build and reuse the build tree across configurations.
- Large or complex libraries: May cause significant build overhead. For heavy projects, consider pre‑built binaries or a dedicated package manager.
- Version drift: If the fetched library changes, the build may silently use a newer version. Mitigation: pin the commit or tag and document the exact revision.
When to Change the Design
Switch from FetchContent to another approach when:
- The library is large (hundreds of MB) or requires a long compile time, making repeated rebuilds impractical.
- You need a binary release (e.g., Windows SDKs) that is not source‑available.
- Your CI environment has strict network policies; a package manager with cached binaries may be preferable.
- Multiple projects rely on the same dependency and you want to share a single build artefact.
Example: Pulling fmt
Below is a minimal reproducible example. Replace <build_dir> with your chosen build directory.
# CMakeLists.txt
cmake_minimum_required(VERSION 3.11)
project(FmtExample)
include(FetchContent)
FetchContent_Declare(
fmt
GIT_REPOSITORY https://github.com/fmtlib/fmt.git
GIT_TAG 8.1.1
)
FetchContent_MakeAvailable(fmt)
add_executable(fmt_demo src/main.cpp)
target_link_libraries(fmt_demo PRIVATE fmt::fmt)
src/main.cpp:
#include <fmt/core.h>
int main() {
fmt::print("Hello, {}!\n", "world");
return 0;
}
Configure and build:
mkdir -p build
cmake -S . -B build
cmake --build build
./build/format_demo
Operational checks you can perform manually:
- Verify the source folder:
ls build/_deps/fmt-src - Check the build logs for
fmt::fmtcompilation. - Remove
build/_depsand re-runcmaketo ensure a clean re‑download.
Limitations:
- Requires CMake 3.11+; older projects must use
ExternalProject_Addor a package manager. - Initial configure can be slow for network‑heavy projects.
- Fetched sources are built every configure; use a dedicated build directory to amortize the cost.
Practical tip: add a FETCHCONTENT_QUIET option to reduce noise, and enable FETCHCONTENT_UPDATES_DISCONNECTED if you want to lock the dependency to a specific commit without re‑checking the remote each time.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.