26. 与 FetchContent 结合:在 Presets 中配置外部依赖管理

说实话,CMake 的 FetchContent 模块是我这几年用得最多的功能之一。以前做项目,最头疼的就是第三方库的管理——要么手动下载源码,要么用 git submodule 搞得一团糟。后来有了 FetchContent,直接在构建时拉取依赖,干净利落。

但问题来了:如果团队里每个人都各自写 FetchContent 的配置,版本不一致怎么办?有人用 v1.0,有人用 master 分支,这项目迟早要炸。嗯,这时候 Presets 就派上用场了。

为什么要把 FetchContent 放进 Presets?

我个人习惯把外部依赖的版本、URL、更新策略这些信息统一放在 Presets 里。原因很简单:

  • 版本统一:所有人用同一个 Presets 文件,依赖版本自然一致
  • 构建可复现:换台机器,拉下来就能编译,不用到处找依赖
  • 团队协作:新同事入职,直接跑 cmake --preset=dev,依赖自动搞定

你想想看,如果每个开发者都在自己的 CMakeLists.txt 里写死版本号,哪天想升级依赖,得挨个通知所有人改代码。这效率,我反正是受不了。

基础用法:在 Presets 中定义依赖版本

最简单的做法,是把 FetchContent 的版本号定义在 Presets 的 cacheVariables 里。举个例子:

{
  "version": 6,
  "configurePresets": [
    {
      "name": "dev",
      "displayName": "开发配置",
      "binaryDir": "${sourceDir}/build/dev",
      "cacheVariables": {
        "FETCHCONTENT_QUIET": "OFF",
        "SPDLOG_VERSION": "v1.12.0",
        "NLOHMANN_JSON_VERSION": "v3.11.2",
        "CURL_VERSION": "7.88.1"
      }
    },
    {
      "name": "release",
      "displayName": "发布配置",
      "binaryDir": "${sourceDir}/build/release",
      "cacheVariables": {
        "FETCHCONTENT_QUIET": "ON",
        "SPDLOG_VERSION": "v1.11.0",
        "NLOHMANN_JSON_VERSION": "v3.10.5",
        "CURL_VERSION": "7.86.0"
      }
    }
  ]
}

然后在 CMakeLists.txt 里这样用:

include(FetchContent)

# spdlog
set(SPDLOG_VERSION "v1.12.0" CACHE STRING "spdlog 版本")
FetchContent_Declare(
  spdlog
  GIT_REPOSITORY https://github.com/gabime/spdlog.git
  GIT_TAG ${SPDLOG_VERSION}
)
FetchContent_MakeAvailable(spdlog)

# nlohmann_json
set(NLOHMANN_JSON_VERSION "v3.11.2" CACHE STRING "json 版本")
FetchContent_Declare(
  nlohmann_json
  GIT_REPOSITORY https://github.com/nlohmann/json.git
  GIT_TAG ${NLOHMANN_JSON_VERSION}
)
FetchContent_MakeAvailable(nlohmann_json)

注意看,我在 CMakeLists.txt 里用了 CACHE STRING 来定义变量。这样 Presets 里的值会覆盖默认值,而如果没指定 Presets,也会有个合理的默认版本。

我的小技巧:开发配置用最新版本,发布配置用稳定版本。这样既能尝鲜,又能保证发布质量。

进阶:用 Presets 控制依赖的获取策略

FetchContent 有几个关键变量可以控制行为:

变量名 作用 推荐值
FETCHCONTENT_QUIET 是否静默下载 开发时 OFF,发布时 ON
FETCHCONTENT_UPDATES_DISCONNECTED 是否禁用更新检查 发布时 ON,加速构建
FETCHCONTENT_SOURCE_DIR_xxx 指定本地源码路径 离线环境或调试时用

我曾经在 CI 环境里遇到过一个问题:每次构建都重新拉取依赖,慢得要命。后来在 Presets 里加了 FETCHCONTENT_UPDATES_DISCONNECTED 为 ON,构建时间直接砍半。

