【C/C++包管理器】vcpkg(by microsoft)

vcpkg 工程实践教程(Ubuntu x86_64 · C++20 · CMake + Ninja)

适用环境

text 复制代码
操作系统:Ubuntu Linux(x86_64)
语言标准:C++20
编译器:GCC(Clang 仅在个别处提及)
构建系统:CMake + Ninja
vcpkg 版本:2026.07.29
vcpkg 仓库根目录:~/workspace/vcpkg
vcpkg 可执行文件:~/workspace/vcpkg/vcpkg

本文目标:能在真实 C++ 工程中熟练使用 vcpkg 管理第三方依赖。不涉及开发 port、维护 registry、交叉编译等内容。

阅读命令前先知道这些记号

本文假定命令在 Ubuntu 的 Bash 中运行:

记号 含义 是否原样输入
~ 当前用户的主目录,例如 /home/alice 是,由 shell 展开
. 当前目录
$VCPKG_ROOT 名为 VCPKG_ROOT 的环境变量的值 是,先按 2.3 配置
<库名><名字> 要由你替换的占位符,例如把 <库名> 换成 fmt
# 说明文字 shell 注释 可以不输入
行末 \ 当前命令尚未结束,下一行仍属于同一条命令 是;也可把多行合并为一行

代码块中若出现 user@ubuntu:...$,它是终端提示符,只用于展示当前位置,不属于命令。configure(配置) 是 CMake 读取规则、探测环境并生成底层构建文件的阶段;build(构建) 才是实际编译和链接源码的阶段。后文会反复用到这两个词。

阅读 JSON 文件前先知道这些语法

本文会出现 vcpkg.jsonCMakePresets.json。JSON 中,{} 表示对象,[] 表示数组,"字段名": 值 表示一个字段;字符串必须使用双引号,相邻字段或数组元素用逗号分隔。JSON 不允许注释、不允许单引号、最后一项后面不允许多余逗号。例如:

json 复制代码
{
  "字符串字段": "一段文本",
  "布尔字段": true,
  "数组字段": ["第一项", "第二项"],
  "对象字段": {
    "内部字段": "值"
  }
}

这只是 JSON 的通用标点规则。一个具体工具允许哪些字段、字段值能否自定义,要由该工具自己的文件规范决定,不能仅凭 JSON 语法判断。


一、vcpkg 是什么,解决什么问题

1.1 一个真实的痛点

假设你的 C++ 项目依赖这些库:

text 复制代码
fmt           ------ 格式化输出
spdlog        ------ 日志
OpenSSL       ------ 加密
Boost         ------ 通用工具集
nlohmann-json ------ JSON
Catch2        ------ 单元测试

如果不用包管理器,你要为每一个库手动完成:

  • 找到正确版本的源码并下载;
  • 处理库与库之间的依赖(spdlog 依赖 fmt,Boost 内部模块互相依赖);
  • 选择合适的版本组合(fmt 10 和 fmt 11 的接口有差异);
  • 逐个配置并编译(每个库的构建系统还不一样);
  • 把安装出来的头文件路径、库文件路径记下来,填进自己的构建脚本;
  • Debug 和 Release 往往要各编译一遍;
  • 换一台开发机,上面全部重来一遍;
  • CI 服务器上,还要再自动化地重来一遍,并保证和本地一致。

这套流程每做一次就是一次出错机会:版本不一致、路径写错、ABI 不兼容、CI 与本地行为不同......这就是 C++ 长期被诟病的"依赖管理地狱"。

1.2 vcpkg的解法

vcpkg 是微软开源的 C/C++ 包管理器。你把"我要什么库"声明出来,剩下的事情它统一完成:

text 复制代码
依赖声明(vcpkg.json)
    ⬇️
依赖解析(含传递依赖、版本求解)
    ⬇️
源码下载(校验 SHA512)
    ⬇️
编译(用你机器上的工具链)
    ⬇️
安装(头文件 + 库 + CMake 配置文件)
    ⬇️
提供给 CMake 使用(find_package 直接可用)

安装出来的每个库都自带标准 CMake 配置文件,因此你的项目里只需要 find_package() + target_link_libraries(),不需要手写任何路径。

1.3 四个工具各管什么

这一点必须一开始就分清楚,后面所有排错都依赖这个心智模型:

plaintext 复制代码
vcpkg  :获取、构建和管理第三方依赖(只管"库从哪来")
CMake  :描述项目结构和构建规则(只管"我的项目怎么组织")
Ninja  :执行具体构建任务(只管"按规则把编译命令跑起来")
GCC    :编译和链接 C++ 代码(只管"把源码变成二进制")

四者的协作关系:

plaintext 复制代码
vcpkg.json
    ⬇️
vcpkg 管理第三方库(下载、编译、安装到项目内)
    ⬇️
CMake 通过 find_package() 找到依赖(由 vcpkg toolchain 引导)
    ⬇️
Ninja 调度编译任务(cmake --build 背后实际干活的人)
    ⬇️
GCC 编译和链接,产出可执行文件

vcpkg 不碰你的代码,CMake 不碰第三方库的源码,Ninja 和 GCC 只是执行者

1.4 和其他方案的关键区别

方案 本质 工程中最重要的区别
vcpkg 源码构建的包管理器,按项目隔离 依赖声明在仓库里,版本可固定,CMake 集成开箱即用
apt 系统包管理器 装的是"系统级"库,版本由发行版决定,不同机器/发行版差异大,无法按项目固定版本
FetchContent CMake 内置,把第三方源码拉进本次构建 轻量依赖(如 header-only)很方便,但每次配置都要处理第三方源码,大依赖会显著拖慢构建,且版本管理靠手工
Git submodule 把第三方仓库作为子模块挂进项目 只解决"源码在哪",编译和集成仍要自己写,更新和 CI 处理都麻烦
手动编译 下载-configure-make install 完全不可复现,仅用于临时救急

有编译产物的中大型依赖交给 vcpkg;apt 只用来装工具链和系统级工具;个别极轻量的 header-only 依赖可以酌情用 FetchContent。


二、Ubuntu 上的安装与基础使用

2.1 安装系统工具链

bash 复制代码
sudo apt update
sudo apt install -y build-essential git curl zip unzip tar pkg-config cmake ninja-build

每个包的作用:

作用
build-essential GCC、g++、make、libc 开发头等编译基础
git 克隆 vcpkg 仓库;很多 port 也用 git 拉源码
curl vcpkg 下载源码包
zip / unzip / tar 解压下载的源码归档
pkg-config 部分 port 构建时用它探测系统工具/库
cmake 构建系统(也是 vcpkg 编译每个 port 时使用的工具)
ninja-build 底层构建执行器

2.2 获取 vcpkg 并固定版本

vcpkg 本体就是一个 Git 仓库,没有传统的安装包,安装方式就是克隆 + 引导:

bash 复制代码
cd ~/workspace
git clone https://github.com/microsoft/vcpkg.git
cd vcpkg
git checkout 2026.07.29
./bootstrap-vcpkg.sh

bootstrap-vcpkg.sh 成功后会在当前仓库根目录生成 vcpkg 可执行文件。下面只节选关键输出;终端提示符中的当前目录应是 ~/workspace/vcpkg

bash 复制代码
user@ubuntu:~/workspace/vcpkg$ ./bootstrap-vcpkg.sh
Downloading vcpkg-glibc...
vcpkg package management program version 2026-07-27-98d7cb0cf1f4686a3e43aa5672b6230c1d56bce8

