跳到主要内容

CMake FetchContent

FetchContent 是 CMake 3.15 引入的模块,允许在配置阶段从外部源(Git 仓库、URL、本地路径等)自动下载并集成依赖。它是现代 CMake 项目管理依赖的推荐方式,特别适用于需要获取预编译库或开源项目的场景。

基本用法

声明和获取

FetchContent 的使用流程非常简单:首先使用 FetchContent_Declare() 声明依赖,然后调用 FetchContent_MakeAvailable() 获取并配置依赖。

include(FetchContent)

FetchContent_Declare(
googletest
GIT_REPOSITORY https://github.com/google/googletest.git
GIT_TAG v1.14.0
)

FetchContent_MakeAvailable(googletest)

# 使用依赖
enable_testing()
target_link_libraries(myapp PRIVATE GTest::gtest_main)

上述代码会自动下载 Google Test 并配置好测试框架。GTest::gtest_main 是一个导入目标,可以直接链接到你的测试可执行文件。

完整示例

cmake_minimum_required(VERSION 3.15)
project(MyProject)

include(FetchContent)

# 声明依赖
FetchContent_Declare(
fmt
GIT_REPOSITORY https://github.com/fmtlib/fmt.git
GIT_TAG 10.0.0
)

# 获取并使其可用
FetchContent_MakeAvailable(fmt)

# 创建目标并链接依赖
add_executable(myapp main.cpp)
target_link_libraries(myapp PRIVATE fmt::fmt)

在这个示例中,CMake 会自动下载 fmt 库并配置其导入目标,然后我们可以使用 fmt::fmt 来链接到项目。

下载方式

FetchContent 支持多种下载源,以下分别介绍。

Git 仓库

这是最常用的方式,适合开源项目。你可以直接指定 Git 仓库地址和版本标签:

FetchContent_Declare(
mylib
GIT_REPOSITORY https://github.com/user/mylib.git
GIT_TAG v1.0.0 # 标签(版本)
# 也可以使用分支
# GIT_TAG main
# 或者直接指定提交哈希
# GIT_TAG abc123def4567890abcdef1234567890abcdef12
)

关键参数说明:

  • GIT_REPOSITORY:Git 仓库的 URL
  • GIT_TAG:可以是版本标签、分支名或完整提交哈希
  • GIT_SUBMODULES:可选,指定需要初始化的子模块
  • GIT_DEPTH:可选,指定浅克隆的深度
# 浅克隆可以加快下载速度
FetchContent_Declare(
mylib
GIT_REPOSITORY https://github.com/user/mylib.git
GIT_TAG main
GIT_DEPTH 1 # 只获取最新提交
)

URL 下载

对于不提供 Git 仓库的依赖,可以直接使用 URL 下载压缩包:

FetchContent_Declare(
mylib
URL https://example.com/mylib-1.0.tar.gz
URL_HASH SHA256=abc123def456789012345678901234567890123456789012345678901234
)

强烈建议同时指定 URL_HASH 以验证下载文件的完整性。支持的哈希算法包括:SHA256、SHA1、MD5 等。

# 自动生成哈希值并更新
# cmake -D FETCHCONTENT_UPDATES_DISCONNECTED=ON myproject

Mercurial (Hg) 仓库

如果依赖托管在 Mercurial 仓库:

FetchContent_Declare(
mylib
HG_REPOSITORY https://bitbucket.org/user/mylib
HG_TAG v1.0.0
)

本地路径

在开发过程中,有时需要使用本地未发布的代码:

FetchContent_Declare(
mylib
SOURCE_DIR /path/to/local/mylib
# 或者使用 DOWNLOADED_DIR 指定下载目录
# DOWNLOADED_DIR ${CMAKE_BINARY_DIR}/mylib
)

需要注意的是,SOURCE_DIR 会优先使用本地路径,如果路径不存在,才会尝试其他下载方式。

定制下载行为

设置下载选项

FetchContent 提供了丰富的选项来控制下载行为:

# 设置下载选项
set(FETCHCONTENT_QUIET ON) # 静默模式
set(FETCHCONTENT_FULLY_DISCONNECTED ON) # 离线模式,掉过更新检查
set(FETCHCONTENT_DISCONNECTED ON) # 禁止自动更新

# 覆盖依赖的构建选项
set(FMT_INSTALL OFF CACHE BOOL "" FORCE)
set(BUILD_TESTING OFF CACHE BOOL "" FORCE)

