18、外部项目:ExternalProject_Add、FetchContent、依赖管理、第三方库集成

说实话,CMake 最让我头疼的部分,就是处理第三方库的依赖。

早期我做 C++ 项目时,经常遇到「库找不到」「版本对不上」「编译选项冲突」这些破事。后来我深入研究了 CMake 的外部项目管理机制,才算是真正解放了双手。今天咱们就聊聊 ExternalProject_AddFetchContent 这两个核心工具。

18.1 为什么需要外部项目管理?

你想想看,一个正经的 C++ 项目,几乎不可能只靠自己。你要用 JSON 解析库、日志库、网络库、测试框架……这些依赖怎么搞?

  • 手动下载:太原始,版本管理混乱
  • 系统包管理器:apt、brew 装的不一定是你想要的版本
  • Git Submodule:能解决一部分问题,但构建流程还是得自己写

CMake 提供了两套官方方案:ExternalProject_AddFetchContent。我个人习惯把前者叫做「构建时下载」,后者叫做「配置时下载」。两者各有千秋,咱们一个一个说。

核心区别一句话:

  • ExternalProject_Addmakeninja 构建阶段执行下载和编译
  • FetchContentcmake .. 配置阶段就下载并引入项目
CMake 外部项目管理流程 CMake 配置开始 FetchContent 配置阶段下载 + 引入 ExternalProject_Add 构建阶段下载 + 编译 add_subdirectory 直接可用 target_link_libraries 直接链接 独立构建,生成安装目录 通过 find_package 或变量引用 项目构建完成

18.2 ExternalProject_Add:老牌选手,功能全面

ExternalProject_Add 是 CMake 最早提供的外部项目方案。它的工作方式很直接:在构建阶段(也就是你执行 make 的时候),它会去下载源码、打补丁、配置、编译、安装。

我记得第一次用这个命令时,被它的参数数量吓了一跳。但用熟了之后,发现它其实很灵活。

基本用法

include(ExternalProject)

ExternalProject_Add(
  project_spdlog
  GIT_REPOSITORY    https://github.com/gabime/spdlog.git
  GIT_TAG           v1.12.0
  CMAKE_ARGS        -DCMAKE_INSTALL_PREFIX=${CMAKE_BINARY_DIR}/third_party
  PREFIX            ${CMAKE_BINARY_DIR}/_deps/spdlog
  INSTALL_DIR       ${CMAKE_BINARY_DIR}/third_party
)

这段代码做了什么事?

  • 从 GitHub 克隆 spdlog 仓库,切换到 v1.12.0 标签
  • _deps/spdlog 目录下执行 CMake 配置和编译
  • 安装到 third_party 目录下

注意:ExternalProject_Add 默认不会自动链接到你的主项目。你需要手动设置 CMAKE_PREFIX_PATH 或者用 find_package 来找到安装好的库。

避坑指南:依赖顺序

我曾经在一个项目中同时依赖了 protobuf 和 grpc,结果 grpc 编译时找不到 protobuf。为什么?因为 ExternalProject_Add 默认是并行构建的!

解决办法是用 DEPENDS 关键字指定依赖顺序:

ExternalProject_Add(
  project_protobuf
  ...
)

ExternalProject_Add(
  project_grpc
  DEPENDS project_protobuf  # 先编译 protobuf
  ...
)

18.3 FetchContent:现代方案,配置即用

如果你用过 CMake 3.11 以上版本,我强烈建议你试试 FetchContent。它最大的好处是:在 cmake .. 这一步就把依赖下载好,然后直接用 add_subdirectory 引入,就像项目本身的一部分一样。

基本用法

include(FetchContent)

FetchContent_Declare(
  fmt
  GIT_REPOSITORY https://github.com/fmtlib/fmt.git
  GIT_TAG        10.1.0
)

FetchContent_MakeAvailable(fmt)

# 现在可以直接链接了
target_link_libraries(my_app PRIVATE fmt::fmt)

看到没?不需要手动设置安装目录,不需要 find_package。下载、配置、引入一气呵成。

