18、外部项目:ExternalProject_Add、FetchContent、依赖管理、第三方库集成
说实话,CMake 最让我头疼的部分,就是处理第三方库的依赖。
早期我做 C++ 项目时,经常遇到「库找不到」「版本对不上」「编译选项冲突」这些破事。后来我深入研究了 CMake 的外部项目管理机制,才算是真正解放了双手。今天咱们就聊聊 ExternalProject_Add 和 FetchContent 这两个核心工具。
18.1 为什么需要外部项目管理?
你想想看,一个正经的 C++ 项目,几乎不可能只靠自己。你要用 JSON 解析库、日志库、网络库、测试框架……这些依赖怎么搞?
- 手动下载:太原始,版本管理混乱
- 系统包管理器:apt、brew 装的不一定是你想要的版本
- Git Submodule:能解决一部分问题,但构建流程还是得自己写
CMake 提供了两套官方方案:ExternalProject_Add 和 FetchContent。我个人习惯把前者叫做「构建时下载」,后者叫做「配置时下载」。两者各有千秋,咱们一个一个说。
核心区别一句话:
ExternalProject_Add在make或ninja构建阶段执行下载和编译FetchContent在cmake ..配置阶段就下载并引入项目
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