多读书多实践,勤思考善领悟

CMake 完整使用教程 之九 超级构建模式

本章的主要内容如下:

  • 使用超级级构建模式
  • 使用超级构建管理依赖项:Ⅰ.Boost库
  • 使用超级构建管理依赖项:Ⅱ.FFTW库
  • 使用超级构建管理依赖项:Ⅲ.Google Test框架
  • 使用超级构建支持项目

每个项目都需要处理依赖关系,使用CMake很容易查询这些依赖关系,是否存在于配置项目中。第3章,展示了如何找到安装在系统上的依赖项,到目前为止我们一直使用这种模式。但是,当不满足依赖关系,我们只能使配置失败,并向用户警告失败的原因。然而,使用CMake可以组织我们的项目,如果在系统上找不到依赖项,就可以自动获取和构建依赖项。本章将介绍和分析ExternalProject.cmakeFetchContent.cmake标准模块,及在超级构建模式中的使用。前者允许在构建时检索项目的依赖项,后者允许我们在配置时检索依赖项(CMake的3.11版本后添加)。使用超级构建模式,我们可以利用CMake作为包管理器:相同的项目中,将以相同的方式处理依赖项,无论依赖项在系统上是已经可用,还是需要重新构建。接下来的5个示例,将带您了解该模式,并展示如何使用它来获取和构建依赖关系。

NOTE:这两个模块都有大量的在线文档。ExternalProject.cmake,可以参考https://cmake.org/cmake/help/v3.5/module/ExternalProject.htmlFetchContent.cmake,可以参考https://cmake.org/cmake/help/v3.11/module/FetchContent.html

8.1 使用超级构建模式

NOTE:此示例代码可以在 https://github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-8/recipe-01 中找到,其中有一个C++示例。该示例在CMake 3.5版(或更高版本)中是有效的,并且已经在GNU/Linux、macOS和Windows上进行过测试。

本示例通过一个简单示例,介绍超级构建模式。我们将展示如何使用ExternalProject_Add命令来构建一个的“Hello, World”程序。

准备工作

本示例将从以下源代码(Hello-World.cpp)构建“Hello, World”可执行文件:

1
2
3
4
5
6
7
8
9
10
11
#include <cstdlib>
#include <iostream>
#include <string>

std::string say_hello() { return std::string("Hello, CMake superbuild world!"); }

int main()
{
std::cout << say_hello() << std::endl;
return EXIT_SUCCESS;
}

项目结构如下:

1
2
3
4
5
.
├── CMakeLists.txt
└── src
├── CMakeLists.txt
└── hello-world.cpp

具体实施

让我们看一下根目录下的CMakeLists.txt:

  1. 声明一个C++11项目,以及CMake最低版本:

    1
    2
    3
    4
    5
    6
    7
    cmake_minimum_required(VERSION 3.5 FATAL_ERROR)

    project(recipe-01 LANGUAGES CXX)

    set(CMAKE_CXX_STANDARD 11)
    set(CMAKE_CXX_EXTENSIONS OFF)
    set(CMAKE_CXX_STANDARD_REQUIRED ON)
  2. 为当前目录和底层目录设置EP_BASE目录属性:

    1
    set_property(DIRECTORY PROPERTY EP_BASE ${CMAKE_BINARY_DIR}/subprojects)
  3. 包括ExternalProject.cmake标准模块。该模块提供了ExternalProject_Add函数:

    1
    include(ExternalProject)
  4. “Hello, World”源代码通过调用ExternalProject_Add函数作为外部项目添加的。外部项目的名称为recipe-01_core:

    1
    ExternalProject_Add(${PROJECT_NAME}_core
  5. 使用SOURCE_DIR选项为外部项目设置源目录:

    1
    2
    SOURCE_DIR
    ${CMAKE_CURRENT_LIST_DIR}/src
  6. src子目录包含一个完整的CMake项目。为了配置和构建它,通过CMAKE_ARGS选项将适当的CMake选项传递给外部项目。例子中,只需要通过C++编译器和C++标准的要求即可:

    1
    2
    3
    4
    5
    CMAKE_ARGS
    -DCMAKE_CXX_COMPILER=${CMAKE_CXX_COMPILER}
    -DCMAKE_CXX_STANDARD=${CMAKE_CXX_STANDARD}
    -DCMAKE_CXX_EXTENSIONS=${CMAKE_CXX_EXTENSIONS}
    -DCMAKE_CXX_STANDARD_REQUIRED=${CMAKE_CXX_STANDARD_REQUIRED}
  7. 我们还设置了C++编译器标志。这些通过使用CMAKE_CACHE_ARGS选项传递到ExternalProject_Add中:

    1
    2
    CMAKE_CACHE_ARGS
    -DCMAKE_CXX_FLAGS:STRING=${CMAKE_CXX_FLAGS}
  8. 我们配置外部项目,使它进行构建:

    1
    2
    BUILD_ALWAYS
    1
  9. 安装步骤不会执行任何操作(我们将在第4节中重新讨论安装,在第10章中安装超级构建,并编写安装程序):

    1
    2
    3
    INSTALL_COMMAND
    ""
    )

现在,我们来看看src/CMakeLists.txt。由于我们将“Hello, World”源文件作为一个外部项目添加,这是一个独立项目的CMakeLists.txt文件:

  1. 这里声明CMake版本最低要求:

    1
    cmake_minimum_required(VERSION 3.5 FATAL_ERROR)
  2. 声明一个C++项目:

    1
    project(recipe-01_core LANGUAGES CXX)
  3. 最终,使用hello-world.cpp源码文件生成可执行目标hello-world

    1
    add_executable(hello-world hello-world.cpp)

配置构建项目:

1
2
3
$ mkdir -p build
$ cmake ..
$ cmake --build .

构建目录的结构稍微复杂一些,subprojects文件夹的内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
build/subprojects/
├── Build
│ └── recipe-01_core
│ ├── CMakeCache.txt
│ ├── CMakeFiles
│ ├── cmake_install.cmake
│ ├── hello-world
│ └── Makefile
├── Download
│ └── recipe-01_core
├── Install
│ └── recipe-01_core
├── Stamp
│ └── recipe-01_core
│ ├── recipe-01_core-configure
│ ├── recipe-01_core-done
│ ├── recipe-01_core-download
│ ├── recipe-01_core-install
│ ├── recipe-01_core-mkdir
│ ├── recipe-01_core-patch
│ └── recipe-01_core-update
└── tmp
└── recipe-01_core
├── recipe-01_core-cache-.cmake
├── recipe-01_core-cfgcmd.txt
└── recipe-01_core-cfgcmd.txt.in

recipe-01_core已经构建到build/subprojects子目录中,称为Build/recipe-01_core(这是我们设置的EP_BASE)。

