Configure-time vs Build-time
CMake operates in two distinct phases, and understanding which commands run when is fundamental to writing correct build logic. Commands in your CMakeLists.txt execute at configure-time (when you run cmake -S . -B build), while the build system produced by CMake executes its rules at build-time (when you run cmake --build build).
flowchart LR
A["cmake -S . -B build"] --> B["Configure Phase"]
B --> C["Generate Phase"]
C --> D["Build System Files
(Makefiles/Ninja)"]
D --> E["cmake --build build"]
E --> F["Build Phase"]
F --> G["Compiled Binaries"]
style B fill:#3B9797,color:#fff
style C fill:#3B9797,color:#fff
style F fill:#132440,color:#fff
What's Available at Each Phase
At configure-time, you have full access to the CMake scripting language — variables, control flow, execute_process(), try_compile(), and file() operations. However, you don't have compiled binaries yet, and generator expressions haven't been evaluated.
At build-time, the native build system takes over. CMake's variables no longer exist — only the rules written into Makefiles or Ninja files execute. Custom commands and custom targets fire during this phase.
add_custom_command — not CMake variables, which are frozen once configuration ends.
# Configure-time: runs NOW during cmake invocation
message(STATUS "Configuring project: ${PROJECT_NAME}")
execute_process(COMMAND git rev-parse HEAD OUTPUT_VARIABLE GIT_HASH)
# Build-time: runs LATER during make/ninja invocation
add_custom_command(
OUTPUT ${CMAKE_BINARY_DIR}/timestamp.txt
COMMAND ${CMAKE_COMMAND} -E echo "Built at: $<$<BOOL:1>:now>"
COMMENT "Generating timestamp"
)
execute_process — Running Commands at Configure-time
The execute_process() command runs one or more external processes during configuration. It's your primary tool for probing the system, extracting version strings, checking tool availability, and running scripts that produce information needed by the build.
cmake_minimum_required(VERSION 3.20)
project(ConfigProbe)
# Simple command execution
execute_process(
COMMAND git rev-parse --short HEAD
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
OUTPUT_VARIABLE GIT_SHORT_HASH
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_QUIET
)
message(STATUS "Git hash: ${GIT_SHORT_HASH}")
Capturing Output
The execute_process() command provides several output-capture keywords:
execute_process(
COMMAND python3 --version
OUTPUT_VARIABLE PYTHON_VERSION_STRING
ERROR_VARIABLE PYTHON_ERROR
RESULT_VARIABLE PYTHON_RESULT
OUTPUT_STRIP_TRAILING_WHITESPACE
)
if(NOT PYTHON_RESULT EQUAL 0)
message(WARNING "Python3 not found: ${PYTHON_ERROR}")
else()
message(STATUS "Found: ${PYTHON_VERSION_STRING}")
endif()
You can also pipe commands together just like a shell pipeline:
# Pipe: equivalent to "cat version.txt | tr -d '\n'"
execute_process(
COMMAND cat ${CMAKE_SOURCE_DIR}/version.txt
COMMAND tr -d "\n"
OUTPUT_VARIABLE FILE_VERSION
OUTPUT_STRIP_TRAILING_WHITESPACE
)
Error Handling
Robust error handling is critical for cross-platform builds. Use RESULT_VARIABLE and optionally COMMAND_ERROR_IS_FATAL (CMake 3.19+):
# CMake 3.19+: automatic fatal error on failure
execute_process(
COMMAND pkg-config --modversion libssl
OUTPUT_VARIABLE SSL_VERSION
OUTPUT_STRIP_TRAILING_WHITESPACE
COMMAND_ERROR_IS_FATAL ANY
)
# Pre-3.19: manual check
execute_process(
COMMAND which clang-format
RESULT_VARIABLE CLANG_FORMAT_RESULT
OUTPUT_VARIABLE CLANG_FORMAT_PATH
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_QUIET
)
if(CLANG_FORMAT_RESULT EQUAL 0)
message(STATUS "clang-format found: ${CLANG_FORMAT_PATH}")
endif()
Detecting Compiler Capabilities at Configure-time
Create a CMakeLists.txt that uses execute_process() to detect whether the system has ccache installed. If found, configure the project to use it as a compiler launcher. If not found, print a status message and continue normally.
try_compile — Testing if Code Compiles
The try_compile() command attempts to compile a source file (or a whole project) at configure-time, storing whether the compilation succeeded. This is the CMake equivalent of autoconf's AC_TRY_COMPILE.
cmake_minimum_required(VERSION 3.20)
project(FeatureProbe CXX)
# Try compiling a test file
try_compile(HAS_FILESYSTEM
${CMAKE_BINARY_DIR}/try_compile
${CMAKE_SOURCE_DIR}/cmake/checks/has_filesystem.cpp
CXX_STANDARD 17
)
if(HAS_FILESYSTEM)
message(STATUS "std::filesystem is available")
target_compile_definitions(myapp PRIVATE HAS_STD_FILESYSTEM)
else()
message(STATUS "std::filesystem not available, using fallback")
endif()
check_cxx_source_compiles
For quick inline checks without creating separate test files, CMake provides the CheckCXXSourceCompiles module:
include(CheckCXXSourceCompiles)
# Check for C++20 concepts support
check_cxx_source_compiles("
#include <concepts>
template<typename T>
concept Numeric = std::integral<T> || std::floating_point<T>;
int main() { return 0; }
" HAS_CONCEPTS)
# Check for specific POSIX function
include(CheckSymbolExists)
check_symbol_exists(mmap "sys/mman.h" HAS_MMAP)
# Check for a compiler flag
include(CheckCXXCompilerFlag)
check_cxx_compiler_flag("-fsanitize=address" HAS_ASAN)
if(HAS_ASAN)
target_compile_options(myapp PRIVATE -fsanitize=address)
target_link_options(myapp PRIVATE -fsanitize=address)
endif()
try_run — Running Compiled Code at Configure-time
The try_run() command compiles and runs a test program at configure-time. This is useful for detecting runtime characteristics like endianness, type sizes, or library behavior:
try_run(RUN_RESULT COMPILE_RESULT
${CMAKE_BINARY_DIR}/try_run
${CMAKE_SOURCE_DIR}/cmake/checks/check_endian.cpp
RUN_OUTPUT_VARIABLE ENDIAN_OUTPUT
)
if(COMPILE_RESULT AND RUN_RESULT EQUAL 0)
message(STATUS "Endianness: ${ENDIAN_OUTPUT}")
endif()
Cross-Compilation Caveats
When cross-compiling, try_run() cannot execute the compiled binary on the host machine (it's built for a different architecture). CMake handles this with a try-run results file:
try_run() will fail during cross-compilation unless you provide pre-cached results via -C TryRunResults.cmake. For portable projects, prefer try_compile() over try_run() wherever possible.
# For cross-compilation, cache the results:
# In TryRunResults.cmake:
set(ENDIAN_RUN_RESULT 0 CACHE STRING "")
set(ENDIAN_RUN_OUTPUT "little" CACHE STRING "")
# Then configure with:
# cmake -S . -B build -DCMAKE_TOOLCHAIN_FILE=arm.cmake -C TryRunResults.cmake
add_custom_command (OUTPUT Form)
The add_custom_command(OUTPUT ...) form generates files at build-time. Unlike execute_process(), these commands don't run during configuration — they produce rules in the build system that fire only when their outputs are needed:
cmake_minimum_required(VERSION 3.20)
project(CodeGen CXX)
# Generate a header from a template at BUILD time
add_custom_command(
OUTPUT ${CMAKE_BINARY_DIR}/generated/config_report.h
COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_BINARY_DIR}/generated
COMMAND python3 ${CMAKE_SOURCE_DIR}/scripts/gen_config.py
--output ${CMAKE_BINARY_DIR}/generated/config_report.h
DEPENDS ${CMAKE_SOURCE_DIR}/scripts/gen_config.py
COMMENT "Generating config_report.h"
VERBATIM
)
add_executable(myapp src/main.cpp ${CMAKE_BINARY_DIR}/generated/config_report.h)
target_include_directories(myapp PRIVATE ${CMAKE_BINARY_DIR}/generated)
The DEPENDS Keyword
The DEPENDS keyword is crucial — it tells the build system to re-run the custom command when any dependency changes:
# DEPENDS on a file: rebuild when input changes
add_custom_command(
OUTPUT ${CMAKE_BINARY_DIR}/messages.pb.h ${CMAKE_BINARY_DIR}/messages.pb.cc
COMMAND protoc --cpp_out=${CMAKE_BINARY_DIR}
-I${CMAKE_SOURCE_DIR}/proto
${CMAKE_SOURCE_DIR}/proto/messages.proto
DEPENDS ${CMAKE_SOURCE_DIR}/proto/messages.proto
COMMENT "Running protoc on messages.proto"
VERBATIM
)
# DEPENDS on a target: rebuild when that target is rebuilt
add_custom_command(
OUTPUT ${CMAKE_BINARY_DIR}/checksum.txt
COMMAND ${CMAKE_COMMAND} -E sha256sum $<TARGET_FILE:myapp>
> ${CMAKE_BINARY_DIR}/checksum.txt
DEPENDS myapp
COMMENT "Computing checksum of myapp"
VERBATIM
)
flowchart TD
A["messages.proto"] -->|DEPENDS| B["add_custom_command
protoc"]
B --> C["messages.pb.h"]
B --> D["messages.pb.cc"]
C --> E["add_executable
myapp"]
D --> E
F["main.cpp"] --> E
E --> G["myapp binary"]
G -->|DEPENDS| H["add_custom_command
sha256sum"]
H --> I["checksum.txt"]
style B fill:#3B9797,color:#fff
style H fill:#3B9797,color:#fff
style E fill:#132440,color:#fff
add_custom_command (TARGET Form)
The TARGET form attaches a command to an existing target, executing it at a specific point in the target's build process:
PRE_BUILD
# Note: PRE_BUILD only works as expected with Visual Studio generators.
# On Makefile/Ninja generators, it behaves like PRE_LINK.
add_custom_command(
TARGET myapp PRE_BUILD
COMMAND ${CMAKE_COMMAND} -E echo "Starting build of myapp..."
VERBATIM
)
PRE_LINK
# Runs after compilation but before linking
add_custom_command(
TARGET myapp PRE_LINK
COMMAND ${CMAKE_COMMAND} -E echo "All objects compiled, about to link..."
VERBATIM
)
POST_BUILD
# Runs after the target is built — most commonly used
add_custom_command(
TARGET myapp POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy $<TARGET_FILE:myapp> ${CMAKE_SOURCE_DIR}/bin/
COMMENT "Copying myapp to bin/"
VERBATIM
)
# Copy shared library dependencies on Windows
add_custom_command(
TARGET myapp POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
$<TARGET_RUNTIME_DLLS:myapp> $<TARGET_FILE_DIR:myapp>
COMMAND_EXPAND_LISTS
COMMENT "Copying runtime DLLs"
VERBATIM
)
add_custom_target — Always-Out-of-Date Targets
The add_custom_target() command creates a target that is always considered out of date — its commands run every time you invoke it. This is perfect for utility tasks like formatting, linting, or documentation generation:
# A "format" target that runs clang-format on all sources
file(GLOB_RECURSE ALL_SOURCES src/*.cpp src/*.h include/*.h)
add_custom_target(format
COMMAND clang-format -i ${ALL_SOURCES}
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
COMMENT "Running clang-format on all sources"
VERBATIM
)
# A "docs" target that runs Doxygen
add_custom_target(docs
COMMAND doxygen ${CMAKE_SOURCE_DIR}/Doxyfile
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
COMMENT "Generating API documentation"
VERBATIM
)
Coordination Targets
Custom targets can also serve as coordination points using DEPENDS and add_dependencies():
# Generate multiple protocol buffer files
add_custom_command(OUTPUT msg_a.pb.cc COMMAND protoc ... DEPENDS msg_a.proto)
add_custom_command(OUTPUT msg_b.pb.cc COMMAND protoc ... DEPENDS msg_b.proto)
# Coordination target: ensures all proto files are generated
add_custom_target(generate_protos
DEPENDS msg_a.pb.cc msg_b.pb.cc
COMMENT "All protocol buffers generated"
)
# Main target depends on the coordination target
add_executable(server src/main.cpp msg_a.pb.cc msg_b.pb.cc)
add_dependencies(server generate_protos)
POST_BUILD Asset Packaging
Create a project where a POST_BUILD custom command on an executable target packages the binary along with a config.json and a README.txt into a dist/ directory. Use cmake -E commands for portability.
file() Operations
The file() command is a Swiss-army knife for filesystem operations at configure-time. Some sub-commands also work at generate-time.
GLOB and GLOB_RECURSE
# Collect source files (use with caution — see warning below)
file(GLOB SOURCES src/*.cpp)
file(GLOB_RECURSE ALL_HEADERS CONFIGURE_DEPENDS include/*.h)
# CONFIGURE_DEPENDS (CMake 3.12+): re-run configure if files change
# Warning: still not recommended for source lists by CMake developers
file(GLOB) to collect source files. If you add or remove a source file, CMake won't know to re-configure unless you use CONFIGURE_DEPENDS (which has performance implications). Prefer explicit source lists.
DOWNLOAD, COPY, and READ/WRITE
# Download a file at configure-time
file(DOWNLOAD
"https://example.com/data/test-vectors.json"
${CMAKE_BINARY_DIR}/test-vectors.json
STATUS DOWNLOAD_STATUS
TIMEOUT 30
EXPECTED_HASH SHA256=abc123...
)
list(GET DOWNLOAD_STATUS 0 STATUS_CODE)
if(NOT STATUS_CODE EQUAL 0)
message(FATAL_ERROR "Download failed: ${DOWNLOAD_STATUS}")
endif()
# Read a file into a variable
file(READ ${CMAKE_SOURCE_DIR}/VERSION.txt PROJECT_VERSION_STRING)
string(STRIP "${PROJECT_VERSION_STRING}" PROJECT_VERSION_STRING)
# Write content to a file
file(WRITE ${CMAKE_BINARY_DIR}/build_info.txt
"Built on: ${CMAKE_SYSTEM_NAME}\n"
"Compiler: ${CMAKE_CXX_COMPILER_ID} ${CMAKE_CXX_COMPILER_VERSION}\n"
)
# Copy files
file(COPY ${CMAKE_SOURCE_DIR}/assets/ DESTINATION ${CMAKE_BINARY_DIR}/assets)
file(GENERATE) — Generate-time File Creation
Unlike other file() sub-commands, file(GENERATE) runs at generate-time (after configuration, before build). This means it can use generator expressions:
# Generate a file with generator expressions
file(GENERATE
OUTPUT ${CMAKE_BINARY_DIR}/$<CONFIG>/paths.txt
CONTENT "Binary: $<TARGET_FILE:myapp>\nConfig: $<CONFIG>\n"
)
# Generate from an input template
file(GENERATE
OUTPUT ${CMAKE_BINARY_DIR}/launch_$<CONFIG>.sh
INPUT ${CMAKE_SOURCE_DIR}/cmake/launch.sh.in
FILE_PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE
)
CMake Script Mode
CMake can execute .cmake files as standalone scripts using the -P flag. Script mode has no concept of targets, generators, or build directories — it's pure CMake scripting:
# Run a CMake script directly
cmake -P scripts/cleanup.cmake
# Pass variables to the script
cmake -DOUTPUT_DIR=/tmp/artifacts -P scripts/package.cmake
Utility Scripts
# scripts/cleanup.cmake — standalone utility script
cmake_minimum_required(VERSION 3.20)
# Remove build artifacts
set(DIRS_TO_CLEAN build dist .cache)
foreach(dir IN LISTS DIRS_TO_CLEAN)
if(EXISTS ${CMAKE_CURRENT_LIST_DIR}/../${dir})
message(STATUS "Removing: ${dir}/")
file(REMOVE_RECURSE ${CMAKE_CURRENT_LIST_DIR}/../${dir})
endif()
endforeach()
message(STATUS "Cleanup complete")
# scripts/version_bump.cmake — bump patch version
cmake_minimum_required(VERSION 3.20)
file(READ "${CMAKE_CURRENT_LIST_DIR}/../VERSION.txt" VERSION)
string(STRIP "${VERSION}" VERSION)
string(REPLACE "." ";" VERSION_LIST "${VERSION}")
list(GET VERSION_LIST 0 MAJOR)
list(GET VERSION_LIST 1 MINOR)
list(GET VERSION_LIST 2 PATCH)
math(EXPR PATCH "${PATCH} + 1")
set(NEW_VERSION "${MAJOR}.${MINOR}.${PATCH}")
file(WRITE "${CMAKE_CURRENT_LIST_DIR}/../VERSION.txt" "${NEW_VERSION}\n")
message(STATUS "Version bumped: ${VERSION} -> ${NEW_VERSION}")
Script mode is also commonly used inside add_custom_command for portable build-time operations:
# Use cmake -P inside a custom command for portable file operations
add_custom_command(
OUTPUT ${CMAKE_BINARY_DIR}/manifest.json
COMMAND ${CMAKE_COMMAND} -P ${CMAKE_SOURCE_DIR}/cmake/generate_manifest.cmake
DEPENDS ${CMAKE_SOURCE_DIR}/cmake/generate_manifest.cmake
COMMENT "Generating build manifest"
VERBATIM
)
Practical Patterns
Downloading Resources at Configure-time
# Download test data only if not already cached
set(TEST_DATA_URL "https://example.com/testdata/v2.tar.gz")
set(TEST_DATA_FILE ${CMAKE_BINARY_DIR}/testdata.tar.gz)
set(TEST_DATA_DIR ${CMAKE_BINARY_DIR}/testdata)
if(NOT EXISTS ${TEST_DATA_DIR})
message(STATUS "Downloading test data...")
file(DOWNLOAD ${TEST_DATA_URL} ${TEST_DATA_FILE}
STATUS DL_STATUS
SHOW_PROGRESS
)
list(GET DL_STATUS 0 DL_CODE)
if(NOT DL_CODE EQUAL 0)
message(FATAL_ERROR "Failed to download test data")
endif()
# Extract
file(ARCHIVE_EXTRACT INPUT ${TEST_DATA_FILE}
DESTINATION ${TEST_DATA_DIR}
)
file(REMOVE ${TEST_DATA_FILE})
endif()
Stamp Files for Caching Expensive Operations
When a custom command is expensive but its inputs rarely change, use a stamp file pattern to avoid unnecessary re-execution:
# Stamp file pattern: only re-run if inputs are newer than stamp
set(CODEGEN_STAMP ${CMAKE_BINARY_DIR}/codegen.stamp)
add_custom_command(
OUTPUT ${CODEGEN_STAMP}
COMMAND python3 ${CMAKE_SOURCE_DIR}/tools/codegen.py
--input ${CMAKE_SOURCE_DIR}/schema/
--output ${CMAKE_BINARY_DIR}/generated/
COMMAND ${CMAKE_COMMAND} -E touch ${CODEGEN_STAMP}
DEPENDS
${CMAKE_SOURCE_DIR}/tools/codegen.py
${CMAKE_SOURCE_DIR}/schema/api.yaml
${CMAKE_SOURCE_DIR}/schema/types.yaml
COMMENT "Running code generator"
VERBATIM
)
add_custom_target(codegen DEPENDS ${CODEGEN_STAMP})
add_dependencies(myapp codegen)
Build-time Resource Validator
Write a CMake script (validate_assets.cmake) that checks all .json files in an assets/ directory for valid JSON syntax using cmake -E and file(READ). Integrate it as a POST_BUILD custom command on your main executable, so asset integrity is verified after every successful build.
Conclusion & Next Steps
Understanding the boundary between configure-time and build-time is one of the most important concepts in CMake mastery. Here's a summary of when to use each tool:
| Task | Phase | Command |
|---|---|---|
| Probe system environment | Configure | execute_process() |
| Test if code compiles | Configure | try_compile() |
| Run code to detect features | Configure | try_run() |
| Generate source files | Build | add_custom_command(OUTPUT) |
| Post-process binaries | Build | add_custom_command(TARGET) |
| Utility tasks (format, lint) | Build | add_custom_target() |
| Filesystem manipulation | Configure | file() |
| Generate-expression-aware files | Generate | file(GENERATE) |
| Portable build scripts | Build | cmake -P |