预设与配置:CMakePresets.json
说实话,CMake 的配置参数多到让人头疼。-DCMAKE_BUILD_TYPE=Release、-DCMAKE_INSTALL_PREFIX=/usr/local……每次都要敲一遍?我刚开始用 CMake 那会儿,经常在终端里翻历史命令,翻得眼花缭乱。后来 CMake 3.19 引入了 CMakePresets.json,我才算真正解脱了。
说白了,预设(Presets)就是一套配置模板。你把常用的编译选项、生成器、环境变量统统写进 JSON 文件里,下次直接 cmake --preset=release 就完事。团队协作时,大家用同一套预设,再也不会出现「我本地能编译,你那边怎么就崩了」的尴尬。
配置预设(Configure Presets)
配置预设是最基础的一类。它定义了 CMake 配置阶段的所有参数。我习惯在项目根目录放一个 CMakePresets.json,结构大概是这样:
{
"version": 6,
"configurePresets": [
{
"name": "default",
"displayName": "默认配置",
"description": "使用 Ninja 生成器,Debug 模式",
"generator": "Ninja",
"binaryDir": "${sourceDir}/build/${presetName}",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug",
"CMAKE_CXX_STANDARD": "17"
}
},
{
"name": "release",
"displayName": "发布配置",
"description": "Release 模式,开启 LTO",
"inherits": "default",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release",
"CMAKE_INTERPROCEDURAL_OPTIMIZATION": "ON"
}
}
]
}
注意 inherits 字段。你可以让 release 预设继承 default 预设,只覆盖需要改的变量。我在项目中遇到过有人把每个预设都从头写到尾,结果改一个公共变量要改七八个地方——嗯,用继承就清爽多了。
核心字段说明:
name:预设的唯一标识,命令行用这个名称generator:指定生成器,比如 Ninja、Visual Studio 17 2022binaryDir:构建目录,支持${sourceDir}、${presetName}等变量cacheVariables:等价于命令行-D参数environment:设置环境变量,比如CC、CXX
小技巧:用 ${sourceDir}/build/${presetName} 作为 binaryDir,不同预设的构建产物自动隔离。你想想看,debug 和 release 的 .o 文件混在一起有多乱?
构建预设(Build Presets)
配置完,下一步就是编译。构建预设对应 cmake --build 的参数。它可以引用某个配置预设,也可以独立存在。
{
"buildPresets": [
{
"name": "default",
"configurePreset": "default",
"jobs": 8,
"targets": ["my_app"]
},
{
"name": "release",
"configurePreset": "release",
"jobs": 8,
"targets": ["my_app", "my_tests"]
}
]
}
这里 configurePreset 字段把构建预设和配置预设绑定在一起。你执行 cmake --build --preset=release,它会自动找到 release 配置预设的构建目录,然后编译指定的 target。
我曾经踩过一个坑:构建预设里忘了写 configurePreset,结果 CMake 不知道去哪个目录编译,直接报错。所以,除非你有特殊需求,否则一定要关联一个配置预设。
注意:构建预设的 targets 字段是数组,可以指定多个目标。如果留空,则编译 all target。我个人建议显式写出目标,避免编译不需要的东西,尤其是大型项目。
测试预设(Test Presets)
测试预设对应 ctest 的参数。它同样可以引用配置预设,还能设置测试过滤、超时、并行度等。
{
"testPresets": [
{
"name": "default",
"configurePreset": "default",
"output": {
"outputOnFailure": true
},
"execution": {
"jobs": 4,
"timeout": 120
},
"filter": {
"include": {
"name": "unit_*"
}
}
}
]
}
你看,filter 字段可以只跑名字以 unit_ 开头的测试。我在做 CI 流水线时,经常把单元测试和集成测试分成两个预设,一个跑得快,一个跑得全。
常用测试预设字段:
| 字段 | 说明 | 示例 |
|---|---|---|
| outputOnFailure | 失败时输出详细信息 | true / false |
| jobs | 并行测试数 | 4 |
| timeout | 单个测试超时(秒) | 120 |
| filter.include.name | 包含匹配的测试 | "unit_*" |
| filter.exclude.name | 排除匹配的测试 | "slow_*" |
工作流预设(Workflow Presets)
工作流预设是 CMake 3.23 引入的。它可以把配置、构建、测试串成一个流水线。你执行一个工作流预设,CMake 会自动按顺序执行多个步骤。
{
"workflowPresets": [
{
"name": "ci",
"steps": [
{
"type": "configure",
"name": "default"
},
{
"type": "build",
"name": "default"
},
{
"type": "test",
"name": "default"
}
]
}
]
}
执行 cmake --workflow --preset=ci,它会先配置,再构建,最后测试。一步到位。我在公司的 CI 脚本里就用这个,再也不用写三行 cmake 命令了。
注意:工作流预设的 steps 里,每个 step 的 type 可以是 configure、build、test 之一。你甚至可以混合不同预设,比如用 release 配置预设 + debug 构建预设?嗯,虽然不常见,但确实可以。
知识体系总览
下面这张图帮你理清四种预设的关系:
预设的继承与变量
预设支持多层继承。你可以定义一个基础预设,然后让其他预设继承它。比如:
{
"configurePresets": [
{
"name": "base",
"hidden": true,
"generator": "Ninja",
"binaryDir": "${sourceDir}/build/${presetName}"
},
{
"name": "debug",
"inherits": "base",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug"
}
},
{
"name": "release",
"inherits": "base",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release"
}
}
]
}
注意 hidden: true。隐藏预设不会出现在命令行补全里,也不能直接使用。它纯粹作为基类存在。我在项目中经常用这个技巧,把公共配置抽出来,避免重复。
可用变量列表:
${sourceDir}:项目根目录${sourceParentDir}:项目根目录的父目录${presetName}:当前预设的名称${generator}:生成器名称${hostSystemName}:主机系统名(Linux、Windows、Darwin)${fileDir}:CMakePresets.json 所在目录
多平台与条件预设
如果你的项目跨平台,可以用 condition 字段让预设只在特定环境下生效:
{
"configurePresets": [
{
"name": "windows-msvc",
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Windows"
},
"generator": "Visual Studio 17 2022",
"architecture": "x64"
},
{
"name": "linux-gcc",
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Linux"
},
"generator": "Ninja",
"cacheVariables": {
"CMAKE_C_COMPILER": "gcc",
"CMAKE_CXX_COMPILER": "g++"
}
}
]
}
这样,在 Windows 上你只能看到 windows-msvc 预设,在 Linux 上只能看到 linux-gcc 预设。不会出现「选了 Linux 预设但在 Windows 上跑不了」的问题。
避坑指南:我曾经在 condition 里写错了变量名,结果预设死活不生效。调试了半天才发现是 ${hostSystemName} 拼成了 ${hostSystem}。记住,CMake 的变量名是大小写敏感的,而且必须用花括号括起来。
实战建议
最后,分享几个我在实际项目中的经验:
- 版本号别写错:CMakePresets.json 的 version 字段从 1 到 6,不同版本支持的特性不同。CMake 3.19 用 version 2,3.21 用 version 3,3.23 用 version 4,3.25 用 version 5,3.27 用 version 6。我建议直接用最新版,除非你要兼容旧 CMake。
- include 机制:可以用
include字段引入其他 JSON 文件,比如CMakeUserPresets.json(这个文件通常被 .gitignore 忽略,适合放个人配置)。 - 不要滥用隐藏预设:隐藏预设虽然好用,但太多层继承会让预设关系变得复杂。我一般只做两层:一层 base,一层具体平台/配置。
- CI 里用工作流预设:把配置、构建、测试打包成一个工作流预设,CI 脚本里一行命令搞定,维护起来也方便。
预设这个东西,说白了就是「把重复劳动交给机器」。你花半小时写好 CMakePresets.json,以后每次编译都能省下敲命令的几秒钟。积少成多,一年下来也能省下好几个小时呢。
公众号:蓝海资料掘金营,微信deep3321