See LICENSE.txt for license information.
Telemetry
---------
vcpkg collects usage data in order to help us improve your experience.
The data collected by Microsoft is anonymous.
You can opt-out of telemetry by re-running the bootstrap-vcpkg script with -disableMetrics,
passing --disable-metrics to vcpkg on the command line,
or by setting the VCPKG_DISABLE_METRICS environment variable.

Read more about vcpkg telemetry at https://learn.microsoft.com/vcpkg/about/privacy
Read the Microsoft Privacy Statement at https://go.microsoft.com/fwlink/?LinkId=521839
user@ubuntu:~/workspace/vcpkg$ ls
CONTRIBUTING.md     CodeQL.yml   NOTICE_pt.txt  bootstrap-vcpkg.bat  ports      toolsrc   versions
CONTRIBUTING_pt.md  LICENSE.txt  README.md      bootstrap-vcpkg.sh   scripts    triplets
CONTRIBUTING_zh.md  NOTICE.txt   SECURITY.md    docs                 shell.nix  vcpkg

为什么项目要固定 vcpkg 版本?

vcpkg 仓库本身就是一个持续更新的"库目录"(registry)。master 分支上的库版本每天都在变。如果不固定,今天你和他人检出不同的 master,同一个 vcpkg.json 可能解析出不同版本的依赖,"在我机器上是好的"就是这么来的。固定到一个 tag,整个团队和 CI 看到的库目录就完全一致。

checkout 之后处于 detached HEAD,有问题吗?

没有任何问题。detached HEAD 只表示你停在一个 tag 上而不是分支上,这正是我们想要的"固定版本"状态。不要在这个目录里做开发提交即可。

bootstrap-vcpkg.sh 做了什么?

获取 vcpkg 命令行工具本体,并在仓库根目录准备好 vcpkg 可执行文件。根据平台和 vcpkg 版本,它可能下载预构建的工具,也可能执行构建过程。该步骤只准备 vcpkg 工具本身,它不安装任何第三方库。

vcpkg 可执行文件从哪来?

就是 bootstrap 的产物,位于仓库根目录:~/workspace/vcpkg/vcpkg。之后所有命令都通过它执行。

git checkout 2026.07.29 中的是 vcpkg 仓库的 tag,而 vcpkg version 显示的是工具自身的内部版本字符串,两者的显示格式或日期不一定逐字相同。是否检出了团队指定版本,应以 git -C ~/workspace/vcpkg describe --tags --exact-match 的结果为准。

2.3 验证与可选的环境变量

bash 复制代码
cd ~/workspace/vcpkg
./vcpkg version
./vcpkg help

version 能正常输出版本号即安装成功。

建议把 vcpkg 加入环境变量,后续任何目录都能直接使用(写入 ~/.bashrc):

bash 复制代码
echo 'export VCPKG_ROOT="$HOME/workspace/vcpkg"' >> ~/.bashrc
echo 'export PATH="$VCPKG_ROOT:$PATH"' >> ~/.bashrc
source ~/.bashrc

之后 vcpkg version 可以直接运行。本文后续命令中:

text 复制代码
$VCPKG_ROOT        = ~/workspace/vcpkg          (仓库根目录,是目录)
$VCPKG_ROOT/vcpkg  = ~/workspace/vcpkg/vcpkg    (可执行文件,是文件)

二者不能混用:toolchain 位于 $VCPKG_ROOT/scripts/...,不是 $VCPKG_ROOT/vcpkg/scripts/...

2.4 基础命令:搜索、安装、查看、删除

以 classic mode(经典模式,第三节会详细对比)的方式体验一遍:

bash 复制代码
cd $VCPKG_ROOT

# 搜索:按名字或关键字查找 port
vcpkg search fmt
vcpkg search json

# 安装
vcpkg install fmt:x64-linux

# 查看当前已安装的库
vcpkg list

# 删除
vcpkg remove fmt:x64-linux

重点理解 fmt:x64-linux 这个写法,它是 port:triplet

plaintext 复制代码
fmt        ------ port 名:vcpkg 中"一个库的打包配方"的名字
x64-linux  ------ triplet:目标平台的描述(架构-操作系统)

本文环境固定为 x64-linux,不需要关心其他 triplet。在 manifest mode 下(本文主推方式),vcpkg 会根据当前平台自动选择 x64-linux,你通常不需要手写。

2.5 vcpkg 仓库目录:对使用者意味着什么

plaintext 复制代码
~/workspace/vcpkg/
├── ports/       # 每个库的"配方":去哪下载、怎么打补丁、怎么编译
├── scripts/     # 工具链与构建脚本,最重要的是 buildsystems/vcpkg.cmake
├── triplets/    # 平台定义(x64-linux.cmake 等)
└── versions/    # 版本数据库:记录每个 port 历史上所有版本
  • scripts/buildsystems/vcpkg.cmake 是 CMake 集成时唯一需要你引用的文件;
  • ports/<名字>/vcpkg.json 可以用来确认一个库的 port 名、可用 feature 和依赖;
  • versions/ 是版本固定机制(baseline)的底层数据来源,不用直接碰。.

2.6 vcpkg 的工作目录:出了问题看哪里

使用过程中会出现这些目录:

plaintext 复制代码
downloads/    # 源码包下载缓存(删除后会重新下载)
buildtrees/   # 每个 port 的编译过程和日志 ★ 排错第一现场
packages/     # 安装前的暂存结果
installed/    # classic mode 的安装结果(manifest mode 不用这里)

另外还有一个用户级缓存目录 ~/.cache/vcpkg,存放编译好的二进制缓存(加速重复构建)。

排错优先级记住一条:第三方库编译失败,先看 buildtrees/<port>/ 下的日志文件(vcpkg 报错时会直接打印日志路径)。


三、工程化使用:manifest mode + CMake 集成(核心)

这是最重要的部分。

3.1 Classic mode 与 Manifest mode

Classic mode(上面已经体验过):

bash 复制代码
vcpkg install fmt:x64-linux

库被装进 vcpkg 仓库的 installed/ 目录,是"全局"的、由用户手动管理 的。它适合:临时测试某个库、快速实验、学习命令。不要用它在团队项目里管理依赖 ------ 因为"装了什么"只存在于每个人的机器上,仓库里没有任何记录。

Manifest mode (真实项目的正确方式):

依赖声明在项目根目录的 vcpkg.json 里,随代码一起提交 Git;本例构建时 vcpkg 自动读取并安装到当前构建目录内部vcpkg_installed/(即 build/vcpkg_installed/),项目之间互不干扰。

text 复制代码
classic mode:依赖由用户手动安装,装在 vcpkg 全局目录,仓库无记录
manifest mode:依赖由项目声明,装在项目目录内,声明文件进 Git

manifest mode 满足团队开发的诉求:

  • Git 管理:依赖清单就是 vcpkg.json,改动有 review、有历史;
  • 可复现:配合 baseline(见3.3),任何人任何机器装出的依赖版本一致;
  • CI 友好:CI 不需要预装任何库,configure 时自动安装;
  • 新人友好:克隆仓库 ➡️ 一条 cmake 命令,环境即就绪。

3.2 创建完整示例项目

目标:一个可以直接运行的 C++20 项目,依赖 fmt、spdlog、nlohmann-json、Catch2。

创建项目结构:

bash 复制代码
mkdir -p ~/workspace/vcpkg-demo/src ~/workspace/vcpkg-demo/tests
cd ~/workspace/vcpkg-demo

最终结构:

text 复制代码
vcpkg-demo/
├── CMakeLists.txt
├── CMakePresets.json
├── vcpkg.json
├── src/
│   └── main.cpp
├── tests/
│   └── test_main.cpp
└── .gitignore

