> For the complete documentation index, see [llms.txt](https://chenxiaowei.gitbook.io/cmake-cookbook/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://chenxiaowei.gitbook.io/cmake-cookbook/8.0-chinese/8.5-chinese.md).

# 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上进行过测试。*

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

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

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

## 准备工作

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

```
.
├── CMakeLists.txt
├── external
│    └── upstream
│        ├── CMakeLists.txt
│        └── message
│            └── CMakeLists.txt
└── src
    ├── CMakeLists.txt
    └── use_message.cpp
```

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

## 具体实施

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

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

   ```
   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`管理所有子项目的布局:

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

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

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

   ```
   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/upstream`的`CMakeLists.txt`中只包含一条命令:

```
add_subdirectory(message)
```

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

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

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

   ```
   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`:

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

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

   ```
     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. 项目安装后进行测试:

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

   ```
     DOWNLOAD_NO_PROGRESS
         1
     LOG_CONFIGURE
         1
     LOG_BUILD
         1
     LOG_INSTALL
         1
   )
   ```
8. 为了确保子项目在超级构建的其余部分中是可见的，我们设置了`message_DIR`目录:

   ```
   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项目:

   ```
   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. 项目需要消息库:

   ```
   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. 声明一个可执行目标，并将其链接到消息动态库:

   ```
   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组合在一起。
