13. CMake ExternalProject:ExternalProject_Add、下载、补丁、构建外部项目

说实话,很多C++项目做到一定规模,就绕不开一个尴尬的问题:依赖的第三方库怎么管理

你想想看,你的项目依赖了OpenSSL、zlib、或者某个GitHub上的小工具库。这些库有自己的构建系统,有自己的版本迭代。你总不能把它们的源码直接塞进你的仓库里吧?

嗯,这时候ExternalProject就派上用场了。

13.1 ExternalProject 是什么?

ExternalProject是CMake官方提供的一个模块。它的核心能力就一句话:在构建你的项目时,自动去下载、打补丁、配置、编译、安装一个外部项目

我个人习惯把它叫做“CMake的包下载器+构建器”。它不像vcpkg或Conan那样是一个完整的包管理器,但它足够轻量,而且完全由CMake控制,不需要额外安装任何工具。

我在项目中遇到过好几次这样的场景:客户的环境不能联网,不能装包管理器,但项目又依赖了七八个第三方库。最后我全靠ExternalProject把依赖全部打包进构建流程,客户只需要执行cmake和make,所有依赖自动搞定。

核心思想: ExternalProject 让你的构建系统具备“自给自足”的能力。它把依赖的获取和构建,变成了你项目构建流程的一部分。

13.2 ExternalProject_Add 的基本用法

先看一个最简单的例子。假设我们要下载一个叫 tinyxml2 的库:

include(ExternalProject)

ExternalProject_Add(
    tinyxml2
    GIT_REPOSITORY    https://github.com/leethomason/tinyxml2.git
    GIT_TAG           master
    CMAKE_ARGS        -DCMAKE_INSTALL_PREFIX=${CMAKE_BINARY_DIR}/external
    INSTALL_DIR       ${CMAKE_BINARY_DIR}/external
)

这段代码做了什么?

  • GIT_REPOSITORY:指定从哪里下载源码
  • GIT_TAG:指定分支或标签,我建议这里用具体的版本号,别用master,否则哪天上游一改,你的构建就炸了
  • CMAKE_ARGS:传递给外部项目的CMake参数
  • INSTALL_DIR:安装路径

执行cmake之后,ExternalProject会在构建目录下自动执行git clone、cmake、make、make install。整个过程对用户来说是透明的。

小技巧: 如果你不想每次都重新下载,可以设置 DOWNLOAD_NO_PROGRESS=1 来减少输出噪音。另外,LOG_DOWNLOAD=1 可以把下载日志单独存到文件里,方便排查问题。

13.3 下载方式:不止是Git

ExternalProject支持多种下载方式。我整理了一张表:

下载方式 参数前缀 适用场景
Git GIT_ 开源项目、GitHub仓库
URL URL tar.gz/zip压缩包
SVN SVN_ 老项目、企业内部仓库
本地目录 SOURCE_DIR 源码已经在本地了

举个例子,如果你要下载一个压缩包:

ExternalProject_Add(
    mylib
    URL               https://example.com/mylib-1.2.3.tar.gz
    URL_HASH          SHA256=abc123...
    CONFIGURE_COMMAND ""
    BUILD_COMMAND     ""
    INSTALL_COMMAND   ""
)

这里我特别说一下 URL_HASH。我曾经因为没加这个参数,被坑过一次——下载的压缩包被中间人篡改了,编译出来的库行为诡异,查了两天才发现是下载的文件不对。从那以后,所有URL下载我必加哈希校验

13.4 打补丁:修改外部项目的源码

有时候你下载的外部项目,需要做一些小修改才能在你的环境里编译。比如改一个宏定义,或者修复一个兼容性问题。

ExternalProject提供了 PATCH_COMMAND 参数:

ExternalProject_Add(
    mylib
    GIT_REPOSITORY https://github.com/example/mylib.git
    GIT_TAG        v1.0
    PATCH_COMMAND  ${CMAKE_COMMAND} -E chdir <SOURCE_DIR>
                   git apply ${CMAKE_SOURCE_DIR}/patches/fix_win32.patch
)

这里 <SOURCE_DIR> 是ExternalProject内置的变量,代表外部项目的源码目录。你可以用 git applypatch 命令,甚至直接执行shell脚本来修改源码。

