> 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/10.0-chinese/10.3-chinese.md).

# 10.3 输出目标

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

可以假设，消息库在开源社区取得了巨大的成功。人们非常喜欢它，并在自己的项目中使用它将消息打印到屏幕上。用户特别喜欢每个打印的消息都有惟一的标识符。但用户也希望，当他们编译并安装了库，库就能更容易找到。这个示例将展示CMake如何让我们导出目标，以便其他使用CMake的项目可以轻松地获取它们。

## 准备工作

源代码与之前的示例一致，项目结构如下:

```
.
├── cmake
│    └── messageConfig.cmake.in
├── CMakeLists.txt
├── src
│    ├── CMakeLists.txt
│    ├── hello- world.cpp
│    ├── Message.cpp
│    └── Message.hpp
└── tests
    ├── CMakeLists.txt
    └── use_target
        ├── CMakeLists.txt
        └── use_message.cpp
```

注意，cmake子目录中添加了一个`messageConfig.cmake.in`。这个文件将包含导出的目标，还添加了一个测试来检查项目的安装和导出是否按预期工作。

## 具体实施

同样，主`CMakeLists.txt`文件相对于前一个示例来说没有变化。移动到包含我们的源代码的子目录`src`中：

1. 需要找到UUID库，可以重用之前示例中的代码：

   ```
   # Search for pkg-config and UUID
   find_package(PkgConfig QUIET)
   if(PKG_CONFIG_FOUND)
       pkg_search_module(UUID uuid IMPORTED_TARGET)
       if(TARGET PkgConfig::UUID)
           message(STATUS "Found libuuid")
           set(UUID_FOUND TRUE)
       endif()
   endif()
   ```
2. 接下来，设置动态库目标并生成导出头文件：

   ```
   add_library(message-shared SHARED "")
   include(GenerateExportHeader)

   generate_export_header(message-shared
     BASE_NAME "message"
     EXPORT_MACRO_NAME "message_EXPORT"
     EXPORT_FILE_NAME "${CMAKE_BINARY_DIR}/${INSTALL_INCLUDEDIR}/messageExport.h"
     DEPRECATED_MACRO_NAME "message_DEPRECATED"
     NO_EXPORT_MACRO_NAME "message_NO_EXPORT"
     STATIC_DEFINE "message_STATIC_DEFINE"
     NO_DEPRECATED_MACRO_NAME "message_NO_DEPRECATED"
     DEFINE_NO_DEPRECATED
     )
   target_sources(message-shared
     PRIVATE
         ${CMAKE_CURRENT_LIST_DIR}/Message.cpp
     )
   ```
3. 为目标设置了`PUBLIC`和`INTERFACE`编译定义。注意`$<INSTALL_INTERFACE:...>`生成器表达式的使用：

   ```
     target_compile_definitions(message-shared
     PUBLIC
         $<$<BOOL:${UUID_FOUND}>:HAVE_UUID>
     INTERFACE
         $<INSTALL_INTERFACE:USING_message>
     )
   ```
4. 链接库和目标属性与前一个示例一样：

   ```
   target_link_libraries(message-static
     PUBLIC
         $<$<BOOL:${UUID_FOUND}>:PkgConfig::UUID>
     )

   set_target_properties(message-static
       PROPERTIES
       POSITION_INDEPENDENT_CODE 1
       ARCHIVE_OUTPUT_NAME "message"
       DEBUG_POSTFIX "_sd"
       RELEASE_POSTFIX "_s"
       PUBLIC_HEADER "Message.hpp;${CMAKE_BINARY_DIR}/${INSTALL_INCLUDEDIR}/messageExport.h"
     )
   ```