{
  "configurePresets": [
    {
      "name": "ci",
      "displayName": "CI 构建",
      "cacheVariables": {
        "FETCHCONTENT_QUIET": "ON",
        "FETCHCONTENT_UPDATES_DISCONNECTED": "ON",
        "SPDLOG_VERSION": "v1.11.0",
        "NLOHMANN_JSON_VERSION": "v3.10.5"
      }
    }
  ]
}

实战:多依赖管理的 Presets 设计

我参与过一个嵌入式项目,依赖了 8 个第三方库。如果每个库都单独写 FetchContent,CMakeLists.txt 会变得又臭又长。我的做法是:

  1. 在 Presets 里定义所有依赖的版本和 URL
  2. 在 CMakeLists.txt 里写一个通用的宏来批量处理
  3. 不同 Presets 可以切换依赖的版本组合

Presets 配置示例:

{
  "configurePresets": [
    {
      "name": "stable",
      "cacheVariables": {
        "DEP_SPDLOG_VERSION": "v1.11.0",
        "DEP_JSON_VERSION": "v3.10.5",
        "DEP_CURL_VERSION": "7.86.0",
        "DEP_OPENSSL_VERSION": "1.1.1t"
      }
    },
    {
      "name": "bleeding",
      "cacheVariables": {
        "DEP_SPDLOG_VERSION": "v1.12.0",
        "DEP_JSON_VERSION": "v3.11.2",
        "DEP_CURL_VERSION": "7.88.1",
        "DEP_OPENSSL_VERSION": "3.1.0"
      }
    }
  ]
}

CMakeLists.txt 里用宏简化:

macro(fetch_dep name repo tag_var)
  set(${tag_var} "main" CACHE STRING "${name} 版本")
  FetchContent_Declare(
    ${name}
    GIT_REPOSITORY ${repo}
    GIT_TAG ${${tag_var}}
  )
  FetchContent_MakeAvailable(${name})
endmacro()

fetch_dep(spdlog https://github.com/gabime/spdlog.git DEP_SPDLOG_VERSION)
fetch_dep(nlohmann_json https://github.com/nlohmann/json.git DEP_JSON_VERSION)
fetch_dep(curl https://github.com/curl/curl.git DEP_CURL_VERSION)

这样,切换 Presets 就能一键切换所有依赖的版本组合。团队里有人想升级依赖,只需要改 Presets 文件,然后大家同步一下就行。

注意:不同版本的依赖可能有兼容性问题。我曾经把 spdlog 从 v1.11 升到 v1.12,结果发现 API 变了,编译报错。所以切换版本后,一定要跑一遍完整的测试。

SVG 流程图:Presets + FetchContent 协作关系

Presets + FetchContent 协作关系图 CMakePresets.json 定义依赖版本号 控制获取策略 CMakeLists.txt FetchContent_Declare FetchContent_MakeAvailable 读取版本 第三方库 spdlog / json / curl GitHub / GitLab 下载源码 构建产物 可执行文件 / 库 编译链接 构建 Presets 配置 CMake 脚本 外部依赖 构建产物

避坑指南:我踩过的几个坑

用 FetchContent + Presets 组合,有几个地方特别容易出问题:

  • 版本号写死 vs 变量引用:千万别在 CMakeLists.txt 里写死版本号,否则 Presets 根本控制不了。一定要用 CACHE STRING 变量。
  • 依赖冲突:两个库依赖同一个第三方库的不同版本怎么办?我遇到过 spdlog 和 fmt 的版本冲突。解决方案是用 FetchContent_GetProperties 检查是否已下载。
  • 网络问题:内网环境拉不了 GitHub。可以在 Presets 里设置 FETCHCONTENT_SOURCE_DIR_xxx 指向本地镜像。

核心要点:Presets 负责「声明」依赖的版本和策略,CMakeLists.txt 负责「执行」下载和构建。两者各司其职,团队协作时只需要维护 Presets 文件即可。

说实话,这套组合拳打好了,团队里再也不会出现「我本地能编译,你那边怎么不行」的尴尬情况。依赖版本统一了,构建环境统一了,剩下的就是安心写代码。

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