FetchContent_MakeAvailable(fmt spdlog)

指定下载目录

默认情况下,FetchContent 会将下载的代码放在 ${CMAKE_BINARY_DIR}/_deps/ 目录下。你可以通过以下方式自定义:

set(FETCHCONTENT_BASE_DIR ${CMAKE_SOURCE_DIR}/third_party)
FetchContent_Declare(
mylib
GIT_REPOSITORY https://github.com/user/mylib.git
)
FetchContent_MakeAvailable(mylib)

使用 PATCH_COMMAND 修补丁代码

有时下载的库需要打补丁才能使用:

FetchContent_Declare(
mylib
GIT_REPOSITORY https://github.com/user/mylib.git
GIT_TAG v1.0.0
PATCH_COMMAND git apply ../mylib.patch
)

下载后执行自定义命令

FetchContent_Declare(
mylib
GIT_REPOSITORY https://github.com/user/mylib.git
GIT_TAG v1.0.0
CONFIGURE_COMMAND "" # 跳过配置
BUILD_COMMAND "" # 跳过构建
INSTALL_COMMAND "" # 跳过安装
TEST_COMMAND "" # 跳过测试
)

与目标集成

链接依赖

获取依赖后,可以通过 target_link_libraries 将其链接到目标:

FetchContent_MakeAvailable(fmt spdlog)

add_executable(myapp main.cpp)
target_link_libraries(myapp PRIVATE fmt::fmt spdlog::spdlog)

会话目标链接修饰符(PUBLICPRIVATEINTERFACE)的含义与普通依赖相同。

获取任何可用目标

有时你不知道依赖提供了哪些导入目标,可以使用 try_compile 或直接查看:

# 检查目标是否可用
if(TARGET fmt::fmt)
target_link_libraries(myapp PRIVATE fmt::fmt)
endif()

# 或者获取库的根目录
FetchContent_GetProperties(mylib)
message(STATUS "mylib source: ${mylib_SOURCE_DIR}")
message(STATUS "mylib binary: ${mylib_BINARY_DIR}")

使用依赖的依赖

如果你的依赖本身也使用 FetchContent,可以使用 FIND_PACKAGE 风格的导入:

# 在声明时设置 FIND_PACKAGE_MODE
FetchContent_Declare(
spdlog
GIT_REPOSITORY https://github.com/gabime/spdlog.git
GIT_TAG v1.12.0
FIND_PACKAGE_MODE NEVER # 不尝试 FIND_PACKAGE
)

# 或者让子依赖使用 find_package
FetchContent_Declare(
mylib
GIT_REPOSITORY https://github.com/user/mylib.git
)
FetchContent_MakeAvailable(mylib)

多依赖管理

批量获取依赖

当你有多个依赖需要管理时,可以一次性获取全部:

include(FetchContent)

# 声明所有依赖
FetchContent_Declare(
fmt
GIT_REPOSITORY https://github.com/fmtlib/fmt.git
GIT_TAG 10.0.0
)

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
)

# 一次性获取所有依赖
FetchContent_MakeAvailable(fmt spdlog nlohmann_json)

# 链接多个依赖
add_executable(myapp main.cpp)
target_link_libraries(myapp PRIVATE fmt::fmt spdlog::spdlog nlohmann_json::nlohmann_json)

检查依赖状态

可以在获取前检查依赖是否已经存在:

include(FetchContent)

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

# 检查是否已经填充(从缓存或其他方式)
FetchContent_GetProperties(fmt)
if(NOT fmt_POPULATED)
FetchContent_MakeAvailable(fmt)
endif()

依赖版本管理

可以通过变量控制依赖版本:

set(FMT_VERSION "10.0.0" CACHE STRING "fmt version")
set(SPDLOG_VERSION "v1.12.0" CACHE STRING "spdlog version")

FetchContent_Declare(
fmt
GIT_REPOSITORY https://github.com/fmtlib/fmt.git
GIT_TAG ${FMT_VERSION}
)

set_target_properties(fmt_verify_propset
PROPERTIES
FETCHCONTENT_SOURCE_DIR_FMT ""
)

FetchContent_MakeAvailable(fmt)

最佳实践

1. 条件获取

建议使用变量控制是否获取依赖,这样可以方便地进行离线开发:

option(FETCHCONTENT_DISABLE_UPDATES "禁用依赖更新检查" OFF)