5. 可执行文件的生成，与前一个示例中使用的命令完全相同：

   ```
   add_executable(hello-world_wDSO hello-world.cpp)

   target_link_libraries(hello-world_wDSO
     PUBLIC
         message-shared
     )

   # Prepare RPATH
   file(RELATIVE_PATH _rel ${CMAKE_INSTALL_PREFIX}/${INSTALL_BINDIR} ${CMAKE_INSTALL_PREFIX})
   if(APPLE)
       set(_rpath "@loader_path/${_rel}")
   else()
       set(_rpath "\$ORIGIN/${_rel}")
   endif()
   file(TO_NATIVE_PATH "${_rpath}/${INSTALL_LIBDIR}" message_RPATH)

   set_target_properties(hello-world_wDSO
     PROPERTIES
       MACOSX_RPATH ON
       SKIP_BUILD_RPATH OFF
       BUILD_WITH_INSTALL_RPATH OFF
       INSTALL_RPATH "${message_RPATH}"
       INSTALL_RPATH_USE_LINK_PATH ON
     )

   add_executable(hello-world_wAR hello-world.cpp)

   target_link_libraries(hello-world_wAR
     PUBLIC
         message-static
     )
   ```

现在，来看看安装规则：

1. 因为CMake可以正确地将每个目标放在正确的地方，所以把目标的安装规则都列在一起。这次，添加了`EXPORT`关键字，这样CMake将为目标生成一个导出的目标文件：

   ```
   install(
     TARGETS
       message-shared
       message-static
       hello-world_wDSO
       hello-world_wAR
     EXPORT
         messageTargets
     ARCHIVE
       DESTINATION ${INSTALL_LIBDIR}
       COMPONENT lib
     RUNTIME
       DESTINATION ${INSTALL_BINDIR}
       COMPONENT bin
     LIBRARY
       DESTINATION ${INSTALL_LIBDIR}
       COMPONENT lib
     PUBLIC_HEADER
       DESTINATION ${INSTALL_INCLUDEDIR}/message
       COMPONENT dev
     )
   ```
2. 自动生成的导出目标文件称为`messageTargets.cmake`，需要显式地指定它的安装规则。这个文件的目标是`INSTALL_CMAKEDIR`，在主`CMakeLists.txt`文件中定义:

   ```
   install(
     EXPORT
         messageTargets
     NAMESPACE
         "message::"
     DESTINATION
         ${INSTALL_CMAKEDIR}
     COMPONENT
         dev
     )
   ```
3. 最后，需要生成正确的CMake配置文件。这些将确保下游项目能够找到消息库导出的目标。为此，首先包括`CMakePackageConfigHelpers.cmake`标准模块：

   ```
   include(CMakePackageConfigHelpers)
   ```
4. 让CMake为我们的库，生成一个包含版本信息的文件:

   ```
   write_basic_package_version_file(
     ${CMAKE_CURRENT_BINARY_DIR}/messageConfigVersion.cmake
     VERSION ${PROJECT_VERSION}
         COMPATIBILITY SameMajorVersion
     )
   ```
5. 使用`configure_package_config_file`函数，我们生成了实际的CMake配置文件。这是基于模板`cmake/messageConfig.cmake.in`文件:

   ```
   configure_package_config_file(
     ${PROJECT_SOURCE_DIR}/cmake/messageConfig.cmake.in
     ${CMAKE_CURRENT_BINARY_DIR}/messageConfig.cmake
     INSTALL_DESTINATION ${INSTALL_CMAKEDIR}
     )
   ```
6. 最后，为这两个自动生成的配置文件设置了安装规则:

   ```
   install(
     FILES
         ${CMAKE_CURRENT_BINARY_DIR}/messageConfig.cmake
         ${CMAKE_CURRENT_BINARY_DIR}/messageConfigVersion.cmake
     DESTINATION
         ${INSTALL_CMAKEDIR}
     )
   ```

`cmake/messageConfig.cmake`的内容是什么？该文件的顶部有相关的说明，可以作为用户文档供使用者查看。让我们看看实际的CMake命令:

1. 占位符将使用`configure_package_config_file`命令进行替换:

   ```
   @PACKAGE_INIT@
   ```
2. 包括为目标自动生成的导出文件:

   ```
   include("${CMAKE_CURRENT_LIST_DIR}/messageTargets.cmake")
   ```
3. 检查静态库和动态库，以及两个“Hello, World”可执行文件是否带有CMake提供的`check_required_components`函数：

   ```
   check_required_components(
       "message-shared"
       "message-static"
       "message-hello-world_wDSO"
       "message-hello-world_wAR"
     )
   ```
