工程级 CMake Target 设计规范 / Engineering Modern CMake Target Design
1. Target 是 CMake 的工程边界
CMake 语法会写不等于工程能复用。真正的问题是:一个库被别人 find_package 后,能否自动获得正确头文件、编译特性、传递依赖和运行时要求,同时不继承你的私有警告、源码路径和本机缓存。
2. 最简单心智模型
target 是构建图节点,也是使用要求的容器。 PUBLIC、PRIVATE、INTERFACE 描述要求怎样沿依赖图传播。 安装导出和 consumer project 是检验 CMake 设计的最终考场。
Mesh::Core
|-- PRIVATE: src/, warnings, implementation-only deps
|-- PUBLIC : include/, cxx_std_20, public dependency targets
+-- INTERFACE on consumers after install/export3. 核心术语
target:CMake 构建图里的命名节点,可以是可执行文件、静态库、动态库或接口库。现代 CMake 主要围绕 target 描述工程关系。
usage requirements:一个 target 对使用者提出的要求,例如 include 路径、编译特性、宏定义和需要继续链接的依赖。
PUBLIC/PRIVATE/INTERFACE:控制要求如何传播的三个作用域。PRIVATE 只给自己,PUBLIC 给自己和消费者,INTERFACE 只给消费者。
install/export:把本仓库里的 target 变成可安装、可被外部项目 find_package 的包边界。
package config:安装包提供给 CMake 的入口文件,负责告诉消费者 target 在哪里、依赖怎样恢复、版本是否匹配。
consumer project:独立于源码树的小项目,用来验证安装后的包是否真的能被外部使用。
4. 机制展开
先从反例看问题
include_directories(${CMAKE_SOURCE_DIR}/include) 会让目录下所有目标都获得 include path,短期方便,长期无法解释依赖来自哪里。
set(CMAKE_CXX_FLAGS ...) 会让测试、工具、第三方子项目一起继承 flags,跨平台时很容易互相污染。
工程级写法要求每个 target 自己声明需求,让依赖流向可以被 code review 和 CI 观察。
最小 target 样例
下面的例子只展示核心思想:公共头暴露的东西 PUBLIC,源文件内部使用的东西 PRIVATE,纯策略可以放在 INTERFACE target。
安装导出才是真正验收
build tree 能用只能说明当前源码目录下可构建;install tree 能被一个全新 consumer 找到,才说明包配置、include 路径和传递依赖没有泄漏本机状态。
导出的 config 不应含开发机绝对路径,公共依赖应通过 find_dependency 重新定位。
5. 最小示例
下面的示例不是完整项目,而是为了把本篇概念落到可审查文本。阅读代码时关注它暴露了哪些边界,而不是照抄每个名字。
cmake_minimum_required(VERSION 3.24)
project(Mesh VERSION 1.2.0 LANGUAGES CXX)
add_library(mesh_core src/mesh.cpp)
add_library(Mesh::Core ALIAS mesh_core)
target_compile_features(mesh_core PUBLIC cxx_std_20)
target_include_directories(mesh_core
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
PRIVATE
src)
target_link_libraries(mesh_core PUBLIC fmt::fmt PRIVATE project_warnings)6. 工程取舍
| 选择 | 收益 | 风险 | 需要的证据 |
|---|---|---|---|
| 只在本机验证 | 反馈最快 | 环境不可复现 | 仅适合草稿,不适合作为发布结论 |
| CI 快速门禁 | 阻止常见错误 | 覆盖不完整 | 明确矩阵、日志和失败归属 |
| 完整发布门禁 | 证据最强 | 成本更高 | 制品、符号、SBOM、ABI 和回滚记录 |
| 最小消费者测试 | 最接近用户 | 需要维护样例 | 独立目录、安装包、运行输出 |
7. 常见错误
- 把“当前源码树能构建”误认为“安装包能被别人消费”。
- 把 PRIVATE 实现细节通过头文件、导出符号或包配置泄漏出去。
- 只保存最终二进制,不保存构建输入、依赖锁、调试符号和版本信息。
- 只在 Debug 测试,不在 Release 或发布配置里测试。
- 把 CI 缓存当成依赖管理,把绿色状态当成发布结论。
- 遇到链接或 ABI 问题时只改 flags,不留下能够复盘的符号证据。
8. 调试与验证方法
定义公开承诺
写清楚本库承诺源码兼容、二进制兼容、协议兼容还是仅内部使用。承诺越模糊,升级事故越难判责。
observe package config
-> isolate consumer project
-> run minimal proof
-> store evidence建立干净环境
用全新 build directory 和最小 consumer project 验证,不依赖源码树中的 include 路径和本机缓存。
observe install/export
-> isolate package config
-> run minimal proof
-> store evidence固定工具版本
记录编译器、CMake、Ninja、包管理器、系统镜像和关键 flags,避免把偶然环境当成规则。
observe PUBLIC/PRIVATE/INTERFACE
-> isolate install/export
-> run minimal proof
-> store evidence保存失败证据
失败日志、符号报告、测试输出和构建命令要能被别人复现,而不是只留一句“CI failed”。
observe usage requirements
-> isolate PUBLIC/PRIVATE/INTERFACE
-> run minimal proof
-> store evidence区分私有与公开
私有实现可以快速迭代;公开头、导出符号、包元数据和 ABI policy 必须谨慎变更。
observe target
-> isolate usage requirements
-> run minimal proof
-> store evidence审查跨平台差异
Windows 的 DLL/import lib/PDB 与 Linux 的 so/SONAME/RPATH/debug file 不同,不能用一个平台的经验套全部。
observe consumer project
-> isolate target
-> run minimal proof
-> store evidence限制全局状态
全局 include、全局 flags、隐式环境变量和手工 PATH 修改都会让构建不可解释。
observe package config
-> isolate consumer project
-> run minimal proof
-> store evidence做升级测试
old consumer + new library、new consumer + package install 是比单纯编译源码更接近用户现场的测试。
observe install/export
-> isolate package config
-> run minimal proof
-> store evidence设置回滚入口
发布前准备上一稳定制品、数据库或文件格式迁移策略、插件兼容说明和符号包。
observe PUBLIC/PRIVATE/INTERFACE
-> isolate install/export
-> run minimal proof
-> store evidence写入 CI
能自动检查的规则不要只写在文章里;把它变成 preset、脚本、workflow 或 release checklist。
observe usage requirements
-> isolate PUBLIC/PRIVATE/INTERFACE
-> run minimal proof
-> store evidence管理例外
允许临时豁免时必须记录原因、过期时间和补偿测试,否则门禁会慢慢失去意义。
observe target
-> isolate usage requirements
-> run minimal proof
-> store evidence保持证据总账
把构建输入、依赖锁、测试、ABI、性能、SBOM、签名和制品 hash 放在同一发布记录中。
observe consumer project
-> isolate target
-> run minimal proof
-> store evidence9. 本篇专属检查表
- 公共头是否暴露该依赖类型。
- 警告 flags 是否 PRIVATE。
- 安装树是否可重定位。
- 是否有 consumer project。
- Presets 是否覆盖 Debug/Release。
| 检查点 | 通过标准 | 失败时优先看哪里 |
|---|---|---|
| target | 有可重复命令、报告或 consumer 证明 | target 属性、CI 日志、符号报告、包元数据 |
| usage requirements | 有可重复命令、报告或 consumer 证明 | target 属性、CI 日志、符号报告、包元数据 |
| PUBLIC/PRIVATE/INTERFACE | 有可重复命令、报告或 consumer 证明 | target 属性、CI 日志、符号报告、包元数据 |
| install/export | 有可重复命令、报告或 consumer 证明 | target 属性、CI 日志、符号报告、包元数据 |
| package config | 有可重复命令、报告或 consumer 证明 | target 属性、CI 日志、符号报告、包元数据 |
| consumer project | 有可重复命令、报告或 consumer 证明 | target 属性、CI 日志、符号报告、包元数据 |
10. 练习任务
- 建立一个只有一个库和一个 consumer 的最小工程,证明安装树可用。
- 故意破坏一个 PUBLIC/PRIVATE 作用域,观察消费者构建失败方式。
- 保存一次链接或 ABI 报告,并写下它能证明什么、不能证明什么。
- 把一个手工验证命令改成 CI 步骤,并让失败消息能指向具体文件或配置。
- 写一份 release evidence 小清单,列出制品、符号、依赖、测试和回滚入口。
12. 总结
《工程级 CMake Target 设计规范 / Engineering Modern CMake Target Design》的核心不是多记几个命令,而是把 target、usage requirements、PUBLIC/PRIVATE/INTERFACE、install/export、package config、consumer project 变成可审查、可复现、可回滚的工程证据。
深化问题 1:usage requirements 怎样变成证据
围绕 usage requirements 写一个项目规则:谁可以修改,修改后跑什么命令,失败时保存哪些输出。
对应的工程动作是:用全新 build directory 和最小 consumer project 验证,不依赖源码树中的 include 路径和本机缓存。
rule: usage requirements
owner: module maintainer
proof: command + report + artifact
failure: block merge or require documented exception深化问题 2:PUBLIC/PRIVATE/INTERFACE 怎样变成证据
围绕 PUBLIC/PRIVATE/INTERFACE 写一个项目规则:谁可以修改,修改后跑什么命令,失败时保存哪些输出。
对应的工程动作是:记录编译器、CMake、Ninja、包管理器、系统镜像和关键 flags,避免把偶然环境当成规则。
rule: PUBLIC/PRIVATE/INTERFACE
owner: module maintainer
proof: command + report + artifact
failure: block merge or require documented exception深化问题 3:install/export 怎样变成证据
围绕 install/export 写一个项目规则:谁可以修改,修改后跑什么命令,失败时保存哪些输出。
对应的工程动作是:失败日志、符号报告、测试输出和构建命令要能被别人复现,而不是只留一句“CI failed”。
rule: install/export
owner: module maintainer
proof: command + report + artifact
failure: block merge or require documented exception深化问题 4:package config 怎样变成证据
围绕 package config 写一个项目规则:谁可以修改,修改后跑什么命令,失败时保存哪些输出。
对应的工程动作是:私有实现可以快速迭代;公开头、导出符号、包元数据和 ABI policy 必须谨慎变更。
rule: package config
owner: module maintainer
proof: command + report + artifact
failure: block merge or require documented exception深化问题 5:consumer project 怎样变成证据
围绕 consumer project 写一个项目规则:谁可以修改,修改后跑什么命令,失败时保存哪些输出。
对应的工程动作是:Windows 的 DLL/import lib/PDB 与 Linux 的 so/SONAME/RPATH/debug file 不同,不能用一个平台的经验套全部。
rule: consumer project
owner: module maintainer
proof: command + report + artifact
failure: block merge or require documented exception深化问题 6:target 怎样变成证据
围绕 target 写一个项目规则:谁可以修改,修改后跑什么命令,失败时保存哪些输出。
对应的工程动作是:全局 include、全局 flags、隐式环境变量和手工 PATH 修改都会让构建不可解释。
rule: target
owner: module maintainer
proof: command + report + artifact
failure: block merge or require documented exception深化问题 7:usage requirements 怎样变成证据
围绕 usage requirements 写一个项目规则:谁可以修改,修改后跑什么命令,失败时保存哪些输出。
对应的工程动作是:old consumer + new library、new consumer + package install 是比单纯编译源码更接近用户现场的测试。
rule: usage requirements
owner: module maintainer
proof: command + report + artifact
failure: block merge or require documented exception深化问题 8:PUBLIC/PRIVATE/INTERFACE 怎样变成证据
围绕 PUBLIC/PRIVATE/INTERFACE 写一个项目规则:谁可以修改,修改后跑什么命令,失败时保存哪些输出。
对应的工程动作是:发布前准备上一稳定制品、数据库或文件格式迁移策略、插件兼容说明和符号包。
rule: PUBLIC/PRIVATE/INTERFACE
owner: module maintainer
proof: command + report + artifact
failure: block merge or require documented exception深化问题 9:install/export 怎样变成证据
围绕 install/export 写一个项目规则:谁可以修改,修改后跑什么命令,失败时保存哪些输出。
对应的工程动作是:能自动检查的规则不要只写在文章里;把它变成 preset、脚本、workflow 或 release checklist。
rule: install/export
owner: module maintainer
proof: command + report + artifact
failure: block merge or require documented exception深化问题 10:package config 怎样变成证据
围绕 package config 写一个项目规则:谁可以修改,修改后跑什么命令,失败时保存哪些输出。
对应的工程动作是:允许临时豁免时必须记录原因、过期时间和补偿测试,否则门禁会慢慢失去意义。
rule: package config
owner: module maintainer
proof: command + report + artifact
failure: block merge or require documented exception深化问题 11:consumer project 怎样变成证据
围绕 consumer project 写一个项目规则:谁可以修改,修改后跑什么命令,失败时保存哪些输出。
对应的工程动作是:把构建输入、依赖锁、测试、ABI、性能、SBOM、签名和制品 hash 放在同一发布记录中。
rule: consumer project
owner: module maintainer
proof: command + report + artifact
failure: block merge or require documented exception深化问题 12:target 怎样变成证据
围绕 target 写一个项目规则:谁可以修改,修改后跑什么命令,失败时保存哪些输出。
对应的工程动作是:写清楚本库承诺源码兼容、二进制兼容、协议兼容还是仅内部使用。承诺越模糊,升级事故越难判责。
rule: target
owner: module maintainer
proof: command + report + artifact
failure: block merge or require documented exception深化问题 13:usage requirements 怎样变成证据
围绕 usage requirements 写一个项目规则:谁可以修改,修改后跑什么命令,失败时保存哪些输出。
对应的工程动作是:用全新 build directory 和最小 consumer project 验证,不依赖源码树中的 include 路径和本机缓存。
rule: usage requirements
owner: module maintainer
proof: command + report + artifact
failure: block merge or require documented exception深化问题 14:PUBLIC/PRIVATE/INTERFACE 怎样变成证据
围绕 PUBLIC/PRIVATE/INTERFACE 写一个项目规则:谁可以修改,修改后跑什么命令,失败时保存哪些输出。
对应的工程动作是:记录编译器、CMake、Ninja、包管理器、系统镜像和关键 flags,避免把偶然环境当成规则。
rule: PUBLIC/PRIVATE/INTERFACE
owner: module maintainer
proof: command + report + artifact
failure: block merge or require documented exception深化问题 15:install/export 怎样变成证据
围绕 install/export 写一个项目规则:谁可以修改,修改后跑什么命令,失败时保存哪些输出。
对应的工程动作是:失败日志、符号报告、测试输出和构建命令要能被别人复现,而不是只留一句“CI failed”。
rule: install/export
owner: module maintainer
proof: command + report + artifact
failure: block merge or require documented exception深化问题 16:package config 怎样变成证据
围绕 package config 写一个项目规则:谁可以修改,修改后跑什么命令,失败时保存哪些输出。
对应的工程动作是:私有实现可以快速迭代;公开头、导出符号、包元数据和 ABI policy 必须谨慎变更。
rule: package config
owner: module maintainer
proof: command + report + artifact
failure: block merge or require documented exception深化问题 17:consumer project 怎样变成证据
围绕 consumer project 写一个项目规则:谁可以修改,修改后跑什么命令,失败时保存哪些输出。
对应的工程动作是:Windows 的 DLL/import lib/PDB 与 Linux 的 so/SONAME/RPATH/debug file 不同,不能用一个平台的经验套全部。
rule: consumer project
owner: module maintainer
proof: command + report + artifact
failure: block merge or require documented exception深化问题 18:target 怎样变成证据
围绕 target 写一个项目规则:谁可以修改,修改后跑什么命令,失败时保存哪些输出。
对应的工程动作是:全局 include、全局 flags、隐式环境变量和手工 PATH 修改都会让构建不可解释。
rule: target
owner: module maintainer
proof: command + report + artifact
failure: block merge or require documented exception深化问题 19:usage requirements 怎样变成证据
围绕 usage requirements 写一个项目规则:谁可以修改,修改后跑什么命令,失败时保存哪些输出。
对应的工程动作是:old consumer + new library、new consumer + package install 是比单纯编译源码更接近用户现场的测试。
rule: usage requirements
owner: module maintainer
proof: command + report + artifact
failure: block merge or require documented exception深化问题 20:PUBLIC/PRIVATE/INTERFACE 怎样变成证据
围绕 PUBLIC/PRIVATE/INTERFACE 写一个项目规则:谁可以修改,修改后跑什么命令,失败时保存哪些输出。
对应的工程动作是:发布前准备上一稳定制品、数据库或文件格式迁移策略、插件兼容说明和符号包。
rule: PUBLIC/PRIVATE/INTERFACE
owner: module maintainer
proof: command + report + artifact
failure: block merge or require documented exception深化问题 21:install/export 怎样变成证据
围绕 install/export 写一个项目规则:谁可以修改,修改后跑什么命令,失败时保存哪些输出。
对应的工程动作是:能自动检查的规则不要只写在文章里;把它变成 preset、脚本、workflow 或 release checklist。
rule: install/export
owner: module maintainer
proof: command + report + artifact
failure: block merge or require documented exception