Table of contents
Open Table of contents
- TL;DR
- 1. CMake 不是构建系统,是构建系统生成器
- 2. 范式分水岭:Old-Style vs Modern CMake
- 3. 核心概念:命令、变量、目标
- 4. 项目骨架:能用的 CMakeLists.txt 模板
- 5. 依赖管理(真实世界的核心战场)
- 6. 跨平台、工具链、CMakePresets
- 7. 安装与导出(开源库作者必会)
- 8. 测试集成
- 9. Pitfalls(每个都是实战踩过的)
- 9.1 用
include_directories而不是target_include_directories - 9.2
file(GLOB)导致增量构建漏文件 - 9.3
target_link_libraries缺 PUBLIC/PRIVATE 关键字 - 9.4
CMAKE_CXX_FLAGS全局污染 - 9.5 in-source build 污染 git
- 9.6
if(VAR)的变量展开惊吓 - 9.7
CMAKE_BUILD_TYPE在多配置生成器下失效 - 9.8 子项目覆盖父项目
CMAKE_CXX_STANDARD - 9.9
add_definitions(-DFOO)的作用域问题 - 9.10
find_package找不到包时的无用错误 - 9.11 INTERFACE 目标误用
- 9.12
string(REGEX REPLACE)处理 Windows 路径
- 9.1 用
- 10. 2026 年 Modern CMake Checklist
- 11. 完整可运行示例项目
- 12. 命令速查
- 13. 版本演进关键点(设
cmake_minimum_required时选哪个) - 延伸阅读方向
- 信息来源
TL;DR
CMake 不是构建系统而是构建系统生成器(读 CMakeLists.txt → 生成 Makefile / Ninja / VS 工程 / Xcode 工程)。Old-Style CMake 靠 include_directories、add_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 | 输出 | 场景 |
|---|---|---|
Ninja | build.ninja | 现代默认,并行最快 |
Ninja Multi-Config | 多配置 ninja | 一个 build 目录同时 Debug/Release |
Unix Makefiles | Makefile | Linux/macOS 老派 |
Visual Studio 17 2022 | .sln/.vcxproj | Windows IDE |
Xcode | .xcodeproj | macOS IDE |
为什么 C++ 项目需要 CMake(没它不行的痛点):
- 编译器方言:GCC/Clang/MSVC 的 flag 完全不同(
-Ivs/I,-std=c++17vs/std:c++17) - 依赖传递:A 依赖 B,B 依赖 C+D,编译 A 时该链什么?手写 Makefile 是依赖地狱
- 第三方库发现:pkg-config 只在 Linux 有,Windows 根本没有统一方案
- 多配置:Debug/Release/ASan/Coverage 同源码多编法
- 跨平台安装:
/usr/localvsC:\Program Files,RPATH,DLL 搜索路径
与其他构建工具对比
| 工具 | 性质 | 优势 | 劣势 | 适用 |
|---|---|---|---|---|
| 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")
为什么是反模式:
- 无作用域:同一棵 CMakeLists 树里所有后续 target 被污染
- 无传递性语义:consumer 不知道 A 需要什么
- 顺序敏感:命令位置决定生效范围,重构即爆炸
- 不可组合:子模块覆盖父模块的全局变量
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 | ✅ | ✅ |
决策口诀:
- 实现里用,头文件里没出现 →
PRIVATE(如只在.cpp里用的boost::regex) - 头文件里出现了 →
PUBLIC(别人用你时必须能看到这个类型) - header-only 库 →
INTERFACE(自身不编译,只传递需求)
典型翻车:
# ❌ A 的头文件 #include <fmt/format.h>,却 PRIVATE 链接
target_link_libraries(A PRIVATE fmt::fmt)
# 结果:B 链接 A 时编译器找不到 fmt 头文件,报 "fmt/format.h: No such file"
为什么 3.15 是分水岭
MSVC_RUNTIME_LIBRARY属性:优雅选择/MTvs/MDinstall(TARGETS)支持更多目标类型,真正生产可用find_package(CONFIG)模式成熟,vcpkg 基于它file(GLOB CONFIGURE_DEPENDS)可靠- Generator expressions 覆盖大多数 target 属性
Henry Schreiner(《Modern CMake》作者):“3.15 前的 CMake 是能用,3.15+ 是好用”。
3. 核心概念:命令、变量、目标
三层模型
- 命令:大小写不敏感(约定用小写),所有参数都是字符串。形如
command(ARG1 ARG2 ...) - 变量:
set(NAME value)设置,${NAME}引用。一切皆字符串或字符串列表(列表用;分隔) - 目标:
add_library/add_executable产出的实体,有属性,有依赖图
变量作用域
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()
| 维度 | function | macro |
|---|---|---|
| 作用域 | 独立 | 内联展开到调用者 |
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):
- 多配置生成器下按 CONFIG 分支
- 导出库时区分 build-tree 和 install-tree 的路径
- 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,无产物
高级:
IMPORTEDtarget:代表外部已存在的库,find_package通常产出这种ALIAStarget:add_library(Foo::foo ALIAS foo),一等命名空间化写法,所有对外暴露的 target 都应该有Namespace::alias
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-based | target_compile_features(foo PUBLIC cxx_std_17) | 导出库必选 |
后者是正确答案,原因:cxx_std_17 是 PUBLIC 使用需求,会通过 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 | 库作者导出 |
MODULE | Find<PackageName>.cmake | CMake 自带或 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_package 和 FetchContent 两种依赖来源——系统装了就用系统的,没装就下源码编。
依赖方案对比
| 方案 | 优势 | 劣势 | 推荐场景 |
|---|---|---|---|
| FetchContent | 0 依赖、纯 CMake、git tag 锁版本 | 首次下载慢,每项目独立拉 | 中小项目首选 |
| vcpkg | 微软维护、manifest 模式、Windows 友好、包最多 | 吃 CMAKE_TOOLCHAIN_FILE | 跨平台中大型项目 |
| Conan 2.x | Profile 灵活、二进制缓存、企业级 | 学习曲线陡、和 CMake 偶尔别扭 | 企业多项目多 profile |
| CPM.cmake | FetchContent 的语法糖 + 下载缓存 | 社区维护,非官方 | 想要 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 单配置
- 单配置(Unix Makefiles / Ninja):
CMAKE_BUILD_TYPE在 configure 时决定 - 多配置(Visual Studio / Ninja Multi-Config / Xcode):一个 build 目录同时含多种配置,
cmake --build build --config Debug运行时选
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)
要点:
NAMESPACE MyApp::强制用户写带命名空间的 target 名,避免冲突find_dependency而不是find_package(前者正确传递QUIET/REQUIRED)BUILD_INTERFACE/INSTALL_INTERFACE必须出现在target_include_directories里,否则导出后路径指向源目录
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_tests | configure 阶段正则扫 .cpp 源码 | 识别不全 | ❌ 老式 |
gtest_discover_tests | build 后运行 --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,否则手写源文件列表。
9.3 target_link_libraries 缺 PUBLIC/PRIVATE 关键字
# ❌ 触发 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
-
cmake_minimum_required(VERSION 3.15...3.29)(range 形式,兼容老版本但启用新 policies) - 激进项目:3.21(
PROJECT_IS_TOP_LEVEL)或 3.25(FetchContentSYSTEM)起步 - Target-based 一切,禁用
include_directories/link_libraries/add_definitions - 每次
target_*显式PUBLIC/PRIVATE/INTERFACE - 按配置、按平台分支一律用 generator expression
-
CMakePresets.json替代 shell 脚本 - 依赖用 FetchContent(中小)或 vcpkg(中大),不要 git submodule
- Out-of-source build,顶层加守卫
- 警告当错误用
target_compile_options(foo PRIVATE -Werror),不要全局 -
CMAKE_EXPORT_COMPILE_COMMANDS=ON让 clangd/IDE 能索引 - 用
ccache:set(CMAKE_CXX_COMPILER_LAUNCHER ccache) -
find_package(xxx CONFIG REQUIRED)而不是 MODULE - 对外暴露的 target 写
Namespace::alias - 导出库写完整
install(EXPORT) + Config.cmake.in
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_features | C++ 标准 / 特性(cxx_std_17) |
target_sources | 追加源文件(3.23+ 支持 FILE_SET HEADERS 管理 public headers) |
target_link_options | 链接 flag |
target_precompile_headers | PCH(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.0 | 2014 | 第一个 Modern 版本,target 相关命令 | 历史起点 |
| 3.1 | 2014 | target_compile_features、cxx_std_* | C++11/14 项目基线 |
| 3.5 | 2016 | CMake 4.0 要求的最低 policy 版本 | 4.0 起这是硬下限 |
| 3.11 | 2018 | FetchContent 引入,INTERFACE 库支持 target_sources | 依赖管理起点 |
| 3.12 | 2018 | file(GLOB CONFIGURE_DEPENDS)、OBJECT 库改进 | 老 LTS 还能跑 |
| 3.14 | 2019 | FetchContent_MakeAvailable(FetchContent 真正生产可用) | FetchContent 成熟 |
| 3.15 | 2019 | 事实现代基线:MSVC_RUNTIME_LIBRARY、install 改进、--loglevel | Modern CMake 真正起点 |
| 3.16 | 2019 | target_precompile_headers、Unity Build、linker launcher | 编译时间优化 |
| 3.19 | 2020 | CMakePresets.json 引入 | 替代 shell 脚本 |
| 3.21 | 2021 | PROJECT_IS_TOP_LEVEL、多配置生成器 install 改进 | 可嵌入项目优雅判断 |
| 3.23 | 2022 | target_sources 支持 FILE_SET HEADERS(管理 public headers 新官方方式) | 头文件导出更清晰 |
| 3.24 | 2022 | FetchContent 的 FIND_PACKAGE_ARGS / OVERRIDE_FIND_PACKAGE | 统一 find_package + FetchContent |
| 3.25 | 2022 | FetchContent_Declare(SYSTEM)、block() 命令 | 第三方警告解耦 |
| 3.27 | 2023 | CMake debugger、generator expression 短路求值($<AND>/$<OR>);移除 FindPythonLibs/FindCUDA | Python/CUDA 项目注意 |
| 3.28 | 2023 | C++20 modules 一等公民(Ninja + Clang/MSVC) | 上 C++20 modules 必选 |
| 4.0 | 2025 | 移除 < 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 modules | 3.28... |
注意:CMake 4.0(2025)已发布,上游依赖会陆续要求 ≥3.5。用 3.15...3.29 的 range 写法避免 minimum 卡死。
延伸阅读方向
- 构建原理:
compile_commands.json的生成机制,clangd/ccls 如何索引 - 跨语言混编:CMake 的 CUDA / Fortran / Objective-C++ 支持
- 依赖锁定:Conan lockfile、vcpkg manifest 的版本固定
- C++20 Modules:CMake 3.28+ 的
CXX_MODULESFILE_SET、BMI 缓存 - CI 集成:
cmake --preset ci+ GitHub Actions 的标准流程 - 构建加速:ccache / sccache 集成、distcc、Unity Build、PCH
信息来源
- Modern CMake 书 by Henry Schreiner — 确定
- CMake 官方文档 — 确定
- CMake Presets 官方 manual — 确定
- CMake 4.0 Release Notes — 确定
- Effective Modern CMake gist — 社区共识
- 本文关键 API 和版本标注已通过 Context7 拉取官方文档交叉验证;少数版本归属(如
FIND_PACKAGE_ARGS归属 3.24)标注为需验证,正文中不影响结论