Skip to content
C++
Build System
Updated 2025-01-01T00:00:00.000Z

CMake — Advanced Patterns

Advanced CMake — FetchContent, install/export, package config files, presets, generator expressions, and modern CMake best practices.

TL;DR

Use FetchContent for dependency fetching, install() + export() for distributable packages, and CMakePresets.json for reproducible build configurations. Prefer target-based CMake — never use global include_directories.


FetchContent — Dependency Management

cmake
include(FetchContent)

FetchContent_Declare(
    googletest
    GIT_REPOSITORY https://github.com/google/googletest.git
    GIT_TAG        v1.14.0
    GIT_SHALLOW    TRUE   # don't fetch full history
)

FetchContent_Declare(
    nlohmann_json
    GIT_REPOSITORY https://github.com/nlohmann/json.git
    GIT_TAG        v3.11.3
    GIT_SHALLOW    TRUE
)

# Fetch all at once (parallel download)
FetchContent_MakeAvailable(googletest nlohmann_json)

target_link_libraries(myapp
    PRIVATE
        nlohmann_json::nlohmann_json
)
target_link_libraries(mytest
    PRIVATE
        GTest::gtest_main
)

Install and Export

cmake
# Install targets
install(TARGETS mylib myapp
    EXPORT MyProjectTargets
    RUNTIME DESTINATION bin
    LIBRARY DESTINATION lib
    ARCHIVE DESTINATION lib
    INCLUDES DESTINATION include
)

# Install headers
install(DIRECTORY include/
    DESTINATION include
    FILES_MATCHING PATTERN "*.h" PATTERN "*.hpp"
)

# Export targets for downstream CMake find_package()
install(EXPORT MyProjectTargets
    FILE       MyProjectTargets.cmake
    NAMESPACE  MyProject::
    DESTINATION lib/cmake/MyProject
)

# Generate config file
include(CMakePackageConfigHelpers)

configure_package_config_file(
    cmake/MyProjectConfig.cmake.in
    ${CMAKE_CURRENT_BINARY_DIR}/MyProjectConfig.cmake
    INSTALL_DESTINATION lib/cmake/MyProject
)

write_basic_package_version_file(
    ${CMAKE_CURRENT_BINARY_DIR}/MyProjectConfigVersion.cmake
    VERSION       ${PROJECT_VERSION}
    COMPATIBILITY AnyNewerVersion
)

install(FILES
    ${CMAKE_CURRENT_BINARY_DIR}/MyProjectConfig.cmake
    ${CMAKE_CURRENT_BINARY_DIR}/MyProjectConfigVersion.cmake
    DESTINATION lib/cmake/MyProject
)

Config file template (cmake/MyProjectConfig.cmake.in)

cmake
@PACKAGE_INIT@

include("${CMAKE_CURRENT_LIST_DIR}/MyProjectTargets.cmake")
check_required_components(MyProject)

Downstream usage

cmake
find_package(MyProject 1.0 REQUIRED)
target_link_libraries(app PRIVATE MyProject::mylib)

CMakePresets.json

json
{
  "version": 6,
  "configurePresets": [
    {
      "name": "base",
      "hidden": true,
      "generator": "Ninja",
      "binaryDir": "${sourceDir}/build/${presetName}",
      "cacheVariables": {
        "CMAKE_EXPORT_COMPILE_COMMANDS": "ON"
      }
    },
    {
      "name": "debug",
      "inherits": "base",
      "cacheVariables": {
        "CMAKE_BUILD_TYPE": "Debug",
        "CMAKE_CXX_FLAGS": "-fsanitize=address,undefined"
      }
    },
    {
      "name": "release",
      "inherits": "base",
      "cacheVariables": {
        "CMAKE_BUILD_TYPE": "Release",
        "CMAKE_INTERPROCEDURAL_OPTIMIZATION": "ON"
      }
    },
    {
      "name": "clang",
      "inherits": "debug",
      "cacheVariables": {
        "CMAKE_CXX_COMPILER": "clang++",
        "CMAKE_C_COMPILER": "clang"
      }
    }
  ],
  "buildPresets": [
    {
      "name": "debug",
      "configurePreset": "debug"
    },
    {
      "name": "release",
      "configurePreset": "release",
      "jobs": 0
    }
  ],
  "testPresets": [
    {
      "name": "debug",
      "configurePreset": "debug",
      "output": { "verbosity": "verbose" }
    }
  ]
}
bash
cmake --preset debug
cmake --build --preset debug
ctest --preset debug