4. 检查目标`PkgConfig::UUID`是否存在。如果没有，我们再次搜索UUID库(只在非Windows操作系统下有效):

   ```
   if(NOT WIN32)
     if(NOT TARGET PkgConfig::UUID)
       find_package(PkgConfig REQUIRED QUIET)
       pkg_search_module(UUID REQUIRED uuid IMPORTED_TARGET)
     endif()
   endif()
   ```

测试一下：

```
$ mkdir -p build
$ cd build
$ cmake -DCMAKE_INSTALL_PREFIX=$HOME/Software/recipe-03 ..
$ cmake --build . --target install
```

安装树应该如下所示：

```
$HOME/Software/recipe-03/
├── bin
│    ├── hello-world_wAR
│    └── hello-world_wDSO
├── include
│    └── message
│        ├── messageExport.h
│        └── Message.hpp
├── lib64
│    ├── libmessage_s.a
│    ├── libmessage.so -> libmessage.so.1
│    └── libmessage.so.1
└── share
    └── cmake
        └── recipe-03
            ├── messageConfig.cmake
            ├── messageConfigVersion.cmake
            ├── messageTargets.cmake
            └── messageTargets-release.cmake
```

出现了一个`share`子目录，其中包含我们要求CMake自动生成的所有文件。现在开始，消息库的用户可以在他们自己的`CMakeLists.txt`文件中找到消息库，只要他们设置`message_DIR`的CMake变量，指向安装树中的`share/cmake/message`目录:

```
find_package(message 1 CONFIG REQUIRED)
```

## 工作原理

这个示例涵盖了很多领域。对于构建系统将要执行的操作，CMake目标是一个非常有用的抽象概念。使用`PRIVATE`、`PUBLIC`和`INTERFACE`关键字，我们可以设置项目中的目标进行交互。在实践中，这允许我们定义目标A的依赖关系，将如何影响目标B(依赖于A)。如果库维护人员提供了适当的CMake配置文件，那么只需很少的CMake命令就可以轻松地解决所有依赖关系。

这个问题可以通过遵循`message-static`、`message-shared`、`hello-world_wDSO`和`hello-world_wAR`目标概述的模式来解决。我们将单独分析`message-shared`目标的CMake命令，这里只是进行一般性讨论：

1. 生成目标在项目构建中列出其依赖项。对UUID库的链接是 `message-shared`的`PUBLIC`需求，因为它将用于在项目中构建目标和在下游项目中构建目标。编译时宏定义和包含目录需要在`PUBLIC`级或`INTERFACE`级目标上进行设置。它们实际上是在项目中构建目标时所需要的，其他的只与下游项目相关。此外，其中一些只有在项目安装之后才会相关联。这里使用了`$<BUILD_INTERFACE:...>`和`$<INSTALL_INTERFACE:...>`生成器表达式。只有消息库外部的下游目标才需要这些，也就是说，只有在安装了目标之后，它们才会变得可见。我们的例子中，应用如下:
   * 只有在项目中使用了`message-shared`库，那么`$<BUILD_INTERFACE:${CMAKE_BINARY_DIR}/${INSTALL_INCLUDEDIR}>`才会扩展成`${CMAKE_BINARY_DIR}/${INSTALL_INCLUDEDIR}`
   * 只有在`message-shared`库在另一个构建树中，作为一个已导出目标，那么`$<INSTALL_INTERFACE:${INSTALL_INCLUDEDIR}>`将会扩展成`${INSTALL_INCLUDEDIR}`
2. 描述目标的安装规则，包括生成文件的名称。
3. 描述CMake生成的导出文件的安装规则`messageTargets.cmake`文件将安装到`INSTALL_CMAKEDIR`。目标导出文件的安装规则的名称空间选项，将把给定字符串前置到目标的名称中，这有助于避免来自不同项目的目标之间的名称冲突。`INSTALL_CMAKEDIR`变量是在主`CMakeLists.txt`文件中设置的:

   ```
   if(WIN32 AND NOT CYGWIN)
       set(DEF_INSTALL_CMAKEDIR CMake)
   else()
       set(DEF_INSTALL_CMAKEDIR share/cmake/${PROJECT_NAME})
   endif()
   set(INSTALL_CMAKEDIR ${DEF_INSTALL_CMAKEDIR} CACHE PATH "Installation directory for CMake files")
   ```

