Table of Contents

  1. FetchContent Overview
  2. Fetching from Git
  3. Fetching Archives
  4. FetchContent vs find_package
  5. FETCHCONTENT_BASE_DIR
  6. Dependency Options Override
  7. Multiple Dependencies
  8. FetchContent with GoogleTest
  9. FetchContent with fmt/spdlog
  10. Best Practices & Pitfalls
  11. Conclusion & Next Steps
Back to CMake Mastery Series

Part 10: Managing Dependencies with FetchContent

June 4, 2026 Wasil Zafar 40 min read

Download, configure, and build third-party dependencies directly within your CMake project using FetchContent — no manual downloads, no git submodules, no system packages required.

FetchContent Module Overview

The FetchContent module (CMake 3.11+) allows you to declare external dependencies that CMake will download and incorporate at configure time. Unlike find_package() which requires dependencies to be pre-installed, FetchContent makes your project self-contained — anyone can clone and build without manually installing dependencies first.

Key Insight: FetchContent downloads and configures dependencies during the CMake configure step (when you run cmake -B build). The downloaded sources become part of your build tree, compiled alongside your own code with the same compiler and settings.

FetchContent_Declare and FetchContent_MakeAvailable

The two essential commands form a declare-then-populate pattern:

# CMakeLists.txt — Basic FetchContent pattern
cmake_minimum_required(VERSION 3.16)
project(FetchDemo LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)

# 1. Include the module
include(FetchContent)

# 2. Declare what to fetch (does NOT download yet)
FetchContent_Declare(
    json                               # Logical name (lowercase)
    GIT_REPOSITORY https://github.com/nlohmann/json.git
    GIT_TAG        v3.11.3             # Pin to specific release
)

# 3. Make it available (downloads if needed, then add_subdirectory)
FetchContent_MakeAvailable(json)