Generator Expressions

cmake
# Compile flags only for specific config
target_compile_options(mylib PRIVATE
    $<$<CONFIG:Debug>:-g -O0>
    $<$<CONFIG:Release>:-O3 -DNDEBUG>
)

# Different flags for different compilers
target_compile_options(mylib PRIVATE
    $<$<CXX_COMPILER_ID:GNU>:-Wall -Wextra>
    $<$<CXX_COMPILER_ID:Clang>:-Wall -Wextra -Wshadow>
    $<$<CXX_COMPILER_ID:MSVC>:/W4>
)

# BUILD vs INSTALL include paths
target_include_directories(mylib PUBLIC
    $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
    $<INSTALL_INTERFACE:include>
)

# Link only when target exists
target_link_libraries(myapp PRIVATE
    $<TARGET_EXISTS:optional_dep>:optional_dep::optional_dep>
)

Modern Target Properties

cmake
add_library(mylib STATIC src/mylib.cpp)

# Set C++ standard on the target (not globally)
target_compile_features(mylib PUBLIC cxx_std_20)
set_target_properties(mylib PROPERTIES
    CXX_EXTENSIONS OFF          # no GNU extensions
    POSITION_INDEPENDENT_CODE ON
)

# Alias for consistent ::namespace naming
add_library(MyProject::mylib ALIAS mylib)

# Header-only library
add_library(headers INTERFACE)
target_include_directories(headers INTERFACE
    $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
    $<INSTALL_INTERFACE:include>
)
target_compile_features(headers INTERFACE cxx_std_20)

Custom Commands and Code Generation

cmake
# Generate a file at build time
add_custom_command(
    OUTPUT  ${CMAKE_CURRENT_BINARY_DIR}/generated.cpp
    COMMAND python3 ${CMAKE_CURRENT_SOURCE_DIR}/codegen.py
            --output ${CMAKE_CURRENT_BINARY_DIR}/generated.cpp
    DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/schema.json
            ${CMAKE_CURRENT_SOURCE_DIR}/codegen.py
    COMMENT "Generating code from schema"
)

add_library(mylib
    src/real.cpp
    ${CMAKE_CURRENT_BINARY_DIR}/generated.cpp  # include generated file
)
target_include_directories(mylib PRIVATE ${CMAKE_CURRENT_BINARY_DIR})

CTest Integration

cmake
include(CTest)
enable_testing()

add_executable(mytest test/test_main.cpp)
target_link_libraries(mytest GTest::gtest_main mylib)

# Register tests
include(GoogleTest)
gtest_discover_tests(mytest
    WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
    PROPERTIES TIMEOUT 30
)

# Custom test command
add_test(NAME integration_test
    COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/scripts/integration_test.sh
    WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
)
set_tests_properties(integration_test PROPERTIES
    ENVIRONMENT "DB_URL=postgresql://localhost/testdb"
)

Compiler Warnings as Errors

cmake
add_library(warnings INTERFACE)
target_compile_options(warnings INTERFACE
    $<$<CXX_COMPILER_ID:GNU,Clang>:
        -Wall -Wextra -Wpedantic
        -Wno-unused-parameter
        -Werror
    >
    $<$<CXX_COMPILER_ID:MSVC>:
        /W4 /WX
        /wd4100  # unreferenced formal parameter
    >
)

target_link_libraries(mylib PRIVATE warnings)
target_link_libraries(myapp PRIVATE warnings)

Toolchain File (Cross-Compilation)

cmake
# toolchain-arm.cmake
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR arm)

set(CMAKE_C_COMPILER   arm-linux-gnueabihf-gcc)
set(CMAKE_CXX_COMPILER arm-linux-gnueabihf-g++)

set(CMAKE_FIND_ROOT_PATH /usr/arm-linux-gnueabihf)
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
bash
cmake -B build-arm \
  -DCMAKE_TOOLCHAIN_FILE=toolchain-arm.cmake \
  -DCMAKE_BUILD_TYPE=Release
Edit on GitHubUpdated 2025-01-01T00:00:00.000Z