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.json 和 CMakePresets.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 规范规定。也就是说,冒号左边的 name、version-string、dependencies、builtin-baseline 都是 vcpkg 认识的固定字段名 ,不能随意改成 project-name、version、libraries 等近义词。冒号右边的值有些由项目作者填写,有些必须从 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 字段;应用项目可省略。若保留,可换成自己的版本,也可按需要改用规范允许的 version、version-semver 或 version-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 时使用。对象中的
name、version>=、default-features和features仍是 vcpkg 规定的固定字段名。
两种写法可以出现在同一个数组中:
json
{
"dependencies": [
"fmt",
{ "name": "spdlog", "version>=": "1.14.0" },
{
"name": "openssl",
"default-features": false,
"features": ["tools"]
}
]
}
其中哪些内容可改:
| 固定字段 | 值的来源与约束 |
|---|---|
name |
自己选择要依赖的 port,但值必须是 registry 中存在的名字,如 spdlog、openssl |
version>= |
自己填写最低可接受版本,但版本必须存在于所用 registry 的版本数据库中 |
default-features |
只能写 JSON 布尔值 true 或 false;本例选择 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": "精确版本" }
]
}
这不是说所有字段每次都要写;它展示的是字段的层级和标点位置。对本教程的应用项目,保留 name、version-string、dependencies 和 builtin-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::fmt;PRIVATE 表示该使用要求不向依赖 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 / -l、include_directories()、link_directories()?
因为 fmt::fmt 这样的 target 是一个"完整的使用说明书":头文件路径、库文件、编译选项、它自己的传递依赖(例如 spdlog 需要 fmt),全部打包在里面。链接 spdlog::spdlog 时,fmt 的相关设置会自动传播过来。手动写路径则要求你自己维护这一切:路径写死、Debug/Release 混淆、传递依赖遗漏,都是经典事故来源。规则:永远链接 target,永远不要硬编码路径。
3.2.3 CMakePresets.json: 避免每次输入长命令
Preset(预设)就是"给一组 CMake 命令行参数起名字"。本例把生成器、构建目录、构建类型和 toolchain 保存为名叫 default、debug 的预设,使团队和 CI 调用同一套参数。
文件名 CMakePresets.json 以及 version、configurePresets、generator、binaryDir 等字段名由 CMake Presets 规范规定,不能自行改名。default、debug、构建目录等值由项目作者选择,但部分值必须引用其他 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 结果"的预设列表
这些顶层字段名不能改成 presetVersion、configurations 或 builds。通常也不能随意增加 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 规范规定 | 本例选择在测试失败时显示输出 | 只能选择布尔值 true 或 false |
示例里 configure、build、test 三类 preset 都取名为 default,只是为了让三条命令容易记,并不是 CMake 的死规定。例如下面也合法:
json
"buildPresets": [
{
"name": "build-release",
"configurePreset": "default"
}
]
此时使用 cmake --build --preset build-release;configurePreset: "default" 仍指向前面名为 default 的 configure preset。
CMake 是怎么识别这个文件的?
当你在包含 CMakePresets.json 的项目根目录运行 cmake --preset default 时,CMake 会:
- 按约定文件名读取
CMakePresets.json(以及存在时的个人文件CMakeUserPresets.json); - 按顶层
version对 JSON 结构做校验; - 在
configurePresets数组中查找name为default的对象; - 展开
${sourceDir}、$env{VCPKG_ROOT}等宏; - 把这些字段转换成一次 configure 所需的参数并执行。
cmake --build --preset default 则到 buildPresets 中找名字;ctest --preset default 到 testPresets 中找名字。三个数组可以各自拥有同名 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 早期被加载,它会:
- 发现项目根目录存在
vcpkg.json➡️ 进入 manifest mode; - 调用 vcpkg 安装缺失依赖到
build/vcpkg_installed/x64-linux/; - 把该目录注入
CMAKE_PREFIX_PATH等搜索路径; - 此后你的
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.ninja、CMakeCache.txt 等 |
cmake --build build |
build/ 中已有的构建规则 |
调用 Ninja/GCC,生成 build/app 和 build/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 default 从 buildPresets 找到关联的 configure preset,进而知道要构建哪个 binaryDir;ctest 同理。
第一次 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-demo,builtin-baseline 约束第三方依赖解析。
同一个 vcpkg.json + 同一个 baseline + 相同 triplet,依赖版本解析结果才具有可复现的基础。 编译器和系统环境仍可能影响最终二进制,因此团队还应统一工具链的大版本。
获取 baseline 的方法(用你固定的 vcpkg 版本的 commit):
bash
# $VCPKG_ROOT 是配置的 vcpkg 仓库根目录的环境变量
git -C $VCPKG_ROOT rev-parse HEAD
把输出(40 位 hash)填入 vcpkg.json 的 builtin-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 执行后面的命令 |
各步骤为什么存在:
- Checkout:runner 初始没有你的源码,必须先取出当前提交;
- Install build tools:安装 GCC、CMake、Ninja 以及 vcpkg 构建 port 可能需要的工具;
- Fetch pinned vcpkg :把与团队约定相同的 vcpkg tag 克隆到
/tmp/vcpkg并生成可执行文件; - Configure:读取仓库里的 preset、manifest 和 toolchain,安装依赖并生成 Ninja 规则;
- Build:编译项目;若失败,job 在此停止;
- 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"
逐项检查:
- 是否指定了 vcpkg toolchain? 最常见原因。检查 preset 里的
toolchainFile,或命令行-DCMAKE_TOOLCHAIN_FILE是否正确。 - 是否删掉旧 build 目录后重新 configure? toolchain 必须在第一次 configure 生效,后补无效。
- vcpkg.json 里是否声明了该依赖? manifest mode 只安装声明过的库。
- find_package() 的名字是否正确? port 名 ≠ CMake 包名:
nlohmann-json→find_package(nlohmann_json);openssl→find_package(OpenSSL);catch2→find_package(Catch2)。名字大小写也敏感。 - 链接的 target 名是否正确? 如
fmt::fmt、Catch2::Catch2WithMain。 - 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::...'
逐项检查:
- 是否对目标调用了
target_link_libraries()?忘了链接是最常见原因。 - 链接的 target 是否正确?(
fmt::fmt而不是fmt、libfmt之类。) - 是否混用了编译器?第三方库是 GCC 编的,你的项目却用了 Clang(或不同大版本 GCC),可能符号不匹配。
- 是否混用了系统库和 vcpkg 库?例如
find_package意外找到了 apt 装的旧版本。确认 toolchain 生效、build 目录干净。 - build 目录里是否有旧缓存?
rm -rf build重来。
5.6 一台机器成功,另一台失败
这是可复现性问题,按清单核对:
- 两边 vcpkg 的 Git tag/commit 是否一致?(
git -C $VCPKG_ROOT describe --tags) vcpkg.json的builtin-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 各自职责边界、上下游协作关系的思维地图,是后续精准排错、快速定位问题所属环节的核心思维依据。 |