跳转到正文
zeno's blog
返回

C++ 工程化(一):Modern CMake 的核心是 target-based 与传递性语义

专题: C++ 工程化

Table of contents

Open Table of contents

TL;DR

CMake 不是构建系统而是构建系统生成器(读 CMakeLists.txt → 生成 Makefile / Ninja / VS 工程 / Xcode 工程)。Old-Style CMake 靠 include_directoriesadd_definitions 这种全局命令驱动,无作用域、无传递性、无法组合;Modern CMake(3.15+ 是真正的基线)把一切挂在 target 上,用 PUBLIC/PRIVATE/INTERFACE 显式表达**“这个使用需求是只给我自己用,还是要传递给 consumer”**。掌握这三个关键字 + generator expressions($<...>)+ CMakePresets.json + FetchContent,就写得出 2026 年的 idiomatic CMake。


1. CMake 不是构建系统,是构建系统生成器

CMake 本身不编译任何东西。两步模型:

# Step 1: configure —— 读 CMakeLists.txt,生成原生构建文件
cmake -S . -B build -G Ninja

# Step 2: build —— 调用原生构建系统(ninja/make/msbuild)
cmake --build build

常见 generator

Generator输出场景
Ninjabuild.ninja现代默认,并行最快
Ninja Multi-Config多配置 ninja一个 build 目录同时 Debug/Release
Unix MakefilesMakefileLinux/macOS 老派
Visual Studio 17 2022.sln/.vcxprojWindows IDE
Xcode.xcodeprojmacOS IDE

为什么 C++ 项目需要 CMake(没它不行的痛点):

与其他构建工具对比

工具性质优势劣势适用
Make构建系统无依赖、简单不跨平台、手写依赖地狱小型 C / 内核
CMake生成器事实标准,生态最广DSL 丑,学习曲线陡C++ 项目默认选
Bazel构建系统超大规模 monorepo、远程缓存、可复现自有生态,集成第三方库痛苦Google 系 monorepo
Meson生成器(调 Ninja)语法漂亮,configure 更快生态比 CMake 小得多Linux 系统库(GLib)
xmake构建系统Lua 脚本,内置包管理小众个人 / 国内中小项目

铁律:开源 C++ 库想让别人用,2026 年只能选 CMake。这不是技术优劣,是生态锁定。


2. 范式分水岭:Old-Style vs Modern CMake

Old-Style(反模式)

# ❌ 全局命令污染当前目录及所有子目录
include_directories(/opt/foo/include)
add_definitions(-DFOO_ENABLED)
link_directories(/opt/foo/lib)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -O2")

为什么是反模式