注意: 如果外部项目是从压缩包下载的(不是git仓库),那源码目录里没有.git目录。这时候不能用 git apply,得用 patch -p1 或者直接 sed 替换。

我个人习惯把补丁文件统一放在项目根目录的 patches/ 文件夹下,命名规则是 库名_描述.patch。这样时间久了也能一眼看出每个补丁是干什么的。

13.5 构建外部项目:控制构建过程

ExternalProject默认会执行 ./configure && make && make install(对于autotools项目)或者 cmake && make && make install(对于CMake项目)。

但你可以完全控制这个过程:

ExternalProject_Add(
    mylib
    URL               https://example.com/mylib.tar.gz
    CONFIGURE_COMMAND <SOURCE_DIR>/configure --prefix=<INSTALL_DIR>
    BUILD_COMMAND     make -j4
    INSTALL_COMMAND   make install
)

如果你不想执行某个步骤,把它设为空字符串就行:

CONFIGURE_COMMAND ""
BUILD_COMMAND     ""
INSTALL_COMMAND   ""

嗯,这里要注意:如果你把BUILD_COMMAND设为空,那外部项目就只下载不解压不编译。有时候你只是想拿到源码,自己手动编译,就可以这么干。

13.6 依赖管理:多个ExternalProject之间的顺序

如果你的项目依赖了A,A又依赖了B,那构建顺序就很重要了。ExternalProject提供了 DEPENDS 参数:

ExternalProject_Add(
    libB
    GIT_REPOSITORY https://github.com/example/libB.git
    GIT_TAG        v1.0
)

ExternalProject_Add(
    libA
    GIT_REPOSITORY https://github.com/example/libA.git
    GIT_TAG        v2.0
    DEPENDS        libB
)

ExternalProject_Add(
    myapp
    SOURCE_DIR     ${CMAKE_SOURCE_DIR}/src
    DEPENDS        libA
)

这样CMake会保证:先构建libB,再构建libA,最后构建myapp。如果你不写DEPENDS,那所有外部项目会并行构建,很可能libA在编译时找不到libB的头文件,直接报错。

经验之谈: 我建议把所有外部依赖的DEPENDS关系画清楚。哪怕你觉得“这个库肯定不会有依赖”,也写上。因为半年后你回来维护代码,看到DEPENDS链就能快速理解项目结构。

13.7 知识体系图

下面这张图展示了ExternalProject的核心工作流程:

ExternalProject 工作流程 1. 下载源码 Git / URL / SVN 2. 打补丁 PATCH_COMMAND 3. 配置 CONFIGURE_COMMAND 4. 构建 BUILD_COMMAND 5. 安装 INSTALL_COMMAND DEPENDS 依赖管理 控制构建顺序 确保依赖先构建 URL_HASH 校验 防止文件被篡改

从这张图可以看出来,ExternalProject的工作流是线性的:下载 → 打补丁 → 配置 → 构建 → 安装。每一步都可以自定义,也可以跳过。

13.8 实战中的避坑指南

最后分享几个我踩过的坑:

  • 缓存问题:ExternalProject默认会把下载的文件缓存到 CMAKE_BINARY_DIR 下的 _deps 目录。如果你改了补丁但没重新下载,它不会重新应用补丁。解决办法是删除 _deps 目录,或者手动设置 DOWNLOAD_NO_EXTRACT=1 强制重新解压。
  • 并行构建冲突:多个ExternalProject同时下载时,如果它们都写同一个缓存目录,可能会冲突。我建议给每个外部项目设置独立的 DOWNLOAD_DIRSOURCE_DIR
  • 跨平台路径:Windows和Linux的路径分隔符不一样。在 PATCH_COMMAND 里写路径时,最好用 ${CMAKE_COMMAND} -E 来做跨平台的文件操作,别直接写 cpcopy
我的习惯: 每次添加新的ExternalProject依赖,我都会先在本地跑一遍完整的 cmake --build .,确认下载、补丁、编译、安装都没问题,再提交代码。因为ExternalProject的报错信息有时候不太直观,提前验证能省很多排查时间。

好了,ExternalProject的核心内容就这些。说白了,它就是CMake给你的一把瑞士军刀——下载、打补丁、构建、安装,全都能干。虽然不如专门的包管理器那么智能,但胜在轻量、可控、不依赖外部工具。下次你的项目需要管理第三方依赖时,不妨试试它。


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