`CMakeLists.txt`的最后一部分生成配置文件。包括`CMakePackageConfigHelpers.cmake`模块，分三步完成:

1. 调用`write_basic_package_version_file`函数生成一个版本文件包。宏的第一个参数是版本控制文件的路径：`messageConfigVersion.cmake`。版本格式为`Major.Minor.Patch`，并使用`PROJECT_VERSION`指定版本，还可以指定与库的新版本的兼容性。例子中，当库具有相同的主版本时，为了保证兼容性，使用了相同的`SameMajorVersion`参数。
2. 接下来，配置模板文件`messageConfig.cmake.in`，该文件位于`cmake`子目录中。
3. 最后，为新生成的文件设置安装规则。两者都将安装在`INSTALL_CMAKEDIR`下。

## 更多信息

消息库的客户现在非常高兴，因为终于可以在自己的系统上安装这个库，对自己的`CMakeLists.txt`进行简单的修改，就能找到消息库：

```
find_package(message VERSION 1 REQUIRED)
```

客户可以用以下方式配置他们的项目:

```
$ cmake -Dmessage_DIR=/path/to/message/share/cmake/message ..
```

我们示例中包含的测试，显示了如何检查目标的安装是否按照计划进行。看看`tests`文件夹的结构，我们注意到`use_target`子目录：

```
tests/
├── CMakeLists.txt
└── use_target
    ├── CMakeLists.txt
    └── use_message.cpp
```

这个目录包含一个使用导出目标的小项目。有趣的部分是在CMakeLists.txt文件中指定的测试:

1. 我们测试小项目，可以配置为使用已安装的库。这是`use-target`测试固件的设置步骤，可以参考第4章第10节:

   ```
   add_test(
     NAME use-target_configure
     COMMAND
       ${CMAKE_COMMAND} -H${CMAKE_CURRENT_LIST_DIR}/use_target
                         -B${CMAKE_CURRENT_BINARY_DIR}/build_use-target
                         -G${CMAKE_GENERATOR}
                         -Dmessage_DIR=${CMAKE_INSTALL_PREFIX}/${
                         INSTALL_CMAKEDIR}
                         -DCMAKE_BUILD_TYPE=$<CONFIGURATION>
     )

   set_tests_properties(use-target_configure
     PROPERTIES
       FIXTURES_SETUP use-target
     )
   ```
2. 测试了小项目可以构建:

   ```
   add_test(
     NAME use-target_build
     COMMAND
       ${CMAKE_COMMAND} --build ${CMAKE_CURRENT_BINARY_DIR}/build_use-target
                         --config $<CONFIGURATION>
     )

   set_tests_properties(use-target_build
     PROPERTIES
       FIXTURES_REQUIRED use-target
     )
   ```
3. 小项目的测试也会运行:

   ```
   set(_test_target)
   if(MSVC)
     set(_test_target "RUN_TESTS")
   else()
     set(_test_target "test")
   endif()

   add_test(
     NAME use-target_test
     COMMAND
       ${CMAKE_COMMAND} --build ${CMAKE_CURRENT_BINARY_DIR}/build_use-target
                         --target ${_test_target}
                         --config $<CONFIGURATION>
     )
   set_tests_properties(use-target_test
     PROPERTIES
       FIXTURES_REQUIRED use-target
     )
   unset(_test_target)
   ```
4. 最后，我们拆除固件:

   ```
   add_test(
     NAME use-target_cleanup
     COMMAND
       ${CMAKE_COMMAND} -E remove_directory ${CMAKE_CURRENT_BINARY_DIR}/build_use-target
     )

   set_tests_properties(use-target_cleanup
     PROPERTIES
       FIXTURES_CLEANUP use-target
     )
   ```

注意，这些测试只能在项目安装之后运行。