hello-world可执行文件在Build/recipe-01_core下创建,其他子文件夹tmp/recipe-01_coreStamp/recipe-01_core包含临时文件,比如:CMake缓存脚本recipe-01_core-cache-.cmake和已执行的外部构建项目的各步骤的时间戳文件。

工作原理

ExternalProject_Add命令可用于添加第三方源。然而,第一个例子展示了,如何将自己的项目,分为不同CMake项目的集合管理。本例中,主CMakeLists.txt和子CMakeLists.txt都声明了一个CMake项目,它们都使用了project命令。

ExternalProject_Add有许多选项,可用于外部项目的配置和编译等所有方面。这些选择可以分为以下几类:

  • Directory:它们用于调优源码的结构,并为外部项目构建目录。本例中,我们使用SOURCE_DIR选项让CMake知道源文件在${CMAKE_CURRENT_LIST_DIR}/src文件夹中。用于构建项目和存储临时文件的目录,也可以在此类选项或目录属性中指定。通过设置EP_BASE目录属性,CMake将按照以下布局为各个子项目设置所有目录:

    1
    2
    3
    4
    5
    6
    TMP_DIR = <EP_BASE>/tmp/<name>
    STAMP_DIR = <EP_BASE>/Stamp/<name>
    DOWNLOAD_DIR = <EP_BASE>/Download/<name>
    SOURCE_DIR = <EP_BASE>/Source/<name>
    BINARY_DIR = <EP_BASE>/Build/<name>
    INSTALL_DIR = <EP_BASE>/Install/<name>
  • Download:外部项目的代码可能需要从在线存储库或资源处下载。

  • UpdatePatch:可用于定义如何更新外部项目的源代码或如何应用补丁。

  • Configure:默认情况下,CMake会假定外部项目是使用CMake配置的。如下面的示例所示,我们并不局限于这种情况。如果外部项目是CMake项目,ExternalProject_Add将调用CMake可执行文件,并传递选项。对于当前的示例,我们通过CMAKE_ARGSCMAKE_CACHE_ARGS选项传递配置参数。前者作为命令行参数直接传递,而后者通过CMake脚本文件传递。示例中,脚本文件位于build/subprojects/tmp/recipe-01_core/recipe-01_core- cache-.cmake。然后,配置如以下所示:

    1
    2
    3
    $ cmake -DCMAKE_CXX_COMPILER=g++ -DCMAKE_CXX_STANDARD=11
    -DCMAKE_CXX_EXTENSIONS=OFF -DCMAKE_CXX_STANDARD_REQUIRED=ON
    -C/home/roberto/Workspace/robertodr/cmake-cookbook/chapter-08/recipe-01/cxx-example/build/subprojects/tmp/recipe-01_core/recipe-01_core-cache-.cmake "-GUnix Makefiles" /home/roberto/Workspace/robertodr/cmake-cookbook/chapter-08/recipe-01/cxx-example/src
  • Build:可用于调整外部项目的实际编译。我们的示例使用BUILD_ALWAYS选项确保外部项目总会重新构建。

  • Install:这些选项用于配置应该如何安装外部项目。我们的示例将INSTALL_COMMAND保留为空,我们将在第10章(编写安装程序)中更详细地讨论与CMake的安装。

  • Test:为基于源代码构建的软件运行测试总是不错的想法。ExternalProject_Add的这类选项可以用于此目的。我们的示例没有使用这些选项,因为“Hello, World”示例没有任何测试,但是在第5节中,您将管理超级构建的项目,届时将触发一个测试步骤。

ExternalProject.cmake定义了ExternalProject_Get_Property命令,该命令对于检索外部项目的属性非常有用。外部项目的属性是在首次调用ExternalProject_Add命令时设置的。例如,在配置recipe-01_core时,检索要传递给CMake的参数可以通过以下方法实现:

1
2
ExternalProject_Get_Property(${PROJECT_NAME}_core CMAKE_ARGS)
message(STATUS "CMAKE_ARGS of ${PROJECT_NAME}_core ${CMAKE_ARGS}")

NOTE:ExternalProject_Add的完整选项列表可以在CMake文档中找到:https://cmake.org/cmake/help/v3.5/module/ExternalProject.html#command:ExternalProject_Add

更多信息

下面的示例中,我们将详细讨论ExternalProject_Add命令的灵活性。然而,有时我们希望使用的外部项目可能需要执行额外的步骤。由于这个原因,ExternalProject.cmake模块定义了以下附加命令:

  1. ExternalProject_Add_Step: 当添加了外部项目,此命令允许将附加的命令作为自定义步骤锁定在其上。参见:https://cmake.org/cmake/help/v3.5/module/externalproject.htm#command:externalproject_add_step
  2. ExternalProject_Add_StepTargets:允许将外部项目中的步骤(例如:构建和测试步骤)定义为单独的目标。这意味着可以从完整的外部项目中单独触发这些步骤,并允许对项目中的复杂依赖项,进行细粒度控制。参见:https://cmake.org/cmake/help/v3.5/module/ExternalProject.htm#command:externalproject_add_steptargets
  3. ExternalProject_Add_StepDependencies:外部项目的步骤有时可能依赖于外部目标,而这个命令的设计目的就是处理这些情况。参见:https://cmake.org/cmake/help/v3.5/module/ExternalProject.html#command:externalproject_add_stepdependencies

8.2 使用超级构建管理依赖项:Ⅰ.Boost库

NOTE:此示例代码可以在 https://github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-8/recipe-02 中找到,其中有一个C++示例。该示例在CMake 3.5版(或更高版本)中是有效的,并且已经在GNU/Linux、macOS和Windows上进行过测试。

Boost库提供了丰富的C++基础工具,在C++开发人员中很受欢迎。第3章中,已经展示了如何在系统上找到Boost库。然而,有时系统上可能没有项目所需的Boost版本。这个示例将展示如何利用超级构建模式来交付代码,并确保在缺少依赖项时,不会让CMake停止配置。我们将重用在第3章第8节的示例代码,以超构建的形式重新组织。这是项目的文件结构:

1
2
3
4
5
6
7
8
9
10
.
├── CMakeLists.txt
├── external
│ └── upstream
│ ├── boost
│ │ └── CMakeLists.txt
│ └── CMakeLists.txt
└── src
├── CMakeLists.txt
└── path-info.cpp

注意到项目源代码树中有四个CMakeLists.txt文件。下面的部分将对这些文件进行详解。

具体实施

