18、FetchContent与交叉编译:使用FetchContent下载并交叉编译第三方源码
说实话,很多做嵌入式开发的朋友,一听到「交叉编译第三方库」就头大。我自己也经历过那种痛苦——手动下载源码包、解压、配置、打补丁、编译,折腾半天发现版本不对,或者编译选项没传对,又得重来一遍。
CMake 的 FetchContent 模块,说白了就是帮你自动干这些脏活累活的。它能在配置阶段自动下载源码,然后直接纳入你的构建体系。配合工具链文件,交叉编译第三方库就变成了一件「声明式」的事情——你只需要告诉它「我要什么、用什么工具链编译」,剩下的交给 CMake。
18.1 FetchContent 的基本用法
先看一个最简单的例子。假设我们要在项目中引入 zlib 这个压缩库:
include(FetchContent)
FetchContent_Declare(
zlib
GIT_REPOSITORY https://github.com/madler/zlib.git
GIT_TAG v1.2.13
)
FetchContent_MakeAvailable(zlib)
就这么几行,CMake 会在配置阶段把 zlib 的源码下载下来,然后调用 add_subdirectory 把它加入构建。你之后可以直接 target_link_libraries(my_app zlib),就像链接自己项目里的库一样自然。
FetchContent_Declare 和 FetchContent_MakeAvailable 分开写。这样在调试时,可以单独查看下载状态,也方便加一些条件判断。
18.2 交叉编译时的问题在哪?
问题来了。上面那段代码在本地编译没问题,但一换成交叉编译,第三方库的 CMakeLists.txt 可能就不听话了。
为什么会这样?因为 FetchContent 下载下来的第三方库,会继承你主项目的 CMake 变量和工具链设置。但有些第三方库的构建脚本写得不规范,它会自己探测编译器、自己找头文件路径,结果就绕过了你的工具链文件。
我遇到过最典型的一个坑:某个库在交叉编译时,自己偷偷调用了 find_package 找本机的 libpng,结果链接了宿主机的库,编译出来的二进制在目标板上跑不起来。
18.3 正确做法:传递工具链与缓存变量
要让 FetchContent 下载的第三方库也走交叉编译,核心思路就一句话:把主项目的工具链变量和关键缓存变量,显式传递给第三方库。
具体做法如下:
# 假设你已经通过 -DCMAKE_TOOLCHAIN_FILE 指定了工具链文件
include(FetchContent)
# 关键:把 CMAKE_TOOLCHAIN_FILE 和 CMAKE_CROSSCOMPILING 传下去
set(FETCHCONTENT_CMAKE_ARGS
-DCMAKE_TOOLCHAIN_FILE=${CMAKE_TOOLCHAIN_FILE}
-DCMAKE_CROSSCOMPILING=${CMAKE_CROSSCOMPILING}
-DCMAKE_SYSROOT=${CMAKE_SYSROOT}
-DCMAKE_FIND_ROOT_PATH=${CMAKE_FIND_ROOT_PATH}
)
FetchContent_Declare(
zlib
GIT_REPOSITORY https://github.com/madler/zlib.git
GIT_TAG v1.2.13
)
# 在 MakeAvailable 之前,设置缓存变量
set(FETCHCONTENT_CMAKE_ARGS "${FETCHCONTENT_CMAKE_ARGS}" CACHE INTERNAL "")
FetchContent_MakeAvailable(zlib)
project() 命令,这会重置一些变量。如果你发现工具链没生效,可以尝试在 FetchContent_Declare 之后、MakeAvailable 之前,手动设置 CMAKE_PROJECT_INCLUDE 指向一个强制加载工具链的脚本。
18.4 实战:交叉编译 libcurl 到 ARM 平台
拿 libcurl 举个例子。这个库依赖 openssl 和 zlib,交叉编译时很容易出问题。我的做法是分三步:
- 先下载并交叉编译
zlib和openssl - 再下载
libcurl,并告诉它依赖库的安装路径 - 最后链接到主项目
代码结构如下:
include(FetchContent)
# 第一步:zlib
FetchContent_Declare(
zlib
GIT_REPOSITORY https://github.com/madler/zlib.git
GIT_TAG v1.2.13
CMAKE_ARGS
-DCMAKE_TOOLCHAIN_FILE=${CMAKE_TOOLCHAIN_FILE}
-DCMAKE_INSTALL_PREFIX=${CMAKE_BINARY_DIR}/_install
)
FetchContent_MakeAvailable(zlib)
# 第二步:openssl(注意 openssl 不是 CMake 项目,需要 ExternalProject)
# 这里省略,假设已经编译好并安装到了 _install 目录
# 第三步:libcurl
FetchContent_Declare(
curl
GIT_REPOSITORY https://github.com/curl/curl.git
GIT_TAG curl-8_4_0
CMAKE_ARGS
-DCMAKE_TOOLCHAIN_FILE=${CMAKE_TOOLCHAIN_FILE}
-DCMAKE_INSTALL_PREFIX=${CMAKE_BINARY_DIR}/_install
-DZLIB_ROOT=${CMAKE_BINARY_DIR}/_install
-DOPENSSL_ROOT_DIR=${CMAKE_BINARY_DIR}/_install
-DBUILD_TESTING=OFF
-DBUILD_CURL_EXE=OFF
)
FetchContent_MakeAvailable(curl)
# 主项目链接
target_link_libraries(my_app PRIVATE CURL::libcurl)
这里有个细节:CMAKE_ARGS 里传的 ZLIB_ROOT 和 OPENSSL_ROOT_DIR,就是告诉 libcurl 的构建系统「你的依赖已经编译好了,别自己再去下载了」。
18.5 避坑指南:我踩过的三个坑
第一个坑:我曾经用 FetchContent 下载了一个库,它内部用了 ExternalProject 来下载另一个库。结果 ExternalProject 在交叉编译时完全无视了工具链文件,因为它是在构建阶段执行的,不是配置阶段。解决方案?要么把那个内部依赖也改成 FetchContent,要么手动传 CMAKE_TOOLCHAIN_FILE 给 ExternalProject 的 CMAKE_ARGS。
第二个坑:有些库的 CMakeLists.txt 里写了 find_package,而且没有指定 NO_DEFAULT_PATH。在交叉编译时,它会先找到宿主机的库。我当时的解决办法是在工具链文件里设置 CMAKE_FIND_ROOT_PATH_MODE_PACKAGE 为 ONLY,强制只搜索交叉编译的 sysroot。
第三个坑:版本锁定。有一次我忘了指定 GIT_TAG,结果 FetchContent 拉下来的是 master 分支的最新代码,第二天构建就挂了,因为上游改了 API。从那以后,我所有 FetchContent_Declare 都强制写死 GIT_TAG 或 URL_HASH。
18.6 知识体系图
下面这张图总结了 FetchContent 与交叉编译的核心流程:
18.7 总结
FetchContent 配合交叉编译,核心就三点:
- 显式传递工具链变量:
CMAKE_TOOLCHAIN_FILE、CMAKE_CROSSCOMPILING、CMAKE_SYSROOT一个都不能少 - 锁定版本:永远指定
GIT_TAG或URL_HASH,别偷懒 - 处理依赖链:如果第三方库还有自己的依赖,确保它们也走交叉编译路径
嗯,做到这三点,你就能在交叉编译项目里,像喝水一样自然地引入第三方库了。