> 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.4-chinese.md).

# 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.cpp`、`sum_integer.cpp`和`sum_integers.hpp`和`test.cpp`。我们将在配置时使用`FetchContent`或`ExternalProject_Add`下载所有必需的Google Test源，在此示例中，只关注在配置时获取依赖项，而不是实际的源代码及其单元测试。

## 具体实施

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

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

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

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

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

   ```
   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. 注意配置时获取内容的方式:

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

   ```
   build/
   ├── ...
   ├── _deps
   │    ├── googletest-build
   │    │    ├── ...
   │    │    └── ...
   │    ├── googletest-src
   │    │    ├── ...
   │    │    └── ...
   │    └── googletest-subbuild
   │         ├── ...
   │         └── ...
   └── ...
   ```

## 工作原理

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

```
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`中。这样就可以获取相应的内容了:

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

```
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`，因为要使用它们对子目录进行配置:

```
add_subdirectory(
  ${googletest_SOURCE_DIR}
  ${googletest_BINARY_DIR}
  )
```

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

1. 首先，配置`fetch_at_configure_step.in`:

   ```
   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`(从主项目的角度):

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

   ```
   # 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/>
