第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适合灵活定制。选哪个,看你的项目规模和团队习惯。

CMake依赖管理方案对比 FetchContent 内联源码依赖 ExternalProject 独立构建依赖 vcpkg 包管理器 Conan 包管理器 特性对比 下载时机 配置阶段 构建阶段 安装阶段 安装阶段 集成方式 add_subdirectory 独立安装 find_package find_package 二进制缓存 不支持 手动实现 支持 支持 非CMake项目 有限 完全支持 需端口 完全支持 推荐场景 小项目,1-3个依赖 轻量内联 复杂异构依赖 独立构建 中等项目,3-10个 开箱即用 大型多平台项目 灵活可控

最后说一句——不管你选哪个方案,记得把依赖的版本锁定。我见过太多项目因为依赖版本没锁,今天还能编译,明天就挂了。锁定版本,加上CI验证,这才是靠谱的做法。

公众号:蓝海资料掘金营,微信 deep3321