CMake for C++ Projects: Targets, Presets, find_package and C++20 Modules

Key takeaways

Full CMake tutorial for C++: cross-platform builds, CMake 3.28+ features, CMakePresets, C++20 modules, find_package, targets, and CI/CD.

Troubleshooting: common CMake errors · Could NOT find ….

Introduction: “Build settings differ per platform—it’s hard to manage”

Common build pain points

When you develop C++ across platforms you often hit:

  • Per-platform build files — separate .vcxproj, Makefile, Xcode projects
  • Different tooling per teammate — Visual Studio, CLion, VS Code
  • Dependency hell — Boost, OpenSSL, etc. installed in different paths
  • Duplicated CI/CD — bespoke scripts in GitHub Actions, GitLab CI
  • Slow builds — full rebuilds grow with project size CMake addresses this:
# ❌ Existing method (management by platform)
# Windows: msbuild MyProject.vcxproj
# Linux: make -f Makefile.linux
# macOS: xcodebuild -project MyProject.xcodeproj
# ✅ CMake (write once, build anywhere)
mkdir build && cd build
cmake ..
cmake --build .
flowchart LR
    subgraph input[Inputs]
        cmake[CMakeLists.txt]
        preset["CMakePresets.json (3.19+)"]
    end
    subgraph cmake_tool[CMake]
        gen[Generator]
    end
    subgraph output[Outputs]
        make["Makefile (Linux)"]
        vs["Visual Studio (Windows)"]
        xcode["Xcode (macOS)"]
        ninja["Ninja (all platforms)"]
    end
    cmake --> gen
    preset --> gen
    gen --> make
    gen --> vs
    gen --> xcode
    gen --> ninja

Core CMake ideas

  • Meta build system: CMake does not compile by itself; it generates platform build systems (Makefiles, .sln, etc.).
  • Targets: Use add_executable / add_library and wire deps with target_link_libraries, target_include_directories.
  • Presets (3.19+): Share settings via CMakePresets.json.
  • C++20 modules (3.28+): First-class module support. Goals:
  • CMake basics (minimal setup, targets, dependencies)
  • CMake 3.28+ (presets, C++20 modules, FetchContent improvements)
  • Real-world patterns (multi-platform, CI/CD, dependencies)
  • Performance (parallel builds, ccache, PCH)
  • Common mistakes and fixes
  • Production patterns Requirements: CMake 3.20+ (3.28+ recommended for latest features)

Problem scenarios: build management pain

Scenario 1: “Each team member has different build settings.”

Context: Windows developers use Visual Studio, Linux developers use Makefile, and macOS developers use Xcode.
Problem: Settings are not synchronized because each person manages different build files.
Outcome: “Does it build in my environment?” Problem occurred
→ Share unified build settings with CMake + CMakePresets.json

Note: “Works on my machine” usually means generators, flags, or paths were not committed—reproducible one-line presets matter more.

Scenario 2: “The external library path varies depending on the environment.”

Context: Boost is C:\boost on Windows, /usr/local/boost on Linux, and /opt/homebrew/boost on macOS.
Problem: Build fails in other environments due to hardcoded paths
Outcome: Each team member must edit CMakeLists.txt
→ Automatic detection with find_package + CMAKE_PREFIX_PATH

Scenario 3: “Build is too slow”

Context: As the project grows, a full build takes 30 minutes.
Problem: Recompile all files and repeat header parsing every time.
Outcome: Decreased development productivity
→ Optimized with ccache, PCH, Ninja, and parallel builds

Scenario 4: “Build setup in CI/CD is complicated”

Context: Different build scripts in GitHub Actions, GitLab CI, and Jenkins
Problem: All CI files need to be modified when changing build settings
Outcome: Increased maintenance burden
→ Unify CI settings with CMakePresets.json
flowchart TB
    subgraph Problems[Build management problems]
        P1[Per-platform build files]
        P2[Mismatched dependency paths]
        P3[Slow builds]
        P4[Duplicated CI/CD config]
    end
    subgraph Solutions[CMake solutions]
        S1[Unified CMakeLists.txt]
        S2[find_package discovery]
        S3[ccache + Ninja + PCH]
        S4[CMakePresets.json]
    end
    P1 --> S1
    P2 --> S2
    P3 --> S3
    P4 --> S4

Minimal CMakeLists.txt

Hello World build

# CMakeLists.txt - CMake project file
# Minimum CMake version (3.20+)
cmake_minimum_required(VERSION 3.20)
# Project metadata (name, version, languages)
project(MyProject VERSION 1.0.0 LANGUAGES CXX)
# C++ standard
set(CMAKE_CXX_STANDARD 20)              # Use C++20
set(CMAKE_CXX_STANDARD_REQUIRED ON)     # Error if C++20 unavailable
set(CMAKE_CXX_EXTENSIONS OFF)           # Disable GNU extensions (portability)
# Executable target
# myapp: target name (output name)
# main.cpp: source file
add_executable(myapp main.cpp)
// main.cpp
#include <iostream>
int main() {
    std::cout << "Hello, CMake!" << std::endl;
    return 0;
}
# Create build directory (out-of-source)
mkdir build
cd build

# Configure (generate build system)
cmake ..
# Internal steps:
# 1. Parse CMakeLists.txt
# 2. Select Generator (platform defaults):
#    - Linux: Unix Makefiles
#    - Windows: Visual Studio 2022
#    - macOS: Unix Makefiles
# 3. Generate build files:
#    - Makefile (Linux)
#    - .sln + .vcxproj (Windows)
#    - .xcodeproj (macOS with Xcode)
# 4. Create CMakeCache.txt (settings cache)

# Build (compile with generated system)
cmake --build .
# Internal: invokes the native build tool
# - Linux: calls make
# - Windows: calls msbuild
# - Ninja: calls ninja

# Run
./myapp  # Linux/macOS
# myapp.exe  # Windows

CMake Build Process Detailed:

Step-by-step flow:

1. Configuration Phase (cmake ..)
   ┌─────────────────────────────────────┐
   │ Parse CMakeLists.txt                │
   │ - project() → project metadata       │
   │ - add_executable() → register target │
   │ - target_link_libraries() → deps    │
   ├─────────────────────────────────────┤
   │ Select Generator                    │
   │ - Explicit: cmake -G "Ninja" ..     │
   │ - Auto: platform default            │
   ├─────────────────────────────────────┤
   │ Compiler Detection                  │
   │ - Detect CMAKE_CXX_COMPILER         │
   │ - Test compiler features            │
   │ - Check ABI (32/64bit)              │
   ├─────────────────────────────────────┤
   │ Create Cache: CMakeCache.txt        │
   │ - Store all variables               │
   │ - Reuse on next run                 │
   └─────────────────────────────────────┘
   
2. Generation Phase
   ┌─────────────────────────────────────┐
   │ Generate build files                │
   │ Unix Makefiles:                     │
   │   - Makefile                        │
   │   - cmake_install.cmake             │
   │ Visual Studio:                      │
   │   - MyProject.sln                   │
   │   - myapp.vcxproj                   │
   │ Ninja:                              │
   │   - build.ninja                     │
   ├─────────────────────────────────────┤
   │ compile_commands.json (optional)    │
   │ - CMAKE_EXPORT_COMPILE_COMMANDS=ON  │
   │ - LSP support (clangd, etc.)        │
   └─────────────────────────────────────┘

3. Build Phase (cmake --build .)
   ┌─────────────────────────────────────┐
   │ Execute generated build system      │
   │ - make (Unix Makefiles)             │
   │ - msbuild (Visual Studio)           │
   │ - ninja (Ninja)                     │
   ├─────────────────────────────────────┤
   │ Dependency graph execution          │
   │ - Header change → recompile sources │
   │ - Library change → relink executables│
   │ - Parallel builds (make -j, ninja)  │
   └─────────────────────────────────────┘

CMakeCache.txt role:
- Save all variables on first run
- Read cache on next run for speed
- Manual edits allowed (ccmake, cmake-gui)
- Delete for clean reconfigure (rm -rf build/)

Generator types:
- Unix Makefiles: traditional, slower
- Ninja: fast, excellent parallelism (recommended!)
- Visual Studio: Windows IDE integration
- Xcode: macOS IDE integration
- Ninja Multi-Config: multiple build types in one directory

Command cheat sheet

CommandMeaning
cmake_minimum_required(VERSION 3.20)Minimum CMake version
project(MyProject VERSION 1.0.0)Project name and version
set(CMAKE_CXX_STANDARD 20)Use C++20
set(CMAKE_CXX_EXTENSIONS OFF)Disable GNU extensions
add_executable(myapp main.cpp)Create executable target
cmake ..Configure from parent CMakeLists.txt
cmake --build .Portable build command

Notable CMake versions

VersionHighlights
3.19CMakePresets.json
3.20C++23 support
3.23FILE_SET (headers)
3.25LINUX variable
3.26SYSTEM property tweaks
3.28Native C++20 modules

Targets: executables and libraries

Executables

# Single source
add_executable(myapp main.cpp)
# Multiple sources
add_executable(myapp
    src/main.cpp
    src/utils.cpp
    src/config.cpp
)
# Using variables
set(SOURCES
    src/main.cpp
    src/utils.cpp
)
add_executable(myapp ${SOURCES})

Libraries

# Static library (.a / .lib)
add_library(mylib STATIC
    src/lib.cpp
    src/helper.cpp
)
# Shared library (.so / .dll)
add_library(mylib SHARED
    src/lib.cpp
    src/helper.cpp
)
# Header-only interface library
add_library(mylib INTERFACE)
target_include_directories(mylib INTERFACE include)
# Link into executable
add_executable(myapp src/main.cpp)
target_link_libraries(myapp PRIVATE mylib)

Note: For SHARED libraries, plan install/deploy rules (RUNTIME DESTINATION, etc.) so .so/.dll resolve at runtime.

PUBLIC vs PRIVATE vs INTERFACE

flowchart TB
    subgraph LibA[Library A]
        A_PRIVATE["PRIVATE headers e.g. src/internal/"]
        A_PUBLIC["PUBLIC headers e.g. include/"]
    end
    
    subgraph LibB[Library B]
        B_CODE[Lib B code]
    end
    
    subgraph App[Application]
        APP_CODE[App code]
    end
    
    A_PRIVATE -.->|visible in| LibA
    A_PUBLIC -->|visible in| LibA
    A_PUBLIC -->|visible to| LibB
    A_PUBLIC -->|visible to| App
    
    LibB -->|links| LibA
    App -->|links| LibB
    
    style A_PRIVATE fill:#FFB6C1
    style A_PUBLIC fill:#90EE90
# PRIVATE: this target only
target_include_directories(mylib PRIVATE src/internal)
# PUBLIC: this target and dependents
target_include_directories(mylib PUBLIC include)
# INTERFACE: dependents only (typical header-only)
target_include_directories(mylib INTERFACE include)

Example: Put src/internal/impl.h in PRIVATE if only mylib uses it; expose include/mylib/api.h as PUBLIC. After myapp links mylib, it sees only PUBLIC headers.


CMake Presets (3.19+)

What is a preset?

Problem: Different team members use different CMake commands.

# Developer A
cmake -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_COMPILER=g++ ..
# Developer B
cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_COMPILER=clang++ -DENABLE_TESTS=ON ..
# CI/CD
cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_TOOLCHAIN_FILE=......

Note: The method of copying and pasting a single long cmake line into a document is prone to breakage, so the purpose is to move it to the preset below. SOLVED: Share build settings with CMakePresets.json.

CMakePresets.json basic structure

{
  "version": 6,
  "cmakeMinimumRequired": {
    "major": 3,
    "minor": 25,
    "patch": 0
  },
  "configurePresets": [
    {
      "name": "default",
      "hidden": true,
      "generator": "Ninja",
      "binaryDir": "${sourceDir}/build/${presetName}",
      "cacheVariables": {
        "CMAKE_CXX_STANDARD": "20",
        "CMAKE_EXPORT_COMPILE_COMMANDS": "ON"
      }
    },
    {
      "name": "debug",
      "displayName": "Debug Build",
      "description": "Debug build with sanitizers",
      "inherits": "default",
      "cacheVariables": {
        "CMAKE_BUILD_TYPE": "Debug",
        "CMAKE_CXX_FLAGS": "-fsanitize=address,undefined"
      }
    },
    {
      "name": "release",
      "displayName": "Release Build",
      "description": "Optimized release build",
      "inherits": "default",
      "cacheVariables": {
        "CMAKE_BUILD_TYPE": "Release"
      }
    },
    {
      "name": "windows-msvc",
      "displayName": "Windows MSVC",
      "inherits": "default",
      "generator": "Visual Studio 17 2022",
      "condition": {
        "type": "equals",
        "lhs": "${hostSystemName}",
        "rhs": "Windows"
      }
    }
  ],
  "buildPresets": [
    {
      "name": "debug",
      "configurePreset": "debug",
      "configuration": "Debug"
    },
    {
      "name": "release",
      "configurePreset": "release",
      "configuration": "Release",
      "jobs": 8
    }
  ],
  "testPresets": [
    {
      "name": "default",
      "configurePreset": "debug",
      "output": {
        "outputOnFailure": true
      }
    }
  ]
}

