89、使用ExternalProject:管理外部项目的下载、配置和构建。

说实话,做C++项目最头疼的事之一,就是处理第三方依赖。

你想想看,项目里用到了Google Test、OpenSSL、或者某个GitHub上的开源库。手动下载?版本对不上。放到仓库里?体积爆炸。写脚本编译?不同平台又不一样。

我个人习惯用CMake的ExternalProject模块来解决这个问题。它就像个智能管家,帮你把外部项目下载、配置、编译、安装,一气呵成。今天我们就来聊聊这个模块怎么用。

ExternalProject是什么?

ExternalProject是CMake内置的一个模块。它的核心能力是:在构建时自动拉取外部源码,并作为独立项目构建

说白了,它会在你的构建目录里,再开一个“子工地”,把外部项目当成一个独立的CMake项目来编译。最后把产出的库和头文件,链接到你的主项目里。

核心流程:

下载源码 → 配置项目 → 编译构建 → 安装产出 → 主项目引用

基本用法:三步搞定

我们先看一个最简单的例子。假设你的项目需要Google Test。

include(ExternalProject)

ExternalProject_Add(
    googletest
    GIT_REPOSITORY    https://github.com/google/googletest.git
    GIT_TAG           release-1.12.1
    CMAKE_ARGS        -DCMAKE_INSTALL_PREFIX=${CMAKE_BINARY_DIR}/third_party
    UPDATE_COMMAND    ""
)

这段代码做了几件事:

  • 从GitHub克隆googletest仓库
  • 切换到release-1.12.1标签
  • 用CMake配置它,安装到指定目录
  • 禁止自动更新(UPDATE_COMMAND设为空)

嗯,这里要注意:UPDATE_COMMAND "" 是我强烈建议加的。不加的话,每次构建都会检查Git更新,浪费时间不说,还容易出问题。

如何引用外部项目的库?

下载编译完了,怎么用呢?你需要告诉主项目去哪里找头文件和库文件。

# 设置查找路径
set(GTEST_INCLUDE_DIR ${CMAKE_BINARY_DIR}/third_party/include)
set(GTEST_LIBRARY ${CMAKE_BINARY_DIR}/third_party/lib/libgtest.a)

# 添加依赖关系
add_dependencies(my_project googletest)

# 链接
target_include_directories(my_project PRIVATE ${GTEST_INCLUDE_DIR})
target_link_libraries(my_project PRIVATE ${GTEST_LIBRARY})

这里有个坑:一定要加add_dependencies。不加的话,CMake不会保证googletest先编译完。我在项目中遇到过链接错误,查了半天才发现是外部库还没编译好。

更复杂的场景:自定义下载方式

不是所有项目都在GitHub上。有时候你需要从URL下载压缩包,或者用SVN。

下载方式 关键参数 示例
Git GIT_REPOSITORY, GIT_TAG GIT_REPOSITORY https://github.com/xxx/yyy.git
URL URL, URL_HASH URL https://example.com/lib.zip
SVN SVN_REPOSITORY, SVN_REVISION SVN_REPOSITORY svn://example.com/trunk
本地目录 SOURCE_DIR SOURCE_DIR ${CMAKE_SOURCE_DIR}/libs

我个人最常用URL方式。因为GitHub有时候访问慢,我会把依赖打成压缩包放到内部服务器上,用URL_HASH校验完整性。

ExternalProject_Add(
    my_lib
    URL               https://internal-server/libs/mylib-2.0.tar.gz
    URL_HASH          SHA256=abc123...
    CMAKE_ARGS        -DCMAKE_INSTALL_PREFIX=${CMAKE_BINARY_DIR}/third_party
)

避坑指南:我曾经踩过的坑

坑1:重复下载

我曾经在CI上发现每次构建都重新下载外部项目。后来发现是构建目录被清理了。解决方案:把下载缓存放到外部目录。

set(EP_BASE ${CMAKE_SOURCE_DIR}/.external_project_cache CACHE PATH "ExternalProject cache")
set(CMAKE_EP_BASE ${EP_BASE})

坑2:多平台路径问题

Windows和Linux的库文件后缀不一样。我建议用find_library来动态查找,而不是硬编码路径。

find_library(GTEST_LIBRARY
    NAMES gtest gtest.lib
    PATHS ${CMAKE_BINARY_DIR}/third_party/lib
    NO_DEFAULT_PATH
)

小技巧:并行构建

ExternalProject默认是串行构建的。如果你的机器核多,可以设置:

ExternalProject_Add_Step(my_lib build
    COMMAND cmake --build . -- -j$(nproc)
    DEPENDEES configure
)

这样能大幅缩短构建时间。

知识体系图

下面这张图展示了ExternalProject的核心工作流程和关键决策点:

ExternalProject 工作流程 1. 下载源码 2. 配置项目 3. 编译构建 4. 安装产出 5. 主项目引用 关键参数 • GIT_REPOSITORY • CMAKE_ARGS • CMAKE_INSTALL_PREFIX • UPDATE_COMMAND • SOURCE_DIR ⚠ 注意事项 必须加 add_dependencies 确保构建顺序 | 建议设置 UPDATE_COMMAND 为空

实战:完整示例

最后,我给你一个可以直接用的模板。这个模板处理了大部分常见场景:

# CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(MyProject)

include(ExternalProject)

# 设置缓存目录,避免重复下载
set(EP_BASE ${CMAKE_SOURCE_DIR}/.ep_cache CACHE PATH "ExternalProject cache")
set(CMAKE_EP_BASE ${EP_BASE})

# 定义外部项目
ExternalProject_Add(
    spdlog
    GIT_REPOSITORY  https://github.com/gabime/spdlog.git
    GIT_TAG         v1.12.0
    CMAKE_ARGS
        -DCMAKE_INSTALL_PREFIX=${CMAKE_BINARY_DIR}/third_party
        -DSPDLOG_BUILD_EXAMPLE=OFF
        -DSPDLOG_BUILD_TESTS=OFF
    UPDATE_COMMAND  ""
)

# 查找安装后的库
set(SPDLOG_INCLUDE_DIR ${CMAKE_BINARY_DIR}/third_party/include)
set(SPDLOG_LIBRARY ${CMAKE_BINARY_DIR}/third_party/lib/libspdlog.a)

# 主项目
add_executable(my_app main.cpp)

# 添加依赖
add_dependencies(my_app spdlog)

# 链接
target_include_directories(my_app PRIVATE ${SPDLOG_INCLUDE_DIR})
target_link_libraries(my_app PRIVATE ${SPDLOG_LIBRARY})

我的建议:

如果你的项目依赖比较多,可以考虑用ExternalProject_Add配合FetchContent。FetchContent更适合小依赖,ExternalProject更适合大型外部项目。两者可以互补使用。

好了,关于ExternalProject的核心用法就这些。记住三个要点:下载、构建、依赖顺序。把这三点处理好,外部依赖管理就不再是头疼的事了。

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