3.2.1 vcpkg.json: 依赖声明

文件名 vcpkg.json 和其中可识别的字段名由 vcpkg manifest 规范规定。也就是说,冒号左边的 nameversion-stringdependenciesbuiltin-baseline 都是 vcpkg 认识的固定字段名 ,不能随意改成 project-nameversionlibraries 等近义词。冒号右边的值有些由项目作者填写,有些必须从 vcpkg 已知的数据中选择,并非全部都能任意写。

在项目根目录创建 vcpkg.json

json 复制代码
{
  "name": "vcpkg-demo",
  "version-string": "0.1.0",
  "dependencies": [
    "fmt",
    "spdlog",
    "nlohmann-json",
    "catch2"
  ],
  "builtin-baseline": "<填入 3.4 节命令查到的 commit hash>"
}

逐项区分"规定的写法"和"可以修改的内容":

示例片段 谁规定 为什么示例这样写 可以怎样修改
"name" vcpkg 规定的字段名 用于填写当前项目/包的名字 不能改字段名;对普通应用项目该字段可以省略
"vcpkg-demo" 项目作者填写 示例项目目录和 CMake 项目都叫 vcpkg-demo,保持一致便于理解 可换成自己的项目名;只能使用小写 ASCII 字母、数字和连字符,例如 image-server
"version-string" vcpkg 规定的版本字段之一 本例用普通字符串表达项目版本 不能发明 project-version 字段;应用项目可省略。若保留,可换成自己的版本,也可按需要改用规范允许的 versionversion-semverversion-date,但同一 manifest 只能选一种版本字段
"0.1.0" 项目作者填写 表示教程项目当前处于初始版本 可换成项目自己的版本;它描述的是当前项目,不是 fmt 等依赖的版本
"dependencies" vcpkg 规定的字段名,值必须是数组 一个项目可以依赖零个或多个 port 字段名不能改;数组内容按项目需要增删
"fmt""spdlog" 项目作者从 registry 中选择 main.cpp 和测试代码确实使用了这些库 不能随便编造名字,必须填写 vcpkg 中存在的 port 名 ;port 名不一定等于 find_package() 使用的 CMake 包名
"builtin-baseline" vcpkg 规定的字段名 启用内置 registry 的版本基准,使团队解析到一致的依赖版本 字段名不能改;值必须换成实际的 vcpkg 仓库 commit,生成方法见 3.4
"<填入......commit hash>" 教程中的占位文字 提醒读者稍后填入本机查到的值 不能原样使用;应替换为 40 位 Git commit hash

这里刻意让 manifest 的 name、CMake project() 的名字和目录名都使用 vcpkg-demo,但三者没有自动绑定关系。把 manifest 的 name 改成另一个合法名称,并不会自动修改可执行文件名;只是工程实践中通常让它们保持一致,以减少混淆。

dependencies 数组中的每一项有两种常用写法:

  • 字符串:只写 port 名,使用默认 features,例如 "fmt"
  • 对象:除了 port 名,还要设置版本或 features 时使用。对象中的 nameversion>=default-featuresfeatures 仍是 vcpkg 规定的固定字段名。

两种写法可以出现在同一个数组中:

json 复制代码
{
  "dependencies": [
    "fmt",
    { "name": "spdlog", "version>=": "1.14.0" },
    {
      "name": "openssl",
      "default-features": false,
      "features": ["tools"]
    }
  ]
}

其中哪些内容可改:

固定字段 值的来源与约束
name 自己选择要依赖的 port,但值必须是 registry 中存在的名字,如 spdlogopenssl
version>= 自己填写最低可接受版本,但版本必须存在于所用 registry 的版本数据库中
default-features 只能写 JSON 布尔值 truefalse;本例选择 false 表示关闭该 port 默认启用的功能
features 自己选择功能,但每个值必须是该 port 实际定义的 feature;本例的 tools 不是任意标签

Linux 下可在 $VCPKG_ROOT/ports/<名字>/vcpkg.json 查看一个 port 定义了哪些 feature,也可以执行 vcpkg search <port名> 查看概要。

把常用整体格式抽象出来如下。代码中冒号左边的字段名都是 vcpkg 规定的,不能替换;右边的中文占位内容才是需要按项目填写的值:

json 复制代码
{
  "name": "项目名",
  "version-string": "项目版本",
  "dependencies": [
    "只有名字的依赖",
    {
      "name": "需要额外选项的依赖",
      "default-features": false,
      "features": ["功能名"],
      "version>=": "最低版本"
    }
  ],
  "builtin-baseline": "40位vcpkg仓库commit",
  "overrides": [
    { "name": "必须固定版本的依赖", "version": "精确版本" }
  ]
}

这不是说所有字段每次都要写;它展示的是字段的层级和标点位置。对本教程的应用项目,保留 nameversion-stringdependenciesbuiltin-baseline 即可。写完后可在项目根目录运行 vcpkg format-manifest vcpkg.json:语法或字段不合法时它会报错,合法时会统一格式。不要在这里使用 --all,该选项用于格式化 vcpkg 自身 ports/ 目录里的全部 manifest。vcpkg 的字段规范不是任意 JSON 都能表达的,完整字段以 vcpkg.json 官方参考 为准。

3.2.2 CMakeLists.txt

vcpkg.json 回答"项目需要哪些第三方库",CMakeLists.txt 回答"要编译哪些目标,以及目标怎样使用这些库"。vcpkg 不会替你生成项目构建规则,因此两个文件缺一不可。

CMakeLists.txt :

cmake 复制代码
cmake_minimum_required(VERSION 3.25)

project(vcpkg-demo VERSION 0.1.0 LANGUAGES CXX)

# C++20, 禁用编译器扩展,保证标准行为
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

if(NOT CMAKE_BUILD_TYPE)
    set(CMAKE_BUILD_TYPE Release)
endif()

# 第三方依赖(全部由 vcpkg 提供)
find_package(fmt CONFIG REQUIRED)
find_package(spdlog CONFIG REQUIRED)
find_package(nlohmann_json CONFIG REQUIRED)
find_package(Catch2 CONFIG REQUIRED)

# 主程序
add_executable(app src/main.cpp)
target_link_libraries(
    app
    PRIVATE
    fmt::fmt
    spdlog::spdlog
    nlohmann_json::nlohmann_json
)

# 单元测试
enable_testing()

add_executable(unit_tests tests/test_main.cpp)
target_link_libraries(
    unit_tests
    PRIVATE
    Catch2::Catch2WithMain
    nlohmann_json::nlohmann_json
    fmt::fmt
)

add_test(NAME unit_tests COMMAND unit_tests)

CMakeLists.txt 不是 shell,也不是 JSON,而是 CMake 语言。基本形状是 命令名(参数...),命令可跨行,参数主要由空格或换行分隔,不使用逗号。按执行顺序看关键命令:

命令 本例中的作用
cmake_minimum_required(VERSION 3.25) 拒绝过旧 CMake,并确定相应的 CMake 策略行为
project(... LANGUAGES CXX) 声明项目并启用 C++;CMake 会在这里探测编译器
set(CMAKE_CXX_STANDARD 20) 要求目标按 C++20 编译
find_package(fmt CONFIG REQUIRED) 以 CONFIG 模式查找 fmt;REQUIRED 表示找不到就终止 configure
add_executable(app src/main.cpp) 创建名为 app 的可执行目标,其源码为 src/main.cpp
target_link_libraries(app PRIVATE fmt::fmt) app 使用 imported target fmt::fmtPRIVATE 表示该使用要求不向依赖 app 的其他目标传播
enable_testing() 为当前项目启用 CTest
add_test(NAME unit_tests COMMAND unit_tests) 向 CTest 注册一个测试,运行前面创建的 unit_tests 可执行目标

