Skip to content
C++
Package Manager
Updated 2026-07-14T00:00:00.000Z

CPM.cmake — CMake Package Manager

CPM.cmake for zero-setup C++ dependency management — CPMAddPackage, version pinning, offline mode, and local overrides.

TL;DR

CPM.cmake is a single-file CMake package manager built on top of FetchContent — no Python, no installers, no system-wide tools required. Drop one file into your repo, call CPMAddPackage, and CMake fetches, configures, and links your dependencies automatically. It works anywhere CMake works: Linux, macOS, Windows, CI runners, Docker containers, and embedded cross-compilation toolchains.

Use CPM when you want minimal setup overhead and tight CMake integration. Prefer vcpkg or Conan when you need binary caching across a large team or you are managing dozens of dependencies with complex version constraints.

cmake
# CMakeLists.txt — a complete working example
cmake_minimum_required(VERSION 3.21)
project(my_app)

include(cmake/CPM.cmake)

CPMAddPackage("gh:fmtlib/fmt#10.1.1")
CPMAddPackage("gh:nlohmann/json#v3.11.3")

add_executable(my_app main.cpp)
target_link_libraries(my_app PRIVATE fmt::fmt nlohmann_json::nlohmann_json)

Setup — Single File Include

CPM requires exactly one file and one include() call. The recommended approach is to commit CPM.cmake directly into your repository under cmake/, eliminating any external dependency for the build system itself.

Download the latest release manually:

bash
mkdir -p cmake
curl -fsSL https://github.com/cpm-cmake/CPM.cmake/releases/latest/download/CPM.cmake \
     -o cmake/CPM.cmake

Then include it near the top of your CMakeLists.txt, before any CPMAddPackage calls:

cmake
cmake_minimum_required(VERSION 3.21)
project(my_project CXX)

include(cmake/CPM.cmake)

If you prefer to auto-download without committing the file, use the bootstrap snippet. CMake's built-in file(DOWNLOAD ...) fetches it at configure time:

cmake
set(CPM_DOWNLOAD_LOCATION "${CMAKE_CURRENT_BINARY_DIR}/cmake/CPM.cmake")

file(DOWNLOAD
     https://github.com/cpm-cmake/CPM.cmake/releases/latest/download/CPM.cmake
     ${CPM_DOWNLOAD_LOCATION}
     STATUS download_status
)

include(${CPM_DOWNLOAD_LOCATION})

This approach always pulls the latest release. For reproducible builds, pin to a specific version URL such as .../releases/download/v0.40.2/CPM.cmake.

Avoiding re-downloads with CPM_SOURCE_CACHE

By default, CPM re-downloads every dependency on each fresh build directory. Set CPM_SOURCE_CACHE to a persistent directory to cache git clones across configure runs and build directories:

cmake
# In CMakeLists.txt (or pass on the command line)
set(CPM_SOURCE_CACHE "$ENV{HOME}/.cpm" CACHE PATH "CPM source cache")

Or pass it at configure time without touching your CMakeLists.txt:

bash
cmake -B build -DCPM_SOURCE_CACHE=~/.cpm

The cache stores bare git repositories. A package fetched once is available instantly on subsequent configures, even across different projects that use CPM.


CPMAddPackage — Core Usage

CPMAddPackage is the single function you will use for almost everything. It accepts a GitHub shorthand string for the common case, or a full keyword form for advanced control.

The shorthand format is gh:owner/repo#tag:

cmake
CPMAddPackage("gh:fmtlib/fmt#10.1.1")
CPMAddPackage("gh:gabime/spdlog#v1.14.1")
CPMAddPackage("gh:nlohmann/json#v3.11.3")

The full keyword form gives you fine-grained control:

cmake
CPMAddPackage(
  NAME             fmt
  VERSION          10.1.1
  GIT_REPOSITORY   https://github.com/fmtlib/fmt.git
  GIT_TAG          10.1.1
  OPTIONS
    "FMT_INSTALL OFF"
    "FMT_DOC OFF"
    "FMT_TEST OFF"
)

