FetchContent Module
What is FetchContent?
FetchContent is a CMake module introduced in version 3.11 that downloads and makes external projects available at configure time. Unlike ExternalProject which builds dependencies at build time, FetchContent integrates external dependencies directly into your build as if they were part of your source tree.
This approach is particularly powerful for modern C++ development where you want header-only libraries or small dependencies to be automatically fetched and built alongside your project. It eliminates the need for users to pre-install dependencies and ensures everyone builds with the same versions.
The key distinction: FetchContent downloads during the CMake configure step and makes the dependency's targets immediately available for use in your CMakeLists.txt. This means you can link against fetched libraries just like local targets.
Basic Usage
Setup
FetchContent requires CMake 3.11 or later (3.14+ recommended for FetchContent_MakeAvailable).
cmake_minimum_required(VERSION 3.14)
project(MyProject)
# Include the module
include(FetchContent)
Simple Example
Here's a complete example fetching the fmt library:
cmake_minimum_required(VERSION 3.14)
project(MyApp)
include(FetchContent)
# Declare what to fetch
FetchContent_Declare(
fmt
GIT_REPOSITORY https://github.com/fmtlib/fmt.git
GIT_TAG 9.1.0 # Specify version tag
)
# Make it available
FetchContent_MakeAvailable(fmt)
# Now use it like a normal library
add_executable(myapp main.cpp)
target_link_libraries(myapp PRIVATE fmt::fmt)
In this example, CMake will:
- Download fmt from GitHub during configuration
- Configure fmt's CMake build
- Make fmt's targets (like
fmt::fmt) available - Allow you to link against them normally
FetchContent_Declare()
This command tells CMake what to fetch and where to find it. It doesn't actually download anything - it just records the information for later use.
Fetching from Git
The most common source is Git repositories:
FetchContent_Declare(
json
GIT_REPOSITORY https://github.com/nlohmann/json.git
GIT_TAG v3.11.2
GIT_SHALLOW ON # Only fetch the specific tag, not full history
GIT_PROGRESS ON # Show download progress
)
GIT_TAG: Can be a tag, branch name, or commit hash. Tags are recommended for reproducibility.
GIT_SHALLOW: Downloads only the specified commit, significantly faster for large repositories. However, you can't switch to other branches/tags without re-fetching.
GIT_PROGRESS: Shows progress during the clone operation, useful for large repositories to confirm it's not stuck.
Using commit hashes for security:
FetchContent_Declare(
spdlog
GIT_REPOSITORY https://github.com/gabime/spdlog.git
GIT_TAG 76fb40d95455f249bd70824ecfcae7a8f0930fa3 # v1.11.0
GIT_SHALLOW ON
)
Commit hashes are immutable - tags can be moved, but hashes cannot. This guarantees you always get exactly the same code.
Fetching from URL
You can download tarballs or zip files:
FetchContent_Declare(
catch2
URL https://github.com/catchorg/Catch2/archive/refs/tags/v3.3.2.tar.gz
URL_HASH SHA256=8361907f4d9bff3ae7c1edb027f813659f793053c99b67837a0c0d34290c2d3f
)
URL_HASH: Verifies the download's integrity. Get the hash from the release page or compute it with sha256sum. This is critical for security - ensures the downloaded file hasn't been tampered with.
When to use URL vs GIT:
- URL: Faster for releases, no Git dependency required
- GIT: Better for development, can track branches, easier to update
Fetching from Local Path
Useful during development or for vendored dependencies:
FetchContent_Declare(
mylib
SOURCE_DIR ${CMAKE_SOURCE_DIR}/external/mylib
)
This treats a local directory as if it were fetched, making it easy to switch between local development and remote fetching.
FetchContent_MakeAvailable()
Introduced in CMake 3.14, this command does the actual work of making the content available. For earlier CMake versions, you need a more verbose approach (shown later).
FetchContent_MakeAvailable(fmt spdlog json)
This single command:
- Downloads the content if not already present
- Runs
add_subdirectory()on it - Makes all targets from those projects available
You can make multiple dependencies available at once, and CMake will parallelize the downloads when possible.
For CMake < 3.14
If you're stuck with CMake 3.11-3.13, use this pattern:
FetchContent_Declare(
fmt
GIT_REPOSITORY https://github.com/fmtlib/fmt.git
GIT_TAG 9.1.0
)
# Manual population
FetchContent_GetProperties(fmt)
if(NOT fmt_POPULATED)
FetchContent_Populate(fmt)
add_subdirectory(${fmt_SOURCE_DIR} ${fmt_BINARY_DIR})
endif()
This is what FetchContent_MakeAvailable() does internally - it's just more verbose.
Configuring Fetched Dependencies
Often you need to control how dependencies are built. FetchContent lets you set their CMake options before they're configured.
Setting Options
# Disable examples and tests for dependencies
set(FMT_INSTALL OFF CACHE BOOL "" FORCE)
set(FMT_TEST OFF CACHE BOOL "" FORCE)
set(FMT_DOC OFF CACHE BOOL "" FORCE)
FetchContent_Declare(
fmt
GIT_REPOSITORY https://github.com/fmtlib/fmt.git
GIT_TAG 9.1.0
)
FetchContent_MakeAvailable(fmt)
The CACHE ... FORCE syntax ensures your settings override the dependency's defaults. This is important because dependencies might have different default values.
Common Options to Disable
Most well-designed CMake projects have options to disable non-essential components:
# For most libraries, consider disabling:
set(DEPENDENCY_INSTALL OFF CACHE BOOL "" FORCE) # Don't install dependency
set(DEPENDENCY_TESTS OFF CACHE BOOL "" FORCE) # Don't build tests
set(DEPENDENCY_EXAMPLES OFF CACHE BOOL "" FORCE) # Don't build examples
set(DEPENDENCY_DOCS OFF CACHE BOOL "" FORCE) # Don't build documentation
set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE) # Force static linking
FetchContent_Declare(...)
FetchContent_MakeAvailable(...)
Why Disable These?
Installation: If you're embedding a dependency, you typically don't want to install it separately from your project.
Tests/Examples: These increase build time significantly and aren't needed when you're just using the library.
Documentation: Usually not needed at build time - you'll read docs online.
BUILD_SHARED_LIBS: Static linking simplifies deployment and ensures ABI compatibility. Dynamic linking is sometimes necessary, but static is often easier.
Multiple Dependencies
Real projects usually have several dependencies. Here's how to manage them cleanly:
include(FetchContent)
# Configure all dependencies first
set(JSON_BuildTests OFF CACHE BOOL "" FORCE)
set(FMT_INSTALL OFF CACHE BOOL "" FORCE)
set(SPDLOG_FMT_EXTERNAL ON CACHE BOOL "" FORCE) # Use our fmt, not spdlog's
set(SPDLOG_INSTALL OFF CACHE BOOL "" FORCE)
# Declare all dependencies
FetchContent_Declare(
json
GIT_REPOSITORY https://github.com/nlohmann/json.git
GIT_TAG v3.11.2
GIT_SHALLOW ON
)
FetchContent_Declare(
fmt
GIT_REPOSITORY https://github.com/fmtlib/fmt.git
GIT_TAG 9.1.0
GIT_SHALLOW ON
)
FetchContent_Declare(
spdlog
GIT_REPOSITORY https://github.com/gabime/spdlog.git
GIT_TAG v1.11.0
GIT_SHALLOW ON
)
# Make all available at once (faster, downloads in parallel)
FetchContent_MakeAvailable(json fmt spdlog)
# Use them
add_executable(myapp main.cpp)
target_link_libraries(myapp PRIVATE
nlohmann_json::nlohmann_json
fmt::fmt
spdlog::spdlog
)
Notice SPDLOG_FMT_EXTERNAL - this tells spdlog to use our fetched fmt instead of bundling its own. This avoids having multiple versions of fmt in your build.
Best Practices
Organize Dependencies
For projects with many dependencies, create a separate file:
cmake_minimum_required(VERSION 3.14)
project(MyProject)
# Fetch all dependencies
include(cmake/Dependencies.cmake)
# Continue with your project
add_executable(myapp main.cpp)
target_link_libraries(myapp PRIVATE
fmt::fmt
spdlog::spdlog
nlohmann_json::nlohmann_json
)
include(FetchContent)
# Configure options
set(FMT_INSTALL OFF CACHE BOOL "" FORCE)
set(SPDLOG_INSTALL OFF CACHE BOOL "" FORCE)
set(JSON_BuildTests OFF CACHE BOOL "" FORCE)
# Declare dependencies
FetchContent_Declare(fmt
GIT_REPOSITORY https://github.com/fmtlib/fmt.git
GIT_TAG 9.1.0
GIT_SHALLOW ON
)
FetchContent_Declare(spdlog
GIT_REPOSITORY https://github.com/gabime/spdlog.git
GIT_TAG v1.11.0
GIT_SHALLOW ON
)
FetchContent_Declare(json
GIT_REPOSITORY https://github.com/nlohmann/json.git
GIT_TAG v3.11.2
GIT_SHALLOW ON
)
# Make available
FetchContent_MakeAvailable(fmt spdlog json)
This separation keeps your main CMakeLists.txt clean and makes dependency management easier.
Caching Downloads
FetchContent downloads to ${CMAKE_BINARY_DIR}/_deps by default. If you clean your build directory, it re-downloads everything. To cache across build directory cleanups:
# Put downloads in a global cache directory
set(FETCHCONTENT_BASE_DIR "${CMAKE_SOURCE_DIR}/.fetchcontent-cache"
CACHE PATH "FetchContent download directory")
Now dependencies are cached in .fetchcontent-cache/ in your source tree (add to .gitignore). They persist even if you delete your build directory.
Updating Dependencies
To update to a newer version:
# Change the version tag
FetchContent_Declare(
fmt
GIT_REPOSITORY https://github.com/fmtlib/fmt.git
GIT_TAG 10.0.0 # Updated from 9.1.0
GIT_SHALLOW ON
)
Then delete the old download:
rm -rf build/_deps/fmt-*
cmake -B build
Or if using a cache directory:
rm -rf .fetchcontent-cache/fmt-*
cmake -B build
Quiet Downloads
Reduce output clutter:
set(FETCHCONTENT_QUIET ON CACHE BOOL "" FORCE)
FetchContent_Declare(...)
FetchContent_MakeAvailable(...)
Only errors will be shown. During initial setup, keep this OFF to see progress.
Advanced Patterns
Optional Dependencies
Make a dependency optional based on availability or user choice:
option(USE_FMT "Use fmt library for formatting" ON)
if(USE_FMT)
FetchContent_Declare(
fmt
GIT_REPOSITORY https://github.com/fmtlib/fmt.git
GIT_TAG 9.1.0
)
FetchContent_MakeAvailable(fmt)
target_link_libraries(myapp PRIVATE fmt::fmt)
target_compile_definitions(myapp PRIVATE HAS_FMT)
endif()
Version Selection
Allow users to override dependency versions:
# Default versions
set(FMT_VERSION "9.1.0" CACHE STRING "fmt library version")
set(SPDLOG_VERSION "v1.11.0" CACHE STRING "spdlog library version")
FetchContent_Declare(
fmt
GIT_REPOSITORY https://github.com/fmtlib/fmt.git
GIT_TAG ${FMT_VERSION}
)
FetchContent_Declare(
spdlog
GIT_REPOSITORY https://github.com/gabime/spdlog.git
GIT_TAG ${SPDLOG_VERSION}
)
Users can now override: cmake -DFMT_VERSION=10.0.0 -B build
Conditional Fetching
Only fetch if not already available on the system:
find_package(fmt QUIET)
if(NOT fmt_FOUND)
message(STATUS "fmt not found on system, fetching...")
FetchContent_Declare(
fmt
GIT_REPOSITORY https://github.com/fmtlib/fmt.git
GIT_TAG 9.1.0
)
FetchContent_MakeAvailable(fmt)
else()
message(STATUS "Using system fmt")
endif()
This gives users flexibility - they can use system packages if available, or let CMake fetch them automatically.
Patch Dependencies
Sometimes you need to modify a dependency. Use patch files:
FetchContent_Declare(
somelib
GIT_REPOSITORY https://github.com/example/somelib.git
GIT_TAG v1.0.0
PATCH_COMMAND git apply ${CMAKE_SOURCE_DIR}/patches/somelib.patch
)
The patch is applied after downloading but before configuring. Keep patches in your repository for reproducibility.
Complete Real-World Example
Here's a comprehensive example for a real project:
cmake_minimum_required(VERSION 3.14)
project(WebServer VERSION 1.0.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Fetch dependencies
include(cmake/Dependencies.cmake)
# Main application
add_executable(webserver
src/main.cpp
src/server.cpp
src/request_handler.cpp
)
target_include_directories(webserver PRIVATE include)
target_link_libraries(webserver PRIVATE
fmt::fmt
spdlog::spdlog
nlohmann_json::nlohmann_json
httplib::httplib
)
# Enable warnings
if(MSVC)
target_compile_options(webserver PRIVATE /W4)
else()
target_compile_options(webserver PRIVATE -Wall -Wextra -pedantic)
endif()
# Installation
install(TARGETS webserver DESTINATION bin)
include(FetchContent)
# Cache directory for faster rebuilds
set(FETCHCONTENT_BASE_DIR "${CMAKE_SOURCE_DIR}/.fetchcontent-cache")
# Quiet mode after initial download
set(FETCHCONTENT_QUIET ON)
# Configure dependency options
set(FMT_INSTALL OFF CACHE BOOL "" FORCE)
set(FMT_TEST OFF CACHE BOOL "" FORCE)
set(FMT_DOC OFF CACHE BOOL "" FORCE)
set(SPDLOG_FMT_EXTERNAL ON CACHE BOOL "" FORCE)
set(SPDLOG_INSTALL OFF CACHE BOOL "" FORCE)
set(SPDLOG_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
set(JSON_BuildTests OFF CACHE BOOL "" FORCE)
set(JSON_Install OFF CACHE BOOL "" FORCE)
# Declare all dependencies
FetchContent_Declare(
fmt
GIT_REPOSITORY https://github.com/fmtlib/fmt.git
GIT_TAG 9.1.0
GIT_SHALLOW ON
)
FetchContent_Declare(
spdlog
GIT_REPOSITORY https://github.com/gabime/spdlog.git
GIT_TAG v1.11.0
GIT_SHALLOW ON
)
FetchContent_Declare(
json
GIT_REPOSITORY https://github.com/nlohmann/json.git
GIT_TAG v3.11.2
GIT_SHALLOW ON
)
FetchContent_Declare(
httplib
GIT_REPOSITORY https://github.com/yhirose/cpp-httplib.git
GIT_TAG v0.12.2
GIT_SHALLOW ON
)
# Make all available (downloads in parallel)
FetchContent_MakeAvailable(fmt spdlog json httplib)
# Log what was fetched
message(STATUS "Dependencies fetched and configured:")
message(STATUS " - fmt")
message(STATUS " - spdlog")
message(STATUS " - nlohmann_json")
message(STATUS " - cpp-httplib")
build/
.fetchcontent-cache/
FetchContent vs find_package()
When should you use each?
Use FetchContent When
✅ Dependency is not commonly system-installed
- Header-only libraries (Eigen, GLM)
- Modern C++ libraries (fmt, spdlog, json)
- Specialized libraries
✅ You need a specific version
- Ensures all developers use the same version
- Reproducible builds across systems
✅ Simplifying setup for users
- No installation prerequisites
- Works out of the box
✅ Active development
- Easy to test new versions
- Can point to development branches
Use find_package() When
✅ Large, stable dependencies
- OpenCV, Qt, Boost
- These are huge and slow to build
✅ System integration is important
- OpenSSL, zlib, pthread
- These often have system-specific optimization
✅ Binary compatibility matters
- Multiple applications sharing libraries
- System-wide updates apply to all apps
✅ Deployment requirements
- Package managers (apt, yum, brew)
- Linux distributions prefer system libraries
Hybrid Approach
Best of both worlds - try system first, fall back to fetch:
find_package(fmt QUIET)
if(fmt_FOUND)
message(STATUS "Using system fmt")
else()
message(STATUS "System fmt not found, fetching...")
FetchContent_Declare(
fmt
GIT_REPOSITORY https://github.com/fmtlib/fmt.git
GIT_TAG 9.1.0
)
FetchContent_MakeAvailable(fmt)
endif()
Common Issues and Solutions
Different Targets Available
Some projects don't create imported targets or use different names:
FetchContent_Declare(
somelib
GIT_REPOSITORY https://github.com/example/somelib.git
GIT_TAG v1.0.0
)
FetchContent_MakeAvailable(somelib)
# Check what targets were created
get_directory_property(targets DIRECTORY ${somelib_SOURCE_DIR} BUILDSYSTEM_TARGETS)
message(STATUS "somelib targets: ${targets}")
# Might need to use:
# target_link_libraries(myapp PRIVATE somelib) # Not somelib::somelib
Header-Only Libraries
Some header-only libraries need special handling:
FetchContent_Declare(
eigen
GIT_REPOSITORY https://gitlab.com/libeigen/eigen.git
GIT_TAG 3.4.0
)
FetchContent_MakeAvailable(eigen)
# Eigen creates Eigen3::Eigen target
target_link_libraries(myapp PRIVATE Eigen3::Eigen)
Build Time Issues
FetchContent can slow down configuration. Mitigate this:
# Use shallow clones
GIT_SHALLOW ON
# Use URL downloads instead of git when possible
URL https://github.com/project/archive/v1.0.tar.gz
# Cache the download directory
set(FETCHCONTENT_BASE_DIR "${CMAKE_SOURCE_DIR}/.fetchcontent-cache")
# Only populate once
FetchContent_GetProperties(lib)
if(NOT lib_POPULATED)
FetchContent_Populate(lib)
endif()
Conflicting Dependencies
Two dependencies might fetch different versions of the same library:
# Force both to use your version
set(COMMON_LIB_VERSION "1.0.0" CACHE STRING "" FORCE)
# Tell dependencies to use external version
set(DEPENDENCY_A_USE_EXTERNAL_COMMON ON CACHE BOOL "" FORCE)
set(DEPENDENCY_B_USE_EXTERNAL_COMMON ON CACHE BOOL "" FORCE)
# Fetch common library first
FetchContent_Declare(common_lib
GIT_REPOSITORY https://github.com/common/lib.git
GIT_TAG ${COMMON_LIB_VERSION}
)
FetchContent_MakeAvailable(common_lib)
# Then fetch dependencies
FetchContent_MakeAvailable(dependency_a dependency_b)
Quick Reference
# Basic usage
include(FetchContent)
FetchContent_Declare(
name
GIT_REPOSITORY https://github.com/user/repo.git
GIT_TAG v1.0.0
GIT_SHALLOW ON
)
FetchContent_MakeAvailable(name)
target_link_libraries(myapp PRIVATE name::name)
# Configure dependency options
set(DEPENDENCY_OPTION VALUE CACHE TYPE "" FORCE)
# Cache downloads
set(FETCHCONTENT_BASE_DIR "${CMAKE_SOURCE_DIR}/.cache")
# From URL
FetchContent_Declare(name
URL https://example.com/file.tar.gz
URL_HASH SHA256=abc123...
)
# Conditional fetch
find_package(name QUIET)
if(NOT name_FOUND)
FetchContent_Declare(...)
FetchContent_MakeAvailable(name)
endif()
Perfect for:
- Header-only libraries
- Modern C++ libraries (fmt, spdlog, json)
- Small to medium dependencies
- Version-sensitive projects
- Simplifying user setup
Consider alternatives for:
- Very large libraries (Boost, Qt, OpenCV)
- System libraries (OpenSSL, zlib)
- When binary compatibility is critical
- When system integration is important