CONFIG 表示读取包提供的 <PackageName>Config.cmake 一类配置文件,而不是使用 CMake 自带或项目提供的 Find<PackageName>.cmake 模块。本例四个 port 经 vcpkg 安装后都能提供相应的 CMake 包配置和 imported target。Catch2::Catch2WithMain 还自带测试入口 main(),所以测试文件里不需要再写一个。

使用 vcpkg 与普通方式到底哪里不同?

find_package()CMake 的命令 ,不是 vcpkg 的命令。有没有使用 vcpkg,CMakeLists.txt 的现代写法通常都相同;真正不同的是"库由谁安装、CMake 去哪里找":

环节 普通的 apt / 手动安装 vcpkg manifest mode
声明依赖 通常写在 README 或安装脚本中,CMake 本身不知道要安装它 写进仓库内的 vcpkg.json
安装位置 /usr/usr/local 或个人指定目录 当前构建目录下的 vcpkg_installed/
告诉 CMake 去哪里找 依赖系统默认搜索路径,或手工设置 CMAKE_PREFIX_PATH/包路径 vcpkg toolchain 自动加入搜索路径
版本与机器一致性 受发行版、机器状态和手工操作影响 由 manifest 与 baseline 共同约束
CMakeLists.txt find_package() + 链接 target 仍然是 find_package() + 链接 target

所以,vcpkg 的核心价值不是发明另一套链接语法,而是把 find_package() 背后的"安装依赖和准备搜索路径"自动化。执行顺序是:toolchain 先让依赖可被找到,随后 CMake 执行到 find_package(fmt CONFIG REQUIRED),读取 fmt 提供的配置文件,并创建 fmt::fmt 这个 imported target;最后 target_link_libraries(app PRIVATE fmt::fmt) 把它的使用要求附加到 app

为什么现代 CMake 用 target,而不是手写 -I / -L / -linclude_directories()link_directories()

因为 fmt::fmt 这样的 target 是一个"完整的使用说明书":头文件路径、库文件、编译选项、它自己的传递依赖(例如 spdlog 需要 fmt),全部打包在里面。链接 spdlog::spdlog 时,fmt 的相关设置会自动传播过来。手动写路径则要求你自己维护这一切:路径写死、Debug/Release 混淆、传递依赖遗漏,都是经典事故来源。规则:永远链接 target,永远不要硬编码路径

3.2.3 CMakePresets.json: 避免每次输入长命令

Preset(预设)就是"给一组 CMake 命令行参数起名字"。本例把生成器、构建目录、构建类型和 toolchain 保存为名叫 defaultdebug 的预设,使团队和 CI 调用同一套参数。

文件名 CMakePresets.json 以及 versionconfigurePresetsgeneratorbinaryDir 等字段名由 CMake Presets 规范规定,不能自行改名。defaultdebug、构建目录等值由项目作者选择,但部分值必须引用其他 preset、使用 CMake 支持的生成器,或符合指定字段的数据类型。

在项目根目录创建名称完全一致的 CMakePresets.json;它的通用 JSON 标点规则见本文开头"阅读 JSON 文件前先知道这些语法":

CMakePresets.json :

json 复制代码
{
    "version": 6,
    "cmakeMinimumRequired": {
        "major": 3,
        "minor": 25,
        "patch": 0
    },
    "configurePresets": [
        {
            "name": "default",
            "displayName": "Ninja Release + vcpkg",
            "generator": "Ninja",
            "binaryDir": "${sourceDir}/build",
            "toolchainFile": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake",
            "cacheVariables": {
                "CMAKE_BUILD_TYPE": "Release"
            }
        },
        {
            "name": "debug",
            "displayName": "Ninja Debug + vcpkg",
            "inherits": "default",
            "binaryDir": "${sourceDir}/build-debug",
            "cacheVariables": {
                "CMAKE_BUILD_TYPE": "Debug"
            }
        }
    ],
    "buildPresets": [
        { "name": "default", "configurePreset": "default" },
        { "name": "debug", "configurePreset": "debug" }
    ],
    "testPresets": [
        {
            "name": "default",
            "configurePreset": "default",
            "output": {
                "outputOnFailure": true
            }
        }
    ]
}

先看哪些结构是 CMake 规定的:

text 复制代码
根对象
├── version                 固定字段:Presets 文件格式版本
├── cmakeMinimumRequired    固定字段:项目要求的最低 CMake 版本
├── configurePresets[]      固定字段:"如何执行 configure"的预设列表
├── buildPresets[]          固定字段:"构建哪个 configure 结果"的预设列表
└── testPresets[]           固定字段:"测试哪个 configure 结果"的预设列表

这些顶层字段名不能改成 presetVersionconfigurationsbuilds。通常也不能随意增加 CMake 不认识的字段;给 IDE 等工具保存扩展信息时应使用规范预留的 vendor 字段。

再逐项区分"固定字段"和"可填写的值":

示例片段 谁规定 示例值为什么这样写 可以怎样修改
"version" CMake 规定的顶层字段 告诉 CMake 按哪一版 Presets JSON 规范解析 字段名不能改
6 从 CMake 支持的 schema 版本中选择 schema 6 由 CMake 3.25 引入,与本文最低版本一致 不是随意的项目版本号;改变前要确认目标 CMake 支持,并检查所用字段是否属于该 schema
"cmakeMinimumRequired"major/minor/patch CMake 规定的字段 明确拒绝低于 3.25.0 的 CMake 数字由项目维护者决定,但应满足 CMakeLists.txt 和 preset schema 的实际最低要求
"configurePresets""buildPresets""testPresets" CMake 规定的数组字段 三类命令分别到对应数组中查 preset 字段名不能改;数组中可按项目需要增加或删除 preset 对象
"name": "default" name 固定;default 自定义 给 preset 一个命令行标识,所以可执行 cmake --preset default 可改成 release 等名字;同一类 preset 中必须唯一,改名后所有引用和命令也要同步修改
"displayName": "Ninja Release + vcpkg" displayName 固定;值自定义 给人看的说明文字 可写任意合适的展示名称,不参与命令匹配
"generator": "Ninja" generator 固定;值受 CMake 限制 本文选 Ninja 作为底层构建工具,等价于 -G Ninja 可改为本机 CMake 支持的生成器名称;运行 cmake --help 可查看,不能杜撰
"binaryDir": "${sourceDir}/build" binaryDir 固定;路径由项目决定 把 Release 生成物统一放到源码目录下的 build/ 可换目录;${sourceDir} 是 CMake 规定的宏,不是作者定义的变量
"toolchainFile": "$env{VCPKG_ROOT}/..." toolchainFile 固定;路径由环境决定 指向本机 vcpkg toolchain,同时避免写死用户名 可换成有效路径;$env{VCPKG_ROOT} 是 CMake 规定的环境变量引用语法,环境变量名 VCPKG_ROOT 是本教程约定并在 2.3 设置的
"cacheVariables" CMake 规定的映射字段 用来表达命令行中的 -D变量=值 其内部键是 CMake cache 变量名;可以是 CMake 定义的变量,也可以是项目在 CMakeLists.txt 中定义的 cache 变量
"CMAKE_BUILD_TYPE": "Release" CMAKE_BUILD_TYPE 是 CMake 定义的变量 单配置 Ninja 需要它选择 Release 构建 可改为 Debug 等有效配置;随意写不存在的变量通常不会产生想要的效果
"inherits": "default" inherits 固定;值是引用 debug 先继承 default,再覆盖构建目录和构建类型 值必须是当前文件或所包含文件中已定义、可继承的 configure preset 名
"configurePreset": "default" configurePreset 固定;值是引用 告诉 build/test preset 使用哪个 configure preset 的 binaryDir 值必须对应已存在的 configure preset,不要求与当前 build/test preset 同名
"outputOnFailure": true 字段名和数据类型由 CTest preset 规范规定 本例选择在测试失败时显示输出 只能选择布尔值 truefalse