从根目录的CMakeLists.txt开始:

  1. 声明一个C++11项目:

    1
    2
    3
    4
    5
    6
    7
    cmake_minimum_required(VERSION 3.5 FATAL_ERROR)

    project(recipe-02 LANGUAGES CXX)

    set(CMAKE_CXX_STANDARD 11)
    set(CMAKE_CXX_EXTENSIONS OFF)
    set(CMAKE_CXX_STANDARD_REQUIRED ON)
  2. EP_BASE进行属性设置:

    1
    set_property(DIRECTORY PROPERTY EP_BASE ${CMAKE_BINARY_DIR}/subprojects)
  3. 我们设置了STAGED_INSTALL_PREFIX变量。此目录将用于安装构建树中的依赖项:

    1
    2
    set(STAGED_INSTALL_PREFIX ${CMAKE_BINARY_DIR}/stage)
    message(STATUS "${PROJECT_NAME} staged install: ${STAGED_INSTALL_PREFIX}")
  4. 项目需要Boost库的文件系统和系统组件。我们声明了一个列表变量来保存这个信息,并设置了Boost所需的最低版本:

    1
    2
    list(APPEND BOOST_COMPONENTS_REQUIRED filesystem system)
    set(Boost_MINIMUM_REQUIRED 1.61)
  5. 添加external/upstream子目录,它将依次添加external/upstream/boost子目录:

    1
    add_subdirectory(external/upstream)
  6. 然后,包括ExternalProject.cmake标准模块,其中定义了ExternalProject_Add命令,它是超级构建的关键:

    1
    include(ExternalProject)
  7. 项目位于src子目录下,我们将它添加为一个外部项目。使用CMAKE_ARGSCMAKE_CACHE_ARGS传递CMake选项:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    ExternalProject_Add(${PROJECT_NAME}_core
    DEPENDS
    boost_external
    SOURCE_DIR
    ${CMAKE_CURRENT_LIST_DIR}/src
    CMAKE_ARGS
    -DCMAKE_CXX_COMPILER=${CMAKE_CXX_COMPILER}
    -DCMAKE_CXX_STANDARD=${CMAKE_CXX_STANDARD}
    -DCMAKE_CXX_EXTENSIONS=${CMAKE_CXX_EXTENSIONS}
    -DCMAKE_CXX_STANDARD_REQUIRED=${CMAKE_CXX_STANDARD_REQUIRED}
    CMAKE_CACHE_ARGS
    -DCMAKE_CXX_FLAGS:STRING=${CMAKE_CXX_FLAGS}
    -DCMAKE_INCLUDE_PATH:PATH=${BOOST_INCLUDEDIR}
    -DCMAKE_LIBRARY_PATH:PATH=${BOOST_LIBRARYDIR}
    BUILD_ALWAYS
    1
    INSTALL_COMMAND
    ""
    )

现在让我们看看external/upstream中的CMakeLists.txt。这个文件只是添加了boost文件夹作为一个额外的目录:

1
add_subdirectory(boost)

external/upstream/boost中的CMakeLists.txt描述了满足对Boost的依赖所需的操作。我们的目标很简单,如果没有安装所需的版本,下载源打包文件并构建它:

  1. 首先,我们试图找到所需Boost组件的最低版本:

    1
    find_package(Boost ${Boost_MINIMUM_REQUIRED} QUIET COMPONENTS "${BOOST_COMPONENTS_REQUIRED}")
  2. 如果找到这些,则添加一个接口库目标boost_external。这是一个虚拟目标,需要在我们的超级构建中正确处理构建顺序:

    1
    2
    3
    4
    5
    6
    if(Boost_FOUND)
    message(STATUS "Found Boost version ${Boost_MAJOR_VERSION}.${Boost_MINOR_VERSION}.${Boost_SUBMINOR_VERSION}")
    add_library(boost_external INTERFACE)
    else()
    # ... discussed below
    endif()
  3. 如果find_package没有成功,或者正在强制进行超级构建,我们需要建立一个本地构建的Boost。为此,我们进入else部分:

    1
    2
    else()
    message(STATUS "Boost ${Boost_MINIMUM_REQUIRED} could not be located, Building Boost 1.61.0 instead.")
  4. 由于这些库不使用CMake,我们需要为它们的原生构建工具链准备参数。首先为Boost设置编译器:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    if(CMAKE_CXX_COMPILER_ID MATCHES "GNU")
    if(APPLE)
    set(_toolset "darwin")
    else()
    set(_toolset "gcc")
    endif()
    elseif(CMAKE_CXX_COMPILER_ID MATCHES ".*Clang")
    set(_toolset "clang")
    elseif(CMAKE_CXX_COMPILER_ID MATCHES "Intel")
    if(APPLE)
    set(_toolset "intel-darwin")
    else()
    set(_toolset "intel-linux")
    endif()
    endif()
  5. 我们准备了基于所需组件构建的库列表,定义了一些列表变量:_build_byproducts,包含要构建的库的绝对路径;_b2_select_libraries,包含要构建的库的列;和_bootstrap_select_libraries,这是一个字符串,与_b2_needed_components具有相同的内容,但格式不同:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    if(NOT "${BOOST_COMPONENTS_REQUIRED}" STREQUAL "")
    # Replace unit_test_framework (used by CMake's find_package) with test (understood by Boost build toolchain)
    string(REPLACE "unit_test_framework" "test" _b2_needed_components "${BOOST_COMPONENTS_REQUIRED}")
    # Generate argument for BUILD_BYPRODUCTS
    set(_build_byproducts)
    set(_b2_select_libraries)
    foreach(_lib IN LISTS _b2_needed_components)
    list(APPEND _build_byproducts ${STAGED_INSTALL_PREFIX}/boost/lib/libboost_${_lib}${CMAKE_SHARED_LIBRARY_SUFFIX})
    list(APPEND _b2_select_libraries --with-${_lib})
    endforeach()
    # Transform the ;-separated list to a ,-separated list (digested by the Boost build toolchain!)
    string(REPLACE ";" "," _b2_needed_components "${_b2_needed_components}")
    set(_bootstrap_select_libraries "--with-libraries=${_b2_needed_components}")
    string(REPLACE ";" ", " printout "${BOOST_COMPONENTS_REQUIRED}")
    message(STATUS " Libraries to be built: ${printout}")
    endif()
  6. 现在,可以将Boost添加为外部项目。首先,在下载选项类中指定下载URL和checksum。DOWNLOAD_NO_PROGRESS设置为1,以禁止打印下载进度信息:

    1
    2
    3
    4
    5
    6
    7
    8
    include(ExternalProject)
    ExternalProject_Add(boost_external
    URL
    https://sourceforge.net/projects/boost/files/boost/1.61.0/boost_1_61_0.zip
    URL_HASH
    SHA256=02d420e6908016d4ac74dfc712eec7d9616a7fc0da78b0a1b5b937536b2e01e8
    DOWNLOAD_NO_PROGRESS
    1
  7. 接下来,设置更新/补丁和配置选项:

    1
    2
    3
    4
    5
    6
    7
    UPDATE_COMMAND
    ""
    CONFIGURE_COMMAND
    <SOURCE_DIR>/bootstrap.sh
    --with-toolset=${_toolset}
    --prefix=${STAGED_INSTALL_PREFIX}/boost
    ${_bootstrap_select_libraries}
  8. 构建选项使用BUILD_COMMAND设置。BUILD_IN_SOURCE设置为1时,表示构建将在源目录中发生。这里,将LOG_BUILD设置为1,以便将生成脚本中的输出记录到文件中:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    BUILD_COMMAND
    <SOURCE_DIR>/b2 -q
    link=shared
    threading=multi
    variant=release
    toolset=${_toolset}
    ${_b2_select_libraries}
    LOG_BUILD
    1
    BUILD_IN_SOURCE
    1
  9. 安装选项是使用INSTALL_COMMAND指令设置的。注意使用LOG_INSTALL选项,还可以将安装步骤记录到文件中:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    INSTALL_COMMAND
    <SOURCE_DIR>/b2 -q install
    link=shared
    threading=multi
    variant=release
    toolset=${_toolset}
    ${_b2_select_libraries}
    LOG_INSTALL
    1
  10. 最后,库列表为BUILD_BYPRODUCTS并关闭 ExternalProject_Add命令:

    1
    2
    3
    BUILD_BYPRODUCTS
    "${_build_byproducts}"
    )
  11. 我们设置了一些变量来指导检测新安装的Boost:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    set(
    BOOST_ROOT ${STAGED_INSTALL_PREFIX}/boost
    CACHE PATH "Path to internally built Boost installation root"
    FORCE
    )
    set(
    BOOST_INCLUDEDIR ${BOOST_ROOT}/include
    CACHE PATH "Path to internally built Boost include directories"
    FORCE
    )
    set(
    BOOST_LIBRARYDIR ${BOOST_ROOT}/lib
    CACHE PATH "Path to internally built Boost library directories"
    FORCE
    )
  12. else分支中,执行的最后一个操作是取消所有内部变量的设置:

    1
    2
    3
    4
    5
    unset(_toolset)
    unset(_b2_needed_components)
    unset(_build_byproducts)
    unset(_b2_select_libraries)
    unset(_boostrap_select_libraries)

