第20章 依赖管理:FetchContent、ExternalProject、vcpkg与Conan集成思路
依赖管理,说白了就是怎么把别人的代码优雅地请进你的项目里。
我刚开始用CMake那会儿,最头疼的就是这个。项目里引了七八个第三方库,有的要编译,有的只要头文件,有的版本还互相打架。那时候我习惯手动下载、手动配置路径,结果换台机器就崩。后来我才意识到——依赖管理这件事,CMake其实给了好几套方案,关键是你得选对路子。
20.1 FetchContent:轻量级的内联依赖
先聊聊FetchContent。这是CMake 3.11引入的模块,我个人觉得它是“最CMake风格”的依赖管理方式。你想想看,直接在CMakeLists.txt里声明依赖,构建时自动下载、自动集成,多省事。
核心思路:FetchContent把外部项目的源码拉下来,然后通过add_subdirectory集成到当前构建中。说白了,就是把别人的项目当成你项目的一个子目录来用。
基本用法是这样的:
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是在配置阶段下载的,所以你的网络环境必须能在CMake配置时访问到Git仓库。我在公司内网遇到过这个问题,后来只好改用本地路径。
小技巧:如果你不想每次都从GitHub拉代码,可以设置FETCHCONTENT_SOURCE_DIR_<名称>变量指向本地缓存目录。这样第一次下载后,后续构建直接用本地缓存,速度飞快。
FetchContent最适合的场景是什么?我总结了几点:
- 依赖项数量少(3-5个以内)
- 依赖项本身也是CMake项目
- 你希望依赖和主项目一起编译,共享编译器设置
- 不需要对外分发源码(比如内部工具)
但FetchContent也有短板。它不支持二进制缓存,每次都是源码编译。如果你的依赖很大(比如Qt、OpenCV),那每次构建都编译一遍,时间上受不了。
20.2 ExternalProject:更独立的依赖构建
ExternalProject比FetchContent更“重”一些。它不是在配置阶段下载,而是在构建阶段执行。这意味着它支持更复杂的构建流程——比如依赖本身不是CMake项目,或者需要特定的编译选项。
我曾经在一个嵌入式项目里用过ExternalProject。那个依赖是用Makefile构建的,而且需要交叉编译工具链。FetchContent搞不定,ExternalProject正好派上用场。
include(ExternalProject)
ExternalProject_Add(
my_dependency
GIT_REPOSITORY https://github.com/example/mydep.git
GIT_TAG v1.2.3
CMAKE_ARGS -DCMAKE_INSTALL_PREFIX=<INSTALL_DIR>
BUILD_COMMAND make -j4
INSTALL_COMMAND make install
)
这里有个坑——ExternalProject默认是独立构建的,它不会继承你主项目的CMake变量。如果你想让它们共享某些设置(比如编译器路径),得手动通过CMAKE_ARGS传进去。我记得有一次忘了传CMAKE_TOOLCHAIN_FILE,结果依赖用本地编译器编译了,链接的时候符号对不上,折腾了我半天。
| 特性 | FetchContent | ExternalProject |
|---|---|---|
| 下载时机 | 配置阶段 | 构建阶段 |
| 集成方式 | add_subdirectory | 独立构建+安装 |
| 支持非CMake项目 | 有限 | 完全支持 |
| 二进制缓存 | 不支持 | 可通过安装目录实现 |
| 适用场景 | 轻量、同构项目 | 复杂、异构项目 |
20.3 vcpkg:C++的包管理器
聊完CMake自带的方案,咱们再看看生态工具。vcpkg是微软开源的C++包管理器,它和CMake的集成做得相当好。
我个人的习惯是:如果项目依赖超过5个,或者依赖本身很复杂(比如有大量可选特性),我会优先考虑vcpkg。它帮你解决了“下载-编译-安装”的全流程,你只需要告诉它你要什么包。
vcpkg和CMake的集成方式很简单:
# 在CMakeLists.txt中
cmake_minimum_required(VERSION 3.10)
project(my_app)
# 通过工具链文件集成vcpkg
# 配置时: cmake -B build -DCMAKE_TOOLCHAIN_FILE=/path/to/vcpkg/scripts/buildsystems/vcpkg.cmake
find_package(fmt CONFIG REQUIRED)
target_link_libraries(my_app PRIVATE fmt::fmt)
vcpkg最让我喜欢的一点是它的“清单模式”。你可以在vcpkg.json里声明所有依赖和版本:
{
"name": "my-app",
"version": "1.0.0",
"dependencies": [
"fmt",
"spdlog",
{
"name": "opencv",
"features": ["core", "imgproc"]
}
]
}
这样整个项目的依赖就一目了然了。而且vcpkg会帮你处理依赖的依赖——比如你装opencv,它会自动把opencv依赖的zlib、libpng这些都装上。
注意:vcpkg默认会把所有包编译成静态库。如果你需要动态库,记得设置VCPKG_LIBRARY_LINKAGE=dynamic。另外,vcpkg的端口(port)质量参差不齐,有些包的配置可能不完整,遇到问题可以去GitHub提issue。
20.4 Conan:更灵活的依赖管理
Conan是另一个流行的C++包管理器。和vcpkg相比,Conan更灵活,但学习曲线也陡一些。
Conan的核心概念是“配方”(recipe)。每个包都有一个conanfile.py,描述了如何下载、编译和打包。你可以从ConanCenter下载别人写好的配方,也可以自己写。
在CMake中使用Conan,通常是通过conanbuildinfo.cmake文件:
# conanfile.txt
[requires]
fmt/10.1.0
spdlog/1.12.0
[generators]
CMakeDeps
CMakeToolchain
# CMakeLists.txt
cmake_minimum_required(VERSION 3.15)
project(my_app)
# 先运行 conan install . --output-folder=build --build=missing
# 然后 cmake -B build -DCMAKE_TOOLCHAIN_FILE=build/conan_toolchain.cmake
find_package(fmt CONFIG REQUIRED)
target_link_libraries(my_app PRIVATE fmt::fmt)
Conan的优势在于它的“配置文件”机制。你可以为不同的平台、编译器、构建类型定义不同的配置。比如我在一个跨平台项目里,用Conan管理了Windows上的MSVC依赖和Linux上的GCC依赖,一份conanfile搞定。
但Conan也有让人头疼的地方。它的版本管理逻辑比较复杂,有时候两个包的版本约束会冲突,解决起来挺费劲的。我建议新手先从vcpkg入手,等对依赖管理有了感觉再尝试Conan。
20.5 如何选择?我的经验总结
说了这么多,到底该用哪个?我根据项目类型给个参考:
- 小项目(1-3个依赖):直接用FetchContent,简单直接,零额外工具
- 中等项目(3-10个依赖):vcpkg清单模式,省心省力
- 大型项目(10+依赖,多平台):Conan,灵活可控
- 依赖本身不是CMake项目:ExternalProject,或者vcpkg的自定义端口
- 公司内部私有依赖:Conan的私有仓库,或者FetchContent指向内部Git
你想想看,依赖管理这件事,本质上就是“把别人的代码请进来”。选什么工具不重要,重要的是你的团队能接受、你的项目能跑通。我见过用FetchContent管理上百个依赖的项目,也见过用Conan只管理两个库的团队——没有绝对的对错,只有合不合适。
一句话总结:FetchContent适合轻量内联,ExternalProject适合复杂异构,vcpkg适合开箱即用,Conan适合灵活定制。选哪个,看你的项目规模和团队习惯。
最后说一句——不管你选哪个方案,记得把依赖的版本锁定。我见过太多项目因为依赖版本没锁,今天还能编译,明天就挂了。锁定版本,加上CI验证,这才是靠谱的做法。