# 4. Use it — nlohmann_json provides the target
add_executable(app main.cpp)
target_link_libraries(app PRIVATE nlohmann_json::nlohmann_json)
FetchContent Workflow
        flowchart TD
            A["include(FetchContent)"] --> B["FetchContent_Declare(dep
GIT_REPOSITORY ... GIT_TAG ...)"] B --> C["FetchContent_MakeAvailable(dep)"] C --> D{Already
populated?} D -->|Yes| E["Skip download
Use cached source"] D -->|No| F["Download from
GIT_REPOSITORY/URL"] F --> G["add_subdirectory(
source_dir, binary_dir)"] E --> G G --> H["Dependency targets
now available"] H --> I["target_link_libraries(
app PRIVATE dep::dep)"]

The separation between FetchContent_Declare and FetchContent_MakeAvailable is intentional — it allows parent projects to override declarations before population occurs.

Fetching from Git

GIT_REPOSITORY and GIT_TAG

Git is the most common source for FetchContent. Key parameters:

# CMakeLists.txt — Git fetch with all common options
cmake_minimum_required(VERSION 3.16)
project(GitFetch LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
include(FetchContent)

FetchContent_Declare(
    spdlog
    GIT_REPOSITORY https://github.com/gabime/spdlog.git
    GIT_TAG        v1.13.0             # Tag (human-readable)
    GIT_SHALLOW    TRUE                # Don't fetch full history
    GIT_PROGRESS   TRUE                # Show download progress
)

FetchContent_MakeAvailable(spdlog)

add_executable(logger main.cpp)
target_link_libraries(logger PRIVATE spdlog::spdlog)

Pinning to Commits vs Tags

You can pin to tags, branches, or exact commit hashes. Each has trade-offs:

# CMakeLists.txt — Different pinning strategies
cmake_minimum_required(VERSION 3.16)
project(PinDemo LANGUAGES CXX)

include(FetchContent)

# Option 1: Tag (readable, but mutable if author force-pushes)
FetchContent_Declare(fmt_tag
    GIT_REPOSITORY https://github.com/fmtlib/fmt.git
    GIT_TAG        10.2.1
)

# Option 2: Commit SHA (immutable, reproducible, but unreadable)
FetchContent_Declare(fmt_commit
    GIT_REPOSITORY https://github.com/fmtlib/fmt.git
    GIT_TAG        0c9fce2ffefecfdce794e1859584e25f9110670e  # v10.2.1
)

# Option 3: Branch (NEVER do this for reproducible builds)
# FetchContent_Declare(fmt_branch
#     GIT_REPOSITORY https://github.com/fmtlib/fmt.git
#     GIT_TAG        master  # BAD: changes constantly
# )
Warning: Never pin to a branch name (main, master, develop) in production projects. Your build will fetch different code on different days, breaking reproducibility. Always use tags or commit SHAs.

Fetching Archives

URL and URL_HASH

For projects that publish release tarballs (faster than git clone), use URL-based fetching:

# CMakeLists.txt — Fetching from archive URL
cmake_minimum_required(VERSION 3.16)
project(ArchiveFetch LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
include(FetchContent)

# Download a release tarball (faster than git clone)
FetchContent_Declare(
    catch2
    URL      https://github.com/catchorg/Catch2/archive/refs/tags/v3.5.2.tar.gz
    URL_HASH SHA256=269543a0f4a5cd7f4b6013d5f03ea4e14c93a6e12a03146cd7445e0f30ed3f40
)

FetchContent_MakeAvailable(catch2)

# Catch2 provides Catch2::Catch2WithMain target
add_executable(tests test_main.cpp)
target_link_libraries(tests PRIVATE Catch2::Catch2WithMain)
Security: Always use URL_HASH with archive downloads. This ensures the downloaded file hasn't been tampered with and provides a deterministic build. Without it, a compromised CDN could inject malicious code.

FetchContent vs find_package

When to Use Each

Both find_package() and FetchContent solve dependency management, but they serve different needs:

Criteria find_package() FetchContent
Dependency installed? Required beforehand Downloaded automatically
Build integration Uses pre-built binaries Compiled from source
Configure speed Fast (already built) Slower (downloads + compiles)
Reproducibility Depends on system Fully reproducible
Best for System libs, large frameworks Small-medium libs, testing

Dependency Provider Pattern (CMake 3.24+)

CMake 3.24 introduced dependency providers — a mechanism that lets find_package() transparently fall back to FetchContent when a system package isn't found:

Dependency Provider Fallback (CMake 3.24+)
        flowchart TD
            A["find_package(fmt REQUIRED)"] --> B["Dependency Provider
intercepts call"] B --> C{System package
available?} C -->|Yes| D["Use system fmt
(pre-installed)"] C -->|No| E["FetchContent_Declare
+ MakeAvailable"] E --> F["Download & build
fmt from source"] D --> G["fmt::fmt target
available"] F --> G
# cmake/dependencies.cmake — Dependency provider setup (CMake 3.24+)
cmake_minimum_required(VERSION 3.24)
include(FetchContent)

# This macro is called by CMake for every find_package() call
macro(myproject_provide_dependency method package_name)
    if("${package_name}" STREQUAL "fmt")
        FetchContent_Declare(fmt
            GIT_REPOSITORY https://github.com/fmtlib/fmt.git
            GIT_TAG        10.2.1
            FIND_PACKAGE_ARGS 10.0  # Version for find_package fallback
        )
        FetchContent_MakeAvailable(fmt)
    endif()
endmacro()

cmake_language(
    SET_DEPENDENCY_PROVIDER myproject_provide_dependency
    SUPPORTED_METHODS FIND_PACKAGE FETCHCONTENT_MAKEAVAILABLE_SERIAL
)
# CMakeLists.txt — Using FIND_PACKAGE_ARGS (simpler approach, CMake 3.24+)
cmake_minimum_required(VERSION 3.24)
project(ProviderDemo LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
include(FetchContent)

# Declare with FIND_PACKAGE_ARGS — try find_package first, fetch if missing
FetchContent_Declare(
    fmt
    GIT_REPOSITORY https://github.com/fmtlib/fmt.git
    GIT_TAG        10.2.1
    FIND_PACKAGE_ARGS 10.0 CONFIG  # Args passed to find_package()
)

# This tries find_package(fmt 10.0 CONFIG) first
# If not found, falls back to fetching from Git
FetchContent_MakeAvailable(fmt)

add_executable(app main.cpp)
target_link_libraries(app PRIVATE fmt::fmt)

FETCHCONTENT_BASE_DIR

By default, FetchContent downloads everything into the build directory. You can change this to share downloads across multiple build configurations or projects:

# Share downloads across Debug and Release builds
cmake -B build-debug -DCMAKE_BUILD_TYPE=Debug \
    -DFETCHCONTENT_BASE_DIR=/tmp/cmake-deps -S .

cmake -B build-release -DCMAKE_BUILD_TYPE=Release \
    -DFETCHCONTENT_BASE_DIR=/tmp/cmake-deps -S .

# Both builds share the same downloaded sources (compiled separately)
# CMakeLists.txt — Setting base dir programmatically
cmake_minimum_required(VERSION 3.16)
project(BaseDirDemo LANGUAGES CXX)

# Cache downloads outside build tree for persistence
set(FETCHCONTENT_BASE_DIR "${CMAKE_SOURCE_DIR}/_deps"
    CACHE PATH "FetchContent download directory")

include(FetchContent)
FetchContent_Declare(json
    GIT_REPOSITORY https://github.com/nlohmann/json.git
    GIT_TAG        v3.11.3
)
FetchContent_MakeAvailable(json)

Dependency Options Override

Setting Options Before MakeAvailable

Many libraries expose CMake options (BUILD_TESTING, BUILD_SHARED_LIBS, etc.). You can override these before calling FetchContent_MakeAvailable:

# CMakeLists.txt — Overriding dependency options
cmake_minimum_required(VERSION 3.16)
project(OptionsDemo LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
include(FetchContent)

# Disable spdlog's tests and examples to speed up build
set(SPDLOG_BUILD_EXAMPLE OFF CACHE BOOL "" FORCE)
set(SPDLOG_BUILD_TESTS OFF CACHE BOOL "" FORCE)
set(SPDLOG_BUILD_BENCH OFF CACHE BOOL "" FORCE)

FetchContent_Declare(
    spdlog
    GIT_REPOSITORY https://github.com/gabime/spdlog.git
    GIT_TAG        v1.13.0
)

FetchContent_MakeAvailable(spdlog)

add_executable(app main.cpp)
target_link_libraries(app PRIVATE spdlog::spdlog)
Hands-On Suppressing Dependency Warnings
Silencing Compiler Warnings from Third-Party Code

When fetching libraries, their code may trigger compiler warnings with your strict flags. Use SYSTEM to suppress them.

SYSTEM warnings CMAKE_CXX_FLAGS
# CMakeLists.txt — Marking fetched deps as SYSTEM (CMake 3.25+)
cmake_minimum_required(VERSION 3.25)
project(SystemDemo LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
include(FetchContent)

# Mark all FetchContent deps as SYSTEM to suppress their warnings
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -Werror")

FetchContent_Declare(
    json
    GIT_REPOSITORY https://github.com/nlohmann/json.git
    GIT_TAG        v3.11.3
    SYSTEM                  # Treat includes as system headers (CMake 3.25+)
)

FetchContent_MakeAvailable(json)

add_executable(app main.cpp)
target_link_libraries(app PRIVATE nlohmann_json::nlohmann_json)

OVERRIDE_FIND_PACKAGE

The OVERRIDE_FIND_PACKAGE option (CMake 3.24+) makes subsequent find_package() calls for the same package use the fetched version instead of searching the system:

# CMakeLists.txt — OVERRIDE_FIND_PACKAGE pattern
cmake_minimum_required(VERSION 3.24)
project(OverrideDemo LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
include(FetchContent)

# Declare with OVERRIDE_FIND_PACKAGE
FetchContent_Declare(
    GTest
    GIT_REPOSITORY https://github.com/google/googletest.git
    GIT_TAG        v1.14.0
    OVERRIDE_FIND_PACKAGE  # Any find_package(GTest) now uses this
)

# Later code (or subdirectories) that call find_package(GTest)
# will automatically get the fetched version
find_package(GTest REQUIRED)

add_executable(tests test_main.cpp)
target_link_libraries(tests PRIVATE GTest::gtest_main)

Multiple Dependencies

Projects typically have multiple dependencies. Declare them all, then populate them together:

# CMakeLists.txt — Multiple dependencies
cmake_minimum_required(VERSION 3.16)
project(MultiDep LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
include(FetchContent)

# Declare all dependencies first
FetchContent_Declare(
    fmt
    GIT_REPOSITORY https://github.com/fmtlib/fmt.git
    GIT_TAG        10.2.1
    GIT_SHALLOW    TRUE
)

FetchContent_Declare(
    spdlog
    GIT_REPOSITORY https://github.com/gabime/spdlog.git
    GIT_TAG        v1.13.0
    GIT_SHALLOW    TRUE
)

FetchContent_Declare(
    json
    GIT_REPOSITORY https://github.com/nlohmann/json.git
    GIT_TAG        v3.11.3
    GIT_SHALLOW    TRUE
)

# spdlog depends on fmt — set this before making available
set(SPDLOG_FMT_EXTERNAL ON CACHE BOOL "" FORCE)

# Populate all at once (order matters if there are interdependencies)
FetchContent_MakeAvailable(fmt spdlog json)

add_executable(app main.cpp)
target_link_libraries(app PRIVATE
    fmt::fmt
    spdlog::spdlog
    nlohmann_json::nlohmann_json
)
Order Matters: When dependency B depends on A, declare and make A available before B. In the example above, fmt must be listed before spdlog in FetchContent_MakeAvailable because spdlog can optionally use an external fmt.

FetchContent with GoogleTest

GoogleTest is the most common FetchContent use case. Here's a complete, working example:

Hands-On Complete GoogleTest Integration
Building and Running Tests with Fetched GTest

Set up a project that fetches GoogleTest, writes tests, and runs them with CTest — all from a clean checkout.

GoogleTest CTest enable_testing
# CMakeLists.txt — Complete GoogleTest with FetchContent
cmake_minimum_required(VERSION 3.16)
project(MyProject LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)

# === Production code ===
add_library(mathlib src/math.cpp)
target_include_directories(mathlib PUBLIC include)

add_executable(app src/main.cpp)
target_link_libraries(app PRIVATE mathlib)

# === Testing ===
enable_testing()
include(FetchContent)

# Prevent GTest from installing alongside our project
set(INSTALL_GTEST OFF CACHE BOOL "" FORCE)

FetchContent_Declare(
    googletest
    GIT_REPOSITORY https://github.com/google/googletest.git
    GIT_TAG        v1.14.0
    GIT_SHALLOW    TRUE
)

FetchContent_MakeAvailable(googletest)

# Include GoogleTest module for gtest_discover_tests
include(GoogleTest)

add_executable(math_tests tests/test_math.cpp)
target_link_libraries(math_tests PRIVATE
    mathlib
    GTest::gtest_main
)

# Automatically discover and register test cases with CTest
gtest_discover_tests(math_tests)
// include/math.h
#pragma once

int add(int a, int b);
int factorial(int n);
// src/math.cpp
#include "math.h"
#include <stdexcept>

int add(int a, int b) { return a + b; }

int factorial(int n) {
    if (n < 0) throw std::invalid_argument("negative input");
    if (n == 0) return 1;
    return n * factorial(n - 1);
}
// tests/test_math.cpp
#include <gtest/gtest.h>
#include "math.h"

TEST(AddTest, PositiveNumbers) {
    EXPECT_EQ(add(2, 3), 5);
    EXPECT_EQ(add(0, 0), 0);
}

TEST(AddTest, NegativeNumbers) {
    EXPECT_EQ(add(-1, -1), -2);
    EXPECT_EQ(add(-1, 1), 0);
}

TEST(FactorialTest, BaseCase) {
    EXPECT_EQ(factorial(0), 1);
    EXPECT_EQ(factorial(1), 1);
}

TEST(FactorialTest, NormalCases) {
    EXPECT_EQ(factorial(5), 120);
    EXPECT_EQ(factorial(10), 3628800);
}

TEST(FactorialTest, NegativeThrows) {
    EXPECT_THROW(factorial(-1), std::invalid_argument);
}
# Build and run tests
cmake -B build -S .
cmake --build build
cd build && ctest --output-on-failure

FetchContent with fmt and spdlog

A common real-world combination: fmt (formatting) and spdlog (logging, built on fmt). This demonstrates handling inter-dependency between fetched libraries:

Hands-On fmt + spdlog Integration
Header-Only and Compiled Dependencies Together

Fetch fmt as a compiled library and configure spdlog to use the external fmt instead of its bundled copy.

fmt spdlog inter-dependency
# CMakeLists.txt — fmt + spdlog with external fmt
cmake_minimum_required(VERSION 3.16)
project(LoggingDemo LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
include(FetchContent)

# === Fetch fmt first (spdlog depends on it) ===
FetchContent_Declare(
    fmt
    GIT_REPOSITORY https://github.com/fmtlib/fmt.git
    GIT_TAG        10.2.1
    GIT_SHALLOW    TRUE
)

# === Fetch spdlog, configured to use external fmt ===
set(SPDLOG_FMT_EXTERNAL ON CACHE BOOL "Use external fmt" FORCE)
set(SPDLOG_BUILD_EXAMPLE OFF CACHE BOOL "" FORCE)

FetchContent_Declare(
    spdlog
    GIT_REPOSITORY https://github.com/gabime/spdlog.git
    GIT_TAG        v1.13.0
    GIT_SHALLOW    TRUE
)

# Order: fmt before spdlog (dependency ordering)
FetchContent_MakeAvailable(fmt spdlog)

# === Application ===
add_executable(app main.cpp)
target_link_libraries(app PRIVATE spdlog::spdlog fmt::fmt)
// main.cpp — Using fmt and spdlog together
#include <fmt/core.h>
#include <fmt/chrono.h>
#include <spdlog/spdlog.h>
#include <spdlog/sinks/basic_file_sink.h>
#include <chrono>

int main() {
    // Direct fmt usage
    std::string greeting = fmt::format("Hello, {}!", "CMake");
    fmt::print("{}\n", greeting);

    // spdlog console logging
    spdlog::info("Application started at {}", 
        std::chrono::system_clock::now());
    
    // spdlog file logging
    auto file_logger = spdlog::basic_logger_mt("file", "app.log");
    file_logger->info("Logging to file works!");

    spdlog::warn("This is a warning with value: {}", 42);
    return 0;
}
# Build and run
cmake -B build -S .
cmake --build build --parallel
./build/app

Best Practices and Pitfalls

FetchContent is powerful but has sharp edges. Follow these guidelines for maintainable builds:

Best Practices:
  • Always pin versions — Use tags or commit SHAs, never branch names
  • Use GIT_SHALLOW TRUE — Dramatically faster clones for large repositories
  • Disable unnecessary targets — Set BUILD_TESTING, BUILD_EXAMPLES to OFF
  • Use URL_HASH for archives — Integrity verification against supply chain attacks
  • Prefer FIND_PACKAGE_ARGS (3.24+) — Try system packages first, fetch as fallback
  • Use SYSTEM (3.25+) — Suppress warnings from third-party code
# CMakeLists.txt — Offline/reproducible build support
cmake_minimum_required(VERSION 3.16)
project(OfflineDemo LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
include(FetchContent)

# FETCHCONTENT_FULLY_DISCONNECTED=ON skips all downloads
# (requires sources already populated in FETCHCONTENT_BASE_DIR)
# Useful for CI with pre-cached deps or air-gapped environments

# FETCHCONTENT_UPDATES_DISCONNECTED=ON skips update checks
# (uses previously downloaded sources without checking for updates)

FetchContent_Declare(
    fmt
    GIT_REPOSITORY https://github.com/fmtlib/fmt.git
    GIT_TAG        10.2.1
    GIT_SHALLOW    TRUE
)

FetchContent_MakeAvailable(fmt)

add_executable(app main.cpp)
target_link_libraries(app PRIVATE fmt::fmt)
# First build: downloads dependencies
cmake -B build -S .
cmake --build build

# Subsequent builds: skip network access
cmake -B build -DFETCHCONTENT_UPDATES_DISCONNECTED=ON -S .

# Fully offline (CI with pre-populated cache)
cmake -B build \
    -DFETCHCONTENT_FULLY_DISCONNECTED=ON \
    -DFETCHCONTENT_BASE_DIR=/ci-cache/cmake-deps \
    -S .
Common Pitfalls:
  • Name collisions — If two fetched libraries use the same target name internally, you'll get conflicts. Use FetchContent_GetProperties to check before declaring.
  • Slow CI builds — Every CI run re-downloads dependencies. Use FETCHCONTENT_BASE_DIR with a persistent cache directory.
  • ABI incompatibility — Fetched libraries are compiled with your project's flags. If you change CMAKE_CXX_STANDARD or CMAKE_POSITION_INDEPENDENT_CODE, all deps recompile.
  • Install contamination — Fetched deps may install their own targets/headers alongside yours. Set INSTALL_*=OFF options or use EXCLUDE_FROM_ALL.
  • Version conflicts — If dep A and dep B both fetch different versions of dep C, the first one declared wins. Plan your dependency tree carefully.
# CMakeLists.txt — Guarding against double-population
cmake_minimum_required(VERSION 3.16)
project(GuardDemo LANGUAGES CXX)

include(FetchContent)

# Check if already declared/populated (useful in multi-project setups)
FetchContent_GetProperties(fmt)
if(NOT fmt_POPULATED)
    FetchContent_Declare(
        fmt
        GIT_REPOSITORY https://github.com/fmtlib/fmt.git
        GIT_TAG        10.2.1
    )
    FetchContent_MakeAvailable(fmt)
endif()

add_executable(app main.cpp)
target_link_libraries(app PRIVATE fmt::fmt)

Conclusion & Next Steps

FetchContent transforms CMake dependency management from "install everything manually" to "clone and build." The key takeaways:

  • FetchContent_Declare + FetchContent_MakeAvailable is the core two-step pattern
  • Pin to tags or commit SHAs for reproducible builds — never branch names
  • Use GIT_SHALLOW and URL_HASH for performance and security
  • Set dependency options before MakeAvailable to control what gets built
  • FIND_PACKAGE_ARGS (3.24+) enables graceful system-or-fetch fallback
  • FETCHCONTENT_FULLY_DISCONNECTED enables offline and cached CI builds
  • Watch for name collisions and version conflicts in complex dependency trees
Official Reference: See the FetchContent module documentation and the Using Dependencies Guide for the complete specification.