Modern CMake(3.0+ 出现,3.15+ 真正成熟

# ✅ 一切挂在 target 上
add_library(foo STATIC src/foo.cpp)
target_include_directories(foo
    PUBLIC  $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
            $<INSTALL_INTERFACE:include>
    PRIVATE src)
target_compile_definitions(foo PRIVATE FOO_INTERNAL)
target_compile_features(foo PUBLIC cxx_std_17)
target_link_libraries(foo PUBLIC fmt::fmt)

PUBLIC / PRIVATE / INTERFACE —— Modern CMake 的灵魂

这是理解 Modern CMake 的唯一关键概念。语义来自”使用需求(usage requirements)传递”:

关键字编译/链接 target 自身传递给 consumer
PRIVATE
INTERFACE
PUBLIC

决策口诀

典型翻车

# ❌ A 的头文件 #include <fmt/format.h>,却 PRIVATE 链接
target_link_libraries(A PRIVATE fmt::fmt)
# 结果:B 链接 A 时编译器找不到 fmt 头文件,报 "fmt/format.h: No such file"

为什么 3.15 是分水岭

Henry Schreiner(《Modern CMake》作者):“3.15 前的 CMake 是能用,3.15+ 是好用”


3. 核心概念:命令、变量、目标

三层模型

变量作用域

function(foo)
    set(X 1)                   # 仅函数内可见
    set(RESULT "hi" PARENT_SCOPE)   # 推到父作用域
endfunction()

# 子目录开辟独立 directory scope
add_subdirectory(sub)          # sub 修改父变量不回传,除非 PARENT_SCOPE

# Cache 变量:持久化到 CMakeCache.txt
set(MY_OPT ON CACHE BOOL "enable X")

function vs macro(高频考点)

function(F)
    set(X 1)        # 新作用域,外部看不到 X
endfunction()

macro(M)
    set(X 1)        # 直接在调用者作用域设置 X
endmacro()
维度functionmacro
作用域独立内联展开到调用者
ARGV/ARGN真正的局部列表文本替换
return()正常返回返回的是调用 macro 的那层函数
推荐度95% 场景用它只在需要修改调用者变量时用

Generator Expressions $<...> —— Modern CMake 的灵魂

关键认知:CMake 分两阶段 —— configure 阶段生成构建文件(此时 if 可用)、generate 阶段才展开 generator expression。多配置生成器(VS/Ninja Multi-Config)configure 只跑一次,但要为 Debug/Release 各产出一份。此时 if(CMAKE_BUILD_TYPE STREQUAL Debug) 失效,必须用 generator expression。

常用形式:

# 按配置切换编译选项
target_compile_options(foo PRIVATE "$<$<CONFIG:Debug>:-O0;-g>")

# 按编译器切换
target_compile_options(foo PRIVATE
    $<$<CXX_COMPILER_ID:MSVC>:/W4 /permissive->
    $<$<CXX_COMPILER_ID:GNU,Clang,AppleClang>:-Wall -Wextra -Wpedantic>)

# BUILD_INTERFACE / INSTALL_INTERFACE —— 导出库必用,无替代方案
target_include_directories(foo PUBLIC
    $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
    $<INSTALL_INTERFACE:include>)

# 组合(3.27+ 短路求值)
target_link_libraries(foo PUBLIC
    $<$<AND:$<PLATFORM_ID:Linux>,$<CONFIG:Debug>>:rt>)

什么时候必须用 generator expression(而不是 if):

  1. 多配置生成器下按 CONFIG 分支
  2. 导出库时区分 build-tree 和 install-tree 的路径
  3. INTERFACE 属性里的条件(INTERFACE 属性不能放 if

目标类型

add_executable(app main.cpp)
add_library(foo STATIC    src/foo.cpp)      # .a / .lib
add_library(foo SHARED    src/foo.cpp)      # .so / .dylib / .dll
add_library(foo MODULE    src/foo.cpp)      # 运行时 dlopen
add_library(foo OBJECT    src/foo.cpp)      # 仅 .o,供其他 target 复用
add_library(foo INTERFACE)                  # header-only,无产物

高级


4. 项目骨架:能用的 CMakeLists.txt 模板

顶层 CMakeLists.txt

# range 形式:最低兼容 3.15,启用最高 3.29 的新 policies
cmake_minimum_required(VERSION 3.15...3.29)

project(MyApp
    VERSION     1.2.3
    DESCRIPTION "Modern CMake 示例"
    LANGUAGES   CXX)

# 仅顶层时启用的开发者设置(3.21+ PROJECT_IS_TOP_LEVEL)
if(PROJECT_IS_TOP_LEVEL)
    set(CMAKE_EXPORT_COMPILE_COMMANDS ON)    # 给 clangd/IDE 用
    set_property(GLOBAL PROPERTY USE_FOLDERS ON)

    if(CMAKE_SOURCE_DIR STREQUAL CMAKE_BINARY_DIR)
        message(FATAL_ERROR "禁止 in-source build,用 cmake -B build")
    endif()

    if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
        set(CMAKE_BUILD_TYPE RelWithDebInfo CACHE STRING "" FORCE)
    endif()
endif()

add_subdirectory(src)
add_subdirectory(app)

if(PROJECT_IS_TOP_LEVEL AND BUILD_TESTING)
    include(CTest)
    add_subdirectory(tests)
endif()

设置 C++ 标准:两种方式的权衡

方式写法适用
全局变量set(CMAKE_CXX_STANDARD 17)快速原型、应用项目
target-basedtarget_compile_features(foo PUBLIC cxx_std_17)导出库必选

后者是正确答案,原因:cxx_std_17PUBLIC 使用需求,会通过 target_link_libraries 传递给 consumer——consumer 自动继承”至少 C++17”的要求。全局变量做不到这种传递。

target_compile_features(foo PUBLIC cxx_std_17)
set_target_properties(foo PROPERTIES CXX_EXTENSIONS OFF)   # 关掉 GNU 扩展,强制 ISO C++

为什么不要用 file(GLOB) 收集源文件

# ❌ 反模式
file(GLOB SRCS src/*.cpp)
add_library(foo ${SRCS})

CMake 只在 configure 阶段展开 glob。新增 .cpp 后,ninja 不会重新 configure,新文件不会进入构建,直到有人手动跑 cmake 触发重新配置——这会导致”我加了文件但没被编译”的诡异 bug。

部分缓解(3.12+,仍不推荐):

file(GLOB SRCS CONFIGURE_DEPENDS src/*.cpp)

CONFIGURE_DEPENDS 让 Ninja 每次构建前 stat 目录触发 re-configure。代价:大型项目扫描开销明显,官方文档原话是 “not guaranteed to work reliably”

正确做法手写源文件列表。增删文件本就该 review,显式列出让 git log CMakeLists.txt 成为清单审计日志。


5. 依赖管理(真实世界的核心战场)

find_package 的两种模式

find_package(fmt 9.0 CONFIG REQUIRED)    # CONFIG 模式
find_package(Threads REQUIRED)           # MODULE 模式(默认)
模式查找对象提供者
CONFIG<PackageName>Config.cmake库作者导出
MODULEFind<PackageName>.cmakeCMake 自带或 CMAKE_MODULE_PATH

铁律优先 CONFIG。CONFIG 由库作者维护,信息最准;MODULE 是”别人猜”,经常过时。vcpkg / Conan 安装的包几乎都提供 CONFIG 文件。

FetchContent(3.11 引入,3.14 后生产可用)

现代写法:

include(FetchContent)

FetchContent_Declare(
    fmt
    GIT_REPOSITORY https://github.com/fmtlib/fmt.git
    GIT_TAG        10.2.1
    GIT_SHALLOW    TRUE
    SYSTEM                    # 3.25+:当系统头文件,不触发警告
    OVERRIDE_FIND_PACKAGE     # 3.24+:后续 find_package(fmt) 走它
)

FetchContent_Declare(
    googletest
    GIT_REPOSITORY https://github.com/google/googletest.git
    GIT_TAG        v1.14.0
    GIT_SHALLOW    TRUE
    FIND_PACKAGE_ARGS NAMES GTest   # 3.24+:先试 find_package,失败才下载
)

FetchContent_MakeAvailable(fmt googletest)

FetchContent_MakeAvailable(3.14 引入)一站式完成下载+add_subdirectory。3.24+ 的 FIND_PACKAGE_ARGS + OVERRIDE_FIND_PACKAGE 统一了 find_packageFetchContent 两种依赖来源——系统装了就用系统的,没装就下源码编。

依赖方案对比

方案优势劣势推荐场景
FetchContent0 依赖、纯 CMake、git tag 锁版本首次下载慢,每项目独立拉中小项目首选
vcpkg微软维护、manifest 模式、Windows 友好、包最多CMAKE_TOOLCHAIN_FILE跨平台中大型项目
Conan 2.xProfile 灵活、二进制缓存、企业级学习曲线陡、和 CMake 偶尔别扭企业多项目多 profile
CPM.cmakeFetchContent 的语法糖 + 下载缓存社区维护,非官方想要 FetchContent 但嫌啰嗦
ExternalProject_Add能跑非 CMake 构建系统(autotools 等)build 阶段才下载,CMake 看不到 target只用于兼容非 CMake 的老库
git submodule不需要网络版本管理地狱、子模块污染父别用,已过时

用 FetchContent 拉 GoogleTest 的完整例子

include(FetchContent)
FetchContent_Declare(
    googletest
    GIT_REPOSITORY https://github.com/google/googletest.git
    GIT_TAG        v1.14.0
)
# Windows 上强制 GTest 用动态 CRT,避免和主项目冲突
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(googletest)

enable_testing()
add_executable(my_tests test_foo.cpp)
target_link_libraries(my_tests PRIVATE GTest::gtest_main)

include(GoogleTest)
gtest_discover_tests(my_tests)

6. 跨平台、工具链、CMakePresets

编译选项的正确方式

# ❌ 全局污染:第三方库也被你的 -Werror 波及
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Werror")

# ✅ 只给自己的 target
target_compile_options(foo PRIVATE
    $<$<CXX_COMPILER_ID:MSVC>:/W4 /permissive->
    $<$<CXX_COMPILER_ID:GNU,Clang,AppleClang>:-Wall -Wextra -Wpedantic -Wshadow>
)
target_compile_definitions(foo PRIVATE MYAPP_VERSION="${PROJECT_VERSION}")

工具链文件(交叉编译)

CMAKE_TOOLCHAIN_FILE 是交叉编译的标准入口:

# arm-linux-toolchain.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 /opt/sysroots/arm)
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
cmake -B build-arm -DCMAKE_TOOLCHAIN_FILE=arm-linux-toolchain.cmake

vcpkg 本质也是 toolchain file-DCMAKE_TOOLCHAIN_FILE=$VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake

CMake Presets(3.19+,2020 后最大的体验进步)

问题:团队里每个人都有自己的 build-debug.sh / build-release.sh,参数散乱、不可复现、CI 和本地不一致。

解决CMakePresets.json 在项目根,入版本控制,IDE(VS/VSCode/CLion)和命令行共享。

完整示例(schema v6,workflow 可用):

{
  "version": 6,
  "cmakeMinimumRequired": { "major": 3, "minor": 25, "patch": 0 },
  "configurePresets": [
    {
      "name": "base",
      "hidden": true,
      "generator": "Ninja",
      "binaryDir": "${sourceDir}/build/${presetName}",
      "cacheVariables": {
        "CMAKE_EXPORT_COMPILE_COMMANDS": "ON",
        "CMAKE_CXX_STANDARD": "20"
      }
    },
    {
      "name": "debug",
      "inherits": "base",
      "cacheVariables": { "CMAKE_BUILD_TYPE": "Debug" }
    },
    {
      "name": "release",
      "inherits": "base",
      "cacheVariables": { "CMAKE_BUILD_TYPE": "RelWithDebInfo" }
    },
    {
      "name": "asan",
      "inherits": "debug",
      "cacheVariables": {
        "CMAKE_CXX_FLAGS": "-fsanitize=address,undefined -fno-omit-frame-pointer"
      }
    }
  ],
  "buildPresets": [
    { "name": "debug", "configurePreset": "debug" },
    { "name": "release", "configurePreset": "release" },
    { "name": "asan", "configurePreset": "asan" }
  ],
  "testPresets": [
    {
      "name": "debug",
      "configurePreset": "debug",
      "output": { "outputOnFailure": true },
      "execution": { "jobs": 8 }
    }
  ],
  "workflowPresets": [
    {
      "name": "ci",
      "steps": [
        { "type": "configure", "name": "debug" },
        { "type": "build", "name": "debug" },
        { "type": "test", "name": "debug" }
      ]
    }
  ]
}

用法:

cmake --list-presets                 # 列出所有 preset
cmake --preset debug                 # configure
cmake --build --preset debug         # build
ctest --preset debug                 # test
cmake --workflow --preset ci         # 一条命令跑完 configure+build+test

CMakeUserPresets.json:个人本地覆盖,.gitignore 掉,用于开发者特异路径。

多配置生成器 vs 单配置

CMAKE_BUILD_TYPE 在多配置生成器下无效。跨生成器一律用 $<CONFIG:...> generator expression。


7. 安装与导出(开源库作者必会)

目标:让用户写 find_package(MyApp CONFIG REQUIRED) + target_link_libraries(app PRIVATE MyApp::myapp_core) 就能用你的库。

完整流程:

include(GNUInstallDirs)                   # 定义 CMAKE_INSTALL_LIBDIR 等标准变量
include(CMakePackageConfigHelpers)

# 1. 安装 target 产物,登记到 export set
install(TARGETS myapp_core
    EXPORT   MyAppTargets
    LIBRARY  DESTINATION ${CMAKE_INSTALL_LIBDIR}
    ARCHIVE  DESTINATION ${CMAKE_INSTALL_LIBDIR}
    RUNTIME  DESTINATION ${CMAKE_INSTALL_BINDIR}
    INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
)

# 2. 安装头文件
install(DIRECTORY include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})

# 3. 生成并安装 Targets 文件
install(EXPORT MyAppTargets
    FILE       MyAppTargets.cmake
    NAMESPACE  MyApp::
    DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/MyApp
)

# 4. 生成版本文件,支持 find_package(MyApp 1.2) 匹配
write_basic_package_version_file(
    "${CMAKE_CURRENT_BINARY_DIR}/MyAppConfigVersion.cmake"
    VERSION       ${PROJECT_VERSION}
    COMPATIBILITY SameMajorVersion
)

# 5. 生成 Config 文件(核心:告诉 find_package 依赖是什么)
configure_package_config_file(
    "${CMAKE_CURRENT_SOURCE_DIR}/cmake/MyAppConfig.cmake.in"
    "${CMAKE_CURRENT_BINARY_DIR}/MyAppConfig.cmake"
    INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/MyApp
)

install(FILES
    "${CMAKE_CURRENT_BINARY_DIR}/MyAppConfig.cmake"
    "${CMAKE_CURRENT_BINARY_DIR}/MyAppConfigVersion.cmake"
    DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/MyApp
)

cmake/MyAppConfig.cmake.in 模板:

@PACKAGE_INIT@

include(CMakeFindDependencyMacro)
find_dependency(fmt 9.0)     # 你依赖谁这里就 find_dependency 谁

include("${CMAKE_CURRENT_LIST_DIR}/MyAppTargets.cmake")
check_required_components(MyApp)

要点


8. 测试集成

include(CTest)          # 自动 enable_testing() 并定义 BUILD_TESTING 选项

add_executable(test_foo test_foo.cpp)
target_link_libraries(test_foo PRIVATE myapp_core)

add_test(NAME foo_basic COMMAND test_foo --basic)
set_tests_properties(foo_basic PROPERTIES
    TIMEOUT 30
    LABELS  "unit"
)

运行:

ctest --test-dir build -j$(nproc) --output-on-failure
ctest --test-dir build -L unit         # 按 label 过滤
ctest --test-dir build -R foo_.*        # 按正则过滤
ctest --test-dir build --rerun-failed   # 只跑上次失败的

GoogleTest:gtest_discover_tests vs gtest_add_tests

命令发现方式参数化/TYPED_TEST推荐
gtest_add_testsconfigure 阶段正则扫 .cpp 源码识别不全❌ 老式
gtest_discover_testsbuild 后运行 --gtest_list_tests,把测试注册到 CTest完整支持✅ 首选
include(GoogleTest)
add_executable(my_tests test_foo.cpp test_bar.cpp)
target_link_libraries(my_tests PRIVATE GTest::gtest_main)
gtest_discover_tests(my_tests)

9. Pitfalls(每个都是实战踩过的)

9.1 用 include_directories 而不是 target_include_directories

老式命令污染当前目录及子目录所有后续 target,无传递性。铁律target_* 系列是唯一答案。

9.2 file(GLOB) 导致增量构建漏文件

见 §4。只有清单确实巨大时才考虑 CONFIGURE_DEPENDS,否则手写源文件列表。

# ❌ 触发 CMake 2.x 兼容模式,之后该 target 的所有 tll 都不能用关键字了
target_link_libraries(foo bar)

规则:target 的第一次 target_link_libraries 必须显式带作用域关键字,否则 CMake 会锁死到”无关键字”模式,后续再用关键字会报 Keywords mixed with plain signature

9.4 CMAKE_CXX_FLAGS 全局污染

# ❌ 第三方库也被你的 -Werror 波及,第三方一个警告 CI 就红
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Werror")

正确target_compile_options(foo PRIVATE -Werror),只作用于自己的 target。

9.5 in-source build 污染 git

cmake .     # ❌ 在源码目录产 CMakeCache.txt/CMakeFiles/

顶层加守卫:

if(CMAKE_SOURCE_DIR STREQUAL CMAKE_BINARY_DIR)
    message(FATAL_ERROR "禁止 in-source build,用 cmake -B build")
endif()

9.6 if(VAR) 的变量展开惊吓

CMake 的 if 对参数双重展开

set(FOO OFF)
set(BAR "FOO")
if(${BAR})      # 展开为 if(FOO) → 再展开为 if(OFF) → FALSE
if(BAR)         # 直接查 BAR 变量是否存在且非 OFF → TRUE

铁律:除非精确知道展开规则,永远写 if(VAR) 不带 ${}

9.7 CMAKE_BUILD_TYPE 在多配置生成器下失效

见 §6。跨生成器一律用 $<CONFIG:Debug>

9.8 子项目覆盖父项目 CMAKE_CXX_STANDARD

第三方库通过 add_subdirectory 进来时可能在自己的 CMakeLists 里 set(CMAKE_CXX_STANDARD 14)。主项目编 17,子项目编 14,链接时 std::string 可能因 libstdc++ dual ABI 而不兼容。解决:优先 find_package 或预装依赖,用 FetchContent 时在 MakeAvailable 之前锁死自己的标准。

9.9 add_definitions(-DFOO) 的作用域问题

include_directories,全局污染。用 target_compile_definitions(foo PRIVATE FOO)

9.10 find_package 找不到包时的无用错误

默认只说”Could not find package configuration file…”,不告诉你搜了哪些目录。

cmake -B build --debug-find                    # 3.17+,打印每次查找过程
cmake -B build -DCMAKE_FIND_DEBUG_MODE=ON      # 同上

9.11 INTERFACE 目标误用

INTERFACE不能有源文件(3.19 前完全不行,3.19+ 可通过 target_sources(foo INTERFACE ...) 加但很别扭)。误把有实现的库写成 INTERFACE 导致”什么都没编译”。

9.12 string(REGEX REPLACE) 处理 Windows 路径

Windows 路径 C:\foo\bar 的反斜杠在 CMake 里是转义字符,手写正则必翻车。用 file(TO_CMAKE_PATH ...) / file(TO_NATIVE_PATH ...) 转换。


10. 2026 年 Modern CMake Checklist


11. 完整可运行示例项目

目录结构

myapp/
├── CMakeLists.txt
├── CMakePresets.json
├── cmake/
│   └── MyAppConfig.cmake.in
├── include/myapp/
│   └── api.hpp
├── src/
│   ├── CMakeLists.txt
│   └── api.cpp
├── app/
│   ├── CMakeLists.txt
│   └── main.cpp
└── tests/
    ├── CMakeLists.txt
    └── test_api.cpp

CMakeLists.txt(顶层)

cmake_minimum_required(VERSION 3.21...3.29)

project(MyApp
    VERSION     1.0.0
    DESCRIPTION "Modern CMake 示例项目"
    LANGUAGES   CXX)

if(PROJECT_IS_TOP_LEVEL)
    set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
    set_property(GLOBAL PROPERTY USE_FOLDERS ON)

    if(CMAKE_SOURCE_DIR STREQUAL CMAKE_BINARY_DIR)
        message(FATAL_ERROR "禁止 in-source build")
    endif()

    if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
        set(CMAKE_BUILD_TYPE RelWithDebInfo CACHE STRING "" FORCE)
    endif()
endif()

option(MYAPP_BUILD_TESTS "Build tests"  ${PROJECT_IS_TOP_LEVEL})
option(MYAPP_INSTALL     "Enable install targets" ${PROJECT_IS_TOP_LEVEL})

add_subdirectory(src)
add_subdirectory(app)

if(MYAPP_BUILD_TESTS)
    enable_testing()
    add_subdirectory(tests)
endif()

if(MYAPP_INSTALL)
    include(GNUInstallDirs)
    include(CMakePackageConfigHelpers)

    install(TARGETS myapp_core
        EXPORT   MyAppTargets
        LIBRARY  DESTINATION ${CMAKE_INSTALL_LIBDIR}
        ARCHIVE  DESTINATION ${CMAKE_INSTALL_LIBDIR}
        RUNTIME  DESTINATION ${CMAKE_INSTALL_BINDIR}
        INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
    install(DIRECTORY include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})

    install(EXPORT MyAppTargets
        FILE      MyAppTargets.cmake
        NAMESPACE MyApp::
        DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/MyApp)

    write_basic_package_version_file(
        "${CMAKE_CURRENT_BINARY_DIR}/MyAppConfigVersion.cmake"
        VERSION       ${PROJECT_VERSION}
        COMPATIBILITY SameMajorVersion)

    configure_package_config_file(
        "${CMAKE_CURRENT_SOURCE_DIR}/cmake/MyAppConfig.cmake.in"
        "${CMAKE_CURRENT_BINARY_DIR}/MyAppConfig.cmake"
        INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/MyApp)

    install(FILES
        "${CMAKE_CURRENT_BINARY_DIR}/MyAppConfig.cmake"
        "${CMAKE_CURRENT_BINARY_DIR}/MyAppConfigVersion.cmake"
        DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/MyApp)
endif()

include/myapp/api.hpp

#pragma once
#include <string>
#include <string_view>

namespace myapp {
std::string greet(std::string_view name);
}

src/CMakeLists.txt

add_library(myapp_core STATIC api.cpp)
add_library(MyApp::myapp_core ALIAS myapp_core)

target_include_directories(myapp_core
    PUBLIC
        $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/../include>
        $<INSTALL_INTERFACE:include>)

target_compile_features(myapp_core PUBLIC cxx_std_17)

target_compile_options(myapp_core PRIVATE
    $<$<CXX_COMPILER_ID:MSVC>:/W4 /permissive->
    $<$<CXX_COMPILER_ID:GNU,Clang,AppleClang>:-Wall -Wextra -Wpedantic>)

set_target_properties(myapp_core PROPERTIES
    CXX_EXTENSIONS OFF
    POSITION_INDEPENDENT_CODE ON)

src/api.cpp

#include "myapp/api.hpp"

namespace myapp {
std::string greet(std::string_view name) {
    return "Hello, " + std::string(name) + "!";
}
}

app/CMakeLists.txt

add_executable(myapp main.cpp)
target_link_libraries(myapp PRIVATE MyApp::myapp_core)

if(MYAPP_INSTALL)
    install(TARGETS myapp RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
endif()

app/main.cpp

#include "myapp/api.hpp"
#include <iostream>

int main() {
    std::cout << myapp::greet("Modern CMake") << '\n';
}

tests/CMakeLists.txt

include(FetchContent)
FetchContent_Declare(
    googletest
    GIT_REPOSITORY https://github.com/google/googletest.git
    GIT_TAG        v1.14.0
    GIT_SHALLOW    TRUE)
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(googletest)

add_executable(test_api test_api.cpp)
target_link_libraries(test_api PRIVATE MyApp::myapp_core GTest::gtest_main)

include(GoogleTest)
gtest_discover_tests(test_api)

tests/test_api.cpp

#include "myapp/api.hpp"
#include <gtest/gtest.h>

TEST(ApiTest, GreetsByName) {
    EXPECT_EQ(myapp::greet("World"), "Hello, World!");
}

cmake/MyAppConfig.cmake.in

@PACKAGE_INIT@

include(CMakeFindDependencyMacro)
# 如果依赖了 fmt,这里 find_dependency(fmt)

include("${CMAKE_CURRENT_LIST_DIR}/MyAppTargets.cmake")
check_required_components(MyApp)

使用方式

cmake --preset debug                  # configure
cmake --build --preset debug          # build
ctest --preset debug                  # test

cmake --workflow --preset ci          # 一键跑 configure+build+test

# 安装
cmake --install build/release --prefix /opt/myapp

12. 命令速查

项目声明

命令作用
cmake_minimum_required(VERSION X...Y)最低版本 + policy range
project(Name VERSION X LANGUAGES CXX)项目声明
include(Module)加载标准模块或 .cmake 文件

目标

命令作用
add_executable(name src...)可执行
add_library(name [STATIC|SHARED|MODULE|OBJECT|INTERFACE] src...)
add_library(alias ALIAS real)别名(Namespace::target
add_custom_target(name [ALL] COMMAND ...)自定义 target
add_custom_command(TARGET ... POST_BUILD ...)挂钩命令

目标属性(target_* 系列)

命令作用
target_include_directories头文件搜索路径
target_link_libraries链接其他 target
target_compile_definitions宏定义
target_compile_options编译 flag
target_compile_featuresC++ 标准 / 特性(cxx_std_17
target_sources追加源文件(3.23+ 支持 FILE_SET HEADERS 管理 public headers)
target_link_options链接 flag
target_precompile_headersPCH(3.16+)
set_target_properties直接设目标属性

依赖

命令作用
find_package(Pkg [ver] CONFIG REQUIRED)查找外部包
find_library / find_path / find_program低级查找
FetchContent_Declare / FetchContent_MakeAvailable源码级依赖

安装与导出

命令作用
install(TARGETS ...)安装 target 产物
install(FILES ...) / install(DIRECTORY ...)安装文件
install(EXPORT ...)导出 target 集合
configure_package_config_file生成 <Pkg>Config.cmake
write_basic_package_version_file版本检查文件
include(GNUInstallDirs)标准安装路径变量

测试

命令作用
enable_testing() / include(CTest)启用测试
add_test(NAME ... COMMAND ...)注册测试
gtest_discover_tests(target)GoogleTest 自动发现

流程控制

命令作用
if/elseif/else/endif条件
foreach/endforeach循环
function/endfunction函数(独立作用域,首选
macro/endmacro宏(无作用域)
return()函数返回
message(STATUS|WARNING|FATAL_ERROR ...)输出

变量 / 字符串 / 文件

命令作用
set(VAR val [CACHE TYPE "doc"] [PARENT_SCOPE])设置
unset(VAR [CACHE])删除
list(APPEND|GET|REMOVE_ITEM|FILTER ...)列表操作
string(REGEX REPLACE|TOLOWER ...)字符串操作
file(READ|WRITE|GLOB|DOWNLOAD|COPY ...)文件操作
configure_file(in out @ONLY)模板替换(生成版本头等)

13. 版本演进关键点(设 cmake_minimum_required 时选哪个)

版本年份关键特性升级理由
3.02014第一个 Modern 版本,target 相关命令历史起点
3.12014target_compile_featurescxx_std_*C++11/14 项目基线
3.52016CMake 4.0 要求的最低 policy 版本4.0 起这是硬下限
3.112018FetchContent 引入,INTERFACE 库支持 target_sources依赖管理起点
3.122018file(GLOB CONFIGURE_DEPENDS)、OBJECT 库改进老 LTS 还能跑
3.142019FetchContent_MakeAvailable(FetchContent 真正生产可用)FetchContent 成熟
3.152019事实现代基线MSVC_RUNTIME_LIBRARYinstall 改进、--loglevelModern CMake 真正起点
3.162019target_precompile_headers、Unity Build、linker launcher编译时间优化
3.192020CMakePresets.json 引入替代 shell 脚本
3.212021PROJECT_IS_TOP_LEVEL、多配置生成器 install 改进可嵌入项目优雅判断
3.232022target_sources 支持 FILE_SET HEADERS(管理 public headers 新官方方式)头文件导出更清晰
3.242022FetchContentFIND_PACKAGE_ARGS / OVERRIDE_FIND_PACKAGE统一 find_package + FetchContent
3.252022FetchContent_Declare(SYSTEM)block() 命令第三方警告解耦
3.272023CMake debugger、generator expression 短路求值$<AND>/$<OR>);移除 FindPythonLibs/FindCUDAPython/CUDA 项目注意
3.282023C++20 modules 一等公民(Ninja + Clang/MSVC)上 C++20 modules 必选
4.02025移除 < 3.5 policy 兼容上游依赖可能需要升级 cmake_minimum_required

实务建议

场景推荐 cmake_minimum_required
开源库3.15...3.28
企业应用3.21...3.29(要 PROJECT_IS_TOP_LEVEL
2026 新项目3.25...3.29(要 FetchContent SYSTEM
上 C++20 modules3.28...

注意:CMake 4.0(2025)已发布,上游依赖会陆续要求 ≥3.5。用 3.15...3.29 的 range 写法避免 minimum 卡死。


延伸阅读方向

信息来源


分享这篇文章:

上一篇
Go RPC:gRPC、HTTP/2 与 proto 契约
下一篇
Go 工具链:Protobuf 的字段编号、Varint 与二进制序列化