ExternalProject_Add Basics
While FetchContent (covered in Part 10) integrates dependencies into your build tree at configure time, ExternalProject takes a fundamentally different approach — it downloads, configures, builds, and installs dependencies as separate build steps during your project's build phase. This makes it ideal for dependencies that don't use CMake or require isolation from your main build.
Including the Module and Basic Usage
ExternalProject is a standard CMake module that must be explicitly included:
cmake_minimum_required(VERSION 3.24)
project(MyProject LANGUAGES CXX)
# Include the ExternalProject module
include(ExternalProject)
# Add an external project — minimal example
ExternalProject_Add(json_ext
GIT_REPOSITORY https://github.com/nlohmann/json.git
GIT_TAG v3.11.3
CMAKE_ARGS
-DCMAKE_INSTALL_PREFIX=${CMAKE_BINARY_DIR}/deps
-DJSON_BuildTests=OFF
)
Every ExternalProject_Add call creates a custom target (here json_ext) that orchestrates a multi-step pipeline: download → update → patch → configure → build → install → test. Each step is a separate command that runs sequentially during the build phase.
cmake_minimum_required(VERSION 3.24)
project(SuperBuildDemo LANGUAGES CXX)
include(ExternalProject)
# Define where dependencies will be installed
set(DEPS_PREFIX ${CMAKE_BINARY_DIR}/deps_install)
# Build zlib from source with full step control
ExternalProject_Add(zlib_ext
GIT_REPOSITORY https://github.com/madler/zlib.git
GIT_TAG v1.3.1
# Configure step
CMAKE_ARGS
-DCMAKE_INSTALL_PREFIX=${DEPS_PREFIX}
-DCMAKE_BUILD_TYPE=Release
# Optionally skip the test step
TEST_COMMAND ""
)
Reference: CMake ExternalProject documentation.
Download Methods
ExternalProject supports multiple source acquisition strategies. The download method you choose depends on where the source lives and how you want to track versions.
GIT_REPOSITORY
The most common approach — clone a Git repository at a specific tag, branch, or commit:
include(ExternalProject)
# Clone from Git with shallow clone for speed
ExternalProject_Add(spdlog_ext
GIT_REPOSITORY https://github.com/gabime/spdlog.git
GIT_TAG v1.13.0
GIT_SHALLOW ON # Only fetch the specified tag (faster)
GIT_PROGRESS ON # Show clone progress
CMAKE_ARGS
-DCMAKE_INSTALL_PREFIX=${CMAKE_BINARY_DIR}/deps
-DSPDLOG_BUILD_EXAMPLE=OFF
-DSPDLOG_BUILD_TESTS=OFF
)
GIT_TAG with a specific tag or commit hash — never use branch names like main or master. Branch names cause non-reproducible builds since the content changes over time.
URL Download
Download a source archive from a URL with hash verification:
include(ExternalProject)
# Download a tarball with integrity check
ExternalProject_Add(boost_ext
URL https://boostorg.jfrog.io/artifactory/main/release/1.84.0/source/boost_1_84_0.tar.gz
URL_HASH SHA256=cc4b893acf645c9d4b698e9a0f08ca8846aa5d6c68275c14c3e7c949c67983d3
# Non-CMake project: override configure/build
CONFIGURE_COMMAND ./bootstrap.sh --prefix=${CMAKE_BINARY_DIR}/deps
BUILD_COMMAND ./b2 install --with-filesystem --with-system
BUILD_IN_SOURCE ON
INSTALL_COMMAND ""
)
Local Source Directories
Use a directory already on disk — useful for monorepos or vendored dependencies:
include(ExternalProject)
# Use a local source directory
ExternalProject_Add(mylib_ext
SOURCE_DIR ${CMAKE_SOURCE_DIR}/third_party/mylib
CMAKE_ARGS
-DCMAKE_INSTALL_PREFIX=${CMAKE_BINARY_DIR}/deps
# Disable download/update since source is local
DOWNLOAD_COMMAND ""
UPDATE_COMMAND ""
)
Configure Arguments
CMAKE_ARGS
Pass options to the external project's CMake configure step. These become -D definitions on the command line:
include(ExternalProject)
ExternalProject_Add(fmt_ext
GIT_REPOSITORY https://github.com/fmtlib/fmt.git
GIT_TAG 10.2.1
CMAKE_ARGS
-DCMAKE_INSTALL_PREFIX=${CMAKE_BINARY_DIR}/deps
-DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE}
-DFMT_DOC=OFF
-DFMT_TEST=OFF
-DFMT_INSTALL=ON
-DCMAKE_POSITION_INDEPENDENT_CODE=ON
-DCMAKE_C_COMPILER=${CMAKE_C_COMPILER}
-DCMAKE_CXX_COMPILER=${CMAKE_CXX_COMPILER}
)
CMAKE_CACHE_ARGS
For arguments containing semicolons (which CMake interprets as list separators), use CMAKE_CACHE_ARGS which writes directly to the cache file:
include(ExternalProject)
ExternalProject_Add(opencv_ext
GIT_REPOSITORY https://github.com/opencv/opencv.git
GIT_TAG 4.9.0
# CMAKE_CACHE_ARGS handles semicolons in values correctly
CMAKE_CACHE_ARGS
-DCMAKE_INSTALL_PREFIX:PATH=${CMAKE_BINARY_DIR}/deps
-DBUILD_LIST:STRING=core;imgproc;highgui
-DWITH_CUDA:BOOL=OFF
-DWITH_FFMPEG:BOOL=OFF
-DBUILD_TESTS:BOOL=OFF
-DBUILD_PERF_TESTS:BOOL=OFF
-DBUILD_EXAMPLES:BOOL=OFF
)
Build Steps
BUILD_COMMAND and BUILD_IN_SOURCE
Override the default build command for non-CMake projects or special build requirements:
include(ExternalProject)
# Build a project that uses autotools (configure/make)
ExternalProject_Add(openssl_ext
URL https://www.openssl.org/source/openssl-3.2.1.tar.gz
URL_HASH SHA256=83c7329fe52c850677d75e5d0b0ca245309b97e8ecbcfdc1dfdc4ab9fac35b39
BUILD_IN_SOURCE ON
CONFIGURE_COMMAND ./config --prefix=${CMAKE_BINARY_DIR}/deps
--openssldir=${CMAKE_BINARY_DIR}/deps/ssl
no-shared
BUILD_COMMAND make -j4
INSTALL_COMMAND make install_sw
TEST_COMMAND ""
)
BUILD_BYPRODUCTS
When using Ninja generator, you must declare output files so Ninja knows what the external project produces. Without this, Ninja cannot establish proper dependency chains:
include(ExternalProject)
set(ZLIB_INSTALL_DIR ${CMAKE_BINARY_DIR}/deps)
ExternalProject_Add(zlib_ext
GIT_REPOSITORY https://github.com/madler/zlib.git
GIT_TAG v1.3.1
CMAKE_ARGS
-DCMAKE_INSTALL_PREFIX=${ZLIB_INSTALL_DIR}
# Declare what files this build produces (required for Ninja)
BUILD_BYPRODUCTS
${ZLIB_INSTALL_DIR}/lib/libz.a
${ZLIB_INSTALL_DIR}/lib/${CMAKE_STATIC_LIBRARY_PREFIX}z${CMAKE_STATIC_LIBRARY_SUFFIX}
)
# Create an imported target that depends on the external project
add_library(ZLIB::ZLIB STATIC IMPORTED GLOBAL)
set_target_properties(ZLIB::ZLIB PROPERTIES
IMPORTED_LOCATION ${ZLIB_INSTALL_DIR}/lib/libz.a
INTERFACE_INCLUDE_DIRECTORIES ${ZLIB_INSTALL_DIR}/include
)
add_dependencies(ZLIB::ZLIB zlib_ext)
Install and Test Steps
Control what happens after the build completes:
include(ExternalProject)
ExternalProject_Add(googletest_ext
GIT_REPOSITORY https://github.com/google/googletest.git
GIT_TAG v1.14.0
CMAKE_ARGS
-DCMAKE_INSTALL_PREFIX=${CMAKE_BINARY_DIR}/deps
-DBUILD_GMOCK=ON
-DINSTALL_GTEST=ON
# Custom install: only install Release config on multi-config generators
INSTALL_COMMAND ${CMAKE_COMMAND} --build . --target install --config Release
# Run GTest's own test suite after building
TEST_COMMAND ${CMAKE_CTEST_COMMAND} --output-on-failure
# Alternatively, skip testing entirely:
# TEST_COMMAND ""
)
You can also add arbitrary commands at any step:
include(ExternalProject)
ExternalProject_Add(mylib_ext
GIT_REPOSITORY https://github.com/example/mylib.git
GIT_TAG v2.0.0
CMAKE_ARGS -DCMAKE_INSTALL_PREFIX=${CMAKE_BINARY_DIR}/deps
# Add a custom patch step between download and configure
PATCH_COMMAND ${CMAKE_COMMAND} -E copy
${CMAKE_SOURCE_DIR}/patches/mylib_fix.patch
/fix.patch
# Run a custom command after install
TEST_COMMAND ${CMAKE_COMMAND} -E echo "mylib_ext installed successfully"
)
Step Targets
By default, ExternalProject creates a single target that runs all steps. You can expose individual steps as separate targets for finer-grained control:
include(ExternalProject)
ExternalProject_Add(heavy_dep
GIT_REPOSITORY https://github.com/example/heavy.git
GIT_TAG v5.0.0
CMAKE_ARGS -DCMAKE_INSTALL_PREFIX=${CMAKE_BINARY_DIR}/deps
)
# Expose individual steps as targets
ExternalProject_Add_StepTargets(heavy_dep
download
configure
build
install
)
# Now you can reference: heavy_dep-download, heavy_dep-configure, etc.
# Useful for CI: build just the download step to cache source
# cmake --build . --target heavy_dep-download
You can also add entirely custom steps that execute between built-in steps:
include(ExternalProject)
ExternalProject_Add(protobuf_ext
GIT_REPOSITORY https://github.com/protocolbuffers/protobuf.git
GIT_TAG v25.3
CMAKE_ARGS -DCMAKE_INSTALL_PREFIX=${CMAKE_BINARY_DIR}/deps
-Dprotobuf_BUILD_TESTS=OFF
)
# Add a custom step that runs after install
ExternalProject_Add_Step(protobuf_ext verify
COMMENT "Verifying protoc installation..."
COMMAND ${CMAKE_BINARY_DIR}/deps/bin/protoc --version
DEPENDEES install # Run after install step
DEPENDERS "" # No steps depend on this
ALWAYS OFF
)
flowchart TD
A[mkdir] --> B[download]
B --> C[update]
C --> D[patch]
D --> E[configure]
E --> F[build]
F --> G[install]
G --> H[test]
D -.->|Custom Step| I[verify_patch]
I -.-> E
G -.->|Custom Step| J[post_install]
style A fill:#f8f9fa,stroke:#3B9797
style B fill:#f8f9fa,stroke:#3B9797
style C fill:#f8f9fa,stroke:#3B9797
style D fill:#f8f9fa,stroke:#3B9797
style E fill:#f8f9fa,stroke:#16476A
style F fill:#f8f9fa,stroke:#16476A
style G fill:#f8f9fa,stroke:#16476A
style H fill:#f8f9fa,stroke:#16476A
style I fill:#fff3cd,stroke:#BF092F
style J fill:#fff3cd,stroke:#BF092F
The Superbuild Pattern
The superbuild pattern is a widely-adopted CMake architecture that solves a fundamental problem: how do you build your project when its dependencies aren't installed on the system and you need to build them from source?
Two-Level CMake Architecture
A superbuild uses two levels of CMake configuration:
- Outer project (superbuild) — Uses
ExternalProject_Addto build all dependencies AND the main project itself - Inner project (your application) — A normal CMakeLists.txt that uses
find_packageto locate pre-installed dependencies
flowchart TB
subgraph Outer["Outer Project (superbuild/CMakeLists.txt)"]
A[ExternalProject: zlib] --> D[ExternalProject: main_project]
B[ExternalProject: libpng] --> D
C[ExternalProject: spdlog] --> D
end
subgraph Inner["Inner Project (src/CMakeLists.txt)"]
E[find_package zlib]
F[find_package PNG]
G[find_package spdlog]
H[add_executable MyApp]
E --> H
F --> H
G --> H
end
D -->|"CMAKE_PREFIX_PATH=deps/"| Inner
style Outer fill:#f0f7f7,stroke:#3B9797
style Inner fill:#f0f4f9,stroke:#16476A
Outer vs Inner Project
A typical superbuild project has this structure:
my_project/
├── CMakeLists.txt # Outer: superbuild that builds deps + inner
├── superbuild/
│ └── CMakeLists.txt # Alternative: separate superbuild file
├── src/
│ ├── CMakeLists.txt # Inner: normal project with find_package
│ ├── main.cpp
│ └── app/
│ ├── CMakeLists.txt
│ └── app.cpp
└── cmake/
└── Superbuild.cmake # Or keep superbuild logic in a module
The outer project treats your own source as just another ExternalProject_Add call, passing the install prefix of all dependencies via CMAKE_PREFIX_PATH.
# Outer CMakeLists.txt (the superbuild)
cmake_minimum_required(VERSION 3.24)
project(MyProjectSuper LANGUAGES NONE) # NONE: no compilation in outer
include(ExternalProject)
set(DEPS_PREFIX ${CMAKE_BINARY_DIR}/deps_install)
# Build zlib
ExternalProject_Add(zlib_ext
GIT_REPOSITORY https://github.com/madler/zlib.git
GIT_TAG v1.3.1
CMAKE_ARGS
-DCMAKE_INSTALL_PREFIX=${DEPS_PREFIX}
-DCMAKE_BUILD_TYPE=Release
)
# Build libpng (depends on zlib)
ExternalProject_Add(libpng_ext
GIT_REPOSITORY https://github.com/glennrp/libpng.git
GIT_TAG v1.6.43
CMAKE_ARGS
-DCMAKE_INSTALL_PREFIX=${DEPS_PREFIX}
-DCMAKE_PREFIX_PATH=${DEPS_PREFIX}
-DCMAKE_BUILD_TYPE=Release
-DPNG_TESTS=OFF
DEPENDS zlib_ext # Ensure zlib is built first
)
# Build the actual project (depends on all libraries)
ExternalProject_Add(main_project
SOURCE_DIR ${CMAKE_SOURCE_DIR}/src
CMAKE_ARGS
-DCMAKE_INSTALL_PREFIX=${CMAKE_BINARY_DIR}/install
-DCMAKE_PREFIX_PATH=${DEPS_PREFIX}
-DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE}
DEPENDS zlib_ext libpng_ext
# Don't download — source is local
DOWNLOAD_COMMAND ""
UPDATE_COMMAND ""
)
Passing Information Between Projects
Since ExternalProject operates at build time, you cannot directly access variables or targets from external projects. Use ExternalProject_Get_Property to retrieve directory paths:
include(ExternalProject)
ExternalProject_Add(spdlog_ext
GIT_REPOSITORY https://github.com/gabime/spdlog.git
GIT_TAG v1.13.0
CMAKE_ARGS -DCMAKE_INSTALL_PREFIX=
)
# Retrieve properties of the external project
ExternalProject_Get_Property(spdlog_ext
SOURCE_DIR
BINARY_DIR
INSTALL_DIR
)
message(STATUS "spdlog source: ${SOURCE_DIR}")
message(STATUS "spdlog build: ${BINARY_DIR}")
message(STATUS "spdlog install: ${INSTALL_DIR}")
# Use INSTALL_DIR to create imported targets
add_library(spdlog::spdlog INTERFACE IMPORTED)
set_target_properties(spdlog::spdlog PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES "${INSTALL_DIR}/include"
)
add_dependencies(spdlog::spdlog spdlog_ext)
The IMPORTED target pattern bridges the gap between ExternalProject (build-time) and your targets (configure-time). By creating an imported library that references the future install location and adding a dependency on the external project target, CMake ensures correct build ordering.
include(ExternalProject)
set(FMT_INSTALL_DIR ${CMAKE_BINARY_DIR}/fmt_install)
ExternalProject_Add(fmt_ext
GIT_REPOSITORY https://github.com/fmtlib/fmt.git
GIT_TAG 10.2.1
CMAKE_ARGS -DCMAKE_INSTALL_PREFIX=${FMT_INSTALL_DIR}
-DFMT_TEST=OFF
BUILD_BYPRODUCTS ${FMT_INSTALL_DIR}/lib/libfmt.a
)
# Create imported target pointing to future install location
add_library(fmt::fmt STATIC IMPORTED GLOBAL)
set_target_properties(fmt::fmt PROPERTIES
IMPORTED_LOCATION "${FMT_INSTALL_DIR}/lib/libfmt.a"
INTERFACE_INCLUDE_DIRECTORIES "${FMT_INSTALL_DIR}/include"
)
add_dependencies(fmt::fmt fmt_ext)
# Now use it like any other target
add_executable(myapp main.cpp)
target_link_libraries(myapp PRIVATE fmt::fmt)
FetchContent vs ExternalProject
Both modules manage external dependencies, but they work at fundamentally different phases of the build:
| Feature | FetchContent (Part 10) | ExternalProject (Part 11) |
|---|---|---|
| When it runs | Configure time | Build time |
| Target visibility | Full — all targets available in your CMakeLists.txt | None — only installed artifacts visible |
| Non-CMake projects | Difficult (requires wrapper) | Native support (custom configure/build commands) |
| Build isolation | Shares your build tree (can cause conflicts) | Fully isolated build directory per project |
| Configure speed | Slows configure (downloads at configure time) | Fast configure (downloads at build time) |
| Use case | CMake-based libraries you want tight integration with | Isolation, non-CMake deps, superbuild architecture |
Complete Superbuild Example
Here's a real-world superbuild that builds zlib, libpng, and a main application that uses both:
# superbuild/CMakeLists.txt
cmake_minimum_required(VERSION 3.24)
project(ImageProcessorSuper LANGUAGES NONE)
include(ExternalProject)
# Common settings
set(EP_PREFIX ${CMAKE_BINARY_DIR}/ep)
set(INSTALL_DIR ${CMAKE_BINARY_DIR}/install)
set(BUILD_TYPE Release)
set(COMMON_CMAKE_ARGS
-DCMAKE_BUILD_TYPE=${BUILD_TYPE}
-DCMAKE_INSTALL_PREFIX=${INSTALL_DIR}
-DCMAKE_PREFIX_PATH=${INSTALL_DIR}
)
# ─── Dependency 1: zlib ───────────────────────────────────────
ExternalProject_Add(ep_zlib
PREFIX ${EP_PREFIX}/zlib
GIT_REPOSITORY https://github.com/madler/zlib.git
GIT_TAG v1.3.1
GIT_SHALLOW ON
CMAKE_ARGS ${COMMON_CMAKE_ARGS}
TEST_COMMAND ""
)
# ─── Dependency 2: libpng (depends on zlib) ───────────────────
ExternalProject_Add(ep_libpng
PREFIX ${EP_PREFIX}/libpng
GIT_REPOSITORY https://github.com/glennrp/libpng.git
GIT_TAG v1.6.43
GIT_SHALLOW ON
CMAKE_ARGS ${COMMON_CMAKE_ARGS}
-DPNG_TESTS=OFF
-DPNG_SHARED=OFF
DEPENDS ep_zlib
TEST_COMMAND ""
)
# ─── Dependency 3: spdlog (standalone) ────────────────────────
ExternalProject_Add(ep_spdlog
PREFIX ${EP_PREFIX}/spdlog
GIT_REPOSITORY https://github.com/gabime/spdlog.git
GIT_TAG v1.13.0
GIT_SHALLOW ON
CMAKE_ARGS ${COMMON_CMAKE_ARGS}
-DSPDLOG_BUILD_EXAMPLE=OFF
-DSPDLOG_BUILD_TESTS=OFF
TEST_COMMAND ""
)
# ─── Main project (depends on all above) ─────────────────────
ExternalProject_Add(ep_main
PREFIX ${EP_PREFIX}/main
SOURCE_DIR ${CMAKE_SOURCE_DIR}/../src
CMAKE_ARGS
-DCMAKE_BUILD_TYPE=${BUILD_TYPE}
-DCMAKE_PREFIX_PATH=${INSTALL_DIR}
-DCMAKE_INSTALL_PREFIX=${CMAKE_BINARY_DIR}/app_install
DEPENDS ep_zlib ep_libpng ep_spdlog
DOWNLOAD_COMMAND ""
UPDATE_COMMAND ""
)
# Build the superbuild
mkdir build && cd build
cmake ../superbuild
cmake --build . -j$(nproc)
# The application is now at build/app_install/bin/image_processor