# 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节的示例代码，以超构建的形式重新组织。这是项目的文件结构:

```
.
├── CMakeLists.txt
├── external
│    └── upstream
│        ├── boost
│        │    └── CMakeLists.txt
│        └── CMakeLists.txt
└── src
    ├── CMakeLists.txt
    └── path-info.cpp
```

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

## 具体实施

从根目录的`CMakeLists.txt`开始：

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

   ```
   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`进行属性设置：

   ```
   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. 项目需要Boost库的文件系统和系统组件。我们声明了一个列表变量来保存这个信息，并设置了Boost所需的最低版本:

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

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

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

   ```
   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文件夹作为一个额外的目录:

```
add_subdirectory(boost)
```

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

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

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

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

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

   ```
   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`具有相同的内容，但格式不同:

   ```
   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，以禁止打印下载进度信息:

   ```
   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. 接下来，设置更新/补丁和配置选项:

   ```
     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，以便将生成脚本中的输出记录到文件中:

   ```
     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`选项，还可以将安装步骤记录到文件中:

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

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

    ```
    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`分支中，执行的最后一个操作是取消所有内部变量的设置:

    ```
    unset(_toolset)
    unset(_b2_needed_components)
    unset(_build_byproducts)
    unset(_b2_select_libraries)
    unset(_boostrap_select_libraries)
    ```

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

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

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

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

   ```
   add_executable(path-info path-info.cpp)
   target_link_libraries(path-info
     PUBLIC
         Boost::filesystem
     )
   ```

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

## 工作原理

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

```
.
├── 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。因此，我们设置了`URL`和`URL_HASH`。`URL_HASH`用于检查下载文件的完整性。由于我们不希望看到下载的进度报告，所以将`DOWNLOAD_NO_PROGRESS`选项设置为true。
3. 更新步骤留空。如果需要重新构建，我们不想再次下载Boost。
4. 配置步骤将使用由Boost在`CONFIGURE_COMMAND`中提供的配置工具完成。由于我们希望超级构建是跨平台的，所以我们使用`<SOURCE_DIR>`变量来引用未打包源的位置:

   ```
   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_BUILD`和`LOG_INSTALL` 直接用于为`ExternalProject_Add`构建和安装操作编写日志文件，而不是输出到屏幕上。
8. 最后，`BUILD_BYPRODUCTS`选项允许`ExternalProject_Add`在后续构建中，跟踪新构建的Boost库。

构建Boost之后，构建目录中的`${STAGED_INSTALL_PREFIX}/Boost`文件夹将包含所需的库。我们需要将此信息传递给我们的项目，该构建系统是在`src/CMakeLists.txt`中生成的。为了实现这个目标，我们在主`CMakeLists.txt`的`ExternalProject_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> *。*


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://chenxiaowei.gitbook.io/cmake-cookbook/8.0-chinese/8.2-chinese.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
