12、CMake FetchContent:FetchContent_Declare、FetchContent_MakeAvailable、管理外部依赖

说实话,在C++项目里处理第三方依赖,一直是个让人头疼的事。
以前我习惯用git submodule,或者干脆把源码拷到项目里。但维护起来真的很麻烦——版本更新要手动拉,冲突了还得自己解决。
后来CMake 3.11引入了FetchContent模块,我试过一次就回不去了。

说白了,FetchContent就是让你在构建时自动下载、编译外部库。
你只需要告诉CMake:我要哪个库、从哪下载、什么版本。剩下的它全包了。

FetchContent_Declare:声明依赖

这是第一步。就像你告诉CMake:“嘿,我需要一个叫spdlog的库,去GitHub上给我拉下来。”

include(FetchContent)

FetchContent_Declare(
    spdlog
    GIT_REPOSITORY https://github.com/gabime/spdlog.git
    GIT_TAG        v1.12.0
)

这里有几个关键点:

  • GIT_REPOSITORY:源码仓库地址。支持Git、SVN、甚至本地路径。
  • GIT_TAG:指定版本。可以是tag、分支名、commit hash。
  • GIT_SHALLOW:设为TRUE可以只拉取最新commit,省带宽。
我的习惯:GIT_TAG尽量用tag或commit hash,别用分支名。
分支名会变,今天能编译的代码,明天可能就炸了。我踩过这个坑。

FetchContent_MakeAvailable:让依赖生效

声明完了,得让它真正生效。这时候就用FetchContent_MakeAvailable。

FetchContent_MakeAvailable(spdlog)

这一行干了三件事:

  1. 检查依赖是否已经下载过(避免重复下载)
  2. 如果没下载,自动执行git clone
  3. 调用add_subdirectory,把依赖的target暴露出来

之后你就可以直接在项目里链接了:

target_link_libraries(my_app PRIVATE spdlog::spdlog)

嗯,就是这么简单。

管理多个依赖

实际项目里不可能只有一个依赖。我通常会这样组织:

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
)

FetchContent_Declare(catch2
    GIT_REPOSITORY https://github.com/catchorg/Catch2.git
    GIT_TAG        v3.4.0
)

# 一次性全部生效
FetchContent_MakeAvailable(spdlog nlohmann_json catch2)

你看,FetchContent_MakeAvailable可以一次接受多个名字。
它会按顺序处理,但互不干扰。

注意:如果两个依赖之间有传递依赖关系,比如A依赖B,
你必须先声明B,再声明A。否则CMake会报错说找不到B的target。

控制下载位置

默认情况下,FetchContent会把源码下载到build目录下的_deps文件夹里。
我个人习惯把它放到项目根目录,方便查看和调试。

set(FETCHCONTENT_BASE_DIR ${CMAKE_SOURCE_DIR}/third_party)
FetchContent_MakeAvailable(spdlog)

这样所有第三方库都会下载到third_party文件夹下,一目了然。

避坑指南:我曾经遇到的问题

我曾经在一个项目里同时用了FetchContent和find_package。
结果发现同一个库被编译了两次——一次是系统安装的,一次是FetchContent下载的。
链接的时候符号冲突,编译过了,运行就崩。

后来我学乖了:

# 先尝试找系统安装的
find_package(spdlog QUIET)
if(NOT spdlog_FOUND)
    # 找不到再用FetchContent
    FetchContent_Declare(spdlog ...)
    FetchContent_MakeAvailable(spdlog)
endif()

这样既利用了系统库,又保证了没有库时的后备方案。

另一个坑:FetchContent下载的库默认是Debug配置。
如果你在Release模式下构建,记得设置:
set(FETCHCONTENT_QUIET OFF) 来查看下载日志,确认配置正确。

知识体系结构图

下面这张图概括了FetchContent的核心流程:

FetchContent 工作流程 FetchContent_Declare 指定仓库、版本、下载方式 检查是否已下载 已存在则跳过下载 下载源码 git clone / 解压 add_subdirectory 暴露target target_link_libraries 链接到你的项目 ✅ 构建完成 整个过程在 CMake configure 阶段完成,无需手动干预

高级用法:覆盖本地版本

有时候你想临时测试一个修改过的库,但又不想改CMakeLists.txt。
FetchContent提供了覆盖机制:

# 在命令行指定
cmake -DFETCHCONTENT_SOURCE_DIR_spdlog=/path/to/my/spdlog ..

# 或者在CMakeLists.txt里设置
set(FETCHCONTENT_SOURCE_DIR_spdlog /path/to/my/spdlog)
FetchContent_MakeAvailable(spdlog)

这样就会用你本地的源码,而不是去GitHub拉取。调试第三方库时特别方便。

总结一下

FetchContent的核心价值就四个字:自动管理
你不用再手动下载、解压、配置第三方库。CMake帮你搞定一切。

我个人建议:
如果你的项目依赖不超过10个,用FetchContent完全够用。
如果依赖很多,或者需要复杂的版本控制,可以考虑vcpkg或Conan。
但FetchContent作为零配置的轻量方案,绝对是首选。

一句话记住:
FetchContent_Declare 声明依赖,FetchContent_MakeAvailable 让它生效。
就这么简单。

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