25、依赖管理:FetchContent模块,ExternalProject_Add,vcpkg与Conan集成
依赖管理,说白了就是解决「别人的代码怎么放进我的项目里」这个问题。
我刚入行那会儿,C++项目的依赖管理基本靠手动下载、解压、配置路径。项目小还好说,一旦涉及三五个第三方库,那简直就是噩梦。版本冲突、路径写死、同事环境不一样……这些坑我全都踩过。
CMake 发展到今天,已经给了我们好几套方案。我个人把它们分成两类:构建时拉取和包管理器集成。今天咱们就把这两类讲透。
一、FetchContent:轻量级的内置方案
FetchContent 是 CMake 3.11 引入的模块。它的思路很简单——在配置阶段就把源码下载下来,然后直接 add_subdirectory 引入。
我特别喜欢它的地方在于:零额外工具。只要你有 CMake,就能用。
核心用法:
include(FetchContent)
FetchContent_Declare(
fmt
GIT_REPOSITORY https://github.com/fmtlib/fmt.git
GIT_TAG 10.1.0
)
FetchContent_MakeAvailable(fmt)
# 之后直接使用 fmt::fmt 即可
target_link_libraries(my_app PRIVATE fmt::fmt)
嗯,这里要注意:FetchContent_MakeAvailable 会帮你处理下载、配置、构建的全过程。下载的内容默认放在 ${CMAKE_BINARY_DIR}/_deps 下。
我的习惯:如果项目依赖不超过 5 个,且都是开源小库,我会优先用 FetchContent。它简单、直接,不需要额外学习成本。
我曾经在一个嵌入式项目里用过它。当时需要引入一个 JSON 解析库,团队里有人想用 vcpkg,但嵌入式工具链不支持。最后我用 FetchContent 三行代码搞定,大家都很满意。
二、ExternalProject_Add:更强大的构建时下载
ExternalProject_Add 比 FetchContent 更早出现(CMake 2.8 就有了)。它的特点是:在构建阶段执行下载和编译。
这意味着什么?意味着你可以用它处理那些不支持 CMake 的项目,或者需要自定义构建步骤的库。
include(ExternalProject)
ExternalProject_Add(
project_zlib
GIT_REPOSITORY https://github.com/madler/zlib.git
GIT_TAG v1.2.13
CMAKE_ARGS -DCMAKE_INSTALL_PREFIX=${CMAKE_BINARY_DIR}/install
BUILD_COMMAND make
INSTALL_COMMAND make install
)
避坑指南:ExternalProject_Add 的变量作用域是独立的。你没法直接在外部 target 里链接它生成的库。通常需要配合 add_library(imported) 或者设置 BUILD_BYPRODUCTS 来打通。
我曾经在一个项目里用 ExternalProject_Add 编译 OpenSSL。那玩意儿构建系统用的是 Perl + Makefile,CMake 根本管不了。但 ExternalProject_Add 可以——我只需要告诉它「下载、运行配置脚本、make、make install」就行了。
| 特性 | FetchContent | ExternalProject_Add |
|---|---|---|
| 执行阶段 | 配置阶段 | 构建阶段 |
| 变量作用域 | 与主项目共享 | 独立作用域 |
| 适用场景 | CMake 友好的库 | 任意构建系统 |
| 依赖传递 | 自动处理 | 需手动配置 |
三、vcpkg:微软出品的包管理器
vcpkg 是微软维护的 C++ 包管理器。它的理念是「一次安装,全局可用」。
我个人觉得 vcpkg 最大的优势是生态丰富——目前有超过 2000 个库。而且它支持 Windows、Linux、macOS,甚至交叉编译。
使用 vcpkg 的典型流程:
- 安装 vcpkg 并集成到 CMake
- 用
vcpkg install fmt安装库 - 在 CMake 中通过
find_package使用
# 在 CMakeLists.txt 中
find_package(fmt CONFIG REQUIRED)
target_link_libraries(my_app PRIVATE fmt::fmt)
我的建议:团队开发时,可以把 vcpkg 的 vcpkg.json 文件纳入版本控制。这样所有成员都能用同一套依赖版本,避免「我机器上能编译」的尴尬。
vcpkg 的 manifest 模式长这样:
{
"name": "my-project",
"version": "1.0.0",
"dependencies": [
"fmt",
"spdlog",
"nlohmann-json"
]
}
然后执行 cmake --preset=default,vcpkg 会自动读取这个文件并安装依赖。
四、Conan:去中心化的包管理方案
Conan 是另一个流行的 C++ 包管理器。和 vcpkg 不同,Conan 是去中心化的——你可以搭建自己的私有仓库。
Conan 的核心概念是 profile 和 recipe。Profile 描述编译环境(编译器、架构、标准库等),Recipe 描述如何构建一个库。
# conanfile.txt
[requires]
fmt/10.1.0
spdlog/1.12.0
[generators]
CMakeDeps
CMakeToolchain
然后在 CMake 中:
# 由 CMakeDeps 生成
find_package(fmt CONFIG REQUIRED)
find_package(spdlog CONFIG REQUIRED)
target_link_libraries(my_app PRIVATE fmt::fmt spdlog::spdlog)
Conan 的独特优势:
- 支持多配置(Debug/Release 共存)
- 可以自定义构建选项
- 私有仓库适合企业级项目
我曾经在一个跨平台项目里用 Conan。项目需要在 Windows 上用 MSVC,在 Linux 上用 GCC,在 macOS 上用 Clang。Conan 的 profile 机制让我可以轻松切换——每个平台一个 profile 文件,构建时指定即可。
五、如何选择?
你想想看,这四种方案各有侧重。我一般这样选:
- 个人项目或小团队:FetchContent 就够了,简单直接
- 需要编译非 CMake 项目:ExternalProject_Add 是唯一选择
- 团队协作,统一环境:vcpkg 的 manifest 模式很合适
- 企业级项目,需要私有仓库:Conan 是首选
其实没有绝对的好坏。我见过有人用 FetchContent 管理上百个依赖,也见过用 Conan 管理三个库的小项目。关键是适合你的场景。
最后提醒一句:不管用哪种方案,一定要把依赖版本锁定。我曾经因为用了 GIT_TAG master,结果某天上游库改了接口,整个项目编译失败。从那以后,我所有依赖都指定具体版本号。
依赖管理这件事,说白了就是把不确定性变成确定性。不管用哪种工具,目标都一样:让项目在任何机器上都能可复现地构建。选一个你顺手的,用熟它,比什么都强。