最后,让我们看看src/CMakeLists.txt。这个文件描述了一个独立的项目:

  1. 声明一个C++项目:

    1
    2
    cmake_minimum_required(VERSION 3.5 FATAL_ERROR)
    project(recipe-02_core LANGUAGES CXX)
  2. 调用find_package寻找项目依赖的Boost。从主CMakeLists.txt中配置的项目,可以保证始终满足依赖关系,方法是使用预先安装在系统上的Boost,或者使用我们作为子项目构建的Boost:

    1
    find_package(Boost 1.61 REQUIRED COMPONENTS filesystem)
  3. 添加可执行目标,并链接库:

    1
    2
    3
    4
    5
    add_executable(path-info path-info.cpp)
    target_link_libraries(path-info
    PUBLIC
    Boost::filesystem
    )

NOTE:导入目标虽然很简单,但不能保证对任意Boost和CMake版本组合都有效。这是因为CMake的FindBoost.cmake模块会创建手工导入的目标。因此,当CMake有未知版本发布时,可能会有Boost_LIBRARIESBoost_INCLUDE_DIRS,没有导入情况(https://stackoverflow.com/questions/42123509/cmake-finds-boost-but-the-imported-targets-not-available-for-boost-version )。

工作原理

此示例展示了如何利用超级构建模式,来整合项目的依赖项。让我们再看一下项目的文件结构:

1
2
3
4
5
6
7
8
9
10
.
├── CMakeLists.txt
├── external
│ └── upstream
│ ├── boost
│ │ └── CMakeLists.txt
│ └── CMakeLists.txt
└── src
├── CMakeLists.txt
└── path-info.cpp

我们在项目源代码树中,引入了4个CMakeLists.txt文件:

  1. CMakeLists.txt将配合超级构建。
  2. external/upstream中的文件将引导我们到boost子目录。
  3. external/upstream/boost/CMakeLists.txt将处理Boost的依赖。
  4. 最后,src下的CMakeLists.txt将构建我们的示例代码(其依赖于Boost)。

external/upstream/boost/CMakeLists.txt文件开始讨论。Boost使用它自己的构建系统,因此需要在ExternalProject_Add中详细配置,以便正确设置所有内容:

  1. 保留目录选项的默认值。

  2. 下载步骤将从在线服务器下载所需版本的Boost。因此,我们设置了URLURL_HASHURL_HASH用于检查下载文件的完整性。由于我们不希望看到下载的进度报告,所以将DOWNLOAD_NO_PROGRESS选项设置为true。

  3. 更新步骤留空。如果需要重新构建,我们不想再次下载Boost。

  4. 配置步骤将使用由Boost在CONFIGURE_COMMAND中提供的配置工具完成。由于我们希望超级构建是跨平台的,所以我们使用<SOURCE_DIR>变量来引用未打包源的位置:

    1
    2
    3
    4
    5
    CONFIGURE_COMMAND
    <SOURCE_DIR>/bootstrap.sh
    --with-toolset=${_toolset}
    --prefix=${STAGED_INSTALL_PREFIX}/boost
    ${_bootstrap_select_libraries}
  5. BUILD_IN_SOURCE选项设置为true,说明这是一个内置的构建。BUILD_COMMAND使用Boost本机构建工具b2。由于我们将在源代码中构建,所以我们再次使用<SOURCE_DIR>变量来引用未打包源代码的位置。

  6. 然后,来看安装选项。Boost使用本地构建工具管理安装。事实上,构建和安装命令可以整合为一个命令。

  7. 输出日志选项LOG_BUILDLOG_INSTALL 直接用于为ExternalProject_Add构建和安装操作编写日志文件,而不是输出到屏幕上。

  8. 最后,BUILD_BYPRODUCTS选项允许ExternalProject_Add在后续构建中,跟踪新构建的Boost库。

构建Boost之后,构建目录中的${STAGED_INSTALL_PREFIX}/Boost文件夹将包含所需的库。我们需要将此信息传递给我们的项目,该构建系统是在src/CMakeLists.txt中生成的。为了实现这个目标,我们在主CMakeLists.txtExternalProject_Add中传递两个额外的CMAKE_CACHE_ARGS:

  1. CMAKE_INCLUDE_PATH: CMake查找C/C++头文件的路径
  2. CMAKE_LIBRARY_PATH: CMake将查找库的路径

将这些变量设置成新构建的Boost安装路径,可以确保正确地获取依赖项。

TIPS:在配置项目时将CMAKE_DISABLE_FIND_PACKAGE_Boost设置为ON,将跳过对Boost库的检测,并始终执行超级构建。参考文档:https://cmake.org/cmake/help/v3.5/variable/CMAKE_DISABLE_FIND_PACKAGE_PackageName.html

8.3 使用超级构建管理依赖项:Ⅱ.FFTW库

NOTE:此示例代码可以在 https://github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-8/recipe-03 中找到,其中有一个C示例。该示例在CMake 3.5版(或更高版本)中是有效的,并且已经在GNU/Linux、macOS和Windows上进行过测试。

对于CMake支持的所有项目,超级构建模式可用于管理相当复杂的依赖关系。正如在前面的示例所演示的,CMake并不需要管理各种子项目。与前一个示例相反,这个示例中的外部子项目将是一个CMake项目,并将展示如何使用超级构建,下载、构建和安装FFTW库。FFTW是一个快速傅里叶变换库,可在http://www.fftw.org 免费获得。

我们项目的代码fftw_example.c位于src子目录中,它将计算源代码中定义的函数的傅里叶变换。

准备工作

这个示例的目录布局,是超级构建中非常常见的结构:

1
2
3
4
5
6
7
8
9
10
.
├── CMakeLists.txt
├── external
│ └── upstream
│ ├── CMakeLists.txt
│ └── fftw3
│ └── CMakeLists.txt
└── src
├── CMakeLists.txt
└── fftw_example.c

代码fftw_example.c位于src子目录中,它将调用傅里叶变换函数。

具体实施

从主CMakeLists.txt开始,这里将整个超级构建过程放在一起:

  1. 声明一个支持C99的项目:

    1
    2
    3
    4
    5
    cmake_minimum_required(VERSION 3.5 FATAL_ERROR)
    project(recipe-03 LANGUAGES C)
    set(CMAKE_C_STANDARD 99)
    set(CMAKE_C_EXTENSIONS OFF)
    set(CMAKE_C_STANDARD_REQUIRED ON)
  2. 和上一个示例一样,我们设置了EP_BASE目录属性和阶段安装目录:

    1
    2
    3
    set_property(DIRECTORY PROPERTY EP_BASE ${CMAKE_BINARY_DIR}/subprojects)
    set(STAGED_INSTALL_PREFIX ${CMAKE_BINARY_DIR}/stage)
    message(STATUS "${PROJECT_NAME} staged install: ${STAGED_INSTALL_PREFIX}")
  3. 对FFTW的依赖关系在external/upstream子目录中检查,我们会将这个子目录添加到构建系统中:

    1
    add_subdirectory(external/upstream)
  4. 包含ExternalProject.cmake模块:

    1
    include(ExternalProject)
  5. 我们为recipe-03_core声明了外部项目。这个项目的源代码在${CMAKE_CURRENT_LIST_DIR}/src文件夹中。该项目设置为FFTW3_DIR选项,选择正确的FFTW库:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    ExternalProject_Add(${PROJECT_NAME}_core
    DEPENDS
    fftw3_external
    SOURCE_DIR
    ${CMAKE_CURRENT_LIST_DIR}/src
    CMAKE_ARGS
    -DFFTW3_DIR=${FFTW3_DIR}
    -DCMAKE_C_STANDARD=${CMAKE_C_STANDARD}
    -DCMAKE_C_EXTENSIONS=${CMAKE_C_EXTENSIONS}
    -DCMAKE_C_STANDARD_REQUIRED=${CMAKE_C_STANDARD_REQUIRED}
    CMAKE_CACHE_ARGS
    -DCMAKE_C_FLAGS:STRING=${CMAKE_C_FLAGS}
    -DCMAKE_PREFIX_PATH:PATH=${CMAKE_PREFIX_PATH}
    BUILD_ALWAYS
    1
    INSTALL_COMMAND
    ""
    )

external/upstream子目录还包含一个CMakeLists.txt:

这个文件中,添加fftw3文件夹作为构建系统中的另一个子目录:

1
add_subdirectory(fftw3)

external/upstream/fftw3中的CMakeLists.txt负责处理依赖关系:

  1. 首先,尝试在系统上找到FFTW3库。注意,我们配置find_package使用的参数:

    1
    find_package(FFTW3 CONFIG QUIET)
  2. 如果找到了库,就可以导入目标FFTW3::FFTW3来链接它。我们向用户打印一条消息,显示库的位置。我们添加一个虚拟INTERFACEfftw3_external。超级建设中,这需要正确地固定子项目之间的依赖树:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    find_package(FFTW3 CONFIG QUIET)

    if(FFTW3_FOUND)
    get_property(_loc TARGET FFTW3::fftw3 PROPERTY LOCATION)
    message(STATUS "Found FFTW3: ${_loc} (found version ${FFTW3_VERSION})")
    add_library(fftw3_external INTERFACE) # dummy
    else()
    # this branch will be discussed below
    endif()
  3. 如果CMake无法找到预安装版本的FFTW,我们将进入else分支。这个分支中,使用ExternalProject_Add下载、构建和安装它。外部项目的名称为fftw3_externalfftw3_external项目将从官方地址下载,下载完成后将使用MD5校验和进行文件完整性检查:

    1
    2
    3
    4
    5
    6
    7
    8
    message(STATUS "Suitable FFTW3 could not be located. Downloading and building!")

    include(ExternalProject)
    ExternalProject_Add(fftw3_external
    URL
    http://www.fftw.org/fftw-3.3.8.tar.gz
    URL_HASH
    MD5=8aac833c943d8e90d51b697b27d4384d
  4. 禁用打印下载进程,并将更新命令定义为空:

    1
    2
    3
    4
    OWNLOAD_NO_PROGRESS
    1
    UPDATE_COMMAND
    ""
  5. 配置、构建和安装输出将被记录到一个文件中:

    1
    2
    3
    4
    5
    6
    LOG_CONFIGURE
    1
    LOG_BUILD
    1
    LOG_INSTALL
    1
  6. fftw3_external项目的安装前缀设置为之前定义的STAGED_INSTALL_PREFIX目录,并关闭FFTW3的测试套件构建:

    1
    2
    3
    CMAKE_ARGS
    -DCMAKE_INSTALL_PREFIX=${STAGED_INSTALL_PREFIX}
    -DBUILD_TESTS=OFF
  7. 如果在Windows上构建,通过生成器表达式设置WITH_OUR_MALLOC预处理器选项,并关闭ExternalProject_Add命令:

    1
    2
    3
    CMAKE_CACHE_ARGS
    -DCMAKE_C_FLAGS:STRING=$<$<BOOL:WIN32>:-DWITH_OUR_MALLOC>
    )
  8. 最后,定义FFTW3_DIR变量并缓存它。CMake将使用该变量作为FFTW3::FFTW3目标的搜索目录:

    1
    2
    3
    4
    5
    6
    7
    include(GNUInstallDirs)

    set(
    FFTW3_DIR ${STAGED_INSTALL_PREFIX}/${CMAKE_INSTALL_LIBDIR}/cmake/fftw3
    CACHE PATH "Path to internally built FFTW3Config.cmake"
    FORCE
    )

src文件夹中的CMakeLists.txt相当简洁:

  1. 同样在这个文件中,我们声明了一个C项目:

    1
    2
    cmake_minimum_required(VERSION 3.5 FATAL_ERROR)
    project(recipe-03_core LANGUAGES C)
  2. 使用find_package来检测FFTW库,再次使用配置检测模式:

    1
    2
    3
    find_package(FFTW3 CONFIG REQUIRED)
    get_property(_loc TARGET FFTW3::fftw3 PROPERTY LOCATION)
    message(STATUS "Found FFTW3: ${_loc} (found version ${FFTW3_VERSION})")
  3. fftw_example.c源文件添加到可执行目标fftw_example:

    1
    add_executable(fftw_example fftw_example.c)
  4. 为可执行目标设置链接库:

    1
    2
    3
    4
    target_link_libraries(fftw_example
    PRIVATE
    FFTW3::fftw3
    )

工作原理

本示例演示了如何下载、构建和安装由CMake管理其构建系统的外部项目。与前一个示例(必须使用自定义构建系统)相反,这个超级构建设置相当简洁。需要注意的是,使用find_package命令了配置选项;这说明CMake首先查找FFTW3Config.cmake,以定位FFTW3库,将库导出为第三方项目获取的目标。目标包含库的版本、配置和位置,即关于如何配置和构建目标的完整信息。如果系统上没有安装库,我们需要声明FFTW3Config.cmake文件的位置。这可以通过设置FFTW3_DIR变量来实现。这是external/upstream/fftw3/CMakeLists.txt文件中的最后一步。使用GNUInstallDirs.cmake模块,我们将FFTW3_DIR设置为缓存变量,以便稍后在超级构建中使用。

TIPS:配置项目时将CMAKE_DISABLE_FIND_PACKAGE_FFTW3设置为ON,将跳过对FFTW库的检测,并始终执行超级构建。参考:https://cmake.org/cmake/help/v3.5/variable/CMAKE_DISABLE_FIND_PACKAGE_PackageName.html

8.4 使用超级构建管理依赖项:Ⅲ.Google Test框架

NOTE:此示例代码可以在 https://github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-8/recipe-04 中找到,其中有一个C++示例。该示例在CMake 3.11版(或更高版本)中是有效的,并且已经在GNU/Linux、macOS和Windows上进行过测试。在库中也有一个例子可以在CMake 3.5下使用。

第4章第3节中,我们使用Google Test框架实现单元测试,并在配置时使用FetchContent模块获取Google Test源(自CMake 3.11开始可用)。本章中,我们将重新讨论这个方法,较少关注测试方面,并更深入地研究FetchContent。它提供了一个简单通用的模块,可以在配置时组装项目依赖项。对于3.11以下的CMake,我们还将讨论如何在配置时使用ExternalProject_Add模拟FetchContent

准备工作

这个示例中,我们将复用第4章第3节的源码,构建main.cppsum_integer.cppsum_integers.hpptest.cpp。我们将在配置时使用FetchContentExternalProject_Add下载所有必需的Google Test源,在此示例中,只关注在配置时获取依赖项,而不是实际的源代码及其单元测试。

具体实施

这个示例中,我们只关注如何获取Google Test源来构建gtest_main,并链接到Google Test库。关于这个目标如何用于测试示例源的讨论,请读者参考第4章第3节:

  1. 首先包括FetchContent模块,它将提供需要的声明、查询和填充依赖项函数:

    1
    include(FetchContent)
  2. 然后,声明内容——名称、存储库位置和要获取的精确版本:

    1
    2
    3
    4
    5
    FetchContent_Declare(
    googletest
    GIT_REPOSITORY https://github.com/google/googletest.git
    GIT_TAG release-1.8.0
    )
  3. 查询内容是否已经被获取/填充:

    1
    FetchContent_GetProperties(googletest)
  4. 前面的函数定义了googletest_POPULATED。如果内容还没有填充,我们获取内容并配置子项目:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    if(NOT googletest_POPULATED)
    FetchContent_Populate(googletest)

    # ...

    # adds the targets: gtest, gtest_main, gmock, gmock_main
    add_subdirectory(
    ${googletest_SOURCE_DIR}
    ${googletest_BINARY_DIR}
    )

    # ...

    endif()
  5. 注意配置时获取内容的方式:

    1
    2
    3
    $ mkdir -p build
    $ cd build
    $ cmake ..
  6. 这将生成以下构建目录树。Google Test源现已就绪,剩下的就交由CMake处理,并提供所需的目标:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    build/
    ├── ...
    ├── _deps
    │ ├── googletest-build
    │ │ ├── ...
    │ │ └── ...
    │ ├── googletest-src
    │ │ ├── ...
    │ │ └── ...
    │ └── googletest-subbuild
    │ ├── ...
    │ └── ...
    └── ...

工作原理

FetchContent模块支持在配置时填充内容。例子中,获取了一个Git库,其中有一个Git标签:

1
2
3
4
5
FetchContent_Declare(
googletest
GIT_REPOSITORY https://github.com/google/googletest.git
GIT_TAG release-1.8.0
)

CMake的3.11版本中,FetchContent已经成为CMake的标准部分。下面的代码中,将尝试在配置时使用ExternalProject_Add模拟FetchContent。这不仅适用于较老的CMake版本,而且可以让我们更深入地了解FetchContent层下面发生了什么,并为使用ExternalProject_Add在构建时获取项目,提供一个有趣的替代方法。我们的目标是编写一个fetch_git_repo宏,并将它放在fetch_git_repo中。这样就可以获取相应的内容了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
include(fetch_git_repo.cmake)

fetch_git_repo(
googletest
${CMAKE_BINARY_DIR}/_deps
https://github.com/google/googletest.git
release-1.8.0
)

# ...

# adds the targets: gtest, gtest_main, gmock, gmock_main
add_subdirectory(
${googletest_SOURCE_DIR}
${googletest_BINARY_DIR}
)

# ...

这类似于FetchContent的使用。在底层实现中,我们将使用ExternalProject_Add。现在打开模块,检查fetch_git_repo.cmake中定义的fetch_git_repo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
macro(fetch_git_repo _project_name _download_root _git_url _git_tag)

set(${_project_name}_SOURCE_DIR ${_download_root}/${_project_name}-src)
set(${_project_name}_BINARY_DIR ${_download_root}/${_project_name}-build)

# variables used configuring fetch_git_repo_sub.cmake
set(FETCH_PROJECT_NAME ${_project_name})
set(FETCH_SOURCE_DIR ${${_project_name}_SOURCE_DIR})
set(FETCH_BINARY_DIR ${${_project_name}_BINARY_DIR})
set(FETCH_GIT_REPOSITORY ${_git_url})
set(FETCH_GIT_TAG ${_git_tag})

configure_file(
${CMAKE_CURRENT_LIST_DIR}/fetch_at_configure_step.in
${_download_root}/CMakeLists.txt
@ONLY
)

# undefine them again
unset(FETCH_PROJECT_NAME)
unset(FETCH_SOURCE_DIR)
unset(FETCH_BINARY_DIR)
unset(FETCH_GIT_REPOSITORY)
unset(FETCH_GIT_TAG)

# configure sub-project
execute_process(
COMMAND
"${CMAKE_COMMAND}" -G "${CMAKE_GENERATOR}" .
WORKING_DIRECTORY
${_download_root}
)

# build sub-project which triggers ExternalProject_Add
execute_process(
COMMAND
"${CMAKE_COMMAND}" --build .
WORKING_DIRECTORY
${_download_root}
)
endmacro()

宏接收项目名称、下载根目录、Git存储库URL和一个Git标记。宏定义了${_project_name}_SOURCE_DIR${_project_name}_BINARY_DIR,我们需要在fetch_git_repo生命周期范围内使用定义的${_project_name}_SOURCE_DIR${_project_name}_BINARY_DIR,因为要使用它们对子目录进行配置:

1
2
3
4
add_subdirectory(
${googletest_SOURCE_DIR}
${googletest_BINARY_DIR}
)

fetch_git_repo宏中,我们希望使用ExternalProject_Add在配置时获取外部项目,通过三个步骤实现了这一点:

  1. 首先,配置fetch_at_configure_step.in:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    cmake_minimum_required(VERSION 3.5 FATAL_ERROR)

    project(fetch_git_repo_sub LANGUAGES NONE)

    include(ExternalProject)

    ExternalProject_Add(
    @FETCH_PROJECT_NAME@
    SOURCE_DIR "@FETCH_SOURCE_DIR@"
    BINARY_DIR "@FETCH_BINARY_DIR@"
    GIT_REPOSITORY
    @FETCH_GIT_REPOSITORY@
    GIT_TAG
    @FETCH_GIT_TAG@
    CONFIGURE_COMMAND ""
    BUILD_COMMAND ""
    INSTALL_COMMAND ""
    TEST_COMMAND ""
    )

    使用configure_file,可以生成一个CMakeLists.txt文件,前面的占位符被fetch_git_repo.cmake中的值替换。注意,前面的ExternalProject_Add命令仅用于获取,而不仅是配置、构建、安装或测试。

  2. 其次,使用配置步骤在配置时触发ExternalProject_Add(从主项目的角度):

    1
    2
    3
    4
    5
    6
    7
    # configure sub-project
    execute_process(
    COMMAND
    "${CMAKE_COMMAND}" -G "${CMAKE_GENERATOR}" .
    WORKING_DIRECTORY
    ${_download_root}
    )
  3. 最后在fetch_git_repo.cmake中触发配置时构建步骤:

    1
    2
    3
    4
    5
    6
    7
    # build sub-project which triggers ExternalProject_Add
    execute_process(
    COMMAND
    "${CMAKE_COMMAND}" --build .
    WORKING_DIRECTORY
    ${_download_root}
    )

这个解决方案的一个优点是,由于外部依赖项不是由ExternalProject_Add配置的,所以不需要通过ExternalProject_Add调用任何配置,将其引导至项目。我们可以使用add_subdirectory配置和构建模块,就像外部依赖项是项目源代码树的一部分一样。聪明的伪装!

更多信息

有关FetchContent选项的详细讨论,请参考https://cmake.org/cmake/help/v3.11/module/FetchContent.html 配置时ExternalProject_Add的解决方案灵感来自Craig Scott,博客文章:https://crascit.com/2015/07/25/cgtest/

8.5 使用超级构建支持项目

NOTE:此示例代码可以在 https://github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-8/recipe-05 中找到,其中有一个C++示例。该示例在CMake 3.5版(或更高版本)中是有效的,并且已经在GNU/Linux、macOS和Windows上进行过测试。

ExternalProjectFetchContent是CMake库中两个非常强大的工具。经过前面的示例,我们应该相信超级构建方法,在管理复杂依赖关系的项目时是多么有用。目前为止,我们已经展示了如何使用ExternalProject来处理以下问题:

  • 存储在源树中的源
  • 从在线服务器上,检索/获取可用的存档资源

前面的示例展示了,如何使用FetchContent处理开源Git存储库中可用的依赖项。本示例将展示,如何使用ExternalProject达到同样的效果。最后,将介绍一个示例,该示例将在第10章第4节中重用。

准备工作

这个超级构建的源代码树现在应该很熟悉了:

1
2
3
4
5
6
7
8
9
10
.
├── CMakeLists.txt
├── external
│ └── upstream
│ ├── CMakeLists.txt
│ └── message
│ └── CMakeLists.txt
└── src
├── CMakeLists.txt
└── use_message.cpp

根目录有一个CMakeLists.txt,我们知道它会配合超级构建。子目录srcexternal中是我们自己的源代码,CMake指令需要满足对消息库的依赖,我们将在本例中构建消息库。

具体实施

目前为止,建立超级构建的过程应该已经很熟悉了。让我们再次看看必要的步骤,从根目录的CMakeLists.txt开始:

  1. 声明一个C++11项目,并对项目构建类型的默认值进行设置。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    cmake_minimum_required(VERSION 3.6 FATAL_ERROR)

    project(recipe-05 LANGUAGES CXX)

    set(CMAKE_CXX_STANDARD 11)
    set(CMAKE_CXX_EXTENSIONS OFF)
    set(CMAKE_CXX_STANDARD_REQUIRED ON)

    if(NOT DEFINED CMAKE_BUILD_TYPE OR "${CMAKE_BUILD_TYPE}" STREQUAL "")
    set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE)
    endif()

    message(STATUS "Build type set to ${CMAKE_BUILD_TYPE}")
  2. 设置EP_BASE目录属性。这将固定ExternalProject管理所有子项目的布局:

    1
    set_property(DIRECTORY PROPERTY EP_BASE ${CMAKE_BINARY_DIR}/subprojects)
  3. 我们设置了STAGED_INSTALL_PREFIX。与之前一样,这个位置将作为依赖项的构建树中的安装目录:

    1
    2
    set(STAGED_INSTALL_PREFIX ${CMAKE_BINARY_DIR}/stage)
    message(STATUS "${PROJECT_NAME} staged install: ${STAGED_INSTALL_PREFIX}")
  4. external/upstream作为子目录添加:

    1
    add_subdirectory(external/upstream)
  5. 添加ExternalProject_Add,这样我们的项目也将由超级构建管理:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    include(ExternalProject)
    ExternalProject_Add(${PROJECT_NAME}_core
    DEPENDS
    message_external
    SOURCE_DIR
    ${CMAKE_CURRENT_SOURCE_DIR}/src
    CMAKE_ARGS
    -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE}
    -DCMAKE_CXX_COMPILER=${CMAKE_CXX_COMPILER}
    -DCMAKE_CXX_STANDARD=${CMAKE_CXX_STANDARD}
    -DCMAKE_CXX_EXTENSIONS=${CMAKE_CXX_EXTENSIONS}
    -DCMAKE_CXX_STANDARD_REQUIRED=${CMAKE_CXX_STANDARD_REQUIRED}
    -Dmessage_DIR=${message_DIR}
    CMAKE_CACHE_ARGS
    -DCMAKE_CXX_FLAGS:STRING=${CMAKE_CXX_FLAGS}
    -DCMAKE_PREFIX_PATH:PATH=${CMAKE_PREFIX_PATH}
    BUILD_ALWAYS
    1
    INSTALL_COMMAND
    ""
    )