示例里 configure、build、test 三类 preset 都取名为 default,只是为了让三条命令容易记,并不是 CMake 的死规定。例如下面也合法:

json 复制代码
"buildPresets": [
  {
    "name": "build-release",
    "configurePreset": "default"
  }
]

此时使用 cmake --build --preset build-releaseconfigurePreset: "default" 仍指向前面名为 default 的 configure preset。

CMake 是怎么识别这个文件的?

当你在包含 CMakePresets.json 的项目根目录运行 cmake --preset default 时,CMake 会:

  1. 按约定文件名读取 CMakePresets.json(以及存在时的个人文件 CMakeUserPresets.json);
  2. 按顶层 version 对 JSON 结构做校验;
  3. configurePresets 数组中查找 namedefault 的对象;
  4. 展开 ${sourceDir}$env{VCPKG_ROOT} 等宏;
  5. 把这些字段转换成一次 configure 所需的参数并执行。

cmake --build --preset default 则到 buildPresets 中找名字;ctest --preset defaulttestPresets 中找名字。三个数组可以各自拥有同名 preset,它们通过 configurePreset 建立关联。可先运行 cmake --list-presets=all 检查 CMake 实际识别到了哪些预设。

toolchainFile 指向 vcpkg 的 toolchain 文件。直接写绝对路径虽然可用,但会把用户名和机器目录写进仓库;使用 $env{VCPKG_ROOT} 更便于团队复用。preset 格式版本 6 需要 CMake 3.25 或更高;不要仅仅通过调低 version 来"兼容"旧 CMake,因为还必须逐项确认所用字段是否被旧格式支持。完整结构见 CMake Presets 官方手册

Debug 与 Release 使用独立的 build 目录 。这是因为 CMAKE_BUILD_TYPE、编译器探测结果和依赖路径都会进入 CMake 缓存;目录分离可避免两种配置互相覆盖。

3.2.4 src/main.cpp

cpp 复制代码
#include <fmt/core.h>
#include <fmt/ranges.h>
#include <nlohmann/json.hpp>
#include <spdlog/spdlog.h>

#include <algorithm>
#include <string>
#include <string_view>
#include <vector>

using json = nlohmann::json;

// C++20:指定初始化(designated initializers)
struct ProjectInfo
{
    std::string name;
    std::string_view compiler;
    int standard;
};

int main()
{
    // 1) fmt:格式化输出(fmt/ranges.h 支持直接打印容器)
    std::vector<int> numbers{1, 2, 3, 4, 5};
    fmt::print("Hello from {}! numbers = {}\n", "vcpkg-demo", numbers);

    // 2) spdlog:日志
    spdlog::set_level(spdlog::level::debug);
    spdlog::info("Application started");
    spdlog::warn("This is a warning message");

    // 3) nlohmann-json:构造并序列化 JSON
    ProjectInfo info{.name = "vcpkg-demo", .compiler = "GCC", .standard = 20};

    json j;
    j["project"] = info.name;
    j["compiler"] = info.compiler;
    j["standard"] = info.standard;
    j["numbers"] = numbers;

    fmt::print("JSON output:\n{}\n", j.dump(2));

    // C++20: ranges
    auto even_count = std::ranges::count_if(numbers, [](int n) { return n % 2 == 0; });
    spdlog::info("Even count = {}", even_count);

    spdlog::info("Done");
    return 0;
}

3.2.5 tests/test_main.cpp

cpp 复制代码
#include <catch2/catch_test_macros.hpp>
#include <fmt/format.h>
#include <nlohmann/json.hpp>

using json = nlohmann::json;

TEST_CASE("JSON round-trip serialization", "[json]") {
    json j;
    j["name"] = "vcpkg-demo";
    j["deps"] = {"fmt", "spdlog", "nlohmann-json"};

    std::string text = j.dump();
    json parsed = json::parse(text);

    REQUIRE(parsed["name"] == "vcpkg-demo");
    REQUIRE(parsed["deps"].size() == 3);
}

TEST_CASE("fmt formats integers", "[fmt]") {
    REQUIRE(fmt::format("{}", 42) == "42");
    REQUIRE(fmt::format("{:05d}", 7) == "00007");
}

3.2.6 .gitignore

text 复制代码
# 构建产物
build/
build-debug/

# vcpkg manifest mode 安装目录(可由 vcpkg.json 重新生成)
vcpkg_installed/

# CMake 生成物(防御性忽略,避免有人在源码目录就地 configure)
CMakeCache.txt
CMakeFiles/
cmake_install.cmake

# 个人本地 preset 覆盖,不入库
CMakeUserPresets.json

3.3 CMake 集成:toolchain 是怎么工作的

第一次看到 toolchain(工具链)这个词,容易误以为它只是"某个普通文件的名字"。在编译领域,工具链 原本指一组协作工具及其规则,例如编译器、链接器、目标平台和相关搜索路径。CMake 把"在项目配置早期描述或调整这些构建环境"的入口称为 toolchain file ,并用变量 CMAKE_TOOLCHAIN_FILE 指定它。

因此,文件之所以叫 toolchain file,不是因为扩展名特殊------它仍是一个 .cmake 脚本------而是因为它通过 CMake 预留的 toolchain 入口被加载。交叉编译的 toolchain 常负责选择编译器和目标平台;vcpkg 的这个 toolchain 主要负责接入依赖安装与查找,通常不替你选择 GCC 或 Clang。

本例集成的关键是一个文件和一个变量:

text 复制代码
文件:$VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake
变量:CMAKE_TOOLCHAIN_FILE

这个 toolchain 文件在 CMake configure 早期被加载,它会:

  1. 发现项目根目录存在 vcpkg.json ➡️ 进入 manifest mode;
  2. 调用 vcpkg 安装缺失依赖到 build/vcpkg_installed/x64-linux/
  3. 把该目录注入 CMAKE_PREFIX_PATH 等搜索路径;
  4. 此后你的 find_package() 就能找到 vcpkg 安装的库。

关键规则:toolchain 必须在第一次 configure 时就指定。中途补上或更换 toolchain,旧的 build 缓存会导致各种诡异问题,正确做法是删除 build 目录重来(见第五节)。

这里还要把两个容易混淆的分类拆开:

text 复制代码
依赖管理模式:Classic mode / Manifest mode
传入 CMake 参数的方式:直接写命令行 / 读取 CMakePresets.json

它们不是一一对应关系。下面 3.3.1 和 3.3.2 都是 Manifest mode ,因为项目根目录存在 vcpkg.json,且加载了 vcpkg toolchain;区别仅是同一组 CMake 参数写在命令行中,还是保存在 preset 中。若没有 manifest,toolchain 也可以帮助 CMake 查找此前以 Classic mode 安装的包,所以"用了 toolchain"本身也不等于"用了 Manifest mode"。

3.3.1 命令行方式(理解原理用)

bash 复制代码
cd ~/workspace/vcpkg-demo

cmake -S . -B build \
    -G Ninja \
    -DCMAKE_BUILD_TYPE=Release \
    -DCMAKE_TOOLCHAIN_FILE="$VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake"

