C++ 依赖管理与包管理:vcpkg 与 Conan 实战

C++ 长期缺乏统一的包管理方案,第三方库往往靠手工编译、系统包管理器或 Git submodule 引入,导致构建不可复现、版本冲突频发。本文系统对比 vcpkg 与 Conan 两套主流方案:vcpkg 的 manifest 模式与注册表机制、Conan 的 conanfile 与包 ID 二进制兼容模型、锁文件与私有仓库搭建、二进制缓存加速 CI,以及与 CMake 的三种集成方式,最后给出一份可直接落地的依赖管理实践清单。

一个 C++ 项目引入 fmt、spdlog、protobuf 之后,构建文档通常会变成一份手工步骤清单:先装系统依赖、再 clone 某个 tag、再 cmake 编译安装到 /usr/local。这套流程在开发者本机能跑通,但在 CI 上、在新同事的机器上、在三年后的容器镜像里几乎必然失败。根本原因是 C++ 缺少统一的 ABI 与构建产物标准,库的「包」形态从未被标准化。vcpkg 与 Conan 是当前最主流的两条路线,本文从机制到落地逐一拆解。

一、C++ 依赖管理的困境

1.1 为什么 C++ 没有 npm

对比其他语言可以看清问题所在:

语言包格式分发内容ABI 保证
JavaScriptnpm 包源码无(解释执行)
Rustcrate源码无(每项目全量编译)
Gomodule源码无(静态链接)
Pythonwheel预编译二进制有限(限版本)
C++无统一标准源码 + 二进制混合无

C++ 的困难来自三个层面:ABI 不统一(同一份代码用 GCC 11 与 GCC 13 编译出的库二进制不兼容)、构建配置爆炸(Debug/Release、静态/动态、异常/RTTI 开关的笛卡尔积)、依赖传播复杂(头文件路径、链接库顺序、编译定义都要向下传递)。任何包管理器都必须先解决这三件事。

1.2 依赖管理的四个层次

一个成熟的依赖管理方案需要同时提供:

  • 获取:从注册表/仓库下载源码或二进制
  • 构建:按当前工具链与配置编译出可用产物
  • 解析:处理传递依赖与版本冲突
  • 缓存:避免重复编译,加速 CI

vcpkg 与 Conan 在这四个层次上的设计哲学截然不同,这也是选型的核心分歧点。

二、vcpkg 机制与实战

2.1 manifest 模式

vcpkg 由微软主导,采用「port 文件描述如何构建」的模型,每个库是一个 portfile.cmake。现代用法是 manifest 模式,依赖声明在项目根目录的 vcpkg.json:

{
  "name": "myapp",
  "version": "1.0.0",
  "dependencies": [
    "fmt",
    "spdlog",
    { "name": "protobuf", "default-features": false, "features": ["lite"] }
  ],
  "builtin-baseline": "a42af01b72c28a8e1d7b48107b33e4f286a55ef6",
  "overrides": [
    { "name": "fmt", "version": "10.2.1" }
  ]
}

builtin-baseline 指向 vcpkg 仓库的一个 commit,是版本锁定的关键:它决定了所有未显式指定版本的依赖解析到哪个时间点。overrides 则用于强制某个依赖的精确版本。

# 使用 manifest 模式安装依赖
vcpkg install --triplet x64-linux

# 在 CMake 配置时自动安装(推荐)
cmake -B build -DCMAKE_TOOLCHAIN_FILE=$VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake

vcpkg 的 triplet 是「目标平台 + 链接方式 + 运行时」的组合,如 x64-linux、x64-windows-static、arm64-osx,它直接对应到那一堆构建配置组合。

2.2 版本控制与注册表

vcpkg 支持自定义注册表,可以搭建私有 port 集合:

{
  "registries": [
    {
      "kind": "git",
      "repository": "https://git.example.com/team/vcpkg-registry",
      "baseline": "5c2b1f0d3a8e4f6b9c0d1e2f3a4b5c6d7e8f9a0b",
      "packages": ["internal-crypto", "company-sdk"]
    }
  ]
}
# 生成版本数据库(注册表维护者操作)
vcpkg x-add-version internal-crypto --overwrite-version

私有注册表让内部库也能像开源库一样被声明式引用,这是 vcpkg 相对 Conan 更「开箱即用」的一点。

2.3 二进制缓存

vcpkg 每次构建产物会按「源码哈希 + 编译选项哈希」计算出一个 ABI 标识,缓存可避免 CI 反复编译同一份依赖:

# 本地文件系统缓存
vcpkg install --binarysource=files,/mnt/cache,readwrite

# 对象存储(如 Azure Blob、S3 兼容)
export VCPKG_BINARY_SOURCES="clear;x-azblob,https://acct.blob.core.windows.net/cache,sas-token,readwrite"

在 GitHub Actions 上配置缓存后,原本需要 20 分钟的依赖编译通常能压缩到 1 分钟以内——这是 vcpkg 落地 CI 收益最大的一环。

三、Conan 机制与实战

3.1 conanfile 与 profile

Conan 是社区驱动的去中心化方案,把「构建配方」与「二进制包」彻底分离。conanfile.py 描述如何构建与打包:

from conan import ConanFile
from conan.tools.cmake import CMake, cmake_layout

class MyAppConan(ConanFile):
    name = "myapp"
    version = "1.0.0"
    settings = "os", "compiler", "build_type", "arch"
    generators = "CMakeDeps", "CMakeToolchain"
    requires = "fmt/10.2.1", "spdlog/1.14.1", "protobuf/3.21.12"

    def layout(self):
        cmake_layout(self)

    def build(self):
        cmake = CMake(self)
        cmake.configure()
        cmake.build()

profile 描述构建环境(编译器、架构、构建类型),依赖解析结果会写入 conan.lock 锁文件,保证 CI 与开发机解析出完全一致的依赖图:

# profiles/linux-release
[settings]
os=Linux
arch=x86_64
compiler=gcc
compiler.version=13
compiler.libcxx=libstdc++11
build_type=Release

[conf]
tools.build:jobs=8
conan install . --profile=linux-release --build=missing
conan lock create . --profile=linux-release   # 生成 conan.lock
conan install . --lockfile=conan.lock          # CI 中复现

3.2 包 ID 与二进制兼容

Conan 最核心的设计是 package_id:它由 settings、options、依赖的 package_id 共同哈希而成,标识一个二进制包是否能在当前环境下复用。

def package_id(self):
    # 默认:编译器版本参与哈希,GCC 12 与 13 的产物不共享
    # 优化:头文件库与编译器版本无关,可放宽
    self.info.settings.compiler.version = "any"
    # 优化:Debug/Release 对纯头文件库无影响
    self.info.settings.build_type = "any"

理解 package_id 是排查「为什么 CI 又从头编译了一遍」的关键:任何参与哈希的设置变化(哪怕只是 compiler.version 从 13.1 变成 13.2)都会导致缓存失效。

3.3 私有仓库

Conan 2 支持多种远程类型,私有包与公共包可以共存:

# 添加官方中心与私有仓库
conan remote add conancenter https://center.conan.io
conan remote add company https://conan.example.com/artifactory/api/conan/company

# 上传私有包
conan upload internal-crypto/1.2.0 -r company --confirm

# 按模式批量上传
conan upload "company-*" -r company --confirm

Artifactory、Nexus 等制品仓库都提供 Conan 仓库支持,权限模型与 Maven/npm 仓库统一,便于企业级治理。

四、两者对比

4.1 架构差异