external/upstreamCMakeLists.txt中只包含一条命令:

1
add_subdirectory(message)

跳转到message文件夹,我们会看到对消息库的依赖的常用命令:

  1. 首先,调用find_package找到一个合适版本的库:

    1
    find_package(message 1 CONFIG QUIET)
  2. 如果找到,会通知用户,并添加一个虚拟INTERFACE库:

    1
    2
    3
    get_property(_loc TARGET message::message-shared PROPERTY LOCATION)
    message(STATUS "Found message: ${_loc} (found version ${message_VERSION})")
    add_library(message_external INTERFACE) # dummy
  3. 如果没有找到,再次通知用户并继续使用ExternalProject_Add:

    1
    message(STATUS "Suitable message could not be located, Building message instead.")
  4. 该项目托管在一个公共Git库中,使用GIT_TAG选项指定下载哪个分支。和之前一样,将UPDATE_COMMAND选项置为空:

    1
    2
    3
    4
    5
    6
    7
    8
    include(ExternalProject)
    ExternalProject_Add(message_external
    GIT_REPOSITORY
    https://github.com/dev-cafe/message.git
    GIT_TAG
    master
    UPDATE_COMMAND
    ""
  5. 外部项目使用CMake配置和构建,传递必要的构建选项:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    CMAKE_ARGS
    -DCMAKE_INSTALL_PREFIX=${STAGED_INSTALL_PREFIX}
    -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE}
    -DCMAKE_CXX_COMPILER=${CMAKE_CXX_COMPILER}
    -DCMAKE_CXX_STANDARD=${CMAKE_CXX_STANDARD}
    -DCMAKE_CXX_EXTENSIONS=${CMAKE_CXX_EXTENSIONS}
    -DCMAKE_CXX_STANDARD_REQUIRED=${CMAKE_CXX_STANDARD_REQUIRED}
    CMAKE_CACHE_ARGS
    -DCMAKE_CXX_FLAGS:STRING=${CMAKE_CXX_FLAGS}
  6. 项目安装后进行测试:

    1
    2
    TEST_AFTER_INSTALL
    1
  7. 我们不希望看到下载进度,也不希望在屏幕上报告配置、构建和安装信息,所以选择关闭ExternalProject_Add:

    1
    2
    3
    4
    5
    6
    7
    8
    9
      DOWNLOAD_NO_PROGRESS
    1
    LOG_CONFIGURE
    1
    LOG_BUILD
    1
    LOG_INSTALL
    1
    )
  8. 为了确保子项目在超级构建的其余部分中是可见的,我们设置了message_DIR目录:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    if(WIN32 AND NOT CYGWIN)
    set(DEF_message_DIR ${STAGED_INSTALL_PREFIX}/CMake)
    else()
    set(DEF_message_DIR ${STAGED_INSTALL_PREFIX}/share/cmake/message)
    endif()

    file(TO_NATIVE_PATH "${DEF_message_DIR}" DEF_message_DIR)
    set(message_DIR ${DEF_message_DIR}
    CACHE PATH "Path to internally built messageConfig.cmake" FORCE)