我的个人习惯:对于小型、头文件为主的库(比如 fmt、spdlog、nlohmann_json),我首选 FetchContent。对于大型、需要独立安装的库(比如 OpenCV、Boost),我倾向于用 ExternalProject_Add 或者系统包管理器。

18.4 依赖管理的实战策略

在实际项目中,依赖管理不是简单的「选一个工具」就完事了。你需要考虑版本锁定、缓存、离线构建等问题。

版本锁定

我建议把所有依赖的版本号集中管理在一个 CMake 文件中:

# versions.cmake
set(FMT_VERSION 10.1.0)
set(SPDLOG_VERSION 1.12.0)
set(GTEST_VERSION 1.14.0)

然后在主 CMakeLists.txt 中 include(versions.cmake)。这样升级版本时只需要改一处。

缓存与离线构建

团队开发时,每个人都去 GitHub 下载依赖,既慢又不可靠。我的做法是:

  • 在 CI 服务器上构建一次,把下载好的源码打包成缓存
  • 使用 FETCHCONTENT_SOURCE_DIR_<uppercaseName> 变量指定本地路径
# 如果本地有缓存,直接使用
if(DEFINED ENV{FMT_SOURCE_DIR})
  set(FETCHCONTENT_SOURCE_DIR_FMT $ENV{FMT_SOURCE_DIR})
endif()

18.5 第三方库集成的常见模式

我总结了三种常见的集成模式,你可以根据项目情况选择:

模式 适用场景 优点 缺点
FetchContent 中小型库、头文件库 配置简单、直接链接 每次 cmake 重新下载(除非缓存)
ExternalProject_Add 大型库、需要独立安装 构建隔离、支持复杂配置 配置繁琐、链接需要额外步骤
系统包管理器 + find_package 稳定、广泛使用的库 无需下载、系统级优化 版本不可控、跨平台麻烦

我的建议:新项目优先用 FetchContent。如果遇到 FetchContent 搞不定的情况(比如需要打补丁、需要自定义构建步骤),再退回到 ExternalProject_Add。别一开始就把事情搞复杂了。

18.6 一个完整的实战例子

最后,我分享一个我在实际项目中用过的模式。这个项目需要 spdlog(日志)、nlohmann_json(JSON 解析)、gtest(测试)。

cmake_minimum_required(VERSION 3.14)
project(MyApp LANGUAGES CXX)

# 引入 FetchContent
include(FetchContent)

# 声明依赖
FetchContent_Declare(
  spdlog
  GIT_REPOSITORY https://github.com/gabime/spdlog.git
  GIT_TAG v1.12.0
)

FetchContent_Declare(
  nlohmann_json
  GIT_REPOSITORY https://github.com/nlohmann/json.git
  GIT_TAG v3.11.2
)

# 测试库只在 Debug 或测试配置下引入
if(BUILD_TESTING)
  FetchContent_Declare(
    googletest
    GIT_REPOSITORY https://github.com/google/googletest.git
    GIT_TAG v1.14.0
  )
endif()

# 一次性下载并引入
FetchContent_MakeAvailable(spdlog nlohmann_json)

if(BUILD_TESTING)
  FetchContent_MakeAvailable(googletest)
  enable_testing()
endif()

# 构建主程序
add_executable(my_app main.cpp)
target_link_libraries(my_app PRIVATE spdlog::spdlog nlohmann_json::nlohmann_json)

# 构建测试
if(BUILD_TESTING)
  add_executable(my_test test_main.cpp)
  target_link_libraries(my_test PRIVATE gtest gtest_main)
  add_test(NAME MyTest COMMAND my_test)
endif()

这段代码看起来是不是很清爽?所有依赖都在配置阶段搞定,主项目和测试项目都能直接链接。我在好几个项目中都用了这个模板,几乎没有出过问题。

嗯,外部项目管理这块,说白了就是「把别人的代码优雅地请进你的项目」。选对工具、做好版本管理、注意缓存策略,你的构建系统就能稳定运行很多年。


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