How to use presets

# Check available presets
cmake --list-presets
# Configured with presets
cmake --preset=debug
# Build with presets
cmake --build --preset=debug
# Test with presets
ctest --preset=default

Practical preset example: multi-platform

{
  "version": 6,
  "configurePresets": [
    {
      "name": "linux-gcc",
      "displayName": "Linux GCC",
      "generator": "Ninja",
      "binaryDir": "${sourceDir}/build/linux-gcc",
      "cacheVariables": {
        "CMAKE_CXX_COMPILER": "g++",
        "CMAKE_BUILD_TYPE": "Release"
      },
      "condition": {
        "type": "equals",
        "lhs": "${hostSystemName}",
        "rhs": "Linux"
      }
    },
    {
      "name": "linux-clang",
      "displayName": "Linux Clang",
      "generator": "Ninja",
      "binaryDir": "${sourceDir}/build/linux-clang",
      "cacheVariables": {
        "CMAKE_CXX_COMPILER": "clang++",
        "CMAKE_BUILD_TYPE": "Release"
      },
      "condition": {
        "type": "equals",
        "lhs": "${hostSystemName}",
        "rhs": "Linux"
      }
    },
    {
      "name": "macos",
      "displayName": "macOS",
      "generator": "Xcode",
      "binaryDir": "${sourceDir}/build/macos",
      "condition": {
        "type": "equals",
        "lhs": "${hostSystemName}",
        "rhs": "Darwin"
      }
    },
    {
      "name": "windows",
      "displayName": "Windows MSVC",
      "generator": "Visual Studio 17 2022",
      "binaryDir": "${sourceDir}/build/windows",
      "architecture": "x64",
      "condition": {
        "type": "equals",
        "lhs": "${hostSystemName}",
        "rhs": "Windows"
      }
    }
  ]
}

Note: As you have more conditional presets, there will be confusion about “which preset is official” if you don’t pin the naming convention to your team document.

Utilizing presets in CI/CD

# .github/workflows/build.yml
name: Build
on: [push, pull_request]
jobs:
  build:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
        preset: [debug, release]
    
    steps:
      - uses: actions/checkout@v3
      
      - name: Install CMake
        uses: lukka/get-cmake@latest
      
      - name: Configure
        run: cmake --preset=${{ matrix.preset }}
      
      - name: Build
        run: cmake --build --preset=${{ matrix.preset }}
      
      - name: Test
        run: ctest --preset=default

User-specific presets (CMakeUserPresets.json)

Team shared settings are written in CMakePresets.json and personal settings are written in CMakeUserPresets.json.

// CMakeUserPresets.json (add to git ignore)
{
  "version": 6,
  "configurePresets": [
    {
      "name": "my-dev",
      "inherits": "debug",
      "cacheVariables": {
        "CMAKE_PREFIX_PATH": "/home/myuser/local",
        "ENABLE_EXPERIMENTAL_FEATURES": "ON"
      }
    }
  ]
}

Note: It is better to place CMakeUserPresets.json in .gitignore, but also place an example file for team member onboarding (CMakeUserPresets.json.example).


Find external libraries: find_package

find_package Internal Mechanism:

find_package(Boost 1.70 REQUIRED COMPONENTS filesystem system) workflow:

1. Module mode search:
   Search FindBoost.cmake in CMAKE_MODULE_PATH
   → Found /usr/share/cmake/Modules/FindBoost.cmake
   → Execute FindBoost.cmake
   
2. Config mode search (if Module fails):
   Search in CMAKE_PREFIX_PATH, CMAKE_SYSTEM_PREFIX_PATH:
   - <prefix>/lib/cmake/Boost/BoostConfig.cmake
   - <prefix>/lib/cmake/boost-1.70.0/BoostConfig.cmake
   - <prefix>/share/cmake/Boost/BoostConfig.cmake
   - <prefix>/Boost*/BoostConfig.cmake
   
   Common paths:
   - Linux: /usr, /usr/local
   - Windows: C:/Program Files/Boost
   - macOS: /usr/local, /opt/homebrew
   
3. Parse BoostConfig.cmake:
   - Check installed Boost version (≥1.70?)
   - Search COMPONENTS (filesystem, system)
   - Create IMPORTED targets:
     * Boost::filesystem
     * Boost::system
   - Set properties for each target:
     * INTERFACE_INCLUDE_DIRECTORIES (header paths)
     * INTERFACE_LINK_LIBRARIES (link libraries)
     * INTERFACE_COMPILE_DEFINITIONS (preprocessor macros)
   
4. REQUIRED option:
   - Error immediately on failure, stop configuration
   - Without option: only sets Boost_FOUND variable (conditional branching)

5. target_link_libraries(myapp PRIVATE Boost::filesystem):
   - Propagate all Boost::filesystem properties to myapp
   - Automatically add header paths (no target_include_directories needed)
   - Automatically add link libraries (libboost_filesystem.a)
   - Handle dependency chain (Boost::filesystem → Boost::system)

find_package Two Modes:

Module mode:
- Execute FindXxx.cmake (CMake-provided or project-provided)
- Variable-based: Xxx_FOUND, Xxx_INCLUDE_DIRS, Xxx_LIBRARIES
- Legacy approach, used for some packages only

Config mode (recommended):
- Execute XxxConfig.cmake (package-provided)
- IMPORTED target-based: Xxx::Xxx
- Modern approach, automatic property propagation
- Installed together with package

Boost example (Config mode)

find_package(Boost 1.70 REQUIRED COMPONENTS filesystem system)