cmake --build build
ctest --test-dir build --output-on-failure
./build/app

第一条命令可以按参数拆开读:

text 复制代码
cmake                         启动 CMake
-S .                          源码目录是当前目录(这里应有 CMakeLists.txt 和 vcpkg.json)
-B build                      生成文件和缓存写入 ./build
-G Ninja                      让 CMake 生成供 Ninja 使用的构建规则
-DCMAKE_BUILD_TYPE=Release    在 build 的 CMake 缓存中设置 Release 配置
-DCMAKE_TOOLCHAIN_FILE=...    第一次 configure 时加载 vcpkg toolchain

它影响的范围仅是当前项目的 build/:依赖装入 build/vcpkg_installed/,CMake 缓存与 Ninja 文件写入 build/,不会把库安装到 /usr。后续命令的输入和效果如下:

命令 读取什么 产生或影响什么
cmake -S ... -B build ... CMakeLists.txt、manifest、toolchain 安装缺失依赖并生成 build/build.ninjaCMakeCache.txt
cmake --build build build/ 中已有的构建规则 调用 Ninja/GCC,生成 build/appbuild/unit_tests
ctest --test-dir build --output-on-failure CMake 注册到 build/ 的测试 运行测试;失败时打印程序输出,不修改源码
./build/app 已构建的可执行文件 运行示例程序;它不是构建步骤

3.3.2 preset 方式(日常推荐)

有了 3.2.3 的 CMakePresets.json,同样的事情变成:

bash 复制代码
cd ~/workspace/vcpkg-demo

cmake --preset default         # configure
cmake --build --preset default # build(等价于 cmake --build build)
ctest --preset default         # 测试
./build/app                    # 运行

cmake --preset default 仍然是 configure,只是把 3.3.1 中 -B-G-D... 的值改为从 configurePresets[name=default] 读取。它仍会写入 build/,也仍会触发 Manifest mode 的依赖安装。cmake --build --preset defaultbuildPresets 找到关联的 configure preset,进而知道要构建哪个 binaryDirctest 同理。

第一次 configure 会看到 vcpkg 逐个下载并编译 fmt、spdlog、nlohmann-json、catch2;之后再 configure 时,已装好的依赖会直接复用。

关于 Clang:vcpkg 和 CMake 使用 configure 时探测到的默认编译器。如需改用 Clang,先 rm -rf build,然后 export CC=clang CXX=clang++ 再重新 configure。注意不要混用:第三方库用 GCC 编的,自己的项目却换成 Clang,可能遇到 ABI/标准库不一致的链接问题。

3.4 版本固定:builtin-baseline

只声明依赖名是不够的。"fmt" 到底是哪个版本?答案是由 baseline 决定

builtin-baseline 是 vcpkg 仓库(即内置 registry)的一个 Git commit。这个 commit 中的版本数据库为每个 port 给出一个基准版本,可以把它理解为"统一查同一版依赖目录"。它不是项目自身的版本:version-string 描述 vcpkg-demobuiltin-baseline 约束第三方依赖解析。

同一个 vcpkg.json + 同一个 baseline + 相同 triplet,依赖版本解析结果才具有可复现的基础。 编译器和系统环境仍可能影响最终二进制,因此团队还应统一工具链的大版本。

获取 baseline 的方法(用你固定的 vcpkg 版本的 commit):

bash 复制代码
# $VCPKG_ROOT 是配置的 vcpkg 仓库根目录的环境变量
git -C $VCPKG_ROOT rev-parse HEAD

把输出(40 位 hash)填入 vcpkg.jsonbuiltin-baseline

补充三个工程中最常用的版本控制手段:

json 复制代码
{
  "name": "vcpkg-demo",
  "version-string": "0.1.0",
  "dependencies": [
    "fmt",
    { "name": "spdlog", "version>=": "1.14.0" }
  ],
  "builtin-baseline": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "overrides": [
    { "name": "catch2", "version": "3.7.1" }
  ]
}
  • builtin-baseline : 固定整个库目录快照(必写);
  • "version>=" : 对单个库设置最低可接受版本;最终选择还要结合 baseline 与其他依赖约束;
  • overrides : 对单个库精确钉死版本,优先级最高,会覆盖一切传递依赖的版本求解。仅在确实需要锁死某个版本时使用,不要滥用。

团队升级依赖的规范流程:统一把 baseline 更新到新快照 ➡️ 全量构建+跑测试 ➡️ 一次性提交 vcpkg.json。不要今天一个人升 fmt、明天另一个人升 spdlog 的零散改动。

便捷命令:在项目目录执行 vcpkg x-update-baseline(实验性命令)会根据当前 vcpkg 仓库 checkout 的 commit 更新 manifest 中已有的 baseline;它不会自动把本地仓库更新到远端最新版本。vcpkg add port <库名> 可以把依赖追加进 vcpkg.json。运行后都应查看 Git diff 并重新构建测试。

3.5 第一次 configure 时到底发生了什么

text 复制代码
cmake --preset default
    ⬇️
CMake 启动,加载 vcpkg toolchain 文件
    ⬇️
toolchain 发现项目根目录有 vcpkg.json(manifest mode)
    ⬇️
vcpkg 解析依赖(含传递依赖,按 builtin-baseline 定版本)
    ⬇️
vcpkg 下载并编译缺失的依赖 ➡️ 安装到 build/vcpkg_installed/x64-linux/
    ⬇️
CMake 继续执行你的 CMakeLists.txt
    ⬇️
find_package() 在 vcpkg_installed 中找到依赖
    ⬇️
生成 Ninja 构建文件(build.ninja)

cmake --build build
    ⬇️
Ninja 读取 build.ninja,调度 GCC 编译 src/ 与 tests/
    ⬇️
链接 vcpkg_installed 中的库,产出 build/app 和 build/unit_tests

再强调一次职责边界:

text 复制代码
vcpkg   负责构建第三方依赖(只在这一步出现)
CMake   负责配置当前项目
Ninja   负责执行构建任务
GCC     负责编译和链接

从此你的日常开发里,vcpkg 几乎是"隐形"的:依赖不变时它什么也不做;只有改了 vcpkg.json,它才会在下一次 configure 时补装差异部分。


四、真实工程中的正确使用方式

4.1 推荐的项目结构

text 复制代码
project/
├── CMakeLists.txt        # 构建规则
├── CMakePresets.json     # 构建入口(含 vcpkg toolchain)
├── vcpkg.json            # 依赖清单(+ builtin-baseline)
├── include/              # 对外头文件(库型项目)
├── src/                  # 源码
├── tests/                # 测试
├── cmake/                # 项目自己的 CMake 模块(如有)
└── .gitignore

4.2 哪些文件进 Git,哪些不进 Git

应提交:

text 复制代码
CMakeLists.txt
CMakePresets.json
vcpkg.json(含 builtin-baseline)
源码与测试
cmake/ 下项目需要的 CMake 模块
.gitignore
README(写明 vcpkg 版本要求与环境准备步骤)

不应提交:

text 复制代码
build/、build-debug/          ------ 构建产物,可完全重建
vcpkg_installed/              ------ 依赖安装结果,由 vcpkg.json 重新生成
CMakeCache.txt、CMakeFiles/   ------ CMake 缓存
下载缓存、二进制缓存           ------ 本就在项目外(~/.cache/vcpkg 等)
CMakeUserPresets.json         ------ 个人本地覆盖

判断标准只有一条:凡是能从仓库内文件完整重新生成的东西,都不进 Git。 vcpkg_installed/ 尤其注意------它可能很大,而且包含二进制,提交它等于同时提交了一份"会过期的依赖副本"。

