8.5 使用超级构建支持项目

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

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

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

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

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

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

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

      1. cmake_minimum_required(VERSION 3.6 FATAL_ERROR)
      2. project(recipe-05 LANGUAGES CXX)
      3. set(CMAKE_CXX_STANDARD 11)
      4. set(CMAKE_CXX_EXTENSIONS OFF)
      5. set(CMAKE_CXX_STANDARD_REQUIRED ON)
      6. if(NOT DEFINED CMAKE_BUILD_TYPE OR "${CMAKE_BUILD_TYPE}" STREQUAL "")
      7. set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE)
      8. endif()
      9. 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. set(STAGED_INSTALL_PREFIX ${CMAKE_BINARY_DIR}/stage)
      2. message(STATUS "${PROJECT_NAME} staged install: ${STAGED_INSTALL_PREFIX}")
      1. add_subdirectory(external/upstream)
    4. 添加ExternalProject_Add,这样我们的项目也将由超级构建管理:

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

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

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

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

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

      1. get_property(_loc TARGET message::message-shared PROPERTY LOCATION)
      2. message(STATUS "Found message: ${_loc} (found version ${message_VERSION})")
      3. 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. include(ExternalProject)
      2. ExternalProject_Add(message_external
      3. GIT_REPOSITORY
      4. https://github.com/dev-cafe/message.git
      5. GIT_TAG
      6. master
      7. UPDATE_COMMAND
      8. ""
    5. 外部项目使用CMake配置和构建,传递必要的构建选项:

      1. CMAKE_ARGS
      2. -DCMAKE_INSTALL_PREFIX=${STAGED_INSTALL_PREFIX}
      3. -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE}
      4. -DCMAKE_CXX_COMPILER=${CMAKE_CXX_COMPILER}
      5. -DCMAKE_CXX_STANDARD=${CMAKE_CXX_STANDARD}
      6. -DCMAKE_CXX_EXTENSIONS=${CMAKE_CXX_EXTENSIONS}
      7. -DCMAKE_CXX_STANDARD_REQUIRED=${CMAKE_CXX_STANDARD_REQUIRED}
      8. CMAKE_CACHE_ARGS
      9. -DCMAKE_CXX_FLAGS:STRING=${CMAKE_CXX_FLAGS}
    6. 我们不希望看到下载进度,也不希望在屏幕上报告配置、构建和安装信息,所以选择关闭:

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

      1. if(WIN32 AND NOT CYGWIN)
      2. set(DEF_message_DIR ${STAGED_INSTALL_PREFIX}/CMake)
      3. else()
      4. set(DEF_message_DIR ${STAGED_INSTALL_PREFIX}/share/cmake/message)
      5. endif()
      6. file(TO_NATIVE_PATH "${DEF_message_DIR}" DEF_message_DIR)
      7. set(message_DIR ${DEF_message_DIR}
      8. CACHE PATH "Path to internally built messageConfig.cmake" FORCE)

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

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

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

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

      1. add_executable(use_message use_message.cpp)
      2. target_link_libraries(use_message
      3. PUBLIC
      4. message::message-shared
      5. )

    示例展示了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:将在安装步骤之前运行测试套件

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