# Internal operation:
# 1. Search and execute BoostConfig.cmake
# 2. Create IMPORTED targets:
#    - Boost::filesystem (includes header paths, link libraries)
#    - Boost::system
# 3. Set Boost_FOUND = TRUE

add_executable(myapp main.cpp)

# Link PRIVATE Boost::filesystem:
# - Only myapp uses Boost (not exposed)
# - Automatically add header path: -I/usr/include/boost
# - Automatically add link library: -lboost_filesystem -lboost_system
target_link_libraries(myapp PRIVATE Boost::filesystem Boost::system)

OpenSSL example (Module mode)

find_package(OpenSSL REQUIRED)

# Internal operation:
# 1. Execute FindOpenSSL.cmake (CMake-provided)
# 2. Search for OpenSSL on system:
#    - Linux: /usr/lib/libssl.so, /usr/include/openssl
#    - Windows: C:/Program Files/OpenSSL-Win64
#    - macOS: /usr/local/opt/openssl@3
# 3. Create IMPORTED targets:
#    - OpenSSL::SSL (libssl)
#    - OpenSSL::Crypto (libcrypto)

add_executable(myapp main.cpp)
target_link_libraries(myapp PRIVATE OpenSSL::SSL OpenSSL::Crypto)

Note: Mixing system OpenSSL and bundled OpenSSL may cause a runtime crash, so make sure to use the same build when deploying.

Handling when search fails

find_package(SomeLib)
if(SomeLib_FOUND)
    target_link_libraries(myapp PRIVATE SomeLib::SomeLib)
else()
    message(WARNING "SomeLib not found, using fallback")
endif()

Using pkg-config

find_package(PkgConfig REQUIRED)
pkg_check_modules(CURL REQUIRED libcurl)
target_link_libraries(myapp PRIVATE ${CURL_LIBRARIES})
target_include_directories(myapp PRIVATE ${CURL_INCLUDE_DIRS})

Note: Variable bases (CURL_LIBRARIES) have less propagation and tooling support than modern IMPORTED targets, so it is better to use the official CMake package or the PkgConfig::libcurl wrapper if possible.

Automatic download of dependencies with FetchContent (3.11+)

include(FetchContent)
# Automatic download of GoogleTest
FetchContent_Declare(
    googletest
    GIT_REPOSITORY https://github.com/google/googletest.git
    GIT_TAG v1.14.0
)
# Make it available
FetchContent_MakeAvailable(googletest)
# link
add_executable(tests test.cpp)
target_link_libraries(tests PRIVATE gtest_main)

Related article: Using vcpkg or Conan allows for more systematic package management. 3.24+ Enhancements: Remove timestamp warning with DOWNLOAD_EXTRACT_TIMESTAMP option.

FetchContent_Declare(
    fmt
    GIT_REPOSITORY https://github.com/fmtlib/fmt.git
    GIT_TAG 10.0.0
    DOWNLOAD_EXTRACT_TIMESTAMP ON  # 3.24+
)

Note: FetchContent is network dependent, so you will need to decide whether to use it in conjunction with the SBOM/Security Scan process.


C++20 module support (3.28+)

What is a C++20 module?

Problems with existing header method:

  • Parsing headers every time (slow compilation)
  • Macro contamination
  • Circular dependency problem C++20 module benefits:
  • Compile only once (fast build)
  • Explicit export (clean interface)
  • Avoid circular dependencies

Using modules in CMake 3.28+