最后,来看一下src目录上的CMakeLists.txt

  1. 同样,声明一个C++11项目:

    1
    2
    3
    4
    5
    6
    7
    cmake_minimum_required(VERSION 3.6 FATAL_ERROR)
    project(recipe-05_core
    LANGUAGES CXX
    )
    set(CMAKE_CXX_STANDARD 11)
    set(CMAKE_CXX_EXTENSIONS OFF)
    set(CMAKE_CXX_STANDARD_REQUIRED ON)
  2. 项目需要消息库:

    1
    2
    3
    find_package(message 1 CONFIG REQUIRED)
    get_property(_loc TARGET message::message-shared PROPERTY LOCATION)
    message(STATUS "Found message: ${_loc} (found version ${message_VERSION})")
  3. 声明一个可执行目标,并将其链接到消息动态库:

    1
    2
    3
    4
    5
    6
    add_executable(use_message use_message.cpp)

    target_link_libraries(use_message
    PUBLIC
    message::message-shared
    )

工作原理

示例展示了ExternalProject_Add的一些新选项:

  1. GIT_REPOSITORY:这可以用来指定包含依赖项源的存储库的URL。CMake还可以使用其他版本控制系统,比如CVS (CVS_REPOSITORY)、SVN (SVN_REPOSITORY)或Mercurial (HG_REPOSITORY)。
  2. GIT_TAG:默认情况下,CMake将检出给定存储库的默认分支。然而,最好依赖于一个稳定的版本。这可以通过这个选项指定,它可以接受Git将任何标识符识别为“版本”信息,例如:Git提交SHA、Git标记或分支名称。CMake所理解的其他版本控制系统也可以使用类似的选项。
  3. TEST_AFTER_INSTALL:依赖项很可能有自己的测试套件,您可能希望运行测试套件,以确保在超级构建期间一切顺利。此选项将在安装步骤之后立即运行测试。

ExternalProject_Add可以理解的其他测试选项如下:

  • TEST_BEFORE_INSTALL:将在安装步骤之前运行测试套件
  • TEST_EXCLUDE_FROM_MAIN:可以从测试套件中,删除对外部项目的主要目标的依赖

这些选项都假定外部项目使用CTest管理测试。如果外部项目不使用CTest来管理测试,我们可以通过TEST_COMMAND选项来执行测试。

即使是为属于自己项目的模块引入超级构建模式,也需要引入额外的层,重新声明小型CMake项目,并通过ExternalProject_Add显式地传递配置设置。引入这个附加层的好处是,清晰地分离了变量和目标范围,这可以帮助管理由多个组件组成的项目中的复杂性、依赖性和名称空间,这些组件可以是内部的,也可以是外部的,并由CMake组合在一起。