OPTIONS forwards CMake cache variables into the dependency's configure step. This is the cleanest way to disable tests, docs, and install targets in third-party code you do not want to build.

DOWNLOAD_ONLY — header-only libraries

For header-only libraries where you only need the source tree and no CMake targets, use DOWNLOAD_ONLY YES:

cmake
CPMAddPackage(
  NAME           expected-lite
  GIT_REPOSITORY https://github.com/martinmoene/expected-lite.git
  GIT_TAG        v0.6.3
  DOWNLOAD_ONLY  YES
)

if(expected-lite_ADDED)
  add_library(expected-lite INTERFACE)
  target_include_directories(expected-lite INTERFACE "${expected-lite_SOURCE_DIR}/include")
endif()

After a DOWNLOAD_ONLY call, <NAME>_SOURCE_DIR is set to the download location. You build the CMake target manually.

EXCLUDE_FROM_ALL

Use EXCLUDE_FROM_ALL YES to prevent a dependency's targets from being included in the default build. This avoids building examples or benchmarks that some libraries enable by default:

cmake
CPMAddPackage(
  NAME           benchmark
  GIT_REPOSITORY https://github.com/google/benchmark.git
  GIT_TAG        v1.8.3
  EXCLUDE_FROM_ALL YES
  OPTIONS
    "BENCHMARK_ENABLE_TESTING OFF"
    "BENCHMARK_ENABLE_INSTALL OFF"
)

Common Package Examples

Copy-paste ready CPMAddPackage calls for the libraries you are most likely to need. The target names listed are the ones to pass to target_link_libraries.

cmake
# fmt — string formatting (target: fmt::fmt)
CPMAddPackage(
  NAME    fmt
  VERSION 10.1.1
  GITHUB_REPOSITORY fmtlib/fmt
  GIT_TAG 10.1.1
  OPTIONS "FMT_INSTALL OFF" "FMT_TEST OFF"
)

# nlohmann_json — JSON (target: nlohmann_json::nlohmann_json)
CPMAddPackage(
  NAME    nlohmann_json
  VERSION 3.11.3
  GITHUB_REPOSITORY nlohmann/json
  GIT_TAG v3.11.3
  OPTIONS "JSON_BuildTests OFF"
)

# spdlog — logging (target: spdlog::spdlog)
CPMAddPackage(
  NAME    spdlog
  VERSION 1.14.1
  GITHUB_REPOSITORY gabime/spdlog
  GIT_TAG v1.14.1
  OPTIONS "SPDLOG_BUILD_EXAMPLE OFF" "SPDLOG_BUILD_TESTS OFF" "SPDLOG_INSTALL OFF"
)

# Catch2 — unit testing (targets: Catch2::Catch2WithMain or Catch2::Catch2)
CPMAddPackage(
  NAME    Catch2
  VERSION 3.7.1
  GITHUB_REPOSITORY catchorg/Catch2
  GIT_TAG v3.7.1
)

# GoogleTest — unit testing (targets: GTest::gtest_main, GTest::gmock)
CPMAddPackage(
  NAME    googletest
  VERSION 1.14.0
  GITHUB_REPOSITORY google/googletest
  GIT_TAG v1.14.0
  OPTIONS "INSTALL_GTEST OFF" "gtest_force_shared_crt ON"
)

# Abseil — Google's C++ base library (target: absl::strings, absl::status, etc.)
CPMAddPackage(
  NAME    abseil-cpp
  VERSION 20240116.2
  GITHUB_REPOSITORY abseil/abseil-cpp
  GIT_TAG 20240116.2
  OPTIONS "ABSL_PROPAGATE_CXX_STD ON" "ABSL_ENABLE_INSTALL OFF"
)

# CLI11 — command-line parsing (target: CLI11::CLI11)
CPMAddPackage(
  NAME    CLI11
  VERSION 2.4.1
  GITHUB_REPOSITORY CLIUtils/CLI11
  GIT_TAG v2.4.1
)

