13. CMake ExternalProject:ExternalProject_Add、下载、补丁、构建外部项目
说实话,很多C++项目做到一定规模,就绕不开一个尴尬的问题:依赖的第三方库怎么管理?
你想想看,你的项目依赖了OpenSSL、zlib、或者某个GitHub上的小工具库。这些库有自己的构建系统,有自己的版本迭代。你总不能把它们的源码直接塞进你的仓库里吧?
嗯,这时候ExternalProject就派上用场了。
13.1 ExternalProject 是什么?
ExternalProject是CMake官方提供的一个模块。它的核心能力就一句话:在构建你的项目时,自动去下载、打补丁、配置、编译、安装一个外部项目。
我个人习惯把它叫做“CMake的包下载器+构建器”。它不像vcpkg或Conan那样是一个完整的包管理器,但它足够轻量,而且完全由CMake控制,不需要额外安装任何工具。
我在项目中遇到过好几次这样的场景:客户的环境不能联网,不能装包管理器,但项目又依赖了七八个第三方库。最后我全靠ExternalProject把依赖全部打包进构建流程,客户只需要执行cmake和make,所有依赖自动搞定。
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 apply、patch 命令,甚至直接执行shell脚本来修改源码。
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的头文件,直接报错。
13.7 知识体系图
下面这张图展示了ExternalProject的核心工作流程:
从这张图可以看出来,ExternalProject的工作流是线性的:下载 → 打补丁 → 配置 → 构建 → 安装。每一步都可以自定义,也可以跳过。
13.8 实战中的避坑指南
最后分享几个我踩过的坑:
- 缓存问题:ExternalProject默认会把下载的文件缓存到
CMAKE_BINARY_DIR下的_deps目录。如果你改了补丁但没重新下载,它不会重新应用补丁。解决办法是删除_deps目录,或者手动设置DOWNLOAD_NO_EXTRACT=1强制重新解压。 - 并行构建冲突:多个ExternalProject同时下载时,如果它们都写同一个缓存目录,可能会冲突。我建议给每个外部项目设置独立的
DOWNLOAD_DIR和SOURCE_DIR。 - 跨平台路径:Windows和Linux的路径分隔符不一样。在
PATCH_COMMAND里写路径时,最好用${CMAKE_COMMAND} -E来做跨平台的文件操作,别直接写cp或copy。
cmake --build .,确认下载、补丁、编译、安装都没问题,再提交代码。因为ExternalProject的报错信息有时候不太直观,提前验证能省很多排查时间。
好了,ExternalProject的核心内容就这些。说白了,它就是CMake给你的一把瑞士军刀——下载、打补丁、构建、安装,全都能干。虽然不如专门的包管理器那么智能,但胜在轻量、可控、不依赖外部工具。下次你的项目需要管理第三方依赖时,不妨试试它。
公众号:蓝海资料掘金营,微信deep3321