用 CMake Presets 固定跨平台构建:从本地开发到 CI 的依赖一致性实践

跨平台 C++ 项目经常出现这样的情况:开发者本机可以正常编译,换到另一台电脑后却遇到依赖找不到、编译器版本不兼容、运行库冲突,或者 CI 环境无法复现本地结果。问题往往不在某一条 CMake 命令,而在于构建环境没有被完整记录。

CMakeLists.txt 只描述了项目如何构建,却不适合单独承担"使用哪个生成器、构建什么配置、依赖从哪里来、工具链如何加载"等环境职责。CMake Presets 可以把这些参数写成版本化配置,vcpkg 则负责依赖获取和工具链接入。两者结合后,开发者和 CI 可以使用同一套配置入口。

一、先明确要固定什么

可复现构建并不等于所有机器都使用完全相同的操作系统。更现实的目标是把影响结果的关键输入显式化:

  1. CMake 最低版本和生成器行为。
  2. C++ 标准、构建类型以及编译器相关选项。
  3. vcpkg 工具链文件的位置。
  4. 依赖清单及其版本基线。
  5. 构建目录、安装目录和缓存策略。
  6. CI 中使用的操作系统、编译器和系统包。

其中,CMake Presets 主要解决第 1、2、3、5 项;vcpkg manifest 和版本基线主要解决第 4 项;CI 配置仍然需要固定操作系统镜像和编译器安装方式。也就是说,Presets 能统一构建入口,但不能自动消除操作系统差异。

二、CMake Presets 的工作方式

CMake Presets 通常分为两类文件:

  • CMakePresets.json:项目共享配置,应提交到版本库。
  • CMakeUserPresets.json:个人机器配置,通常不提交,用于覆盖本地路径或私有选项。

一个 preset 可以继承另一个 preset。常见的组织方式是先定义公共配置,再分别定义 Debug、Release 和 CI 入口。配置阶段使用 cmake --preset 名称,构建阶段使用 cmake --build --preset 名称,从而避免每个人手写一组不同参数。

需要注意,Presets 的具体字段受 CMake 版本支持范围影响。团队应先约定最低 CMake 版本,并在开发机和 CI 中执行同一版本范围内的命令。

三、创建一个最小项目

目录结构可以从下面的布局开始:

text 复制代码
sample-project/
├── CMakeLists.txt
├── CMakePresets.json
├── vcpkg.json
├── vcpkg-configuration.json
├── src/
│   └── main.cpp
└── .gitignore

CMakeLists.txt 只保留项目结构和目标定义:

cmake 复制代码
cmake_minimum_required(VERSION 3.25)
project(sample_project VERSION 1.0 LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

add_executable(sample_app src/main.cpp)

if (MSVC)
    target_compile_options(sample_app PRIVATE /W4)
else()
    target_compile_options(sample_app PRIVATE -Wall -Wextra -Wpedantic)
endif()

这里没有直接写死 vcpkg 路径。工具链应该在配置入口中传入,这样项目文件不会绑定某台机器的目录结构。

四、用 manifest 描述依赖

假设程序需要使用一个 JSON 库,可以在 vcpkg.json 中声明依赖:

json 复制代码
{
  "name": "sample-project",
  "version-string": "1.0.0",
  "dependencies": [
    "nlohmann-json"
  ]
}

vcpkg manifest 模式会根据这个文件安装项目依赖。为了让不同时间执行安装时尽量保持一致,可以增加 vcpkg-configuration.json,指定 registry 基线:

json 复制代码
{
  "default-registry": {
    "kind": "builtin",
    "baseline": "填写团队确认过的 vcpkg baseline"
  }
}

baseline 不是随意填写的版本号,而应当来自团队实际采用的 vcpkg 提交基线。提交前应通过当前 vcpkg 工具验证该基线可用,并将 vcpkg 本身的获取方式也写入开发文档或 CI 初始化步骤。若项目使用私有 registry,还需要根据仓库权限配置访问凭据,但凭据不应写入版本库。

五、编写共享 Preset

下面的示例假设环境变量 VCPKG_ROOT 指向 vcpkg 根目录。它使用 Ninja 作为生成器,因此运行环境需要提前安装 Ninja;也可以根据团队平台改用 Visual Studio 或其他生成器。

json 复制代码
{
  "version": 6,
  "cmakeMinimumRequired": {
    "major": 3,
    "minor": 25,
    "patch": 0
  },
  "configurePresets": [
    {
      "name": "base",
      "hidden": true,
      "generator": "Ninja",
      "binaryDir": "${sourceDir}/build/${presetName}",
      "cacheVariables": {
        "CMAKE_TOOLCHAIN_FILE": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake",
        "CMAKE_EXPORT_COMPILE_COMMANDS": "ON"
      }
    },
    {
      "name": "debug",
      "inherits": "base",
      "cacheVariables": {
        "CMAKE_BUILD_TYPE": "Debug"
      }
    },
    {
      "name": "release",
      "inherits": "base",
      "cacheVariables": {
        "CMAKE_BUILD_TYPE": "Release"
      }
    }
  ],
  "buildPresets": [
    {
      "name": "debug",
      "configurePreset": "debug"
    },
    {
      "name": "release",
      "configurePreset": "release"
    }
  ]
}

这里有几个重要约束:

  • binaryDir 按 preset 分开,避免 Debug 和 Release 共享同一份缓存。
  • CMAKE_TOOLCHAIN_FILE 在第一次配置前传入。工具链文件会影响依赖查找和编译器环境,配置完成后再修改,通常需要清理构建目录重新配置。
  • CMAKE_BUILD_TYPE 适用于单配置生成器,例如 Ninja。若改用 Visual Studio 这类多配置生成器,应在构建时通过 --config Debug 或 --config Release 指定配置,不能机械照搬该字段。
  • VCPKG_ROOT 属于机器环境,不应把绝对路径提交到 CMakePresets.json。

六、执行步骤

首先安装与团队约定范围相符的 CMake、编译器和 Ninja,然后准备 vcpkg,并设置环境变量。例如在类 Unix 环境中:

bash 复制代码
export VCPKG_ROOT="$HOME/tools/vcpkg"

Windows PowerShell 可以使用:

powershell 复制代码
$env:VCPKG_ROOT = "C:/tools/vcpkg"

在项目根目录执行配置和构建:

bash 复制代码
cmake --preset debug
cmake --build --preset debug

发布构建使用:

bash 复制代码
cmake --preset release
cmake --build --preset release

如果项目包含测试,可以在 CMakeLists.txt 中启用 CTest:

cmake 复制代码
enable_testing()
add_test(NAME sample_app_runs COMMAND sample_app)

随后执行:

bash 复制代码
ctest --test-dir build/debug --output-on-failure

这里的路径必须与 preset 的 binaryDir 一致。更复杂的项目可以额外定义 testPresets,但应先确认所使用的 CMake 版本支持相应字段。

七、接入 CI 的关键做法

CI 的目标不是重新发明一套构建脚本,而是调用仓库已经定义好的 preset。一个简化的 GitHub Actions 示例如下:

yaml 复制代码
name: build

on:
  push:
  pull_request:

jobs:
  linux:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install tools
        run: |
          sudo apt-get update
          sudo apt-get install -y ninja-build

      - name: Prepare vcpkg
        shell: bash
        run: |
          git clone https://github.com/microsoft/vcpkg.git "$RUNNER_TEMP/vcpkg"
          "$RUNNER_TEMP/vcpkg/bootstrap-vcpkg.sh" -disableMetrics
          echo "VCPKG_ROOT=$RUNNER_TEMP/vcpkg" >> "$GITHUB_ENV"

      - name: Configure
        run: cmake --preset release

      - name: Build
        run: cmake --build --preset release --parallel

      - name: Test
        run: ctest --test-dir build/release --output-on-failure

示例中的依赖获取方式只是演示流程。生产环境还应考虑缓存策略、网络可用性、依赖源审计和工具版本固定。特别是缓存不能代替版本控制:当 vcpkg.json、baseline、编译器或 preset 发生变化时,应让缓存键随之变化,否则旧缓存可能掩盖配置问题。

如果 CI 需要在 Windows、macOS 和 Linux 上运行,建议先分别确认生成器和编译器差异,再决定是否共用一个 preset 名称。共享名称只有在命令语义确实一致时才有价值,不能为了形式统一而隐藏平台特定参数。

八、常见问题

1. 为什么修改了工具链路径却没有生效?

CMake 会把工具链相关结果写入缓存。若已有构建目录,修改工具链路径后仍可能继续使用旧值。应检查:

bash 复制代码
cmake -LA -N build/debug | grep CMAKE_TOOLCHAIN_FILE

确认无误后删除对应构建目录,再执行 preset 配置。删除前应确认目录中没有需要保留的手工文件。

2. Debug 和 Release 能否共用一个构建目录?

单配置生成器不建议这样做。两种配置会共享缓存和中间文件,容易产生难以判断的增量构建结果。为每个配置使用独立目录是更清晰的做法。

3. vcpkg 安装失败是否说明 Preset 有问题?

不一定。失败可能来自网络、编译器、系统包、端口版本或平台支持范围。应先查看 vcpkg 的完整错误输出,再区分是依赖解析失败、端口构建失败,还是 CMake 找包阶段失败。

4. 能否把本机路径写入共享配置?

不建议。绝对路径会让其他开发者和 CI 直接失效。可以使用环境变量、用户级 preset 或 CI 注入变量表达机器差异,并在项目文档中列出必需的环境变量。

5. Presets 是否等于完全可重复构建?

不是。它统一了 CMake 层面的配置入口,但编译器补丁版本、系统库、CPU 架构和外部下载内容仍可能不同。对强复现要求较高的项目,还需要固定容器镜像、工具链文件、依赖基线,必要时使用制品仓库或离线依赖缓存。

总结

CMake Presets 的价值不只是减少命令行输入,更重要的是把构建约定变成可审查、可复用的项目配置。vcpkg manifest 负责描述依赖,baseline 负责约束依赖时间点,CI 则调用同一套 preset 验证构建结果。

落地时可以按以下顺序推进:先拆分 Debug 和 Release 构建目录,再把工具链路径移入环境变量,随后提交依赖清单和版本基线,最后让 CI 只调用 preset。完成这些步骤后,剩余差异会集中在操作系统和工具链本身,排查范围也会比"每台机器各写一套命令"更明确。

相关推荐
苏supper2 小时前
记一次Nacos鉴权报错unknown user,403排查|SecurityProxy源码,fastjson2扩展包缺失
java·后端
\光辉岁月/2 小时前
6.mybatisplus学习-BaseMapper、Service、常用注解
java·mybatis·mybatisplus
微信开发api2 小时前
基于WTAPI构建社群运营平台:自动拉群与会话承接链路设计
java·大数据·网络·数据库·微信·自动化
小七在进步2 小时前
类和对象(五)
java·开发语言·算法
Zhou1411362 小时前
Git_02_GitLab协作与CI_CD
git·ci/cd·gitlab
驭渊的小故事2 小时前
SpringBoot 配置文件详解:properties 与 yml 从入门到实战
java·开发语言·笔记
Wx-bishekaifayuan2 小时前
Springcloud中学在线学习平台58883-计算机课程设计、毕业设计
java·vue.js·spring boot·学习·spring·spring cloud·课程设计
一嘴一个橘子2 小时前
java 中使用 lua 脚本
java
码智社2 小时前
Spring Framework 全方位技术解析
java