# Boost.URL — header-only URL parsing from Boost (DOWNLOAD_ONLY approach)
CPMAddPackage(
  NAME    boostorg_url
  GIT_REPOSITORY https://github.com/boostorg/url.git
  GIT_TAG boost-1.85.0
  DOWNLOAD_ONLY YES
)

Link your targets as usual:

cmake
add_executable(my_app main.cpp)
target_link_libraries(my_app PRIVATE
  fmt::fmt
  nlohmann_json::nlohmann_json
  spdlog::spdlog
  CLI11::CLI11
)

add_executable(my_tests tests.cpp)
target_link_libraries(my_tests PRIVATE Catch2::Catch2WithMain)

Version Pinning and Reproducibility

Pinning to a tag gives repeatable builds; pinning to a commit hash gives immutable builds. The difference matters when a library maintainer moves or re-creates a tag.

Using a version tag (common, usually sufficient):

cmake
CPMAddPackage(
  NAME    fmt
  VERSION 10.1.1
  GITHUB_REPOSITORY fmtlib/fmt
  GIT_TAG 10.1.1   # resolved at fetch time; tag could theoretically move
)

Using a commit SHA for an absolute pin:

cmake
CPMAddPackage(
  NAME           fmt
  GITHUB_REPOSITORY fmtlib/fmt
  GIT_TAG        e69e5f977d458f2650bb346dadf2ad30c5320281  # v10.1.1 exactly
)

Pinning by commit hash means the source will never change regardless of what happens to the upstream repository. This is the highest-confidence approach for security-sensitive or compliance-driven projects.

Tracking the main branch

You can follow a moving branch, but builds then vary depending on when they run:

cmake
CPMAddPackage(
  NAME    my_lib
  GITHUB_REPOSITORY acme/my_lib
  GIT_TAG main   # NOT reproducible — only for rapid prototyping
)

Avoid GIT_TAG main in production CMakeLists.txt files.

CPM vs vcpkg baselines

vcpkg locks the entire package ecosystem to a single baseline commit, guaranteeing that all packages were tested together. CPM pins each package individually, which gives you more flexibility but requires you to verify compatibility yourself. For multi-library projects where interaction between library versions matters, vcpkg's baseline model is a meaningful advantage.


CPM_SOURCE_CACHE — Offline Mode

Once all packages are cached, CPM works with no network connection. The source cache stores bare git clones, so a configure that hits the cache is as fast as reading from disk.

Set the cache path persistently in CMakeLists.txt:

cmake
set(CPM_SOURCE_CACHE "$ENV{HOME}/.cpm" CACHE PATH "CPM source cache directory")

Or inject it via environment variable, which is useful in CI without touching source files:

bash
export CPM_SOURCE_CACHE=/opt/cpm-cache
cmake -B build -G Ninja

The directory layout inside the cache:

cpp
~/.cpm/
  fmtlib/fmt/10.1.1/          ← bare git clone
  nlohmann/json/v3.11.3/
  gabime/spdlog/v1.14.1/
  catchorg/Catch2/v3.7.1/

Force re-download

Setting CPM_DOWNLOAD_ALL=ON bypasses the cache and re-clones every package. Use this to verify that upstream repositories still contain the expected content:

bash
cmake -B build -DCPM_DOWNLOAD_ALL=ON

Docker workflow with pre-populated cache

Pre-warm the cache in a base image so application containers skip the network step entirely:

dockerfile
FROM ubuntu:24.04 AS cpm-cache

RUN apt-get update && apt-get install -y cmake git ninja-build

# Fetch only the dependencies — no application source needed
COPY cmake/CPM.cmake /cache/cmake/CPM.cmake
COPY cmake/dependencies.cmake /cache/cmake/dependencies.cmake