4.3 团队使用流程与红线

标准流程:

text 复制代码
克隆项目
    ⬇️
准备固定版本的 vcpkg(git checkout 到团队约定 tag)
    ⬇️
cmake --preset default   ← vcpkg 自动安装依赖
    ⬇️
cmake --build --preset default
    ⬇️
ctest --preset default

团队红线:

  • 不要 要求每个开发者手动 apt install 一串开发库再手动 vcpkg install 一堆依赖 ------ 依赖必须全部由 vcpkg.json 描述;
  • 不要CMakeLists.txt 里硬编码个人路径(/home/<USER>/...);
  • 不要 依赖系统中偶然存在的第三方库 ------ find_package 找到的应该是 vcpkg 装的版本;
  • 不要让不同开发者随意使用不同 vcpkg 版本 ------ 在 README 和 CI 中写死 tag;
  • 不要vcpkg_installed/ 或 build 产物提交进仓库;
  • 不要让 CI 永远追着 vcpkg master 跑 ------ 那等于放弃可复现性。

4.4 CI 示例(GitHub Actions)

CI(Continuous Integration,持续集成)是"每次提交或提出合并请求时,由一台干净机器自动编译并测试项目"。它的目的不是替代本地开发,而是尽早发现"漏提交文件""只在我的机器能编译""修改破坏了测试"等问题。

