第21章 预编译与外部库:ExternalProject_Add()、FetchContent()、导入预编译库
嵌入式项目里,跟外部库打交道是家常便饭。说白了,就是你的代码要依赖别人写好的东西——可能是芯片厂商的SDK,可能是某个加密算法库,也可能是你自己团队之前编译好的静态库。怎么优雅地把这些外部依赖请进你的CMake项目?这就是今天要聊的核心。
我个人习惯把外部库分成三类:需要从源码编译的、需要从网络拉取的、以及已经编译好的二进制库。CMake给了我们三套不同的工具来应对。嗯,咱们一个一个说。
21.1 ExternalProject_Add():源码编译的老大哥
这个模块是CMake里最早用来处理外部项目的方案。它的工作模式很简单:在构建阶段,把外部源码下载下来,单独编译,然后链接到你的主项目里。
我在项目中遇到过最典型的场景——给STM32项目集成FreeRTOS。FreeRTOS的源码需要跟你的项目一起编译,但人家有自己的构建系统。用ExternalProject_Add(),就可以在CMake配置阶段把FreeRTOS拉下来,然后作为外部项目编译。
include(ExternalProject)
ExternalProject_Add(
freertos
GIT_REPOSITORY https://github.com/FreeRTOS/FreeRTOS.git
GIT_TAG V10.4.1
SOURCE_DIR ${CMAKE_SOURCE_DIR}/third_party/freertos
CONFIGURE_COMMAND ""
BUILD_COMMAND ""
INSTALL_COMMAND ""
)
# 然后手动指定头文件和源文件路径
set(FREERTOS_SRC
${CMAKE_SOURCE_DIR}/third_party/freertos/FreeRTOS/Source/tasks.c
${CMAKE_SOURCE_DIR}/third_party/freertos/FreeRTOS/Source/list.c
# ... 其他源文件
)
add_library(freertos STATIC ${FREERTOS_SRC})
target_include_directories(freertos PUBLIC
${CMAKE_SOURCE_DIR}/third_party/freertos/FreeRTOS/Source/include
)
注意:ExternalProject_Add()是在构建阶段执行的,不是配置阶段。这意味着你不能在同一个CMakeLists.txt里直接使用ExternalProject_Add()生成的目标。你需要用add_dependencies()来确保构建顺序。
为什么说它是老大哥?因为它功能最全——支持Git、SVN、URL下载、甚至本地目录。但缺点也很明显:配置和构建是分离的,用起来有点绕。我个人现在只在处理那些构建系统特别奇葩的第三方库时才用它。
21.2 FetchContent():现代CMake的优雅方案
CMake 3.11引入的FetchContent模块,说白了就是ExternalProject_Add()的升级版。它最大的改进是:在配置阶段就把依赖拉下来,然后你可以直接使用add_subdirectory()把它当成项目的一部分。
我建议你在新项目里优先考虑FetchContent。它让依赖管理变得特别清爽——你不需要操心构建顺序,也不需要手动指定源文件路径。
include(FetchContent)
FetchContent_Declare(
nlohmann_json
GIT_REPOSITORY https://github.com/nlohmann/json.git
GIT_TAG v3.10.5
)
FetchContent_MakeAvailable(nlohmann_json)
# 现在可以直接用了
target_link_libraries(my_app PRIVATE nlohmann_json::nlohmann_json)
你看,是不是简洁多了?FetchContent_MakeAvailable()会自动下载、配置、构建,然后把目标暴露给你。我曾经在一个物联网网关项目里,用FetchContent同时拉了MQTT库、JSON库和HTTP库,整个CMakeLists.txt也就几十行。
小技巧:如果你不想每次构建都重新下载,可以设置FETCHCONTENT_SOURCE_DIR_<uppercaseName>变量指向本地缓存目录。我一般会在项目根目录建一个.cache文件夹,专门放这些下载好的依赖。
21.3 导入预编译库:处理二进制文件
嵌入式项目里,你经常会遇到这种情况:芯片厂商给你一个.a文件和一个头文件,让你直接用。这时候既不需要ExternalProject_Add(),也不需要FetchContent()——你只需要告诉CMake这个库在哪。
导入预编译库的核心是创建IMPORTED目标。我刚开始用CMake时,总是直接link_directories()然后target_link_libraries()写库名。后来发现这种方式不够清晰,也不方便管理不同编译配置。
# 创建一个导入的静态库目标
add_library(my_prebuilt_lib STATIC IMPORTED GLOBAL)
# 设置库文件的路径,区分Debug和Release
set_target_properties(my_prebuilt_lib PROPERTIES
IMPORTED_LOCATION_DEBUG ${CMAKE_SOURCE_DIR}/lib/debug/libmylib.a
IMPORTED_LOCATION_RELEASE ${CMAKE_SOURCE_DIR}/lib/release/libmylib.a
)
# 设置头文件路径
target_include_directories(my_prebuilt_lib INTERFACE
${CMAKE_SOURCE_DIR}/include
)
# 使用的时候
target_link_libraries(my_app PRIVATE my_prebuilt_lib)
嗯,这里要注意一个坑:IMPORTED_LOCATION后面跟的配置类型(_DEBUG、_RELEASE)必须跟你项目的CMAKE_BUILD_TYPE匹配。我曾经因为Debug和Release混用,折腾了一下午才找到问题。
21.4 三种方案的对比与选择
我把这三种方案整理成了表格,方便你快速决策:
| 方案 | 适用场景 | 执行阶段 | 优点 | 缺点 |
|---|---|---|---|---|
| ExternalProject_Add() | 需要从源码编译,构建系统复杂 | 构建阶段 | 功能全面,支持各种下载方式 | 配置与构建分离,使用不便 |
| FetchContent() | 需要从源码编译,构建系统标准 | 配置阶段 | 简洁优雅,直接集成 | 需要CMake 3.11+ |
| 导入预编译库 | 已有二进制文件,无需编译 | 配置阶段 | 零编译,直接使用 | 需要手动管理不同配置 |
我个人现在的选择标准很简单:
- 如果库的构建系统是CMake,或者能简单适配——用FetchContent()
- 如果库的构建系统特别奇葩,或者需要打补丁——用ExternalProject_Add()
- 如果厂商直接给了.a/.so文件——用导入预编译库
21.5 实战:一个完整的嵌入式项目依赖管理
最后,我给你看一个我实际用过的例子。这是一个基于STM32的传感器采集项目,依赖了三个外部库:
cmake_minimum_required(VERSION 3.14)
project(sensor_collector C ASM)
# 1. 用FetchContent拉取CMSIS-DSP库
include(FetchContent)
FetchContent_Declare(
cmsis_dsp
GIT_REPOSITORY https://github.com/ARM-software/CMSIS-DSP.git
GIT_TAG v1.10.0
)
FetchContent_MakeAvailable(cmsis_dsp)
# 2. 用ExternalProject_Add处理厂商SDK(构建系统是Makefile)
include(ExternalProject)
ExternalProject_Add(
vendor_sdk
URL https://vendor.com/sdk/stm32f4_sdk.zip
SOURCE_DIR ${CMAKE_SOURCE_DIR}/third_party/vendor_sdk
CONFIGURE_COMMAND ""
BUILD_COMMAND make -C ${CMAKE_SOURCE_DIR}/third_party/vendor_sdk
INSTALL_COMMAND ""
)
add_dependencies(vendor_sdk_target vendor_sdk)
# 3. 导入预编译的加密库
add_library(crypto_lib STATIC IMPORTED GLOBAL)
set_target_properties(crypto_lib PROPERTIES
IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/lib/crypto/libcrypto.a
)
target_include_directories(crypto_lib INTERFACE
${CMAKE_SOURCE_DIR}/lib/crypto/include
)
# 主项目
add_executable(sensor_collector
main.c
sensor.c
uart.c
)
target_link_libraries(sensor_collector PRIVATE
cmsis_dsp
crypto_lib
)
# 确保厂商SDK先构建好
add_dependencies(sensor_collector vendor_sdk)
你看,三种方案在一个项目里共存,各司其职。CMSIS-DSP用FetchContent直接集成,厂商SDK用ExternalProject_Add单独构建,加密库直接导入二进制文件。这就是实际项目中常见的混合使用方式。
避坑指南:我曾经在一个项目里,用FetchContent拉取了一个很大的库,结果每次cmake配置都要等好久。后来发现可以设置FETCHCONTENT_QUIET为OFF来查看下载进度,或者用FETCHCONTENT_UPDATES_DISCONNECTED跳过更新检查。这些小参数,关键时刻能救命。
好了,关于预编译和外部库的内容就聊到这儿。这三种方案你可以在项目里灵活组合,没有银弹,只有最适合当前场景的选择。
公众号:蓝海资料掘金营,微信deep3321