WORKDIR /cache
RUN cmake -S . -B build \
      -DCPM_SOURCE_CACHE=/cpm-cache \
      -DCPM_DOWNLOAD_ALL=ON

# Application image
FROM ubuntu:24.04
COPY --from=cpm-cache /cpm-cache /cpm-cache
ENV CPM_SOURCE_CACHE=/cpm-cache

Extract your CPMAddPackage calls into a cmake/dependencies.cmake file included by a minimal top-level CMakeLists.txt. The cache layer is then rebuilt only when dependencies.cmake changes, not on every code change.

CI with GitHub Actions cache

yaml
# .github/workflows/build.yml
jobs:
  build:
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/checkout@v4

      - name: Restore CPM cache
        uses: actions/cache@v4
        with:
          path: ~/.cpm
          key: cpm-${{ hashFiles('cmake/dependencies.cmake') }}
          restore-keys: cpm-

      - name: Configure
        run: cmake -B build -G Ninja -DCPM_SOURCE_CACHE=~/.cpm

      - name: Build
        run: cmake --build build

The cache key hashes your dependencies file. When you bump a version or add a package the key changes, the cache misses once, and subsequent runs hit the new cache.


Local Overrides

Replace any CPM-managed dependency with a local checkout for active development. Instead of fetching from git, CPM uses the directory you specify:

cmake
CPMAddPackage(
  NAME       mylib
  SOURCE_DIR /home/dev/projects/mylib   # absolute path to local clone
)

All other CPMAddPackage arguments are ignored when SOURCE_DIR is present. The local directory is used as-is — CMake adds it with add_subdirectory. Changes to source files in that directory are picked up on the next build without re-running CMake configure.

This pattern is ideal when you are simultaneously developing your application and a library it depends on. Point SOURCE_DIR at your library's working tree during development, then switch back to the GIT_TAG form when you are done.

CPM_LOCAL_PACKAGES_ONLY

Set CPM_LOCAL_PACKAGES_ONLY=ON to prevent CPM from downloading anything. Every CPMAddPackage call must either resolve from the source cache or have a SOURCE_DIR. Useful for fully air-gapped builds:

bash
cmake -B build \
  -DCPM_SOURCE_CACHE=/opt/cpm-cache \
  -DCPM_LOCAL_PACKAGES_ONLY=ON

If a package is not found in the cache and has no SOURCE_DIR, CMake will produce a fatal error with a clear message.

OVERRIDE_FIND_PACKAGE

Some CMake projects call find_package(fmt) internally rather than using CPM directly. OVERRIDE_FIND_PACKAGE YES intercepts those calls and routes them through CPM:

cmake
CPMAddPackage(
  NAME              fmt
  GITHUB_REPOSITORY fmtlib/fmt
  GIT_TAG           10.1.1
  OVERRIDE_FIND_PACKAGE YES
)

# Any subsequent find_package(fmt) in this project or its subdirectories
# will use the CPM-managed version above instead of the system installation.

This is useful when you add a dependency that itself has dependencies — you control the exact version even for indirect requirements.


Integrating with find_package

CPMFindPackage tries find_package first and falls back to CPMAddPackage only if the system does not have the library. This is the right choice for libraries that might be installed system-wide (via apt, brew, or vcpkg) on some machines and not others:

cmake
CPMFindPackage(
  NAME    fmt
  VERSION 10.1.1
  GITHUB_REPOSITORY fmtlib/fmt
  GIT_TAG 10.1.1
)

The resolution order is:

  1. If find_package(fmt 10.1.1) succeeds, use the system library.
  2. If not found, fall back to fetching from git via CPM.

This approach is useful for library authors who want to support both "user installs the library" and "build it from source" workflows with a single CMakeLists.txt. Application authors who want strict reproducibility should use plain CPMAddPackage and skip the system lookup.

You can also check whether CPM added a package (vs found it via find_package) using the <NAME>_ADDED variable:

cmake
CPMFindPackage(NAME fmt VERSION 10.1.1 GITHUB_REPOSITORY fmtlib/fmt GIT_TAG 10.1.1)

