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 就是来解决这个问题的——它把构建配置「标准化」了。

核心价值: 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 都指定了 binaryDircacheVariables,甚至 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」的乌龙。

注意: 如果你的项目同时使用多配置生成器(如 Visual Studio)和单配置生成器(如 Ninja),建议在预设中分别处理。对于 Ninja,显式设置 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 与工具链文件、构建类型的核心关系:

CMakePresets.json 核心结构 CMakePresets.json 工具链文件 CMAKE_TOOLCHAIN_FILE 指定编译器/链接器/架构 构建类型 CMAKE_BUILD_TYPE Debug / Release / MinSizeRel 缓存变量 cacheVariables 芯片型号/功能开关等 使用:cmake --preset <预设名> 三者结合,实现一键配置交叉编译环境 团队共享,减少重复劳动

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,哪怕你目前只有一个构建配置。因为随着项目发展,构建配置只会越来越多,到时候再重构就麻烦了。先搭好架子,后面加配置就像填空一样简单。

好了,关于 CMakePresets.json 与工具链文件的结合,以及构建类型的配置,就讲到这里。记住,预设的核心目的是「把复杂留给自己,把简单留给使用者」。你的团队成员只需要知道 cmake --preset xxx 就够了,剩下的交给预设去处理。


公众号:蓝海资料掘金营,微信deep3321