CMake管理外部依赖的模块
在 CMake 中,FetchContent
和 ExternalProject
都是管理外部依赖的模块,但它们的 设计目标、使用场景和执行时机 有本质区别。以下通过对比表格、代码示例和场景分析详细说明它们的区别。
核心区别对比表
特性 | FetchContent | ExternalProject |
---|---|---|
执行阶段 | 配置阶段(cmake 命令运行时) | 构建阶段(make /ninja 运行时) |
集成方式 | 直接作为项目子目录 (add_subdirectory ) | 独立构建,作为外部进程 |
源码位置 | 默认在 build/_deps 目录下 | 默认在 build/ExternalProject 相关目录 |
构建控制 | 自动集成到主项目构建流程 | 需手动管理配置、编译、安装等步骤 |
典型场景 | 头文件库、小型依赖项 | 需编译的复杂库(如 Boost、OpenCV) |
适用阶段 | CMake 配置阶段 | CMake 构建阶段 |
灵活性 | 简单易用,但定制性弱 | 复杂但高度可定制(支持分阶段操作) |
版本要求 | CMake ≥3.11 | CMake ≥2.8.11(基础功能) |
场景示例分析
场景1:集成一个仅头文件库(如 spdlog)
推荐使用 FetchContent
无需编译,直接包含头文件即可使用:
cmake_minimum_required(VERSION 3.14)
project(MyApp)include(FetchContent)# 声明依赖
FetchContent_Declare(spdlogGIT_REPOSITORY https://github.com/gabime/spdlog.gitGIT_TAG v1.11.0 # 固定版本
)# 自动下载并添加子目录
FetchContent_MakeAvailable(spdlog)# 直接使用 spdlog
add_executable(my_app main.cpp)
target_link_libraries(my_app PRIVATE spdlog::spdlog_header_only)
场景2:编译一个需要构建的库(如 GoogleTest)
推荐使用 ExternalProject
需独立编译并安装到系统目录:
cmake_minimum_required(VERSION 3.10)
project(MyProject)include(ExternalProject)# 定义 GoogleTest 的构建流程
ExternalProject_Add(googletestGIT_REPOSITORY https://github.com/google/googletest.gitGIT_TAG release-1.12.1CMAKE_ARGS -DCMAKE_INSTALL_PREFIX=${CMAKE_INSTALL_PREFIX} # 安装到系统目录TEST_COMMAND "" # 跳过测试
)# 主项目链接已安装的库
add_executable(my_test test.cpp)
add_dependencies(my_test googletest) # 确保先构建 googletest
target_link_libraries(my_test PRIVATE GTest::GTest GTest::Main)
关键区别详解
1. 执行时机
-
FetchContent
在运行cmake
配置项目时下载并解压源码,依赖项会成为主项目的一部分,直接参与配置和构建。 -
ExternalProject
在运行make
或ninja
构建项目时才下载和编译依赖项,作为独立进程运行。
2. 源码集成方式
-
FetchContent
通过add_subdirectory()
将依赖项源码直接嵌入主项目,共享同一个构建目录和变量作用域。 -
ExternalProject
依赖项完全独立构建,需通过find_package()
或手动指定头文件/库路径。
3. 典型使用场景
场景 | 推荐模块 | 原因 |
---|---|---|
引入单头文件库(如 fmt) | FetchContent | 无需编译,直接包含头文件 |
引入需编译的静态库 | ExternalProject | 独立构建,避免污染主项目配置 |
需要修改依赖项代码 | ExternalProject | 可通过 PATCH_COMMAND 修改源码 |
多阶段构建(如下载+编译) | ExternalProject | 支持分阶段控制(下载、配置、编译、安装) |
混合使用场景示例
若需在项目中同时使用两种模块(例如:主项目用 FetchContent
,某个子依赖用 ExternalProject
):
cmake_minimum_required(VERSION 3.14)
project(HybridExample)# 使用 FetchContent 引入头文件库
include(FetchContent)
FetchContent_Declare(fmtURL https://github.com/fmtlib/fmt/releases/download/9.1.0/fmt-9.1.0.zipURL_HASH SHA256=abcdef...
)
FetchContent_MakeAvailable(fmt)# 使用 ExternalProject 构建复杂库
include(ExternalProject)
ExternalProject_Add(my_heavy_libURL http://example.com/heavy-lib.tar.gzCONFIGURE_COMMAND <源码路径>/configure --prefix=${CMAKE_INSTALL_PREFIX}BUILD_COMMAND makeINSTALL_COMMAND make install
)# 主项目链接两者
add_executable(app main.cpp)
target_link_libraries(app PRIVATE fmt::fmt)
add_dependencies(app my_heavy_lib) # 确保先构建 my_heavy_lib
如何选择?
-
优先
FetchContent
:
依赖项轻量、无需复杂构建流程、需要直接调用其 CMake 目标时。 -
选择
ExternalProject
:
依赖项需要独立构建、需自定义步骤(如打补丁)、或构建流程复杂(如 Autotools 项目)。
总结
FetchContent
是“轻量级依赖管理”,适合简单集成。ExternalProject
是“重型构建工具”,适合完全控制外部项目流程。- 根据依赖项的性质(是否需要编译、是否需修改代码)和项目需求(是否需要隔离构建环境)选择合适工具。
为什么 FetchContent_Declare
中可以使用 PATCH_COMMAND
?
尽管 PATCH_COMMAND
在官方文档中属于 ExternalProject
模块,但在实际使用中,某些 CMake 版本(尤其是较新版本)允许在 FetchContent_Declare
中使用 PATCH_COMMAND
。这是因为 FetchContent
底层借用了 ExternalProject
的机制来实现依赖管理,从而间接支持了一些 ExternalProject
的参数。以下是详细解释和注意事项:
1. 底层实现机制
FetchContent
模块在内部调用了 ExternalProject
的功能来管理依赖项的下载和配置。因此,FetchContent_Declare
的某些参数(如 PATCH_COMMAND
)实际上是通过 ExternalProject_Add
传递的。尽管官方文档未明确列出这些参数,但在实践中它们可能被隐式支持。
示例代码分析
FetchContent_Declare(san_cmakeGIT_REPOSITORY https://github.com/arsenm/sanitizers-cmakeGIT_TAG masterSOURCE_DIR external/sanitizers-cmakePATCH_COMMAND sed -i 's/old_text/new_text/' ${san_cmake_SOURCE_DIR}/CMakeLists.txt
)
- 实际行为:
当调用FetchContent_MakeAvailable(san_cmake)
时,CMake 会通过ExternalProject
的流程处理依赖项,期间执行PATCH_COMMAND
。
2. 版本兼容性
- CMake ≥3.14:
部分版本开始支持在FetchContent
中直接使用ExternalProject
的参数(如PATCH_COMMAND
),但需谨慎使用,因为这是非官方行为。 - CMake ❤️.14:
此类版本可能直接报错,因为参数未被识别。
3. 使用 PATCH_COMMAND
的风险
尽管功能上有效,但需要注意以下问题:
- 非官方支持
CMake 官方文档未明确说明FetchContent
支持PATCH_COMMAND
,未来版本可能移除此特性。 - 跨平台兼容性
补丁命令(如sed
)在不同操作系统(Windows/macOS/Linux)中的行为可能不同,需额外处理。 - 路径依赖
必须确保${san_cmake_SOURCE_DIR}
在PATCH_COMMAND
执行时已正确赋值(需在FetchContent_Populate
之后)。
4. 正确用法示例
若需在 FetchContent
中安全使用 PATCH_COMMAND
,应结合 FetchContent_Populate
显式控制流程:
代码示例
cmake_minimum_required(VERSION 3.14)
project(MyProject)include(FetchContent)# 声明依赖项
FetchContent_Declare(san_cmakeGIT_REPOSITORY https://github.com/arsenm/sanitizers-cmakeGIT_TAG masterSOURCE_DIR ${CMAKE_SOURCE_DIR}/external/sanitizers-cmakePATCH_COMMAND sed -i.bak 's/cmake_minimum_required(VERSION 2.8.12)/cmake_minimum_required(VERSION 2.8.12...3.27)/' ${san_cmake_SOURCE_DIR}/CMakeLists.txt
)# 手动触发下载和补丁
FetchContent_GetProperties(san_cmake)
if(NOT san_cmake_POPULATED)FetchContent_Populate(san_cmake) # 此步骤会执行 PATCH_COMMANDadd_subdirectory(${san_cmake_SOURCE_DIR} ${san_cmake_BINARY_DIR})
endif()# 使用依赖项
include(${san_cmake_SOURCE_DIR}/cmake/sanitize-helpers.cmake)
5. 替代方案推荐
若需更稳定的修补机制,建议以下方法:
方法1:通过 ExternalProject
显式控制
include(ExternalProject)ExternalProject_Add(san_cmakeGIT_REPOSITORY https://github.com/arsenm/sanitizers-cmakeGIT_TAG masterSOURCE_DIR ${CMAKE_SOURCE_DIR}/external/sanitizers-cmakePATCH_COMMAND sed -i 's/.../' <SOURCE_DIR>/CMakeLists.txtCONFIGURE_COMMAND ""BUILD_COMMAND ""INSTALL_COMMAND ""
)# 主项目中引用
add_subdirectory(${CMAKE_SOURCE_DIR}/external/sanitizers-cmake)
方法2:下载后手动修补
FetchContent_Declare(san_cmake ...)
FetchContent_MakeAvailable(san_cmake)# 在下载完成后执行修补
add_custom_command(TARGET san_cmakePOST_BUILDCOMMAND sed -i 's/.../' ${san_cmake_SOURCE_DIR}/CMakeLists.txt
)
6. 总结
- 可用性:
在较新 CMake 版本中,FetchContent_Declare
确实可以借用ExternalProject
的PATCH_COMMAND
,但属于非官方行为。 - 风险:
跨平台兼容性差,未来版本可能不再支持。 - 推荐做法:
若需修补依赖项,优先使用ExternalProject
或在FetchContent
下载后通过add_custom_command
执行修补。