if(fmt_ADDED)
  message(STATUS "fmt was fetched by CPM from source")
else()
  message(STATUS "fmt was found via find_package (system/vcpkg)")
endif()

Dependency Graph and Conflicts

CPM deduplicates packages by name across the entire build graph. When two packages both declare a dependency on fmt, CPM fetches it once and both packages share the single target.

Deduplication uses the package NAME. If package A requests fmt 10.1.1 and package B requests fmt 9.1.0, CPM uses whichever it encounters first and emits a warning. The second request is silently satisfied by the already-loaded version:

cpp
-- CPM: Adding package fmt@10.1.1 (https://github.com/fmtlib/fmt)
-- CPM: fmt@9.1.0 already added, skipping (using 10.1.1)

Resolving version conflicts

To force a specific version regardless of what dependencies request, add the CPMAddPackage call for that package at the top of your root CMakeLists.txt, before any add_subdirectory or CPMAddPackage that would transitively pull it in:

cmake
include(cmake/CPM.cmake)

# Force fmt 10.1.1 for the entire build graph
CPMAddPackage(
  NAME    fmt
  VERSION 10.1.1
  GITHUB_REPOSITORY fmtlib/fmt
  GIT_TAG 10.1.1
)

# Now add your main dependencies — they will reuse the fmt above
CPMAddPackage("gh:gabime/spdlog#v1.14.1")  # spdlog depends on fmt; will get 10.1.1

CPM_USE_LOCAL_PACKAGES

Set CPM_USE_LOCAL_PACKAGES=ON to prefer system-installed packages over CPM-fetched ones for every CPMAddPackage call. Equivalent to making every call behave like CPMFindPackage:

bash
cmake -B build -DCPM_USE_LOCAL_PACKAGES=ON

This is useful on developer machines where libraries are installed via the system package manager, saving configure time.


Performance in CI

A warm CPM source cache makes configure time nearly identical to a local developer machine. Cold configure (no cache, all packages fetched from network) is the main cost.

Typical times for a project with 8 dependencies:

ScenarioConfigure time
Cold (no cache, network fetch)45–90 seconds
Warm cache (git clones on disk)3–8 seconds
Conan binary download (cold)60–180 seconds
Conan binary download (warm)5–15 seconds

CPM's warm-cache performance is comparable to Conan's warm binary cache, but CPM always compiles from source — there is no binary reuse. For large projects with slow compilation, Conan's binary caching is a significant advantage. CPM's strength is simplicity, not build-time optimization.

GitHub Actions with caching

Cache the CPM_SOURCE_CACHE directory based on a hash of your dependencies. The full workflow:

yaml
name: CI

on: [push, pull_request]

jobs:
  build:
    runs-on: ubuntu-24.04

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Install build tools
        run: sudo apt-get install -y cmake ninja-build

      - name: Cache CPM packages
        id: cache-cpm
        uses: actions/cache@v4
        with:
          path: ~/.cpm
          key: cpm-v1-${{ runner.os }}-${{ hashFiles('cmake/dependencies.cmake', 'cmake/CPM.cmake') }}
          restore-keys: |
            cpm-v1-${{ runner.os }}-

      - name: Configure
        run: |
          cmake -B build -G Ninja \
            -DCMAKE_BUILD_TYPE=Release \
            -DCPM_SOURCE_CACHE=~/.cpm

      - name: Build
        run: cmake --build build --parallel

      - name: Test
        run: ctest --test-dir build --output-on-failure

Include both CPM.cmake and your dependencies file in the cache key hash. A change to either invalidates the cache and triggers a fresh download.

Splitting dependencies into a separate file

To maximise cache hit rate, keep your CPMAddPackage calls in a dedicated file:

cmake
# cmake/dependencies.cmake
include(cmake/CPM.cmake)

CPMAddPackage("gh:fmtlib/fmt#10.1.1")
CPMAddPackage("gh:nlohmann/json#v3.11.3")
CPMAddPackage("gh:gabime/spdlog#v1.14.1")
CPMAddPackage("gh:catchorg/Catch2#v3.7.1")
cmake
# CMakeLists.txt
cmake_minimum_required(VERSION 3.21)
project(my_project CXX)

include(cmake/dependencies.cmake)

add_subdirectory(src)
add_subdirectory(tests)

This way the CMakeLists.txt can change freely without invalidating the CPM cache key.


CPM vs FetchContent vs vcpkg vs Conan

Every tool solves the same problem differently. Choose based on your project's size, team size, and infrastructure constraints.

CPM.cmakeFetchContent (raw)vcpkgConan 2
SetupOne file, one includeBuilt into CMake, verboseClone vcpkg repo, set toolchainpip install, profile detect
Config fileCMakeLists.txtCMakeLists.txtvcpkg.jsonconanfile.txt / .py
Binary cachingNo (always source)NoYes (remote cache)Yes (Conan Center)
Offline supportYes (source cache)LimitedYes (binary cache)Yes (binary cache)
Ecosystem sizeAny git repoAny git repo2,000+ ports1,500+ packages
CMake integrationNative, seamlessNative, seamlessToolchain file requiredGenerator files required
Version lockingPer-package GIT_TAGPer-package GIT_TAGBaseline commit (all-or-nothing)conan.lock file
Learning curveMinimalLow (but verbose)Low–MediumMedium–High
Best forSmall–medium projects, CMake-first teamsOne-off source includesLarge teams, Windows-heavy projectsEnterprise, binary reuse priority

When to use CPM

  • Your project has fewer than 20–30 dependencies.
  • You want zero non-CMake tooling requirements.
  • You are distributing your CMakeLists.txt and want consumers to have a simple build experience.
  • You are working in embedded or cross-compilation contexts where system package managers are unavailable.

When to consider alternatives

  • You need pre-built binaries to keep CI under 10 minutes on a large codebase: use Conan.
  • You need fine-grained control over ABI compatibility and platform triplets: use vcpkg.
  • You are on a large team with a monorepo and want ecosystem-wide version locking: use vcpkg baselines.

Troubleshooting

Most CPM problems are CMake version issues or network failures.

CPM silently uses the wrong version. This happens when two packages request the same dependency at different versions. CPM uses the first one encountered. Fix it by adding an explicit CPMAddPackage for the conflicting package at the top of your root CMakeLists.txt.

Configure fails with "could not find git". CPM requires git on PATH. On minimal CI images, install it with apt-get install -y git or equivalent.

Packages re-download on every configure. CPM_SOURCE_CACHE is not set or is set to a path that does not persist between runs. Verify the cache path with cmake -B build -DCPM_SOURCE_CACHE=~/.cpm and check that the directory is not cleared between CI steps.

Build fails with "target not found". The package's CMakeLists.txt defines targets with different names than expected. Check the package's own documentation or its CMakeLists.txt for the correct exported target names.

DOWNLOAD_ONLY package has no targets. DOWNLOAD_ONLY YES intentionally skips CMake target creation. You must define add_library / target_include_directories manually using <NAME>_SOURCE_DIR.

cmake
CPMAddPackage(NAME tl-expected GITHUB_REPOSITORY TartanLlama/expected GIT_TAG v1.1.0 DOWNLOAD_ONLY YES)

if(tl-expected_ADDED)
  add_library(tl::expected INTERFACE IMPORTED)
  target_include_directories(tl::expected INTERFACE "${tl-expected_SOURCE_DIR}/include")
endif()

See Also

  • vcpkg — Microsoft's package manager with binary caching and manifest mode
  • Conan — Python-based package manager with Conan Center and binary reuse
  • CMake — the build system CPM is built on
  • hunter — an older CMake package manager; CPM is the modern successor
Edit on GitHubUpdated 2026-07-14T00:00:00.000Z