Table of Contents

  1. configure_file Basics
  2. Version Headers
  3. Feature Configuration
  4. Git Hash Embedding
  5. Build-time Code Generation
  6. Python-based Generators
  7. file(GENERATE)
  8. String Replacement Patterns
  9. Generated Source Dependencies
  10. Practical Example
  11. Conclusion & Next Steps
Back to CMake Mastery Series

Part 15: Generating Source Code

June 4, 2026 Wasil Zafar 35 min read

Transform templates into source code at configure-time with configure_file, leverage generator expressions in file(GENERATE), and orchestrate build-time code generators like protoc and Python scripts.

configure_file Basics

The configure_file() command copies a file from one location to another, substituting CMake variable references in the process. It's the primary mechanism for generating configuration headers, version files, and any source that needs build-system information baked in.

configure_file Flow
---
config:
  layout: elk
---
flowchart LR
    A["config.h.in
(template)"] --> B["configure_file()"] C["CMake Variables
PROJECT_VERSION
HAS_FEATURE_X"] --> B B --> D["config.h
(generated)"] D --> E["#include 'config.h'
in C++ source"] classDef process stroke:#2dd4bf,fill:#f0fdfa classDef generated stroke:#132440,fill:#e0f2fe,color:#000 class B process class D generated

Substitution Syntax

CMake supports two substitution patterns in template files:

# CMakeLists.txt
set(PROJECT_NAME "MyApp")
set(VERSION_MAJOR 2)
set(VERSION_MINOR 5)
set(VERSION_PATCH 1)

configure_file(
    ${CMAKE_SOURCE_DIR}/src/config.h.in
    ${CMAKE_BINARY_DIR}/generated/config.h
)
// src/config.h.in — the template file
#pragma once

// Both @VAR@ and ${VAR} are substituted by default
#define APP_NAME "@PROJECT_NAME@"
#define APP_VERSION_MAJOR ${VERSION_MAJOR}
#define APP_VERSION_MINOR ${VERSION_MINOR}
#define APP_VERSION_PATCH ${VERSION_PATCH}
#define APP_VERSION "@VERSION_MAJOR@.@VERSION_MINOR@.@VERSION_PATCH@"
// Generated output: build/generated/config.h
#pragma once

#define APP_NAME "MyApp"
#define APP_VERSION_MAJOR 2
#define APP_VERSION_MINOR 5
#define APP_VERSION_PATCH 1
#define APP_VERSION "2.5.1"
Key Insight: By default, configure_file() replaces both @VAR@ and ${VAR} patterns. If your template contains literal ${...} syntax (like shell scripts or Makefiles), use the @ONLY option to restrict substitution to @VAR@ patterns only.

Version Headers

One of the most common uses of configure_file() is generating a version header that embeds the project version directly into compiled code:

cmake_minimum_required(VERSION 3.20)
project(MyApp VERSION 3.1.4 LANGUAGES CXX)

# PROJECT_VERSION, PROJECT_VERSION_MAJOR, etc. are set automatically
configure_file(
    ${CMAKE_SOURCE_DIR}/cmake/version.h.in
    ${CMAKE_BINARY_DIR}/include/version.h
)

add_executable(myapp src/main.cpp)
target_include_directories(myapp PRIVATE ${CMAKE_BINARY_DIR}/include)
// cmake/version.h.in
#pragma once

#define MYAPP_VERSION_MAJOR @PROJECT_VERSION_MAJOR@
#define MYAPP_VERSION_MINOR @PROJECT_VERSION_MINOR@
#define MYAPP_VERSION_PATCH @PROJECT_VERSION_PATCH@
#define MYAPP_VERSION_STRING "@PROJECT_VERSION@"

Version from Git Tags

# Extract version from the latest Git tag (e.g., v3.1.4)
execute_process(
    COMMAND git describe --tags --abbrev=0
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
    OUTPUT_VARIABLE GIT_TAG
    OUTPUT_STRIP_TRAILING_WHITESPACE
    ERROR_QUIET
)

if(GIT_TAG MATCHES "^v([0-9]+)\\.([0-9]+)\\.([0-9]+)")
    set(VERSION_FROM_GIT "${CMAKE_MATCH_1}.${CMAKE_MATCH_2}.${CMAKE_MATCH_3}")
    message(STATUS "Version from Git tag: ${VERSION_FROM_GIT}")
else()
    set(VERSION_FROM_GIT "${PROJECT_VERSION}")
endif()

# Also get the full describe string (e.g., v3.1.4-7-gabcdef0)
execute_process(
    COMMAND git describe --tags --always --dirty
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
    OUTPUT_VARIABLE GIT_DESCRIBE
    OUTPUT_STRIP_TRAILING_WHITESPACE
    ERROR_QUIET
)

configure_file(src/version.h.in ${CMAKE_BINARY_DIR}/include/version.h)

Feature Configuration

The #cmakedefine directive in template files provides conditional compilation based on CMake variables:

#cmakedefine

// config.h.in
#pragma once

// #cmakedefine becomes #define if variable is truthy, or /* #undef */ if falsy
#cmakedefine HAS_OPENSSL
#cmakedefine HAS_ZLIB
#cmakedefine HAS_THREADS

// #cmakedefine with a value
#cmakedefine CACHE_SIZE @CACHE_SIZE@
# CMakeLists.txt
find_package(OpenSSL)
find_package(ZLIB)
find_package(Threads)

set(HAS_OPENSSL ${OPENSSL_FOUND})
set(HAS_ZLIB ${ZLIB_FOUND})
set(HAS_THREADS ${Threads_FOUND})
set(CACHE_SIZE 4096)

configure_file(config.h.in ${CMAKE_BINARY_DIR}/config.h)
// Generated config.h (assuming OpenSSL found, ZLIB not found, Threads found)
#pragma once

#define HAS_OPENSSL
/* #undef HAS_ZLIB */
#define HAS_THREADS

#define CACHE_SIZE 4096

#cmakedefine01

The #cmakedefine01 variant always produces a #define — with value 1 (truthy) or 0 (falsy). This is safer for #if checks that expect numeric values:

// config.h.in
#cmakedefine01 HAS_OPENSSL
#cmakedefine01 HAS_ZLIB
#cmakedefine01 ENABLE_DEBUG_LOGGING

// Generated (ZLIB not found):
#define HAS_OPENSSL 1
#define HAS_ZLIB 0
#define ENABLE_DEBUG_LOGGING 1

Conditional Compilation in Source

// src/main.cpp
#include "config.h"

#if HAS_OPENSSL
    #include <openssl/ssl.h>
#endif

int main() {
    #if HAS_OPENSSL
        SSL_library_init();
        std::cout << "SSL support enabled\n";
    #else
        std::cout << "Running without SSL\n";
    #endif
    return 0;
}
Experiment Feature Detection
Multi-Platform config.h

Create a config.h.in template that uses #cmakedefine01 to expose: whether the platform is Linux/Windows/macOS, whether std::filesystem is available (use try_compile from Part 14), and the system's page size (detected via execute_process). Include the generated header in a main.cpp that prints a system capabilities report.

#cmakedefine01 try_compile cross-platform

Git Hash Embedding

A common requirement is embedding the Git commit hash into the binary for traceability. The naive approach has a subtle problem:

# PROBLEM: This only captures the hash at CONFIGURE time.
# If you commit new code without reconfiguring, the hash is stale.
execute_process(
    COMMAND git rev-parse HEAD
    OUTPUT_VARIABLE GIT_HASH
    OUTPUT_STRIP_TRAILING_WHITESPACE
)
configure_file(git_version.h.in ${CMAKE_BINARY_DIR}/git_version.h)

Build-time Git Hash (Correct Approach)

To get the hash at build-time (so it updates on every build without reconfiguring), use a custom command that runs a CMake script:

# cmake/UpdateGitHash.cmake — script run at BUILD time
execute_process(
    COMMAND git rev-parse --short HEAD
    WORKING_DIRECTORY ${SOURCE_DIR}
    OUTPUT_VARIABLE GIT_HASH
    OUTPUT_STRIP_TRAILING_WHITESPACE
    ERROR_QUIET
)

execute_process(
    COMMAND git describe --tags --always --dirty
    WORKING_DIRECTORY ${SOURCE_DIR}
    OUTPUT_VARIABLE GIT_DESCRIBE
    OUTPUT_STRIP_TRAILING_WHITESPACE
    ERROR_QUIET
)

configure_file(
    ${SOURCE_DIR}/cmake/git_version.h.in
    ${BINARY_DIR}/include/git_version.h
)
# CMakeLists.txt — run the script at BUILD time
add_custom_target(git_version_header
    COMMAND ${CMAKE_COMMAND}
        -DSOURCE_DIR=${CMAKE_SOURCE_DIR}
        -DBINARY_DIR=${CMAKE_BINARY_DIR}
        -P ${CMAKE_SOURCE_DIR}/cmake/UpdateGitHash.cmake
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
    COMMENT "Updating git version header"
    VERBATIM
)

add_executable(myapp src/main.cpp)
target_include_directories(myapp PRIVATE ${CMAKE_BINARY_DIR}/include)
add_dependencies(myapp git_version_header)
// cmake/git_version.h.in
#pragma once
#define GIT_HASH "@GIT_HASH@"
#define GIT_DESCRIBE "@GIT_DESCRIBE@"
Key Insight: Because git_version_header is an add_custom_target (always out of date), the Git hash header is regenerated on every build. If the hash hasn't changed, the header file content stays the same and the compiler won't recompile dependent files (thanks to build system dependency tracking).

Build-time Code Generation

Protocol Buffers (protoc)

cmake_minimum_required(VERSION 3.20)
project(ProtoExample CXX)

find_package(Protobuf REQUIRED)

# Define .proto files
set(PROTO_FILES
    ${CMAKE_SOURCE_DIR}/proto/messages.proto
    ${CMAKE_SOURCE_DIR}/proto/services.proto
)

# Generate C++ sources from .proto files
set(PROTO_SRCS)
set(PROTO_HDRS)

foreach(PROTO_FILE IN LISTS PROTO_FILES)
    get_filename_component(PROTO_NAME ${PROTO_FILE} NAME_WE)
    set(PROTO_SRC ${CMAKE_BINARY_DIR}/proto/${PROTO_NAME}.pb.cc)
    set(PROTO_HDR ${CMAKE_BINARY_DIR}/proto/${PROTO_NAME}.pb.h)

    add_custom_command(
        OUTPUT ${PROTO_SRC} ${PROTO_HDR}
        COMMAND ${Protobuf_PROTOC_EXECUTABLE}
            --cpp_out=${CMAKE_BINARY_DIR}/proto
            -I${CMAKE_SOURCE_DIR}/proto
            ${PROTO_FILE}
        DEPENDS ${PROTO_FILE}
        COMMENT "Generating protobuf: ${PROTO_NAME}"
        VERBATIM
    )

    list(APPEND PROTO_SRCS ${PROTO_SRC})
    list(APPEND PROTO_HDRS ${PROTO_HDR})
endforeach()

add_library(proto_lib STATIC ${PROTO_SRCS} ${PROTO_HDRS})
target_include_directories(proto_lib PUBLIC ${CMAKE_BINARY_DIR}/proto)
target_link_libraries(proto_lib PUBLIC protobuf::libprotobuf)

add_executable(server src/main.cpp)
target_link_libraries(server PRIVATE proto_lib)

Flex/Bison

find_package(FLEX REQUIRED)
find_package(BISON REQUIRED)

BISON_TARGET(Parser
    ${CMAKE_SOURCE_DIR}/src/parser.y
    ${CMAKE_BINARY_DIR}/parser.cpp
    DEFINES_FILE ${CMAKE_BINARY_DIR}/parser.h
)

FLEX_TARGET(Lexer
    ${CMAKE_SOURCE_DIR}/src/lexer.l
    ${CMAKE_BINARY_DIR}/lexer.cpp
)

ADD_FLEX_BISON_DEPENDENCY(Lexer Parser)

add_executable(compiler
    src/main.cpp
    ${BISON_Parser_OUTPUTS}
    ${FLEX_Lexer_OUTPUTS}
)
target_include_directories(compiler PRIVATE ${CMAKE_BINARY_DIR})

Python-based Generators

Many projects use Python scripts to generate repetitive C/C++ code (lookup tables, register maps, serialization routines). Here's how to integrate them properly:

find_package(Python3 REQUIRED COMPONENTS Interpreter)

# Generate lookup tables from a YAML schema
add_custom_command(
    OUTPUT
        ${CMAKE_BINARY_DIR}/generated/lookup_tables.h
        ${CMAKE_BINARY_DIR}/generated/lookup_tables.cpp
    COMMAND Python3::Interpreter
        ${CMAKE_SOURCE_DIR}/tools/generate_tables.py
        --input ${CMAKE_SOURCE_DIR}/data/tables.yaml
        --output-dir ${CMAKE_BINARY_DIR}/generated
    DEPENDS
        ${CMAKE_SOURCE_DIR}/tools/generate_tables.py
        ${CMAKE_SOURCE_DIR}/data/tables.yaml
    COMMENT "Generating lookup tables from tables.yaml"
    VERBATIM
)

add_library(generated_tables
    ${CMAKE_BINARY_DIR}/generated/lookup_tables.cpp
)
target_include_directories(generated_tables PUBLIC
    ${CMAKE_BINARY_DIR}/generated
)

Dependency Tracking

If your Python script imports other modules or reads multiple data files, list them all in DEPENDS:

# Track all Python sources and data files
file(GLOB GENERATOR_DEPS
    ${CMAKE_SOURCE_DIR}/tools/generate_tables.py
    ${CMAKE_SOURCE_DIR}/tools/codegen_utils.py
    ${CMAKE_SOURCE_DIR}/data/*.yaml
)

add_custom_command(
    OUTPUT ${CMAKE_BINARY_DIR}/generated/registers.h
    COMMAND Python3::Interpreter ${CMAKE_SOURCE_DIR}/tools/generate_tables.py
        --input ${CMAKE_SOURCE_DIR}/data/registers.yaml
        --output ${CMAKE_BINARY_DIR}/generated/registers.h
    DEPENDS ${GENERATOR_DEPS}
    COMMENT "Generating register definitions"
    VERBATIM
)
Experiment Code Generation Pipeline
Python-generated Enum from JSON

Write a Python script that reads a JSON file containing error codes ({"TIMEOUT": 1, "CONNECTION_REFUSED": 2, ...}) and generates a C++ header with an enum class and a to_string() function. Wire it into CMake with add_custom_command so the header is regenerated whenever the JSON file changes.

Python codegen add_custom_command DEPENDS

file(GENERATE) — Generate-time File Creation

The file(GENERATE) command produces files at generate-time — after configuration but before the build system runs. Its unique advantage is support for generator expressions:

# Generate a config file that knows the final binary path
file(GENERATE
    OUTPUT ${CMAKE_BINARY_DIR}/$<CONFIG>/app_paths.h
    CONTENT "#pragma once
#define APP_BINARY_PATH \"$<TARGET_FILE:myapp>\"
#define APP_CONFIG \"$<CONFIG>\"
#define APP_INSTALL_PREFIX \"${CMAKE_INSTALL_PREFIX}\"
"
)

Generator Expressions in Generated Files

# Generate per-configuration launch scripts
file(GENERATE
    OUTPUT ${CMAKE_BINARY_DIR}/launch_$<CONFIG>.sh
    CONTENT "#!/bin/bash
# Auto-generated launch script for $<CONFIG> build
export LD_LIBRARY_PATH=$<TARGET_FILE_DIR:mylib>:$$LD_LIBRARY_PATH
exec $<TARGET_FILE:myapp> \"$$@\"
"
    FILE_PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE
    CONDITION $<NOT:$<PLATFORM_ID:Windows>>
)
Official Reference: The CONDITION keyword (CMake 3.20+) allows conditional file generation based on generator expressions. See the file(GENERATE) documentation for all available options.
Code Generation Pipeline
        flowchart TD
            A["version.h.in"] -->|configure_file| B["version.h"]
            C["config.h.in"] -->|configure_file| D["config.h"]
            E["messages.proto"] -->|add_custom_command
protoc| F["messages.pb.h
messages.pb.cc"] G["schema.yaml"] -->|add_custom_command
Python| H["generated.h
generated.cpp"] I["template.in"] -->|file GENERATE| J["per-config output"] B --> K["myapp"] D --> K F --> K H --> K style B fill:#3B9797,color:#fff style D fill:#3B9797,color:#fff style F fill:#132440,color:#fff style H fill:#132440,color:#fff style J fill:#16476A,color:#fff

String Replacement Patterns

@ONLY — Restricting Substitution

When your template contains literal ${...} patterns (common in shell scripts, Makefiles, or Perl code), use @ONLY to limit substitution to @VAR@ syntax only:

# Template contains ${VARIABLE} that should NOT be substituted
configure_file(
    ${CMAKE_SOURCE_DIR}/scripts/install.sh.in
    ${CMAKE_BINARY_DIR}/install.sh
    @ONLY
)
# scripts/install.sh.in
#!/bin/bash
# @CMAKE_INSTALL_PREFIX@ is substituted by CMake
# ${HOME} is left as-is (literal shell variable)
PREFIX="@CMAKE_INSTALL_PREFIX@"
CONFIG_DIR="${HOME}/.config/@PROJECT_NAME@"

mkdir -p "${CONFIG_DIR}"
cp @CMAKE_BINARY_DIR@/myapp "${PREFIX}/bin/"
echo "Installed to ${PREFIX}/bin/myapp"

Escaping Special Characters

# Use NEWLINE_STYLE to control line endings
configure_file(
    ${CMAKE_SOURCE_DIR}/cmake/config.rc.in
    ${CMAKE_BINARY_DIR}/config.rc
    NEWLINE_STYLE WIN32   # Force CRLF for Windows resource files
)

# For templates that shouldn't have ANY substitution (just copy):
configure_file(
    ${CMAKE_SOURCE_DIR}/data/template.txt
    ${CMAKE_BINARY_DIR}/template.txt
    COPYONLY
)

Generated Source Dependencies

Ensuring that generated files trigger rebuilds when their inputs change requires careful use of DEPENDS:

Proper Rebuild Triggers

# WRONG: No DEPENDS — generated file will only be created once
add_custom_command(
    OUTPUT ${CMAKE_BINARY_DIR}/generated.h
    COMMAND python3 ${CMAKE_SOURCE_DIR}/gen.py > ${CMAKE_BINARY_DIR}/generated.h
)

# CORRECT: Rebuilds when script or data changes
add_custom_command(
    OUTPUT ${CMAKE_BINARY_DIR}/generated.h
    COMMAND python3 ${CMAKE_SOURCE_DIR}/gen.py > ${CMAKE_BINARY_DIR}/generated.h
    DEPENDS
        ${CMAKE_SOURCE_DIR}/gen.py
        ${CMAKE_SOURCE_DIR}/data/input.json
    COMMENT "Regenerating generated.h"
    VERBATIM
)

# ALSO CORRECT: For configure_file, CMake tracks dependencies automatically
# (re-runs configure if .in file or referenced variables change)
configure_file(config.h.in ${CMAKE_BINARY_DIR}/config.h)
Important: configure_file() automatically creates a dependency — CMake will re-run configuration if the input template changes. But add_custom_command() requires explicit DEPENDS to track input changes.

Practical Example — Complete Project

Here's a complete project demonstrating all code generation techniques together:

# Project structure
myproject/
├── CMakeLists.txt
├── cmake/
│   ├── version.h.in
│   ├── config.h.in
│   └── UpdateGitVersion.cmake
├── proto/
│   └── messages.proto
├── tools/
│   └── generate_error_codes.py
├── data/
│   └── errors.json
└── src/
    └── main.cpp
# CMakeLists.txt — complete code generation example
cmake_minimum_required(VERSION 3.20)
project(CodeGenDemo VERSION 1.2.3 LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# =============================================================
# 1. Version header (configure_file)
# =============================================================
configure_file(
    ${CMAKE_SOURCE_DIR}/cmake/version.h.in
    ${CMAKE_BINARY_DIR}/include/version.h
)

# =============================================================
# 2. Feature configuration (configure_file + #cmakedefine)
# =============================================================
find_package(OpenSSL QUIET)
find_package(ZLIB QUIET)
option(ENABLE_LOGGING "Enable debug logging" ON)

set(HAS_OPENSSL ${OPENSSL_FOUND})
set(HAS_ZLIB ${ZLIB_FOUND})

configure_file(
    ${CMAKE_SOURCE_DIR}/cmake/config.h.in
    ${CMAKE_BINARY_DIR}/include/config.h
)

# =============================================================
# 3. Git hash at BUILD time (custom target + cmake -P)
# =============================================================
add_custom_target(update_git_version ALL
    COMMAND ${CMAKE_COMMAND}
        -DSOURCE_DIR=${CMAKE_SOURCE_DIR}
        -DBINARY_DIR=${CMAKE_BINARY_DIR}
        -P ${CMAKE_SOURCE_DIR}/cmake/UpdateGitVersion.cmake
    COMMENT "Updating Git version info"
    VERBATIM
)

# =============================================================
# 4. Python code generator (add_custom_command)
# =============================================================
find_package(Python3 REQUIRED COMPONENTS Interpreter)

add_custom_command(
    OUTPUT ${CMAKE_BINARY_DIR}/include/error_codes.h
    COMMAND Python3::Interpreter
        ${CMAKE_SOURCE_DIR}/tools/generate_error_codes.py
        --input ${CMAKE_SOURCE_DIR}/data/errors.json
        --output ${CMAKE_BINARY_DIR}/include/error_codes.h
    DEPENDS
        ${CMAKE_SOURCE_DIR}/tools/generate_error_codes.py
        ${CMAKE_SOURCE_DIR}/data/errors.json
    COMMENT "Generating error codes from errors.json"
    VERBATIM
)

# =============================================================
# 5. Build the executable with all generated sources
# =============================================================
add_executable(demo src/main.cpp
    ${CMAKE_BINARY_DIR}/include/error_codes.h
)
target_include_directories(demo PRIVATE ${CMAKE_BINARY_DIR}/include)
add_dependencies(demo update_git_version)

if(OPENSSL_FOUND)
    target_link_libraries(demo PRIVATE OpenSSL::SSL)
endif()
if(ZLIB_FOUND)
    target_link_libraries(demo PRIVATE ZLIB::ZLIB)
endif()
// cmake/version.h.in
#pragma once
#define PROJECT_VERSION_STRING "@PROJECT_VERSION@"
#define PROJECT_VERSION_MAJOR @PROJECT_VERSION_MAJOR@
#define PROJECT_VERSION_MINOR @PROJECT_VERSION_MINOR@
#define PROJECT_VERSION_PATCH @PROJECT_VERSION_PATCH@
// cmake/config.h.in
#pragma once
#cmakedefine01 HAS_OPENSSL
#cmakedefine01 HAS_ZLIB
#cmakedefine01 ENABLE_LOGGING
// src/main.cpp
#include "version.h"
#include "config.h"
#include "git_version.h"
#include "error_codes.h"
#include <iostream>

int main() {
    std::cout << "Version: " << PROJECT_VERSION_STRING << "\n";
    std::cout << "Git: " << GIT_DESCRIBE << "\n";
    std::cout << "OpenSSL: " << (HAS_OPENSSL ? "yes" : "no") << "\n";
    std::cout << "ZLIB: " << (HAS_ZLIB ? "yes" : "no") << "\n";
    std::cout << "Logging: " << (ENABLE_LOGGING ? "on" : "off") << "\n";
    return 0;
}
Experiment Full Integration
Build the Complete Code Generation Project

Implement the full project shown above. Create the cmake/UpdateGitVersion.cmake script (refer to the Git hash section) and a tools/generate_error_codes.py that reads a JSON file and produces a C++ header with an enum. Verify that changing errors.json causes only the error codes header to regenerate, while changing the Git commit updates only the Git version header.

configure_file add_custom_command add_custom_target Python

Conclusion & Next Steps

Code generation is one of CMake's most powerful capabilities. By choosing the right tool for each task, you can keep your build system efficient and your generated code always up to date:

Technique Phase Use Case
configure_file()ConfigureVersion headers, config.h, templates with CMake variables
file(GENERATE)GeneratePer-config files, paths using generator expressions
add_custom_commandBuildprotoc, flex/bison, Python generators, any external tool
add_custom_targetBuild (always)Git hash updates, tasks that must run every build

Key principles to remember:

  • Use @ONLY when templates contain literal ${} syntax
  • Prefer #cmakedefine01 over #cmakedefine for safer #if checks
  • Always specify DEPENDS in add_custom_command for proper rebuild tracking
  • Use add_custom_target for operations that should run every build (like Git hash)
  • Use file(GENERATE) when you need generator expressions in generated content
Official Reference: For complete documentation, see configure_file, file(GENERATE), and the generator expressions manual in the CMake Reference Documentation.