cmake_minimum_required(VERSION 3.28)
project(ModuleExample CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Enable module support
set(CMAKE_CXX_SCAN_FOR_MODULES ON)
# Libraries (including modules)
add_library(mylib)
target_sources(mylib
    PUBLIC
        FILE_SET CXX_MODULES FILES
src/mymodule.cppm # Module interface file
    PRIVATE
        src/impl.cpp
)
# Run file
add_executable(myapp main.cpp)
target_link_libraries(myapp PRIVATE mylib)

Module file example

// src/mymodule.cppm (module interface)
export module mymodule;
export namespace mylib {
    int add(int a, int b);
    int multiply(int a, int b);
}
// src/impl.cpp (module implementation)
module mymodule;
namespace mylib {
    int add(int a, int b) {
        return a + b;
    }
    
    int multiply(int a, int b) {
        return a * b;
    }
}

Note: Invalid module declaration in the implementation file will lead to link/visibility errors, so manage the interface and implementation pairs as a set.

// main.cpp (using module)
import mymodule;
#include <iostream>
int main() {
    std::cout << mylib::add(2, 3) << std::endl;       // 5
    std::cout << mylib::multiply(4, 5) << std::endl;  // 20
    return 0;
}

Mixed use of module + header

add_library(mylib)
target_sources(mylib
    PUBLIC
        FILE_SET HEADERS FILES
include/legacy.h # legacy header
        FILE_SET CXX_MODULES FILES
src/newmodule.cppm # new module
    PRIVATE
        src/legacy.cpp
        src/newmodule.cpp
)
target_include_directories(mylib PUBLIC include)

Module support status by compiler (as of 2026)

compilerversionModule support
GCC14+Fully supported
Clang17+Fully supported
MSVC19.36+ (VS 2022)Fully supported

Build type and compilation options

Build type

# Set default values
if(NOT CMAKE_BUILD_TYPE)
    set(CMAKE_BUILD_TYPE Release)
endif()
# Flags for each build type
set(CMAKE_CXX_FLAGS_DEBUG "-g -O0 -DDEBUG")
set(CMAKE_CXX_FLAGS_RELEASE "-O3 -DNDEBUG")
set(CMAKE_CXX_FLAGS_RELWITHDEBINFO "-O2 -g")
set(CMAKE_CXX_FLAGS_MINSIZEREL "-Os -DNDEBUG")

Note: The multi-configuration generator (Visual Studio, Xcode) selects --config instead of CMAKE_BUILD_TYPE, so do not confuse it with this pattern.

# Debug build
cmake -DCMAKE_BUILD_TYPE=Debug ..
# Release build
cmake -DCMAKE_BUILD_TYPE=Release ..
# Optimize including debug information
cmake -DCMAKE_BUILD_TYPE=RelWithDebInfo ..

Target-specific compilation options

add_executable(myapp main.cpp)
# Enable warning
target_compile_options(myapp PRIVATE
    -Wall -Wextra -Wpedantic
$<$<CONFIG:Debug>:-Werror> # Converts warnings to errors only in Debug.
)
# Preprocessing definitions
target_compile_definitions(myapp PRIVATE
    APP_VERSION="1.0"
    $<$<CONFIG:Debug>:DEBUG_MODE>
)
# header path
target_include_directories(myapp PRIVATE
    ${CMAKE_SOURCE_DIR}/include
    ${CMAKE_BINARY_DIR}/generated
)

Generator Expression

# Branch by build type
target_compile_options(myapp PRIVATE
    $<$<CONFIG:Debug>:-O0 -g>
    $<$<CONFIG:Release>:-O3>
)
# Compiler-specific branching
target_compile_options(myapp PRIVATE
    $<$<CXX_COMPILER_ID:GNU>:-fno-exceptions>
    $<$<CXX_COMPILER_ID:MSVC>:/EHsc>
)

Note: Generator expressions are strings, so debugging is difficult. If they become complicated, split target_compile_options into multiple lines or wrap it in a custom property to ensure readability.


Project structure and subdirectories

project/
├── CMakeLists.txt # Root CMake settings
├── src/
│ ├── CMakeLists.txt # Source build settings
│ ├── main.cpp
│ └── lib/
│ ├── CMakeLists.txt
│ ├── lib.cpp
│ └── lib.h
├── include/ # public header
│ └── mylib/
│ └── api.h
├── tests/
│ ├── CMakeLists.txt
│ └── test_main.cpp
├── external/ # Submodule, external library
└── build/ # Build output (git ignore)

root CMakeLists.txt

cmake_minimum_required(VERSION 3.20)
project(MyProject VERSION 1.0.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
# options
option(BUILD_TESTS "Build tests" ON)
option(BUILD_SHARED_LIBS "Build shared libraries" OFF)
# subdirectory
add_subdirectory(src)
if(BUILD_TESTS)
    enable_testing()
    add_subdirectory(tests)
endif()

src/CMakeLists.txt

# library
add_library(mylib
    lib/lib.cpp
    lib/lib.h
)
target_include_directories(mylib
    PUBLIC ${CMAKE_SOURCE_DIR}/include
    PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}
)
# Run file
add_executable(myapp main.cpp)
target_link_libraries(myapp PRIVATE mylib)

tests/CMakeLists.txt

find_package(GTest REQUIRED)
add_executable(tests
    test_main.cpp
    test_lib.cpp
)
target_link_libraries(tests PRIVATE
    mylib
    GTest::gtest
    GTest::gtest_main
)
add_test(NAME MyTests COMMAND tests)

Complete example: multi-target project

Issue 1: Using absolute paths

Cause: Hardcoding absolute paths breaks builds in other environments.

# ❌ Incorrect use
target_include_directories(myapp PRIVATE /usr/local/include)
# ✅ Correct usage: Using CMake variables
target_include_directories(myapp PRIVATE ${CMAKE_SOURCE_DIR}/include)

Note: Always make sure you use source tree variables and not installed package paths.

Issue 2: Building from source directory

Cause: Running cmake . from the source directory causes the build files to get mixed up with the source.

# ❌ Incorrect use
cd /project
cmake .
# ✅ Correct usage: out-of-source build
mkdir build
cd build
cmake ..

Issue 3: Cache issues

Symptom: Changing CMake settings is not reflected. Cause: Previous settings cached in CMakeCache.txt.

# Solution 1: Clear cache
rm CMakeCache.txt
cmake ..
# Solution 2: Recreate the build directory
cd ..
rm -rf build
mkdir build && cd build
cmake ..

Note: Please note any important -D flags or move them to a preset before deleting them.

Cause: If the library linking order is incorrect, an undefined reference error occurs.

# ❌ Wrong order: lib1 depends on lib2, but lib2 comes after
target_link_libraries(myapp PRIVATE lib2 lib1)
# ✅ Correct order: dependent libraries first
target_link_libraries(myapp PRIVATE lib1 lib2)

Issue 5: PUBLIC/PRIVATE confusion

Symptom: Header not found, or unnecessary header exposed.

# ❌ Misuse: Internal header as PUBLIC
target_include_directories(mylib PUBLIC src/internal)
# ✅ Correct use
target_include_directories(mylib
PUBLIC include # exposed to the outside world
PRIVATE src/internal # Internal use only
)

Issue 6: find_package fails

Symptom: Could not find package SomeLib. Cause: The library is not installed or is in a location that CMake cannot find.

# Solution 1: Install package
sudo apt install libboost-dev  # Linux
brew install boost             # macOS
# Solution 2: Specify CMAKE_PREFIX_PATH
cmake -DCMAKE_PREFIX_PATH=/custom/install/path ..
# Solution 3: Specify the path manually
cmake -DBoost_ROOT=/usr/local/boost ..

Frequently occurring problems and solutions

Pattern 1: Compile warnings as errors

function(enable_strict_warnings target)
    if(MSVC)
        target_compile_options(${target} PRIVATE /W4 /WX)
    else()
        target_compile_options(${target} PRIVATE
            -Wall -Wextra -Wpedantic -Werror
        )
    endif()
endfunction()
add_executable(myapp main.cpp)
enable_strict_warnings(myapp)

Pattern 2: Post-processing by build type

# Remove symbols with strip when building release
if(CMAKE_BUILD_TYPE STREQUAL "Release")
    add_custom_command(TARGET myapp POST_BUILD
        COMMAND ${CMAKE_STRIP} $<TARGET_FILE:myapp>
    )
endif()

Pattern 3: Installation Rules

# Run file installation
install(TARGETS myapp
    RUNTIME DESTINATION bin
)
# Install library
install(TARGETS mylib
    LIBRARY DESTINATION lib
    ARCHIVE DESTINATION lib
)
# Install header
install(DIRECTORY include/
    DESTINATION include
)
# Install configuration file
install(FILES config.json
    DESTINATION etc
)
# installation
cmake --build . --target install
# or
make install

Pattern 4: Generate version information

# version.h.in
#define APP_VERSION_MAJOR @PROJECT_VERSION_MAJOR@
#define APP_VERSION_MINOR @PROJECT_VERSION_MINOR@
#define APP_VERSION_PATCH @PROJECT_VERSION_PATCH@
# CMakeLists.txt
project(MyProject VERSION 1.2.3)
configure_file(version.h.in ${CMAKE_BINARY_DIR}/version.h)
target_include_directories(myapp PRIVATE ${CMAKE_BINARY_DIR})

Pattern 5: Conditional compilation

option(ENABLE_LOGGING "Enable logging" ON)
if(ENABLE_LOGGING)
    target_compile_definitions(myapp PRIVATE ENABLE_LOGGING)
    target_sources(myapp PRIVATE src/logger.cpp)
endif()

Pattern 6: Platform-specific sources

if(WIN32)
    target_sources(myapp PRIVATE src/platform/windows.cpp)
elseif(APPLE)
    target_sources(myapp PRIVATE src/platform/macos.cpp)
elseif(UNIX)
    target_sources(myapp PRIVATE src/platform/linux.cpp)
endif()

Issue 6: find_package fails

Symptom: Could not find package SomeLib. Cause: The library is not installed or is in a location that CMake cannot find.

# Solution 1: Install package
sudo apt install libboost-dev  # Linux
brew install boost             # macOS
# Solution 2: Specify CMAKE_PREFIX_PATH
cmake -DCMAKE_PREFIX_PATH=/custom/install/path ..
# Solution 3: Specify the path manually
cmake -DBoost_ROOT=/usr/local/boost ..

Issue 7: Presets not working

Symptom: “No such preset” error when running cmake --preset=debug. Cause: CMake version is lower than 3.19, or JSON syntax error.

# Solution 1: Check CMake version
Requires cmake --version # 3.19 or higher
# Solution 2: JSON grammar verification
# Use VSCode or online JSON validator
# Solution 3: Check the preset list
cmake --list-presets

Performance Optimization

Parallel build

# Use all CPU cores
cmake --build . -j$(nproc)
# or make
make -j$(nproc)

Use Ninja

Ninja is a faster build system than Make.

# Create a build system with Ninja
cmake -G Ninja ..
# Build
ninja

Compilation caching with ccache

find_program(CCACHE_PROGRAM ccache)
if(CCACHE_PROGRAM)
    set(CMAKE_CXX_COMPILER_LAUNCHER "${CCACHE_PROGRAM}")
endif()

Precompile Header (PCH)

target_precompile_headers(myapp PRIVATE
    <iostream>
    <vector>
    <string>
)

Unity build (3.16+)

Reduce compilation time by merging multiple source files into one.

set(CMAKE_UNITY_BUILD ON)
set(CMAKE_UNITY_BUILD_BATCH_SIZE 16)
add_executable(myapp ${SOURCES})

Best practices/best practices

Target-based approach (Modern CMake)

# ❌ The old-fashioned way (global variables)
include_directories(include)
link_libraries(mylib)
add_definitions(-DDEBUG)
# ✅ Modern method (target-based)
target_include_directories(myapp PRIVATE include)
target_link_libraries(myapp PRIVATE mylib)
target_compile_definitions(myapp PRIVATE DEBUG)

Utilize Generator Expression

# Flags for each build type
target_compile_options(myapp PRIVATE
    $<$<CONFIG:Debug>:-O0 -g>
    $<$<CONFIG:Release>:-O3 -DNDEBUG>
)
# Compiler-specific flags
target_compile_options(myapp PRIVATE
    $<$<CXX_COMPILER_ID:GNU>:-Wall -Wextra>
    $<$<CXX_COMPILER_ID:MSVC>:/W4>
    $<$<CXX_COMPILER_ID:Clang>:-Weverything>
)
# Links by platform
target_link_libraries(myapp PRIVATE
    $<$<PLATFORM_ID:Linux>:pthread>
    $<$<PLATFORM_ID:Windows>:ws2_32>
)

Utilizing the interface library

# Common settings to interface library
add_library(common_settings INTERFACE)
target_compile_features(common_settings INTERFACE cxx_std_20)
target_compile_options(common_settings INTERFACE
    $<$<CXX_COMPILER_ID:GNU>:-Wall -Wextra -Wpedantic>
    $<$<CXX_COMPILER_ID:MSVC>:/W4>
)
# Apply to all targets
target_link_libraries(myapp PRIVATE common_settings)
target_link_libraries(mylib PRIVATE common_settings)

Automatic generation of version information

# version.h.in
#pragma once
#define PROJECT_VERSION_MAJOR @PROJECT_VERSION_MAJOR@
#define PROJECT_VERSION_MINOR @PROJECT_VERSION_MINOR@
#define PROJECT_VERSION_PATCH @PROJECT_VERSION_PATCH@
#define PROJECT_VERSION "@PROJECT_VERSION@"
# CMakeLists.txt
project(MyProject VERSION 1.2.3)
configure_file(
    ${CMAKE_SOURCE_DIR}/version.h.in
    ${CMAKE_BINARY_DIR}/generated/version.h
    @ONLY
)
target_include_directories(myapp PRIVATE ${CMAKE_BINARY_DIR}/generated)

Clarification of installation rules

# Run file installation
install(TARGETS myapp
    RUNTIME DESTINATION bin
)
# Install library
install(TARGETS mylib
    LIBRARY DESTINATION lib
    ARCHIVE DESTINATION lib
    RUNTIME DESTINATION bin
)
# Install headers (using FILE_SET, 3.23+)
install(TARGETS mylib
    FILE_SET HEADERS DESTINATION include
)
# Create Config file (can find_package from other projects)
install(EXPORT MyLibTargets
    FILE MyLibTargets.cmake
    NAMESPACE MyLib::
    DESTINATION lib/cmake/MyLib
)

production pattern

Pattern 1: Conditional feature activation

option(ENABLE_LOGGING "Enable logging" ON)
option(ENABLE_PROFILING "Enable profiling" OFF)
option(BUILD_EXAMPLES "Build examples" OFF)
if(ENABLE_LOGGING)
    target_compile_definitions(myapp PRIVATE ENABLE_LOGGING)
    target_sources(myapp PRIVATE src/logger.cpp)
endif()
if(ENABLE_PROFILING)
    target_link_libraries(myapp PRIVATE profiler)
endif()
if(BUILD_EXAMPLES)
    add_subdirectory(examples)
endif()

Pattern 2: Platform-specific source management

# common source
set(COMMON_SOURCES
    src/main.cpp
    src/core.cpp
)
# Platform-specific sources
if(WIN32)
    list(APPEND PLATFORM_SOURCES src/platform/windows.cpp)
elseif(APPLE)
    list(APPEND PLATFORM_SOURCES src/platform/macos.cpp)
elseif(UNIX)
    list(APPEND PLATFORM_SOURCES src/platform/linux.cpp)
endif()
add_executable(myapp ${COMMON_SOURCES} ${PLATFORM_SOURCES})

Pattern 3: Compile warnings as errors (CI/CD)

function(enable_warnings_as_errors target)
    if(MSVC)
        target_compile_options(${target} PRIVATE /W4 /WX)
    else()
        target_compile_options(${target} PRIVATE
            -Wall -Wextra -Wpedantic -Werror
        )
    endif()
endfunction()
# Activated only in CI environment
if(DEFINED ENV{CI})
    enable_warnings_as_errors(myapp)
endif()

Pattern 4: Post-processing by build type

# Strip when building Release
if(CMAKE_BUILD_TYPE STREQUAL "Release" AND UNIX)
    add_custom_command(TARGET myapp POST_BUILD
        COMMAND ${CMAKE_STRIP} --strip-all $<TARGET_FILE:myapp>
        COMMENT "Stripping binary"
    )
endif()
# Print build information
add_custom_command(TARGET myapp POST_BUILD
    COMMAND ${CMAKE_COMMAND} -E echo "Build complete: $<TARGET_FILE:myapp>"
    COMMAND ${CMAKE_COMMAND} -E echo "Build type: ${CMAKE_BUILD_TYPE}"
)

Pattern 5: Fix dependency versions

include(FetchContent)
# Manage with version variable
set(GOOGLETEST_VERSION "1.14.0")
set(FMT_VERSION "10.0.0")
set(SPDLOG_VERSION "1.12.0")
FetchContent_Declare(
    googletest
    GIT_REPOSITORY https://github.com/google/googletest.git
    GIT_TAG v${GOOGLETEST_VERSION}
GIT_SHALLOW ON # Download only the latest commits
)
FetchContent_Declare(
    fmt
    GIT_REPOSITORY https://github.com/fmtlib/fmt.git
    GIT_TAG ${FMT_VERSION}
    GIT_SHALLOW ON
)
FetchContent_MakeAvailable(googletest fmt)

Pattern 6: Developer Mode

option(DEVELOPER_MODE "Enable developer mode" OFF)
if(DEVELOPER_MODE)
# Enable all warnings
    add_compile_options(-Wall -Wextra -Wpedantic)
    
# Enable Sanitizer
    add_compile_options(-fsanitize=address,undefined)
    add_link_options(-fsanitize=address,undefined)
    
# Export compilation commands (used by clangd, ccls, etc.)
    set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
    
# test build
    set(BUILD_TESTS ON)
endif()

Organize and Checklist

Summary of CMake key concepts

conceptDescriptionExample
TargetBuild artifacts (executable files, libraries)add_executable, add_library
AttributeTarget-specific settingstarget_include_directories
DependencyLink relationship between targetstarget_link_libraries
PresetReusable Build SettingsCMakePresets.json
GeneratorCreating platform-specific build systemsNinja, Make, Visual Studio

CMake adoption checklist

# ✅ Default settings
- [ ] Install CMake 3.20 or higher (3.28+ recommended)
- [ ] Use out-of-source build (build/ directory)
- [ ] C++ standard settings (CMAKE_CXX_STANDARD)
- [ ] Set CMAKE_CXX_EXTENSIONS OFF
# ✅ Target management
- [ ] Target-based approach (add_executable, add_library)
- [ ] PUBLIC/PRIVATE/INTERFACE clearly distinguished
- Use [ ] target_* commands (avoid global commands)
# ✅ Dependency management
- Find external libraries with [ ] find_package
- [ ] Automatic download with FetchContent (optional)
- [ ] Version fixation (reproducible build)
# ✅ Build settings
- [ ] Create CMakePresets.json (shared with team)
- [ ] Build type setting (Debug/Release)
- [ ] Enable warning (-Wall -Wextra or /W4)
- [ ] Turn warnings into errors in CI/CD (-Werror)
# ✅ Test
- [ ] enable_testing() + add_test()
- [ ] CTest integration
# ✅ Installation and Deployment
- Create [ ] install() rule
- [ ] Config file creation (find_package support)
# ✅Performance
- [ ] Enable parallel builds (-j)
- [ ] Consider using Ninja
- [ ] ccache settings
- [ ] PCH utilization (big project)
# ✅ Latest features (optional)
- [ ] C++20 module support (3.28+)
- [ ] Use FILE_SET (3.23+)
- [ ] Unity build (3.16+)
CMake versionRecommendations
3.20+Basic features available
3.23+Header management with FILE_SET
3.25+Using LINUX variables
3.28+C++20 module support

Practical Tips: Using CMake Efficiently

  1. Maintain multiple build directories
# Maintain debug, release, and profiling simultaneously
   cmake --preset=debug
   cmake --preset=release
   cmake --preset=profile
   
# Build each when needed
   cmake --build build/debug
   cmake --build build/release
  1. Export compile command (for LSP server)
# Create compile_commands.json (used by clangd, ccls, etc.)
   set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
   
# Create a symbolic link at the root
   # ln -s build/compile_commands.json .
  1. Measuring Build Time
# Measure build time
   time cmake --build build
   
# or CMake built-in function
   cmake --build build --verbose
  1. Selective build by target
# Build only specific targets (no full build required)
   cmake --build build --target mylib
   cmake --build build --target tests
   
# Build multiple targets simultaneously
   cmake --build build --target mylib --target myapp
  1. Check cache variables
# Check current cache variable
   cmake -L build
   
# Check advanced variables
   cmake -LA build
   
# Check specific variables
   cmake -L build | grep CMAKE_CXX_COMPILER

Quick reference: CMake command cheat sheet

# 🏗️ Project settings
cmake -S . -B build # source: current, build: build/
cmake --preset=debug # Use preset (3.19+)
cmake -DCMAKE_BUILD_TYPE=Release # Specify build type
# 🔨Build
cmake --build build # Run build
cmake --build build -j8 # parallel build (8 cores)
cmake --build build --target myapp # specific target only
# 🧪Test
ctest --test-dir build # Run tests
ctest --preset=default # Test with preset
# 📦 Install
cmake --install build # Run installation
cmake --install build --prefix /usr/local # Specify path
# 🧹 Organization
cmake --build build --target clean # Delete build results
rm -rf build # complete initialization

Troubleshooting: Quick problem solving

SymptomsCauseSolution
Could not find packageLibrary not installedSpecify apt install, brew install, or CMAKE_PREFIX_PATH
Undefined referenceLink order errorCheck target_link_libraries order
Header not foundMissing include pathadd target_include_directories
Cache issuesCMakeCache.txt Outdatedrm CMakeCache.txt or cmake --fresh
No presetsCMake version lowCMake 3.19+ upgrade
Slow buildUse MakeSwitch to Ninja (-G Ninja)

Build system performance comparison

build systemspeedParallelizationcross platformlearning curve
MakeNormalLIMITEDLinux/macOSEasy
NinjaFastExcellentAll platformsEasy
Visual StudioNormalExcellentWindowsEasy
XcodeNormalExcellentmacOSmiddle

Recommended: Ninja (fastest and supports all platforms)

Next steps


FAQ

Q1: What is the difference between CMake and Make?

A: CMake is a tool that creates a build system (meta build system), and Make is a tool that performs the build. When CMake creates a Makefile, Make reads that Makefile and compiles it.

CMakeLists.txt → [CMake] → Makefile → [Make] → Executable file

Q2: What is out-of-source build?

A: Separate source and build directories. If you create a build/ directory and run cmake .. inside it, the build files will be generated only in build/, keeping your source clean.

# ✅ Correct way
mkdir build && cd build
cmake ..
# ❌ Wrong method (mixing source and build files)
cmake .

Q3: When should I use PUBLIC vs PRIVATE vs INTERFACE?

A:

  • PRIVATE: only used inside this target (implementation detail)
  • PUBLIC: Use this target + targets linking this target as well (public API)
  • INTERFACE: Use only targets that link to this target (header-only library)
# internal header
target_include_directories(mylib PRIVATE src/internal)
# public header
target_include_directories(mylib PUBLIC include)
# Header-only interface library
target_include_directories(header_only INTERFACE include)

Q4: When to use CMakePresets.json?

A: Used when sharing build settings in a team project or CI/CD. Personal settings are written in CMakeUserPresets.json and added to git ignore.

# Without presets (complicated)
cmake -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_COMPILER=g++ -DENABLE_TESTS=ON ..
# Use presets (simple)
cmake --preset=debug

Q5: What if find_package fails?

A:

  1. Verify library installation
  2. Specify CMAKE_PREFIX_PATH
  3. Use package manager (vcpkg, conan)
# 1. Installation
sudo apt install libboost-dev  # Linux
brew install boost             # macOS
# 2. Specify the route
cmake -DCMAKE_PREFIX_PATH=/usr/local ..
# 3. Use vcpkg
cmake -DCMAKE_TOOLCHAIN_FILE=/path/to/vcpkg.cmake ..

Q6: Do I need to use the C++20 module?

A: As of 2026, all major compilers (GCC 14+, Clang 17+, MSVC 19.36+) support it and its use is recommended for new projects. Migrate existing projects gradually. Advantages: Fast compilation, clean interface, avoids circular dependencies. Disadvantage: Increased build system complexity, caution required when mixed with legacy code

Q7: The build is slow. How do I optimize it?

A:

  1. Using Ninja (cmake -G Ninja)
  2. Parallel build (cmake --build . -j$(nproc))
  3. Enable ccache
  4. Use PCH (precompile header)
  5. Consider Unity Build (large projects)
# enable ccache
find_program(CCACHE_PROGRAM ccache)
if(CCACHE_PROGRAM)
    set(CMAKE_CXX_COMPILER_LAUNCHER "${CCACHE_PROGRAM}")
endif()
# Unity build
set(CMAKE_UNITY_BUILD ON)

Q8: What are CMake learning resources?

A:

Q9: How to migrate an existing Makefile project to CMake?

A: Migrate gradually, step by step.

# Step 1: Create a minimal CMakeLists.txt
cmake_minimum_required(VERSION 3.20)
project(MyProject)
file(GLOB_RECURSE SOURCES "src/*.cpp") # Temporarily use GLOB
add_executable(myapp ${SOURCES})
# Step 2: Explicitly list source files
set(SOURCES
    src/main.cpp
    src/module1.cpp
    # ...
)
# Step 3: Separate by target
add_library(mylib src/lib.cpp)
add_executable(myapp src/main.cpp)
target_link_libraries(myapp PRIVATE mylib)
# Step 4: Structuring subdirectories
add_subdirectory(src)
add_subdirectory(tests)

Q10: How to completely reset the CMake cache?

A:

# Method 1: Delete the build directory (most obvious)
rm -rf build
mkdir build && cd build
cmake ..
# Method 2: Delete only CMakeCache.txt
rm CMakeCache.txt
cmake ..
# Method 3: Initialization with CMake command
cmake --fresh ... # CMake 3.24+

Q11: How to maintain multiple build types simultaneously?

A: Separate build directories or use presets.

# Method 1: Separate directories
mkdir build-debug && cd build-debug
cmake -DCMAKE_BUILD_TYPE=Debug ..
mkdir build-release && cd build-release
cmake -DCMAKE_BUILD_TYPE=Release ..
# Method 2: Use presets (recommended)
cmake --preset=debug
cmake --preset=release
# Generated in build/debug and build/release respectively

Q12: How to include Git commit hashes in versions in CMake?

A:

# Get Git commit hash
execute_process(
    COMMAND git rev-parse --short HEAD
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
    OUTPUT_VARIABLE GIT_COMMIT_HASH
    OUTPUT_STRIP_TRAILING_WHITESPACE
)
# Create version header
configure_file(
    ${CMAKE_SOURCE_DIR}/version.h.in
    ${CMAKE_BINARY_DIR}/version.h
)
# version.h.in
#define VERSION_MAJOR @PROJECT_VERSION_MAJOR@
#define VERSION_MINOR @PROJECT_VERSION_MINOR@
#define GIT_COMMIT "@GIT_COMMIT_HASH@"

CMake 3.28+ allows you to efficiently build cross-platform projects including C++20 modules. Share team settings and take advantage of the latest features to increase productivity with CMakePresets.json.


Good article to read together (internal link)