if(NOT EXISTS ${CMAKE_BINARY_DIR}/_deps/fmt-src)
message(STATUS "正在下载 fmt...")
FetchContent_MakeAvailable(fmt)
else()
message(STATUS "使用已缓存的 fmt")
# 即使存在,也要调用 MakeAvailable 以配置目标
FetchContent_MakeAvailable(fmt)
endif()

2. 统一依赖版本管理

在实际项目中,建议将所有依赖声明集中在一个文件(如 cmake/FetchContent.cmake)中:

# cmake/FetchContent.cmake
include(FetchContent)

set(FETCHCONTENT_QUIET ON)

# 依赖版本统一管理
set(DEP_FMT_VERSION "10.0.0")
set(DEP_SPDLOG_VERSION "v1.12.0")
set(DEP_GOOGLETEST_VERSION "v1.14.0")

FetchContent_Declare(fmt GIT_REPOSITORY https://github.com/fmtlib/fmt.git GIT_TAG ${DEP_FMT_VERSION})
FetchContent_Declare(spdlog GIT_REPOSITORY https://github.com/gabime/spdlog.git GIT_TAG ${DEP_SPDLOG_VERSION})
FetchContent_Declare(googletest GIT_REPOSITORY https://github.com/google/googletest.git GIT_TAG ${DEP_GOOGLETEST_VERSION})

FetchContent_MakeAvailable(fmt spdlog googletest)

然后在主 CMakeLists.txt 中包含:

# CMakeLists.txt
cmake_minimum_required(VERSION 3.15)
project(MyProject)

include(cmake/FetchContent.cmake)

add_executable(myapp main.cpp)
target_link_libraries(myapp PRIVATE fmt::fmt spdlog::spdlog)

3. 使用预编译二进制

对于大型库(如 Boost),可以结合 URL 下载和预编译二进制:

FetchContent_Declare(
boost
URL https://archives.boost.io/release/1.84.0/source/boost_1_84_0.tar.gz
URL_HASH SHA256=6e0403e744dc85db3fe8a4059b3e0820a198a81492fad26cf875b63b03094405
DOWNLOAD_EXTRACT_TIMESTAMP ON
)

4. 子模块处理

如果依赖包含 Git 子模块,需要正确处理:

FetchContent_Declare(
mylib
GIT_REPOSITORY https://github.com/user/mylib.git
GIT_TAG v1.0.0
GIT_SUBMODULES "submodule1" "submodule2" # 只初始化特定子模块
# 或者使用 GIT_SUBMODULES_RECURSE ON 获取所有子模块
)

5. 错误处理

添加错误处理以提高调试效率:

include(FetchContent)
include(FetchContent)

FetchContent_Declare(
fmt
GIT_REPOSITORY https://github.com/fmtlib/fmt.git
GIT_TAG ${FMT_VERSION}
)

FetchContent_MakeAvailable(fmt)

# 验证获取成功
if(NOT TARGET fmt::fmt)
message(FATAL_ERROR "fmt 目标不可用,请检查 FetchContent 配置")
endif()

6. 离线构建支持

在 CI/CD 环境中,可以使用离线模式避免重复下载:

# 用户可以使用 -D FETCHCONTENT_FULLY_DISCONNECTED=ON 跳过网络操作
# 但前提是依赖已经被缓存
if(FETCHCONTENT_FULLY_DISCONNECTED)
message(STATUS "离线模式:依赖必须已存在于构建目录中")
endif()

小结

本章我们学习了 FetchContent 模块的核心知识:

  1. FetchContent 基础:使用 FetchContent_Declare() 声明和 FetchContent_MakeAvailable() 获取依赖
  2. 下载方式:支持 Git 仓库、URL 下载、Mercurial 仓库和本地路径多种方式
  3. 定制下载:可以控制下载目录、设置补丁命令、覆盖构建选项
  4. 目标集成:通过 target_link_libraries 链接到目标,使用 FetchContent_GetProperties 获取信息
  5. 最佳实践:条件获取、离线构建支持、版本统一管理

练习

  1. 使用 FetchContent 下载并链接 fmt 库到你的项目
  2. 下载 Google Test 并设置测试用例
  3. 管理多个外部依赖(如 fmt、spdlog、nlohmann_json),一次性获取并使用
  4. 尝试使用 URL 下载方式获取一个非 Git 仓库的库
  5. 配置离线构建模式,验证依赖缓存的正确性