GitHub Actions 是 GitHub 提供的一种 CI 服务。它约定读取仓库中的 .github/workflows/*.yml 文件:文件一旦提交到 GitHub,匹配 on 中事件时,GitHub 就会创建临时 runner,按文件描述的步骤执行。你不需要在本机手动运行这个 yml;可在 GitHub 仓库的 Actions 页面查看每次执行和日志。

本例要创建的完整路径是:

text 复制代码
vcpkg-demo/
└── .github/
    └── workflows/
        └── ci.yml

YAML 也是一种结构化文本格式,但语法与 JSON 不同:它主要依赖缩进表示层级键: 值 表示字段,- 表示列表项,# 后是注释。必须使用空格缩进,不要使用 Tab。下面工作流表达的核心流程与本地一致:

text 复制代码
checkout → 获取固定版本 vcpkg → bootstrap → configure → build → ctest

.github/workflows/ci.yml

yaml 复制代码
name: ci

on:
  push:
  pull_request:
  workflow_dispatch:

jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      contents: read
    env:
      VCPKG_ROOT: /tmp/vcpkg
      VCPKG_VERSION: "2026.07.29"   # 与团队约定一致
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Install build tools
        run: |
          sudo apt update
          sudo apt install -y build-essential git curl zip unzip tar pkg-config cmake ninja-build
          # 注:GitHub 官方 runner 已预装 cmake/ninja,此步为通用写法

      - name: Fetch pinned vcpkg
        run: |
          git clone https://github.com/microsoft/vcpkg.git "$VCPKG_ROOT"
          git -C "$VCPKG_ROOT" checkout "$VCPKG_VERSION"
          "$VCPKG_ROOT/bootstrap-vcpkg.sh"

      - name: Configure
        run: cmake --preset default

      - name: Build
        run: cmake --build --preset default

      - name: Test
        run: ctest --preset default

从外到内读这份 YAML:

层级/字段 作用
name: ci Actions 页面显示的工作流名称
on 触发条件:push、pull request,以及网页手动触发
jobs 一个工作流包含的任务集合;这里仅有名为 build 的 job
runs-on: ubuntu-latest 为该 job 分配 Ubuntu runner
permissions.contents: read 只授予读取仓库内容所需权限
env 为此 job 的所有步骤设置环境变量,preset 会读取其中的 VCPKG_ROOT
steps 在同一 runner 上按从上到下顺序执行的步骤列表
uses: actions/checkout@v4 调用现成 action,把当前 commit 的仓库内容下载到 runner
run 让 runner 的 shell 执行后面的命令

各步骤为什么存在:

  1. Checkout:runner 初始没有你的源码,必须先取出当前提交;
  2. Install build tools:安装 GCC、CMake、Ninja 以及 vcpkg 构建 port 可能需要的工具;
  3. Fetch pinned vcpkg :把与团队约定相同的 vcpkg tag 克隆到 /tmp/vcpkg 并生成可执行文件;
  4. Configure:读取仓库里的 preset、manifest 和 toolchain,安装依赖并生成 Ninja 规则;
  5. Build:编译项目;若失败,job 在此停止;
  6. Test:运行单元测试,测试返回非零退出码时该次 CI 标红。

首次 push 后,打开仓库的 Actions → ci → build,展开每个 step 即可查看命令输出。push 或 pull request 旁出现绿色勾表示所有步骤成功,红叉表示至少一步失败。

GitHub 托管 runner 通常是一次性的,不能假设上一次 workflow 的 ~/.cache/vcpkg 会在下一次自动保留。跨次运行复用二进制缓存需要显式配置缓存 action,属于优化而不是正确运行本例的前提。GitHub 对工作流、job、step 的关系可参见 GitHub Actions 官方说明


五、常见问题排查

按出现频率排列。排查前先记住总原则:CMake 的问题看 configure 输出与 cache,vcpkg 的问题看 buildtrees/ 日志,链接的问题看 target 与编译器一致性。

5.1 CMake 找不到依赖

典型报错:

text 复制代码
Could not find a package configuration file provided by "fmt"

逐项检查:

  1. 是否指定了 vcpkg toolchain? 最常见原因。检查 preset 里的 toolchainFile,或命令行 -DCMAKE_TOOLCHAIN_FILE 是否正确。
  2. 是否删掉旧 build 目录后重新 configure? toolchain 必须在第一次 configure 生效,后补无效。
  3. vcpkg.json 里是否声明了该依赖? manifest mode 只安装声明过的库。
  4. find_package() 的名字是否正确? port 名 ≠ CMake 包名:nlohmann-jsonfind_package(nlohmann_json)opensslfind_package(OpenSSL)catch2find_package(Catch2)。名字大小写也敏感。
  5. 链接的 target 名是否正确?fmt::fmtCatch2::Catch2WithMain
  6. triplet 是否为 x64-linux configure 输出里可以看到 vcpkg 使用的 triplet。

5.2 修改 toolchain / preset 后仍然无效

原因:CMake 把编译器、路径等结果缓存在 build/CMakeCache.txt 里,configure 过的目录不会自动推翻旧结论。

解决:

bash 复制代码
rm -rf build
cmake --preset default

经验法则:凡是动了 toolchain、编译器、triplet、vcpkg 版本,一律先删 build 目录。这能消灭一大半"我明明改了为什么没生效"的问题。

5.3 第三方库编译失败

vcpkg 报错时会直接打印日志位置,形如:

test 复制代码
See logs for more information:
  $VCPKG_ROOT/buildtrees/spdlog/install-x64-linux-out.log

排查步骤:

bash 复制代码
ls $VCPKG_ROOT/buildtrees/<port名>/        # 查看该 port 的全部日志
less $VCPKG_ROOT/buildtrees/<port名>/install-x64-linux-out.log

常见原因:缺少系统工具(回到 2.1 补齐 apt 包)、GCC 版本过旧不支持该库的 C++ 标准、磁盘空间不足、源码被之前的失败构建污染(删除 buildtrees/<port名>/ 重试)。

5.4 下载失败

症状:curl 超时、连接 GitHub 失败、SHA512 mismatch

逐项检查:

  • 网络 / DNS / 代理 :先确认能 curl -I https://github.com;公司网络需配置 https_proxy
  • GitHub 访问不稳定 :重试通常即可;downloads/ 中已有完整缓存的包不会重复下载。
  • SHA512 校验失败 :多半是上次下载中断留下半个文件。删除 downloads/ 中对应文件后重试。

5.5 链接失败(undefined reference)

典型报错:

test 复制代码
undefined reference to `fmt::v10::...'

逐项检查:

  1. 是否对目标调用了 target_link_libraries()?忘了链接是最常见原因。
  2. 链接的 target 是否正确?(fmt::fmt 而不是 fmtlibfmt 之类。)
  3. 是否混用了编译器?第三方库是 GCC 编的,你的项目却用了 Clang(或不同大版本 GCC),可能符号不匹配。
  4. 是否混用了系统库和 vcpkg 库?例如 find_package 意外找到了 apt 装的旧版本。确认 toolchain 生效、build 目录干净。
  5. build 目录里是否有旧缓存?rm -rf build 重来。

5.6 一台机器成功,另一台失败

这是可复现性问题,按清单核对:

  • 两边 vcpkg 的 Git tag/commit 是否一致?(git -C $VCPKG_ROOT describe --tags
  • vcpkg.jsonbuiltin-baseline 是否一致?(有人本地改了没提交?)
  • GCC 版本差异是否过大?(g++ --version;建议团队统一大版本)
  • 失败机器上是否残留了 apt 安装的同名开发库,被 CMake 优先找到?
  • 是否有依赖只在某台机器上手动装过、却没写进 vcpkg.json
  • 是否有人提交了 CMakeUserPresets.json 或带个人路径的本地配置?

5.7 磁盘空间过大

各目录的清理策略:

目录 能否删 删除后果
build/build-debug/ 可以 下次 configure + build 完全重建(最安全的清理)
vcpkg_installed/(build 内) 可以 下次 configure 时 vcpkg 按 manifest 重新安装
$VCPKG_ROOT/buildtrees/ 可以 只是编译过程与日志,随下次构建重建
$VCPKG_ROOT/packages/ 可以 安装暂存区,自动重建
$VCPKG_ROOT/downloads/ 可以 源码缓存丢失,需要时重新下载
$VCPKG_ROOT/installed/ 可以 classic mode 的库全部卸载,需要重新 vcpkg install
~/.cache/vcpkg 可以 二进制缓存清空,下次构建需要重新编译依赖(变慢但不出错)

日常释放空间的第一选择是删 build/buildtrees/downloads/~/.cache/vcpkg 属于"用空间换时间"的缓存,磁盘紧张时再清。


六、存在但不展开的内容

以下主题在 vcpkg 生态中真实存在,遇到时再查官方文档即可,本文不教学:修改官方 port、编写自定义 port、向 vcpkg 提交 PR、vcpkg 源码分析、自定义/私有 registry、overlay ports、overlay triplets、Windows/macOS 集成、ARM/RISC-V 与交叉编译、vcpkg 内部实现细节、复杂二进制缓存服务。


七、vcpkg 工程使用速查表

🟢安装 vcpkg(Ubuntu x86_64)

bash 复制代码
# 安装依赖
sudo apt install -y build-essential git curl zip unzip tar pkg-config cmake ninja-build
# 克隆 vcpkg
cd ~/workspace && git clone https://github.com/microsoft/vcpkg.git
# 切换到 vcpkg 目录
cd vcpkg
# 切换到 vcpkg 2026.07.29 tag 版本
git checkout 2026.07.29
# 初始化 vcpkg
./bootstrap-vcpkg.sh

🟢搜索依赖

bash 复制代码
# 搜索 port
vcpkg search <关键字>
# 查看某个 port 的定义和可用 feature
less "$VCPKG_ROOT/ports/<port名>/vcpkg.json"

🟢classic 安装(仅用于临时实验)

bash 复制代码
vcpkg install fmt:x64-linux
vcpkg list
vcpkg remove fmt:x64-linux

🟢manifest 声明(真实项目)

bash 复制代码
# 在项目根目录创建并编辑 manifest(即 vcpkg.json 文件)
项目根目录 vcpkg.json:name / version-string / dependencies / builtin-baseline
# baseline 获取:
git -C $VCPKG_ROOT rev-parse HEAD
# 检查并格式化 manifest
vcpkg format-manifest vcpkg.json

🟢CMake toolchain

text 复制代码
$VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake
通过 CMakePresets.json 的 toolchainFile 引用(或 -DCMAKE_TOOLCHAIN_FILE=...)

🟢CMake configure / 构建 / 测试

bash 复制代码
cmake --preset default
cmake --build --preset default
ctest --preset default
./build/app

🟢清理 build

bash 复制代码
rm -rf build
# 改了 toolchain/编译器/triplet/vcpkg 版本后必做

🟢常见排错目录

text 复制代码
编译第三方库失败   → $VCPKG_ROOT/buildtrees/<port>/(日志)
下载问题          → $VCPKG_ROOT/downloads/
CMake 找不到包    → configure 输出 + build/CMakeCache.txt
classic 安装结果  → $VCPKG_ROOT/installed/
manifest 安装结果 → build/vcpkg_installed/x64-linux/

🟢应提交 Git

text 复制代码
CMakeLists.txt
CMakePresets.json
vcpkg.json
源码
测试
cmake 模块
.gitignore

🟢不应提交 Git

text 复制代码
build/
vcpkg_installed/
CMakeCache.txt
CMakeFiles/
编译产物
CMakeUserPresets.json

🟢团队三固定

text 复制代码
固定 vcpkg tag(2026.07.29)
固定 builtin-baseline
固定 GCC 大版本

术语解释

术语 解释 说明
CI Continuous Integration, 持续集成 CI是一套自动化的代码构建与验证流程。在软件开发中,CI 系统(常见的如 GitHub Actions、Jenkins、GitLab CI、Azure Pipelines)会在团队成员每次提交代码时,自动拉取最新代码,按照预设的脚本完整执行一遍「拉取依赖 → 编译项目 → 运行单元测试 → 产出构建产物」的全流程。
ABI Application Binary Interface, 应用程序二进制接口 二进制层面的约定 ------ 保证编译后的可执行程序,和已经编译好的库文件(.a/.so/.lib/.dll)链接、运行时,底层能正确交互。
心智模型 Mental Model,认知思维框架 人脑中对系统运作方式的简化认知框架。在本文的工具链场景中,特指清晰界定 vcpkg、CMake、Ninja、GCC 各自职责边界、上下游协作关系的思维地图,是后续精准排错、快速定位问题所属环节的核心思维依据。
相关推荐
SNAKEpc121381 小时前
OpenGL(十一)- 变换管线
c语言·c++·算法·矩阵·图形渲染
小小龙学IT1 小时前
Day 26-27 项目实战:从零构建一个高并发聊天室(epoll + 线程池)
linux·服务器·c语言·开发语言·网络
牢姐与蒯2 小时前
c++高级数据结构之图(基本概念,存储结构,BFS,DFS,最小生成树)
数据结构·c++·
c238562 小时前
MySQL 基础用法(下):查询进阶与核心特性
android·c语言·c++·mysql
苍煜2 小时前
Git Worktree 多工作树实战教学-工作多分支实用教程
git
梦想的旅途22 小时前
企业微信自动化:自动发送文本、图片、文件
前端·数据库·microsoft
码匠许师傅2 小时前
【C++ 面试真题】 C++ 中的 static 有什么作用?
c++·面试
郭涤生3 小时前
C++ 零拷贝(Zero-Copy)
linux·开发语言·c++
乐观勇敢坚强的老彭3 小时前
计算机内存中的堆和栈
c++