98、CMake Presets:使用CMakePresets.json标准化构建配置

说实话,我见过太多CMake项目了。有的项目里,构建脚本写得跟天书似的,每次换人维护都得折腾半天。我自己也踩过不少坑——最典型的就是,团队里每个人都在自己的命令行里敲不同的CMake参数,结果出来的构建产物千奇百怪,调试起来简直要命。

嗯,CMake Presets就是来解决这个问题的。它用一份CMakePresets.json文件,把所有的构建配置标准化。说白了,就是把你平时在命令行里敲的那些参数,全部写进一个JSON文件里,大家统一用。

为什么需要CMake Presets?

我刚开始用CMake的时候,项目还小,就我一个人。后来团队扩大到十几个人,问题就来了:

  • 有人用-DCMAKE_BUILD_TYPE=Debug,有人用-DCMAKE_BUILD_TYPE=Release
  • 有人开了-DCMAKE_EXPORT_COMPILE_COMMANDS=ON,有人没开
  • 有人用Ninja,有人用Makefiles

每次排查问题,都得先问一句:「你用的什么参数?」你说烦不烦?

CMake Presets就是官方给出的标准化方案。从CMake 3.19开始支持,3.21之后基本稳定。我个人建议,新项目直接上Presets,老项目也尽快迁移。

CMakePresets.json的基本结构

先看一个最简单的例子:

{
  "version": 3,
  "configurePresets": [
    {
      "name": "default",
      "displayName": "默认配置",
      "description": "使用Ninja构建,Debug模式",
      "generator": "Ninja",
      "binaryDir": "${sourceDir}/build/default",
      "cacheVariables": {
        "CMAKE_BUILD_TYPE": "Debug",
        "CMAKE_EXPORT_COMPILE_COMMANDS": "ON"
      }
    }
  ]
}

这个文件放在项目的根目录下。然后你只需要敲:

cmake --preset default

CMake就会自动读取CMakePresets.json,按照default这个预设来配置项目。你看,是不是清爽多了?

配置预设(configurePresets)详解

配置预设是最核心的部分。它控制的是cmake -S . -B build这一步的行为。我一般会定义几个常用的:

预设名称 用途 典型参数
debug 日常开发调试 Debug模式,开启编译命令导出
release 发布版本构建 Release模式,优化等级O2
ci 持续集成环境 Release模式,开启测试,静态分析
coverage 代码覆盖率检查 Debug模式,开启覆盖率标志

举个例子,我项目里常用的debug预设:

{
  "name": "debug",
  "displayName": "Debug构建",
  "generator": "Ninja",
  "binaryDir": "${sourceDir}/build/debug",
  "cacheVariables": {
    "CMAKE_BUILD_TYPE": "Debug",
    "CMAKE_CXX_FLAGS_DEBUG": "-g -O0 -fsanitize=address",
    "CMAKE_EXPORT_COMPILE_COMMANDS": "ON"
  },
  "environment": {
    "CC": "clang",
    "CXX": "clang++"
  }
}

这里有个小技巧——${sourceDir}${sourceParentDir}这两个变量,CMake会自动替换成实际的源码目录路径。我刚开始用的时候还傻乎乎地写绝对路径,后来才发现有这好东西。

构建预设(buildPresets)

配置预设搞定之后,构建预设就简单多了。它对应的是cmake --build build这一步:

{
  "buildPresets": [
    {
      "name": "debug",
      "displayName": "Debug构建",
      "configurePreset": "debug",
      "jobs": 8,
      "targets": ["my_app", "my_tests"]
    },
    {
      "name": "release",
      "displayName": "Release构建",
      "configurePreset": "release",
      "jobs": 8
    }
  ]
}

使用的时候:

cmake --build --preset debug

它会自动找到对应的配置预设,然后执行构建。你想想看,以前你得先cmake -S . -B build/debug -G Ninja -DCMAKE_BUILD_TYPE=Debug,再cmake --build build/debug --target my_app -j8,现在一行搞定。

测试预设(testPresets)

测试预设是CMake 3.20之后加入的。它控制ctest的行为:

{
  "testPresets": [
    {
      "name": "unit",
      "displayName": "单元测试",
      "configurePreset": "debug",
      "output": {
        "outputOnFailure": true
      },
      "execution": {
        "noTestsAction": "error",
        "stopOnFailure": false
      }
    }
  ]
}

运行测试:

ctest --preset unit

我曾经在一个项目里,测试脚本写得乱七八糟,每次跑测试都得翻文档看参数。后来统一用Presets管理,新同事来了直接ctest --preset all就完事,省心多了。

预设的继承与覆盖

这个功能很实用。你可以定义一个基础预设,然后其他预设继承它:

{
  "configurePresets": [
    {
      "name": "base",
      "hidden": true,
      "generator": "Ninja",
      "cacheVariables": {
        "CMAKE_EXPORT_COMPILE_COMMANDS": "ON"
      }
    },
    {
      "name": "debug",
      "inherits": "base",
      "cacheVariables": {
        "CMAKE_BUILD_TYPE": "Debug"
      }
    },
    {
      "name": "release",
      "inherits": "base",
      "cacheVariables": {
        "CMAKE_BUILD_TYPE": "Release",
        "CMAKE_CXX_FLAGS": "-O2"
      }
    }
  ]
}

注意base预设设置了"hidden": true,这样它不会出现在cmake --list-presets的输出里,只作为模板使用。这个设计我觉得挺巧妙的,避免了用户误用。

条件预设与平台适配

不同平台可能需要不同的配置。CMake Presets支持条件判断:

{
  "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上执行cmake --preset default时,会自动匹配到MSVC的预设;在Linux上则匹配到GCC的预设。跨平台项目用这个,简直不要太爽。

避坑指南

我曾经踩过的坑:

  • 版本号写错:CMake Presets的version字段必须写对。version 2支持CMake 3.19-3.20,version 3支持3.21+。我一开始写了version 2,结果3.21的新特性用不了,排查了半天。
  • 路径分隔符:在Windows上,路径分隔符要用正斜杠/,不要用反斜杠\。JSON里反斜杠是转义字符,写错了会解析失败。
  • 环境变量覆盖:Presets里设置的环境变量会覆盖系统环境变量。我有一次在Presets里设了CC=gcc,结果系统里明明装了clang,构建时却一直用gcc,找了半天才发现是Presets的问题。

知识体系总览

下面这张图,是我自己总结的CMake Presets核心结构。你看一眼就能明白各个部分的关系:

CMake Presets 知识体系 CMakePresets.json configurePresets buildPresets testPresets generator cacheVariables binaryDir configurePreset targets jobs configurePreset output execution 继承 / 条件判断 / 环境变量

实际项目中的最佳实践

根据我自己的经验,总结几条实用的建议:

我的个人习惯:

  • 统一命名规范:所有预设名称都用小写字母,用连字符分隔。比如debug-clangrelease-gcc,一看就明白。
  • 不要写死路径:尽量用${sourceDir}${sourceParentDir}这些变量,别写绝对路径。否则换台机器就废了。
  • 版本控制CMakePresets.json一定要提交到Git里。它是项目的一部分,不是个人配置。
  • 配合CI使用:在CI脚本里直接用cmake --preset ci,这样本地和CI的构建配置完全一致,再也不用担心「在我机器上能跑」的问题了。

嗯,关于CMake Presets,核心内容就这些了。说白了,它就是一份标准化的构建配置文件,让团队协作更顺畅,让跨平台构建更省心。如果你还在手动敲CMake参数,我建议你赶紧试试Presets——相信我,用上了就回不去了。


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