维度vcpkgConan
主导方微软社区(JFrog 支持)
包描述portfile.cmake + vcpkg.jsonconanfile.py / .txt
版本锁定builtin-baseline + overridesconan.lock 锁文件
二进制缓存ABI 哈希 + 文件/云缓存package_id + 远程仓库
私有库自定义注册表(git)私有 remote(Artifactory)
学习曲线低中高
生态规模2000+ 端口1800+ 中心包 + 企业私库
与 CMake 集成toolchain 文件(最简)CMakeDeps 生成器

4.2 选型建议

  • 中小项目、快速起步:选 vcpkg。toolchain 一行搞定,manifest 声明式清晰,CI 集成成本最低
  • 大型企业、多语言制品统一治理:选 Conan。锁文件、私有仓库、package_id 精细控制更适合规模化
  • 需要精细控制构建选项与 ABI:选 Conan。options 与 package_id 的表达能力更强
  • 已有 Artifactory/Nexus 体系:Conan 与制品仓库的集成更自然
  • Windows 生态、Visual Studio 用户:vcpkg 与 MSVC 的集成度最高

两者并非互斥:部分团队用 vcpkg 管理第三方开源库,用 Conan 管理内部私有库,但混用会显著增加心智负担,除非有明确收益,否则不建议。

五、与 CMake 集成

5.1 三种集成方式

无论用哪个包管理器,最终都要落到 CMake。核心是用 find_package 的 CONFIG 模式消费依赖提供的 xxxConfig.cmake:

cmake_minimum_required(VERSION 3.25)
project(myapp LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

find_package(fmt CONFIG REQUIRED)
find_package(spdlog CONFIG REQUIRED)
find_package(Protobuf CONFIG REQUIRED)

add_executable(myapp src/main.cpp)
target_link_libraries(myapp PRIVATE
    fmt::fmt
    spdlog::spdlog
    protobuf::libprotobuf)

三种集成方式的差异在于 xxxConfig.cmake 从哪来:

# 1. vcpkg toolchain:配置阶段自动注入前缀路径
cmake -B build -DCMAKE_TOOLCHAIN_FILE=$VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake

# 2. Conan:先生成再配置
conan install . --output-folder=build --build=missing
cmake -B build -DCMAKE_TOOLCHAIN_FILE=build/conan_toolchain.cmake

# 3. CMake Presets:把上述命令固化进 CMakePresets.json(推荐)
cmake --preset conan-release

第三种方式把工具链路径、缓存变量、环境变量全部固化到 CMakePresets.json,让「clone 下来就能构建」成为现实:

{
  "version": 6,
  "configurePresets": [
    {
      "name": "conan-release",
      "generator": "Ninja",
      "binaryDir": "${sourceDir}/build/Release",
      "cacheVariables": {
        "CMAKE_BUILD_TYPE": "Release",
        "CMAKE_TOOLCHAIN_FILE": "${sourceDir}/build/Release/generators/conan_toolchain.cmake"
      }
    }
  ]
}

5.2 常见坑

  • 工具链文件冲突:Conan 与 vcpkg 的 toolchain 不能同时使用,必须二选一
  • 静态库链接顺序:C++ 静态库有依赖顺序要求,用 target_link_libraries 传递依赖而非手工排列
  • Debug/Release 混用:依赖的构建类型必须与主项目一致,否则在 MSVC 上会因 _ITERATOR_DEBUG_LEVEL 不匹配而链接失败
  • 动态库运行时路径:Windows 上 DLL 需拷贝到可执行文件目录,Linux 上需设置 RPATH
  • protoc 版本不一致:生成的 .pb.cc 与运行时库版本必须匹配,需把 protoc 版本也纳入依赖管理

关于 ABI 兼容与符号可见性的更多细节,参见 https://plumephp.com/cpp-cross-platform-build-matrix/;而 target_link_libraries 的传播语义(PUBLIC/PRIVATE/INTERFACE)则是 https://plumephp.com/cpp-cmake-project/ 的核心内容。

六、工程实践清单

把依赖管理落地为团队规范,建议固化以下条目:

  • 依赖清单入库:vcpkg.json / conanfile.py 与锁文件必须提交到 Git,禁止隐式依赖系统库
  • 锁定版本:vcpkg 用 builtin-baseline + overrides,Conan 用 conan.lock,CI 中强制校验
  • 缓存 CI:把二进制缓存挂到持久化存储,可把构建时间降低一个数量级
  • 统一工具链:用 CMake Presets 固化编译器、生成器与工具链文件,杜绝「我这儿能跑」
  • 定期升级依赖:季度级批量升级 + 完整回归,避免版本落后到无法升级
  • 私有库也要有 CI:内部库同样要走构建、测试、上传的流水线,否则会成为新的「手工步骤」
  • 记录构建环境:在 CI 中打印编译器版本、包管理器版本、依赖解析结果,便于事后追溯

相关阅读

  • https://plumephp.com/cpp-cmake-project/ — target-based CMake 与现代工程化实践
  • https://plumephp.com/cpp-cross-platform-build-matrix/ — CMake Presets 与 CI 构建矩阵、ABI 兼容
  • https://plumephp.com/cpp-engineering-practices/ — 代码规范、静态分析与 CI/CD 整体工程实践

延伸阅读

  • https://plumephp.com/posts/devops/ — CI/CD 流水线设计与制品仓库治理
  • https://plumephp.com/posts/docker/ — 用容器固化 C++ 构建环境与依赖缓存

文末完整示例

# 完整可运行示例:CMakeLists.txt + vcpkg.json + CMakePresets.json 三件套
# 目录结构:
#   myapp/
#     CMakeLists.txt
#     CMakePresets.json
#     vcpkg.json
#     src/main.cpp
# 构建:cmake --preset default && cmake --build build

# ===================== vcpkg.json =====================
# {
#   "name": "myapp",
#   "version": "1.0.0",
#   "dependencies": ["fmt", "spdlog"],
#   "builtin-baseline": "a42af01b72c28a8e1d7b48107b33e4f286a55ef6"
# }

# ===================== CMakePresets.json =====================
# {
#   "version": 6,
#   "configurePresets": [
#     {
#       "name": "default",
#       "generator": "Ninja",
#       "binaryDir": "${sourceDir}/build",
#       "cacheVariables": {
#         "CMAKE_BUILD_TYPE": "Release",
#         "CMAKE_TOOLCHAIN_FILE": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake"
#       }
#     }
#   ]
# }

# ===================== CMakeLists.txt =====================
cmake_minimum_required(VERSION 3.25)
project(myapp LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

# 默认只允许 CONFIG 模式,避免误匹配系统库
set(CMAKE_FIND_PACKAGE_PREFER_CONFIG ON)

find_package(fmt CONFIG REQUIRED)
find_package(spdlog CONFIG REQUIRED)

add_executable(myapp src/main.cpp)

# 用现代 target 用法:依赖以 INTERFACE 形式传播
target_link_libraries(myapp PRIVATE
    fmt::fmt
    spdlog::spdlog)

target_compile_options(myapp PRIVATE
    $<$<CXX_COMPILER_ID:GNU,Clang>:-Wall -Wextra>)

# 打印依赖解析结果,便于排查版本问题
message(STATUS "fmt    : ${fmt_VERSION}")
message(STATUS "spdlog : ${spdlog_VERSION}")
// src/main.cpp —— 消费被管理的依赖
#include <fmt/core.h>
#include <spdlog/spdlog.h>

int main() {
    spdlog::info("依赖管理演示启动");
    fmt::print("fmt 格式化:{}\n", fmt::format("pi ≈ {:.3f}", 3.14159));
    spdlog::warn("这是一条来自 spdlog 的告警");
    return 0;
}

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「cpp」更多文章

  1. C++ 移动语义与完美转发:从右值引用到引用折叠
  2. C++ 模糊测试与覆盖率:libFuzzer、AFL++ 与 Sanitizer
  3. C++ 无锁数据结构:栈、队列与安全内存回收