19、ExternalProject与交叉编译:使用ExternalProject_Add管理外部项目的交叉编译

说实话,做嵌入式开发最头疼的事情之一,就是管理那些第三方库。你辛辛苦苦配好了交叉编译工具链,结果要用的库还得手动下载、打补丁、配置、编译……一套流程下来,头发都掉不少。

我个人习惯用 ExternalProject_Add 来解决这个问题。它就像个自动化管家,能帮我们把外部项目的下载、配置、编译、安装全部管起来。今天我们就聊聊,怎么让它跟交叉编译配合好。

ExternalProject_Add 是什么?

说白了,ExternalProject_Add 是 CMake 提供的一个模块。它允许你在构建当前项目时,自动去处理外部依赖项目。你只需要告诉它:

  • 去哪里下载源码
  • 用什么配置方式(CMake、autotools、makefile 都行)
  • 编译选项是什么
  • 安装到哪里

剩下的,CMake 帮你搞定。我在项目中遇到过好几次这样的情况:客户给的 SDK 里有一堆第三方库,每个都要手动编译。用上 ExternalProject_Add 之后,一个 cmake --build 命令全部搞定,省心多了。

交叉编译的核心问题

交叉编译跟本地编译最大的区别是什么?

工具链不一样。你本地用的是 gcc,目标板用的是 arm-linux-gnueabihf-gcc。如果 ExternalProject_Add 不知道这个区别,它就会用本地编译器去编译外部项目,结果生成的可执行文件在目标板上跑不了。

嗯,这里要注意:ExternalProject_Add 默认不会自动继承你当前项目的 CMake 工具链设置。你得手动把交叉编译相关的变量传进去。

实战:给 ExternalProject_Add 传递交叉编译参数

假设我们有一个主项目,需要依赖一个叫 libfoo 的外部库。这个库也是用 CMake 构建的。我们的主项目已经配置好了交叉编译工具链,现在要让 libfoo 也用同样的工具链编译。

代码大概是这样的:

include(ExternalProject)

set(EXTERNAL_INSTALL_DIR ${CMAKE_BINARY_DIR}/external_install)

ExternalProject_Add(libfoo
    GIT_REPOSITORY    https://github.com/example/libfoo.git
    GIT_TAG           v1.2.3
    SOURCE_DIR        ${CMAKE_BINARY_DIR}/libfoo-src
    BINARY_DIR        ${CMAKE_BINARY_DIR}/libfoo-build
    INSTALL_DIR       ${EXTERNAL_INSTALL_DIR}
    CMAKE_ARGS
        -DCMAKE_TOOLCHAIN_FILE=${CMAKE_TOOLCHAIN_FILE}
        -DCMAKE_INSTALL_PREFIX=${EXTERNAL_INSTALL_DIR}
        -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE}
        -DCMAKE_C_COMPILER=${CMAKE_C_COMPILER}
        -DCMAKE_CXX_COMPILER=${CMAKE_CXX_COMPILER}
        -DCMAKE_SYSROOT=${CMAKE_SYSROOT}
    BUILD_ALWAYS       OFF
)

看到 CMAKE_ARGS 那块了吗?我把当前项目的 CMAKE_TOOLCHAIN_FILE 直接传进去了。这样 libfoo 在配置时就会加载同一个工具链文件,交叉编译的设置就保持一致了。

关键点:一定要把 CMAKE_TOOLCHAIN_FILE 传进去。这是保证外部项目使用正确交叉工具链的核心。

处理非 CMake 项目

不是所有外部项目都用 CMake。有些老项目用的是 autotools 或者纯 Makefile。怎么办?

ExternalProject_Add 提供了 CONFIGURE_COMMANDBUILD_COMMANDINSTALL_COMMAND 这些选项,你可以自定义每一步的命令。

举个例子,一个用 autotools 的库:

ExternalProject_Add(libbaz
    URL               https://example.com/libbaz-1.0.tar.gz
    SOURCE_DIR        ${CMAKE_BINARY_DIR}/libbaz-src
    CONFIGURE_COMMAND
        ${CMAKE_BINARY_DIR}/libbaz-src/configure
        --host=arm-linux-gnueabihf
        --prefix=${EXTERNAL_INSTALL_DIR}
        CC=${CMAKE_C_COMPILER}
        CXX=${CMAKE_CXX_COMPILER}
        --with-sysroot=${CMAKE_SYSROOT}
    BUILD_COMMAND      make -j4
    INSTALL_COMMAND    make install
)

这里我手动指定了 --host 参数,告诉 configure 脚本目标架构是 ARM。同时把编译器路径也传进去了。我曾经在一个项目里用这种方式处理了七八个 autotools 库,虽然配置起来麻烦点,但一次配好,后面就自动化了。

依赖顺序与构建流程

外部项目编译好了,主项目怎么链接它?

你需要告诉主项目:先编译外部项目,再编译主项目。用 add_dependencies 可以实现:

add_dependencies(my_main_app libfoo libbaz)

然后,主项目要能找到外部项目的头文件和库文件。通常的做法是:

include_directories(${EXTERNAL_INSTALL_DIR}/include)
link_directories(${EXTERNAL_INSTALL_DIR}/lib)

target_link_libraries(my_main_app
    ${EXTERNAL_INSTALL_DIR}/lib/libfoo.a
    ${EXTERNAL_INSTALL_DIR}/lib/libbaz.a
)

注意:如果外部项目是动态库(.so),交叉编译时要注意目标板上的运行时链接路径。你可能需要设置 CMAKE_INSTALL_RPATH 或者把库拷贝到目标板的 /usr/lib 下。

知识结构图

下面这张图展示了 ExternalProject 与交叉编译配合时的核心流程:

主项目 (CMakeLists.txt) ExternalProject_Add(libfoo) CMAKE_ARGS -DCMAKE_TOOLCHAIN_FILE=... CONFIGURE_COMMAND --host=arm-linux ... 外部项目编译 (交叉工具链)

从图中可以看到,主项目通过 ExternalProject_Add 管理外部项目,关键是要把交叉编译的参数传递过去。无论是 CMake 项目还是 autotools 项目,原理都一样:让外部项目知道它该用什么编译器、什么目标架构。

避坑指南

我在这上面踩过不少坑,分享几个典型的:

  • 缓存问题:ExternalProject 默认会缓存构建目录。如果你改了工具链文件,记得删掉 BINARY_DIR 下的内容再重新构建。不然 CMake 可能不会重新配置。
  • 路径空格:交叉编译工具链的路径里如果有空格(比如 Windows 上的 Program Files),记得用双引号包起来。我曾经因为这个折腾了半天。
  • Sysroot 设置:如果外部项目依赖了目标板上的系统库(比如 libc、libm),一定要设置 CMAKE_SYSROOT。不然链接器会去本地找,找到的库跟目标板不匹配。
  • 并行编译:BUILD_COMMAND make -j${N} 可以加速编译。但要注意,有些老旧的 Makefile 不支持并行编译,会报错。这时候老老实实单线程编译吧。

小技巧:如果你不确定外部项目是否真的用了交叉编译器,可以在 CMAKE_ARGS 里加一个 -DCMAKE_VERBOSE_MAKEFILE=ON。这样编译时会打印出完整的编译命令,一眼就能看出来用的是不是交叉编译器。

总结一下

ExternalProject_Add 配合交叉编译,核心就三件事:

  1. 把当前项目的工具链文件传给外部项目
  2. 根据外部项目的构建系统,选择合适的参数传递方式
  3. 处理好依赖顺序和链接路径

做到这三点,你的交叉编译环境就能自动化起来。我个人觉得,这比手动管理外部项目要高效得多。尤其是项目大了、依赖多了之后,省下的时间可不是一星半点。

好了,今天就聊到这里。希望这些经验对你有帮助。


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