16、使用CMake预设:CMakePresets.json与工具链文件的结合、构建类型配置
说实话,CMake预设这个功能刚出来的时候,我还有点抵触。心想「又多了一个配置文件,学不动了」。但真正用上之后,嗯,真香。尤其是当你需要频繁切换交叉编译工具链、调试和发布构建类型时,CMakePresets.json 能帮你省掉大量重复的 cmake 命令行参数。
我个人习惯把 CMakePresets.json 看作是「构建配置的声明式管理」。你不需要每次都在终端里敲 -DCMAKE_TOOLCHAIN_FILE=xxx.cmake -DCMAKE_BUILD_TYPE=Release 这种长串,而是把这些参数固化在 JSON 文件里,一键选择预设即可。
16.1 为什么需要 CMakePresets.json?
先说说痛点。我在做一个嵌入式项目时,需要同时维护三个目标平台:
- 本地 x86 调试(快速验证逻辑)
- ARM Cortex-M4 交叉编译(Release 固件)
- ARM Cortex-M4 交叉编译(Debug 带符号)
每次切换,我都要手动改 CMake 命令行参数,或者维护三个不同的 build 脚本。时间一长,脚本越来越乱,同事之间还容易用错参数。CMakePresets.json 就是来解决这个问题的——它把构建配置「标准化」了。
16.2 CMakePresets.json 的基本结构
一个典型的 CMakePresets.json 文件放在项目根目录,结构如下:
{
"version": 6,
"configurePresets": [
{
"name": "default",
"displayName": "本地 Debug",
"description": "使用本地工具链,Debug 构建",
"generator": "Ninja",
"binaryDir": "${sourceDir}/build/default",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug"
}
},
{
"name": "arm-release",
"displayName": "ARM Release",
"description": "ARM 交叉编译,Release 固件",
"generator": "Ninja",
"binaryDir": "${sourceDir}/build/arm-release",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release",
"CMAKE_TOOLCHAIN_FILE": "${sourceDir}/cmake/arm-gcc-toolchain.cmake"
}
},
{
"name": "arm-debug",
"displayName": "ARM Debug",
"description": "ARM 交叉编译,Debug 带符号",
"generator": "Ninja",
"binaryDir": "${sourceDir}/build/arm-debug",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug",
"CMAKE_TOOLCHAIN_FILE": "${sourceDir}/cmake/arm-gcc-toolchain.cmake"
}
}
],
"buildPresets": [
{
"name": "default",
"configurePreset": "default"
},
{
"name": "arm-release",
"configurePreset": "arm-release"
},
{
"name": "arm-debug",
"configurePreset": "arm-debug"
}
]
}
你看,每个 configurePreset 都指定了 binaryDir、cacheVariables,甚至 generator。这样不同平台的构建产物完全隔离,不会互相污染。
binaryDir 中使用 ${sourceDir}/build/${presetName} 这种模式,这样预设名和构建目录天然对应,一目了然。
16.3 工具链文件与预设的结合
工具链文件(toolchain file)是交叉编译的核心。在 CMakePresets.json 中,你只需要在 cacheVariables 里设置 CMAKE_TOOLCHAIN_FILE 即可。但这里有个坑——路径必须是绝对路径,或者使用 ${sourceDir} 这种宏。
我曾经见过同事直接写相对路径,结果在 CI 机器上构建失败,因为工作目录不同。所以,请务必使用 ${sourceDir} 或 ${fileDir} 这类 CMake 提供的宏。
来看一个更完整的工具链文件示例(arm-gcc-toolchain.cmake):
# 设置目标系统
set(CMAKE_SYSTEM_NAME Generic)
set(CMAKE_SYSTEM_PROCESSOR arm)
# 指定编译器
set(CMAKE_C_COMPILER arm-none-eabi-gcc)
set(CMAKE_CXX_COMPILER arm-none-eabi-g++)
# 指定链接器
set(CMAKE_LINKER arm-none-eabi-ld)
# 查找工具链路径(可选,但推荐)
set(TOOLCHAIN_PREFIX arm-none-eabi-)
set(CMAKE_FIND_ROOT_PATH /usr/local/arm-gcc)
# 不要搜索宿主系统的库和头文件
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
然后在预设中引用它:
"cacheVariables": {
"CMAKE_TOOLCHAIN_FILE": "${sourceDir}/cmake/arm-gcc-toolchain.cmake"
}
这样,当你运行 cmake --preset arm-release 时,CMake 会自动加载工具链文件,完成交叉编译环境的配置。
16.4 构建类型配置的灵活处理
构建类型(CMAKE_BUILD_TYPE)在交叉编译中尤其重要。Release 固件要优化尺寸和速度,Debug 固件要保留调试符号。在预设中,我们可以为不同场景定义不同的构建类型。
但有个细节要注意:有些生成器(比如 Visual Studio)是多配置生成器,它们不认 CMAKE_BUILD_TYPE,而是通过 --config 参数指定。对于 Ninja 和 Makefile 这类单配置生成器,CMAKE_BUILD_TYPE 是必须的。
我个人建议在预设中明确指定 CMAKE_BUILD_TYPE,这样无论谁用你的预设,构建类型都是确定的,不会出现「我明明要 Release,怎么编译出来是 Debug」的乌龙。
CMAKE_BUILD_TYPE;对于 Visual Studio,可以在 buildPresets 中通过 configuration 字段指定。
16.5 实战:一个完整的嵌入式项目预设
下面是我在一个 STM32 项目中实际使用的预设结构,供你参考:
{
"version": 6,
"configurePresets": [
{
"name": "stm32-debug",
"displayName": "STM32 Debug",
"generator": "Ninja",
"binaryDir": "${sourceDir}/build/stm32-debug",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug",
"CMAKE_TOOLCHAIN_FILE": "${sourceDir}/cmake/stm32-gcc.cmake",
"STM32_CHIP": "STM32F407VG",
"ENABLE_SEMIHOSTING": "ON"
}
},
{
"name": "stm32-release",
"displayName": "STM32 Release",
"generator": "Ninja",
"binaryDir": "${sourceDir}/build/stm32-release",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release",
"CMAKE_TOOLCHAIN_FILE": "${sourceDir}/cmake/stm32-gcc.cmake",
"STM32_CHIP": "STM32F407VG",
"ENABLE_SEMIHOSTING": "OFF"
}
},
{
"name": "host-test",
"displayName": "本地单元测试",
"generator": "Ninja",
"binaryDir": "${sourceDir}/build/host-test",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug",
"BUILD_TESTING": "ON"
}
}
],
"buildPresets": [
{
"name": "stm32-debug",
"configurePreset": "stm32-debug"
},
{
"name": "stm32-release",
"configurePreset": "stm32-release"
},
{
"name": "host-test",
"configurePreset": "host-test"
}
]
}
使用方式很简单:
# 配置
cmake --preset stm32-debug
# 构建
cmake --build --preset stm32-debug
你看,两条命令搞定。不用再记那些又臭又长的参数了。
16.6 预设的继承与复用
如果你的项目有多个相似预设(比如不同芯片型号),可以用 inherits 字段来减少重复。举个例子:
{
"configurePresets": [
{
"name": "base-arm",
"hidden": true,
"generator": "Ninja",
"cacheVariables": {
"CMAKE_TOOLCHAIN_FILE": "${sourceDir}/cmake/arm-gcc.cmake"
}
},
{
"name": "stm32f4-debug",
"inherits": "base-arm",
"binaryDir": "${sourceDir}/build/stm32f4-debug",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug",
"STM32_CHIP": "STM32F407VG"
}
},
{
"name": "stm32f4-release",
"inherits": "base-arm",
"binaryDir": "${sourceDir}/build/stm32f4-release",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release",
"STM32_CHIP": "STM32F407VG"
}
}
]
}
hidden: true 表示这个预设不会出现在用户的选择列表中,只作为基类被继承。这样你只需要维护一份工具链配置,不同芯片、不同构建类型各自继承即可。
16.7 本章知识体系图
下面这张图总结了 CMakePresets.json 与工具链文件、构建类型的核心关系:
16.8 避坑指南
最后分享几个我踩过的坑:
- 路径问题: 我曾经在
CMAKE_TOOLCHAIN_FILE中用了相对路径,结果在 IDE 中配置失败。后来统一改用${sourceDir}宏,再也没出过问题。 - 版本兼容: CMakePresets.json 的版本号(version)很重要。version 3 支持基本功能,version 6 支持更多高级特性(如条件预设)。建议使用 version 6,兼容 CMake 3.21+。
- 隐藏预设: 如果你的基类预设不想让用户直接使用,记得加
"hidden": true。否则用户可能会误选,导致构建失败。 - 环境变量: 有些工具链需要设置环境变量(如
PATH),可以在预设中使用environment字段来设置,避免污染全局环境。
好了,关于 CMakePresets.json 与工具链文件的结合,以及构建类型的配置,就讲到这里。记住,预设的核心目的是「把复杂留给自己,把简单留给使用者」。你的团队成员只需要知道 cmake --preset xxx 就够了,剩下的交给预设去处理。
公众号:蓝海资料掘金营,微信deep3321