21、CMake中的预设模式:CMakePresets.json详解、构建预设、测试预设、打包预设
说实话,CMake 的预设模式(Presets)是我近几年最喜欢的一个特性。以前做项目,尤其是跨平台项目,每次都要跟团队成员说「你记得加这个参数」「别忘了那个选项」,结果总有人漏掉。后来我干脆写了个脚本,但脚本多了又乱。直到 CMake 3.19 引入了 CMakePresets.json,我才觉得这事儿终于被「官方」解决了。
这一章,我就带你彻底搞懂预设模式。说白了,它就是一套标准化的配置模板,把构建、测试、打包的常用参数都写进 JSON 文件里。你想想看,一个文件搞定所有环境,多省心。
预设模式解决了什么问题?
我在项目中遇到过最头疼的事:同一个项目,Windows 上要指定 -G "Visual Studio 17 2022",Linux 上要用 -G "Unix Makefiles",macOS 上还得加个 -DCMAKE_OSX_DEPLOYMENT_TARGET。每次换机器都要重新敲一遍命令,或者翻文档找参数。
预设模式就是把这些「环境相关」的配置固化下来。你只需要写一次,团队所有人都能复用。而且它支持继承、条件判断、变量替换,非常灵活。
CMakePresets.json 文件结构
这个 JSON 文件通常放在项目根目录。它的顶层结构是这样的:
{
"version": 6,
"configurePresets": [],
"buildPresets": [],
"testPresets": [],
"packagePresets": [],
"workflowPresets": []
}
嗯,这里要注意:version 字段必须写,而且不同 CMake 版本支持的版本号不同。CMake 3.19 支持 version 1,到 3.27 已经支持 version 6 了。我建议你直接用最新版,功能最全。
构建预设(configurePresets)
构建预设是最常用的。它定义了 cmake -S . -B build 时需要的所有参数。来看一个实际例子:
{
"configurePresets": [
{
"name": "default",
"displayName": "默认配置",
"description": "适用于大多数开发环境",
"generator": "Ninja",
"binaryDir": "${sourceDir}/build/${presetName}",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug",
"CMAKE_CXX_STANDARD": "17"
}
},
{
"name": "release",
"inherits": "default",
"displayName": "发布配置",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release",
"CMAKE_INTERPROCEDURAL_OPTIMIZATION": "ON"
}
}
]
}
你看,release 预设通过 inherits 继承了 default 的所有设置,只覆盖了构建类型和开启 LTO。这就是继承的好处——避免重复写相同的参数。
构建预设(buildPresets)
构建预设对应的是 cmake --build 命令。它需要引用一个 configurePreset,因为构建必须基于某个配置。看例子:
{
"buildPresets": [
{
"name": "debug",
"configurePreset": "default",
"jobs": 8,
"targets": ["my_app"]
},
{
"name": "release",
"configurePreset": "release",
"jobs": 8,
"cleanFirst": true
}
]
}
这里 cleanFirst 表示构建前先清理。我曾经因为忘记清理旧文件,导致调试了半天才发现链接的是旧库。嗯,从那以后我习惯在发布构建时加上这个选项。
测试预设(testPresets)
测试预设对应 ctest 命令。它同样需要引用一个 configurePreset:
{
"testPresets": [
{
"name": "unit",
"configurePreset": "default",
"output": {
"outputOnFailure": true
},
"execution": {
"jobs": 4,
"timeout": 120
},
"filter": {
"include": {
"name": "test_*"
}
}
}
]
}
我个人习惯把单元测试和集成测试分开成不同的预设。比如 unit 只跑 test_ 开头的测试,integration 跑 it_ 开头的。这样在 CI 里可以并行跑,节省时间。
filter 支持正则表达式。但要注意转义字符,JSON 里写正则有点麻烦。我建议尽量用简单的通配符,比如 test_*。
打包预设(packagePresets)
打包预设是 CMake 3.23 才引入的,对应 cpack 命令。它需要引用一个 configurePreset 和一个 buildPreset:
{
"packagePresets": [
{
"name": "zip",
"configurePreset": "release",
"buildPreset": "release",
"generators": ["ZIP"],
"variables": {
"CPACK_PACKAGE_NAME": "MyApp",
"CPACK_PACKAGE_VERSION": "1.0.0"
}
},
{
"name": "deb",
"inherits": "zip",
"generators": ["DEB"],
"variables": {
"CPACK_DEBIAN_PACKAGE_MAINTAINER": "dev@example.com"
}
}
]
}
你看,打包预设也支持继承。我通常会在一个基础预设里设置好包名、版本号,然后各个平台(deb、rpm、zip)去继承并添加平台特有的变量。
条件预设与平台判断
预设模式还支持条件判断,根据当前系统环境自动选择配置。这个功能非常实用:
{
"configurePresets": [
{
"name": "windows",
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Windows"
},
"generator": "Visual Studio 17 2022",
"architecture": "x64"
},
{
"name": "linux",
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Linux"
},
"generator": "Ninja",
"cacheVariables": {
"CMAKE_C_COMPILER": "gcc-12"
}
}
]
}
为什么会这样?因为不同平台的编译器、生成器都不一样。有了条件预设,你只需要执行 cmake --preset=windows 或 cmake --preset=linux,CMake 会自动匹配当前系统。
工作流预设(workflowPresets)
工作流预设是 CMake 3.23 引入的,它可以把多个步骤串起来。比如先配置、再构建、再测试、最后打包:
{
"workflowPresets": [
{
"name": "ci",
"steps": [
{
"type": "configure",
"name": "default"
},
{
"type": "build",
"name": "debug"
},
{
"type": "test",
"name": "unit"
},
{
"type": "package",
"name": "zip"
}
]
}
]
}
执行 cmake --workflow --preset=ci,就会自动完成整个流程。我在 CI 脚本里就是这么用的,省去了写一堆 shell 命令的麻烦。
知识体系结构图
下面这张图展示了预设模式的整体关系:
实际使用技巧
最后分享几个我在项目中积累的经验:
- 版本控制: 把
CMakePresets.json提交到 Git 仓库,但CMakeUserPresets.json(用户私有预设)要加到.gitignore里。这样团队共享公共配置,个人可以覆盖自己的偏好。 - 变量替换: 预设支持
${sourceDir}、${presetName}、${hostSystemName}等内置变量。我经常用${presetName}来区分不同构建目录,比如build/debug、build/release。 - 环境变量: 可以通过
environment字段设置环境变量。比如指定CC和CXX编译器路径,避免系统默认的编译器版本不对。 - 隐藏预设: 如果某个预设不想让用户直接使用,可以设置
"hidden": true。它只能被其他预设继承,不会出现在--list-presets输出中。
cacheVariables 里写了一个布尔值 false,结果 CMake 把它当成了字符串。后来发现必须写成 "FALSE" 或 "OFF" 这种 CMake 认识的字符串形式。记住,JSON 里的布尔值 true/false 和 CMake 的 ON/OFF 不是一回事。
预设模式用好了,真的能大幅提升开发效率。尤其是团队协作时,每个人都能用统一的配置,再也不用担心「在我机器上能编译」这种问题了。你试试看,把项目里的构建参数整理成预设,你会发现世界清净了很多。