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 仓库的 URLGIT_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)
会话目标链接修饰符(PUBLIC、PRIVATE、INTERFACE)的含义与普通依赖相同。
获取任何可用目标
有时你不知道依赖提供了哪些导入目标,可以使用 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 模块的核心知识:
- FetchContent 基础:使用
FetchContent_Declare()声明和FetchContent_MakeAvailable()获取依赖 - 下载方式:支持 Git 仓库、URL 下载、Mercurial 仓库和本地路径多种方式
- 定制下载:可以控制下载目录、设置补丁命令、覆盖构建选项
- 目标集成:通过
target_link_libraries链接到目标,使用FetchContent_GetProperties获取信息 - 最佳实践:条件获取、离线构建支持、版本统一管理
练习
- 使用 FetchContent 下载并链接 fmt 库到你的项目
- 下载 Google Test 并设置测试用例
- 管理多个外部依赖(如 fmt、spdlog、nlohmann_json),一次性获取并使用
- 尝试使用 URL 下载方式获取一个非 Git 仓库的库
- 配置离线构建模式,验证依赖缓存的正确性