引言:施工队的”供应链升级”
在上节课(12.1)里,我们的 CMake “施工队长”拿到了一本《标准作业手册》——学会了用 CMake Presets 统一管理配置、构建和测试流程,让团队协作变得井井有条。但不知你想过没有:队长手里的图纸(CMakeLists.txt)再漂亮,如果水泥、钢筋、玻璃这些原材料(第三方库)的供应链出了问题,整个工程照样会停摆。
在早期的 C++ 项目中,”采购”第三方库往往是个体力活:手动下载源码、解压、编译、安装到系统目录,再让 CMake 通过 find_package 去”碰运气”寻找。这种方式就像施工队长每次都要亲自去矿山挖铁矿、去森林砍木头——低效且不可复现。
现代 C++ 生态已经有了成熟的”建材供应商”:vcpkg、Conan、CPM.cmake 等工具。它们不是 CMake 的替代品,而是 CMake 的最佳搭档。这节课,我们就来看看如何让施工队长把这些供应商无缝接入自己的工作流,实现从”手动采购”到”供应链自动化”的跃迁。
vcpkg Manifest 模式:声明式依赖清单
如果你在 Windows 或跨平台 C++ 社区里混,一定听说过 vcpkg。这个由微软发起的 C++ 包管理器,经过多年的发展,已经从早期的”全局安装”模式进化出了更现代的 Manifest 模式(清单模式)。
从全局安装到项目级清单
在传统的 vcpkg 用法中,你需要在系统全局安装库:
vcpkg install boost jsoncpp
这会导致两个问题:一是不同项目可能需要不同版本的 Boost,全局只能装一个;二是团队成员每台机器都要手动执行安装命令,极易出现”我电脑上能编,你电脑上找不到包”的尴尬。
Manifest 模式借鉴了 Node.js 的 package.json 和 Rust 的 Cargo.toml 思想:在项目根目录下放一个 vcpkg.json,声明项目需要什么依赖,vcpkg 会在构建时自动把它们下载、编译并集成到 CMake 中。
{
"name": "my-awesome-app",
"version": "1.0.0",
"dependencies": [
"fmt",
"spdlog",
{
"name": "openssl",
"version>=": "3.0.0"
}
],
"overrides": [
{
"name": "fmt",
"version": "9.1.0"
}
]
}
这段 JSON 就像是一份采购清单:项目需要 fmt、spdlog 和版本不低于 3.0 的 openssl,并且不管 vcpkg 库里 fmt 更新到了 10.x,我们都锁定用 9.1.0 版本(通过 overrides)。
与 CMake 的集成
有了清单, CMake 怎么知道去 vcpkg 的仓库里”进货”呢?答案是 工具链文件(Toolchain File)。在配置项目时,指定 vcpkg 提供的工具链:
cmake -B build -S . -DCMAKE_TOOLCHAIN_FILE=[vcpkg-root]/scripts/buildsystems/vcpkg.cmake
如果你使用了 12.1 节学过的 CMake Presets,可以把这行写进 configurePresets 里,让工具链自动加载:
{
"name": "default",
"hidden": false,
"generator": "Ninja",
"toolchainFile": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake"
}
一旦集成完成, CMakeLists.txt 里就可以像平常一样使用 find_package,vcpkg 会自动处理好头文件路径和库链接,施工队长完全不用操心钢筋从哪个港口运来的。
find_package(fmt CONFIG REQUIRED)
find_package(spdlog CONFIG REQUIRED)
add_executable(my_app main.cpp)
target_link_libraries(my_app PRIVATE fmt::fmt spdlog::spdlog)
Conan 2.x:Pythonic 的依赖工程化
如果说 vcpkg 像是一家”标准化建材超市”,那么 Conan 则更像是一家支持深度定制的工业品供应商。Conan 2.x 在 2023 年正式发布后,与 CMake 的集成方式发生了质的飞跃,彻底告别了 1.x 时代那种繁琐的 conan_basic_setup() 宏。
Conan 2.x 的架构变革
Conan 2.x 采用了更清晰的分层设计。它不再直接修改你的 CMakeLists.txt,而是通过生成器(Generators)输出标准的 CMake 配置文件,让 CMake “自然而然”地找到包。最常用的一对组合是:
- CMakeDeps:为每个依赖生成
XXX-config.cmake文件,供find_package使用。 - CMakeToolchain:生成一个工具链文件,处理编译器标志、标准库、构建类型等配置。
conanfile.py 与 CMake 的”双剑合璧”
在 Conan 2.x 中,推荐用 Python 脚本 conanfile.py 来描述依赖,而不是简单的 conanfile.txt。这样可以精细控制选项(options)、设置(settings)和依赖版本。
from conan import ConanFile
from conan.tools.cmake import cmake_layout, CMakeToolchain, CMakeDeps
class MyAppConan(ConanFile):
name = "my_app"
version = "1.0"
settings = "os", "compiler", "build_type", "arch"
requires = "fmt/10.1.1", "spdlog/1.12.0", "openssl/3.1.2"
def generate(self):
deps = CMakeDeps(self)
deps.generate()
tc = CMakeToolchain(self)
tc.generate()
def layout(self):
cmake_layout(self)
这份 conanfile.py 就是我们的”采购合同书”:它规定了要哪些库、什么版本,并且通过 generate() 方法告诉 Conan 输出 CMake 能读懂的配置文件。
构建流程变成了两步:
- 安装依赖并生成 CMake 配置:
conan install . --build=missing - 使用 Conan 生成的工具链配置 CMake:
cmake -B build -S . -DCMAKE_TOOLCHAIN_FILE=build/generators/conan_toolchain.cmake
CMakeLists.txt 依然保持干净,只需按惯例使用 find_package:
cmake_minimum_required(VERSION 3.23)
project(my_app CXX)
find_package(fmt REQUIRED)
find_package(spdlog REQUIRED)
find_package(OpenSSL REQUIRED)
add_executable(${PROJECT_NAME} src/main.cpp)
target_link_libraries(${PROJECT_NAME} PRIVATE fmt::fmt spdlog::spdlog OpenSSL::SSL)
这种方式的最大优势是极致的灵活性。Conan 支持自定义设置(比如针对嵌入式 ARM 交叉编译)、私有仓库(Artifactory),以及精细的二进制包管理(同样的源码在不同编译器/选项下会生成不同的二进制包并缓存)。对于企业级项目,这相当于给施工队配备了一套完整的 ERP 采购系统。
CPM.cmake:极简主义的”快读通道”
前面两位(vcpkg 和 Conan)都是重量级的”供应商”,需要单独安装客户端、维护仓库索引。但对于很多中小型项目,或者快速原型开发,你可能只想在 CMakeLists.txt 里写几行代码,就能拉下 GitHub 上的某个库直接使用。这时候,CPM.cmake 就是最佳选择了。
FetchContent 的”升级皮肤”
还记得我们在 4.3 节学过的 FetchContent 吗?CPM.cmake 本质上是对 FetchContent 的一层轻量级封装,但它提供了更简洁的 API 和版本管理语义。
使用 CPM 只需要两步:先下载 CPM.cmake 脚本(通常放在 cmake/CPM.cmake),然后在 CMakeLists.txt 中引入:
# 必须在 add_subdirectory 或 find_package 之前
include(cmake/CPM.cmake)
# 像"点外卖"一样添加依赖
CPMAddPackage(
NAME fmt
GITHUB_REPOSITORY fmtlib/fmt
GIT_TAG 10.1.1
)
CPMAddPackage(
NAME spdlog
GITHUB_REPOSITORY gabime/spdlog
GIT_TAG v1.12.0
OPTIONS
"SPDLOG_FMT_EXTERNAL ON" # 让 spdlog 使用外部的 fmt
)
add_executable(my_app main.cpp)
target_link_libraries(my_app PRIVATE fmt::fmt spdlog::spdlog)
注意这里的美妙之处:CPM 会自动处理 add_subdirectory,把源码拉下来并构建。如果多个依赖都依赖了同一个库(比如 spdlog 和另一个库都用了 fmt),CPM 还会自动做去重(Dedup),避免重复拉取。
对于 CMake 版本较新(3.24+)的项目,CPM 甚至能直接利用 FetchContent 的 FIND_PACKAGE_ARGS,优先寻找系统已安装的包,找不到再自动下载,兼具灵活性与便捷性。
何时选择 CPM
CPM 最适合以下场景:
- 小型到中型项目:不想引入额外的包管理器安装,保持”纯 CMake”环境。
- 源码级依赖优先:希望直接编译依赖的源码,而不是预编译的二进制包(方便调试和跨平台)。
- 快速原型与教学:几行代码就能跑起来,没有复杂的仓库配置。
但它也有短板:没有内置的二进制包缓存机制,每次 CI/CD 都可能重新下载编译;也不如 Conan/vcpkg 那样有庞大的中心化仓库和严格的版本锁定能力。
选型指南:没有银弹,只有场景
看到这里,你可能有点晕:三个工具看起来都不错,我到底该用哪个?别急,我们来一张”施工场景对照表”,帮你做决策:
方案横向对比
| 维度 | vcpkg (Manifest) | Conan 2.x | CPM.cmake |
|---|---|---|---|
| 安装成本 | 需安装 vcpkg 工具 | 需安装 Python + Conan | 仅需 CMake(单文件引入) |
| 依赖声明 | vcpkg.json |
conanfile.py |
直接在 CMakeLists.txt |
| 二进制缓存 | 支持(Binary Caching) | 强大(内置 Artifactory 支持) | 无(每次通常重新编译) |
| 版本锁定 | overrides + baseline |
lockfile(conan.lock) | Git Tag 或版本号 |
| 私有仓库 | 支持 Registry | 完美支持(私有 Artifactory) | 任意 Git 仓库即可 |
| CMake 集成 | Toolchain 文件 | CMakeDeps + CMakeToolchain | add_subdirectory |
| 最佳场景 | Windows/跨平台开源项目 | 企业级、多配置复杂项目 | 中小型项目、快速原型 |
选型建议
- 如果你是个人开发者或开源项目维护者,追求简单和跨平台一致性,vcpkg Manifest 是很好的起点。它与 Visual Studio 集成度极高,社区包数量庞大。
- 如果你在企业环境中工作,有严格的合规要求(必须用私有仓库、精确控制二进制包、多编译器矩阵构建),Conan 2.x 几乎是工业标准。它的 Python 脚本化能力也让复杂依赖逻辑变得可控。
- 如果你在写一个小工具、教学示例,或者项目依赖不超过 10 个且都是 Git 上的热门库,CPM.cmake 能让你保持极简,避免在包管理器上花费过多运维成本。
当然,现实世界中并不是单选题。很多大型项目会混合使用:用 vcpkg/Conan 管理大型基础库(Boost、OpenSSL),用 CPM 拉取几个小众的 Header-only 库。施工队长最重要的能力,不是只用一把锤子,而是知道哪面墙该用锤子、哪面墙该用冲击钻。
小结
这节课,我们给 CMake “施工队长”升级了三条现代化的”供应链”:
- vcpkg Manifest 用声明式 JSON 清单解决了”全局安装地狱”,配合 Toolchain 文件与 CMake 无缝衔接。
- Conan 2.x 通过
CMakeDeps和CMakeToolchain实现了企业级的依赖工程化,是复杂项目的首选。 - CPM.cmake 在极简与功能之间找到了平衡点,让中小型项目也能享受自动拉取依赖的便利。
工具的选择永远取决于项目的规模、团队的运维能力和部署环境。但无论你选择哪条路,核心理念都是一致的:让 CMake 专注构建,让包管理器专注依赖,各司其职,才能盖出稳固的高楼。
在下节课(12.3)中,我们将把目光投向未来——看看 CMake 4.x 会带来哪些新特性,以及 C++20 Modules 这场”模块化革命”将如何重塑我们的构建系统。敬请期待!


没有回复内容