第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跳过更新检查。这些小参数,关键时刻能救命。

外部库集成方案决策流程 外部库来源 有源码,构建系统复杂 有源码,构建系统标准 只有二进制文件 ExternalProject_Add() 构建阶段执行 FetchContent() 配置阶段执行 导入预编译库 IMPORTED目标 ⚠ 注意构建顺序 需用add_dependencies() ✅ 推荐首选 需要CMake 3.11+ ⚠ 区分Debug/Release IMPORTED_LOCATION_DEBUG 根据实际情况,三种方案可混合使用

好了,关于预编译和外部库的内容就聊到这儿。这三种方案你可以在项目里灵活组合,没有银弹,只有最适合当前场景的选择。


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