18、与包管理器结合:vcpkg + FetchContent、Conan + FetchContent、Hunter + FetchContent
说实话,CMake 的 FetchContent 模块刚出来那会儿,我第一反应是:这不就是包管理器该干的事吗?后来在项目里用久了才发现,FetchContent 和包管理器其实是互补关系,不是替代关系。
你想想看,包管理器帮你管理系统级的依赖,FetchContent 帮你拉取源码级的依赖。两者结合,才是现代 C++ 项目依赖管理的完整方案。今天我就把三种主流组合——vcpkg、Conan、Hunter 分别怎么跟 FetchContent 配合,给你讲透。
为什么需要结合?
先说说我个人的理解。包管理器(vcpkg、Conan、Hunter)解决的是「依赖的二进制分发」问题。你写个 CMakeLists.txt,指定需要 fmt、spdlog,包管理器自动下载编译好的库文件,省时省力。
但包管理器有个短板:它管不了那些「没有发布到包管理器的库」。比如公司内部的私有库,或者某个 GitHub 上只有源码的小众项目。这时候 FetchContent 就派上用场了。
所以最佳实践是:能用包管理器的就用包管理器,包管理器搞不定的,用 FetchContent 兜底。
核心思路:包管理器负责「稳定、常用、已发布」的依赖,FetchContent 负责「临时、私有、未发布」的依赖。两者通过 CMake 的 find_package() 和 add_subdirectory() 机制无缝衔接。
vcpkg + FetchContent
vcpkg 是我用得最多的包管理器。它跟 CMake 的集成非常自然,通过工具链文件就能搞定。
先看一个典型场景:项目依赖 fmt(通过 vcpkg 安装),同时依赖一个内部工具库 internal_utils(只有源码,没发布到 vcpkg)。
# CMakeLists.txt
cmake_minimum_required(VERSION 3.14)
project(MyProject LANGUAGES CXX)
# vcpkg 会自动设置 CMAKE_TOOLCHAIN_FILE
# 所以 find_package 能找到 vcpkg 安装的库
find_package(fmt CONFIG REQUIRED)
# 内部工具库用 FetchContent 拉取
include(FetchContent)
FetchContent_Declare(
internal_utils
GIT_REPOSITORY https://gitlab.internal.com/tools/internal_utils.git
GIT_TAG v1.2.0
)
FetchContent_MakeAvailable(internal_utils)
add_executable(my_app main.cpp)
target_link_libraries(my_app PRIVATE fmt::fmt internal_utils)
这里有个细节要注意:vcpkg 安装的库通过 find_package 找到,FetchContent 拉取的库通过 add_subdirectory 加入。两者在 target_link_libraries 里用起来完全一样,CMake 会自动处理头文件路径和链接库。
个人经验:我在一个跨平台项目里用过这个组合。Windows 上用 vcpkg 装 OpenSSL,Linux 上用系统包管理器装,但内部通信库始终用 FetchContent 拉取。这样既保证了核心依赖的一致性,又避免了平台差异带来的麻烦。
Conan + FetchContent
Conan 跟 vcpkg 的思路不太一样。Conan 更强调「可复现的构建」,每个依赖的版本、配置、选项都精确控制。跟 FetchContent 结合时,需要多一步「传递依赖信息」的操作。
Conan 的典型用法是:在 conanfile.txt 里声明依赖,CMake 里通过 conanbuildinfo.cmake 导入。但如果你同时用 FetchContent,需要确保 FetchContent 拉取的库也能找到 Conan 提供的依赖。
# conanfile.txt
[requires]
fmt/8.1.1
spdlog/1.10.0
[generators]
CMakeDeps
CMakeToolchain
# CMakeLists.txt
cmake_minimum_required(VERSION 3.14)
project(MyProject LANGUAGES CXX)
# Conan 生成的 CMake 配置文件
find_package(fmt CONFIG REQUIRED)
find_package(spdlog CONFIG REQUIRED)
# 用 FetchContent 拉取一个需要 fmt 的第三方库
include(FetchContent)
FetchContent_Declare(
awesome_lib
GIT_REPOSITORY https://github.com/example/awesome_lib.git
GIT_TAG v2.0.0
)
FetchContent_MakeAvailable(awesome_lib)
add_executable(my_app main.cpp)
target_link_libraries(my_app PRIVATE fmt::fmt spdlog::spdlog awesome_lib)
这里有个坑:如果 awesome_lib 内部也依赖 fmt,但版本跟 Conan 提供的不一致,就会出问题。我曾经被这个坑过——两个版本的 fmt 符号冲突,链接阶段报了一堆 undefined reference。
避坑指南:我曾经在 Conan + FetchContent 组合里遇到版本冲突。解决方案是:在 FetchContent_Declare 里显式指定版本,或者用 OVERRIDE_FIND_PACKAGE 参数让 FetchContent 拉取的库优先使用 Conan 提供的版本。
# 强制 FetchContent 拉取的库使用 Conan 的 fmt
FetchContent_Declare(
awesome_lib
GIT_REPOSITORY https://github.com/example/awesome_lib.git
GIT_TAG v2.0.0
OVERRIDE_FIND_PACKAGE
)
Hunter + FetchContent
Hunter 跟 vcpkg、Conan 不太一样。它本质上是一个 CMake 模块集合,通过 FetchContent 自身来管理依赖。说白了,Hunter 就是用 FetchContent 实现的一个包管理器。
所以 Hunter + FetchContent 的组合,其实是「用 FetchContent 管理 Hunter,Hunter 再管理其他依赖」。听起来有点绕,但用起来很简洁。
# CMakeLists.txt
cmake_minimum_required(VERSION 3.14)
project(MyProject LANGUAGES CXX)
# 引入 Hunter
include(FetchContent)
FetchContent_Declare(
hunter
GIT_REPOSITORY https://github.com/cpp-pm/hunter.git
GIT_TAG v0.23.300
)
FetchContent_MakeAvailable(hunter)
# Hunter 会自动下载并管理依赖
hunter_add_package(fmt)
find_package(fmt CONFIG REQUIRED)
# 同时用 FetchContent 拉取其他库
FetchContent_Declare(
my_private_lib
GIT_REPOSITORY https://gitlab.internal.com/libs/my_private_lib.git
GIT_TAG v1.0.0
)
FetchContent_MakeAvailable(my_private_lib)
add_executable(my_app main.cpp)
target_link_libraries(my_app PRIVATE fmt::fmt my_private_lib)
Hunter 的好处是:所有依赖的版本都在一个 hunter.cmake 文件里集中管理,不会出现版本冲突。但坏处是:Hunter 的包更新速度比 vcpkg 慢,有些新库可能没有收录。
我的建议:如果你的项目完全开源,而且所有依赖都在 Hunter 的收录范围内,直接用 Hunter 就够了。但如果项目里有私有库或者小众库,还是得用 FetchContent 兜底。
三种组合对比
| 组合方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| vcpkg + FetchContent | 集成简单,Windows 生态好,库数量多 | 跨平台配置稍麻烦,版本锁定不够精细 | Windows 为主、需要大量第三方库的项目 |
| Conan + FetchContent | 版本控制精确,可复现性强,支持自定义配置 | 学习曲线陡,需要额外维护 conanfile | 对版本一致性要求高的企业项目 |
| Hunter + FetchContent | 纯 CMake 方案,无需额外工具,版本集中管理 | 库收录有限,更新慢,大型项目编译慢 | 中小型开源项目,依赖库较少 |
核心逻辑图
下面这张图展示了三种组合的核心工作流程。你可以看到,无论哪种组合,最终都是通过 CMake 的 find_package 和 add_subdirectory 两条路径汇合到目标可执行文件。
实际项目中的选择策略
说了这么多,到底该选哪种组合?我个人的经验是分三步走:
- 先看团队技术栈:如果团队已经用了 vcpkg,就别强行换 Conan。工具链的一致性比工具本身的好坏更重要。
- 再看依赖来源:90% 的依赖都在包管理器里,就用包管理器 + 少量 FetchContent。反过来,如果一半依赖都是私有库,不如直接用 FetchContent 统一管理。
- 最后看构建环境:CI/CD 环境里,Conan 的缓存机制能大幅加速构建。本地开发的话,vcpkg 的「一键安装」体验更好。
一个小技巧:不管用哪种组合,我都会在 CMakeLists.txt 开头加一段判断逻辑:如果 find_package 找不到,就 fallback 到 FetchContent。这样既支持包管理器,也支持纯源码构建,一劳永逸。
# 优雅的 fallback 方案
find_package(fmt QUIET)
if(NOT fmt_FOUND)
include(FetchContent)
FetchContent_Declare(fmt
GIT_REPOSITORY https://github.com/fmtlib/fmt.git
GIT_TAG 8.1.1
)
FetchContent_MakeAvailable(fmt)
endif()
嗯,三种组合的核心思路就是这样。说白了,包管理器和 FetchContent 不是二选一的问题,而是怎么搭配更顺手的问题。你在项目里用哪种组合?欢迎在实践中摸索出最适合自己的方案。
公众号:蓝海资料掘金营,微信deep3321