22、CMake与外部依赖:FetchContent、ExternalProject_Add、add_subdirectory
做嵌入式开发,最头疼的事之一就是管理外部依赖。你想想看,项目里用了lwIP、FreeRTOS、mbedTLS这些库,总不能把源码全拷进自己仓库吧?版本怎么管?更新怎么办?
CMake给了我们三种主流方案:add_subdirectory、FetchContent和ExternalProject_Add。这三种方式各有各的脾气,用错了地方,够你喝一壶的。今天我就把它们的区别、适用场景和坑,一次性给你讲透。
22.1 三种方式的核心区别
先看一张总览图,心里有个谱:
22.2 add_subdirectory:最直接,但最"脏"
这是最原始的方式。你把第三方库的源码放在项目里,然后用add_subdirectory把它加进来。说白了,就是把别人的CMakeLists.txt拉到你的构建流程里一起跑。
优点:简单粗暴,直接就能用target_link_libraries链接。
缺点:外部库的变量、宏、target会污染你的作用域。我遇到过好几次,某个库偷偷定义了DEBUG宏,结果把我的调试信息全搞乱了。
# 最简单的用法
add_subdirectory(third_party/lwIP)
target_link_libraries(my_app PRIVATE lwIP)
# 但如果你要控制变量传播
add_subdirectory(third_party/lwIP EXCLUDE_FROM_ALL)
# EXCLUDE_FROM_ALL 可以防止外部库的target被默认构建
注意:我曾经在一个项目里,用add_subdirectory加了个老版本的FreeRTOS。结果它内部定义了一个叫configUSE_TICKLESS_IDLE的变量,跟我自己的配置冲突了。查了两天才找到原因。所以,用这种方式前,一定要检查外部库的CMakeLists.txt里有没有全局变量。
22.3 FetchContent:现代CMake的"下载即用"
这是CMake 3.11引入的功能,3.14以后更稳定。我个人非常喜欢这种方式——它能在配置阶段自动下载依赖,然后像add_subdirectory一样直接使用。
为什么会推荐它?因为你在CMakeLists.txt里写清楚依赖的Git仓库和版本,CMake会自动帮你拉代码、切分支、然后编译。团队里其他人只需要cmake ..,一切自动完成。
# 引入FetchContent模块
include(FetchContent)
# 声明依赖
FetchContent_Declare(
mbedtls
GIT_REPOSITORY https://github.com/Mbed-TLS/mbedtls.git
GIT_TAG v3.4.0
GIT_SHALLOW TRUE # 只拉最新commit,省时间
)
# 让CMake去下载并添加
FetchContent_MakeAvailable(mbedtls)
# 然后就可以直接链接了
target_link_libraries(my_app PRIVATE mbedtls::mbedtls)
我的习惯:在嵌入式项目里,我会配合GIT_SHALLOW TRUE和GIT_PROGRESS TRUE,这样下载快,而且能看到进度。另外,如果依赖的库比较大(比如OpenCV),我会在第一次配置时泡杯咖啡——确实要等一会儿。
22.3.1 处理依赖的依赖
FetchContent最强大的地方在于,它能自动处理传递依赖。比如你依赖的库A又依赖库B,只要A的CMakeLists.txt里也用了FetchContent,CMake会自动去拉B。
# 但要注意:如果多个依赖都用了同一个子依赖
# 可以用 OVERRIDE_FIND_PACKAGE 来避免重复下载
FetchContent_Declare(
zlib
GIT_REPOSITORY https://github.com/madler/zlib.git
GIT_TAG v1.2.13
OVERRIDE_FIND_PACKAGE # 告诉CMake:别去找系统的zlib了,用我指定的
)
避坑指南:我曾经在一个项目里,同时依赖了libA和libB,它们都依赖libC但版本不同。FetchContent默认会使用先声明的那一个版本。如果你需要强制指定版本,可以用FetchContent_SetPopulated或者FetchContent_GetProperties来手动控制。
22.4 ExternalProject_Add:终极武器,但别滥用
ExternalProject_Add是CMake里最"重"的依赖管理方式。它在构建阶段(而不是配置阶段)去下载和编译外部项目。这意味着,你的主项目配置完了,外部项目才开始动工。
什么时候需要用?我总结了几种场景:
- 外部项目使用非CMake构建系统(比如Makefile、Autotools)
- 需要给外部项目打补丁
- 外部项目有复杂的配置选项
- 你想让外部项目完全独立构建,不污染主项目
include(ExternalProject)
ExternalProject_Add(
libusb
GIT_REPOSITORY https://github.com/libusb/libusb.git
GIT_TAG v1.0.26
SOURCE_DIR ${CMAKE_SOURCE_DIR}/third_party/libusb
BINARY_DIR ${CMAKE_BINARY_DIR}/third_party/libusb
CONFIGURE_COMMAND ./configure --prefix=${CMAKE_BINARY_DIR}/install
BUILD_COMMAND make -j4
INSTALL_COMMAND make install
# 如果不需要安装,可以设为空
# INSTALL_COMMAND ""
)
22.4.1 如何链接ExternalProject构建的库
这里有个大坑:ExternalProject_Add构建出来的target,不能直接用target_link_libraries链接。因为它在构建阶段才生成,配置阶段根本不知道它的存在。
解决办法是手动指定库文件路径:
# 先构建外部项目
ExternalProject_Add(my_third_party
...
INSTALL_DIR ${CMAKE_BINARY_DIR}/install
)
# 手动创建导入的target
add_library(my_third_party::my_third_party STATIC IMPORTED GLOBAL)
set_target_properties(my_third_party::my_third_party PROPERTIES
IMPORTED_LOCATION ${CMAKE_BINARY_DIR}/install/lib/libmy_third_party.a
)
# 添加头文件路径
target_include_directories(my_third_party::my_third_party INTERFACE
${CMAKE_BINARY_DIR}/install/include
)
# 最后,确保外部项目先构建
add_dependencies(my_app my_third_party)
target_link_libraries(my_app PRIVATE my_third_party::my_third_party)
我的经验:ExternalProject_Add最适合那些"一次构建,多次使用"的场景。比如交叉编译工具链里的某个库,你只需要构建一次,后面所有项目都能用。但如果你频繁修改外部库的代码,调试起来会很痛苦——每次都要重新构建整个外部项目。
22.5 三种方式的选择策略
说了这么多,到底该怎么选?我根据自己的项目经验,整理了一个决策表:
| 场景 | 推荐方式 | 原因 |
|---|---|---|
| 内部小库,源码已在本项目 | add_subdirectory | 最简单,直接链接 |
| 开源库,需要自动下载 | FetchContent | 配置阶段完成,使用方便 |
| 开源库,需要打补丁 | FetchContent + PATCH_COMMAND | 可以在下载后自动打补丁 |
| 非CMake项目 | ExternalProject_Add | 支持任意构建系统 |
| 交叉编译,需要独立构建 | ExternalProject_Add | 完全隔离,不污染主项目 |
| 大型依赖(如Qt、Boost) | find_package + 系统安装 | 不要用CMake去构建它们 |
22.6 实战:一个完整的嵌入式项目依赖管理
最后,我给你看一个我实际项目中的例子。这个项目用到了FreeRTOS、lwIP和mbedTLS,我用FetchContent统一管理:
cmake_minimum_required(VERSION 3.16)
project(my_iot_device C ASM)
# 设置嵌入式交叉编译器
set(CMAKE_C_COMPILER arm-none-eabi-gcc)
# 引入FetchContent
include(FetchContent)
# ---- FreeRTOS ----
FetchContent_Declare(
freertos
GIT_REPOSITORY https://github.com/FreeRTOS/FreeRTOS.git
GIT_TAG V10.5.1
GIT_SHALLOW TRUE
SOURCE_SUBDIR FreeRTOS/Source # 只取Source目录
)
FetchContent_MakeAvailable(freertos)
# ---- lwIP ----
FetchContent_Declare(
lwip
GIT_REPOSITORY https://github.com/lwip-tcpip/lwip.git
GIT_TAG STABLE-2_1_3_RELEASE
GIT_SHALLOW TRUE
)
FetchContent_MakeAvailable(lwip)
# ---- mbedTLS ----
FetchContent_Declare(
mbedtls
GIT_REPOSITORY https://github.com/Mbed-TLS/mbedtls.git
GIT_TAG v3.4.0
GIT_SHALLOW TRUE
)
FetchContent_MakeAvailable(mbedtls)
# ---- 我的应用 ----
add_executable(my_app
main.c
network.c
crypto.c
)
target_link_libraries(my_app PRIVATE
freertos::freertos
lwIP::lwIP
mbedtls::mbedtls
)
target_include_directories(my_app PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/config
)
小技巧:第一次配置时,CMake会把所有依赖下载到${CMAKE_BINARY_DIR}/_deps目录。如果你网络不好,可以手动下载后放到那里,CMake会跳过下载步骤。嗯,这个技巧在离线开发环境特别有用。
好了,关于CMake的外部依赖管理,核心就是这些。记住:add_subdirectory适合小项目,FetchContent适合大多数场景,ExternalProject_Add是最后的手段。选对了工具,你的构建系统会清爽很多。
公众号:蓝海资料掘金营,微信deep3321