多读书多实践,勤思考善领悟

CMake 完整使用教程 之十一 编写安装程序

本章主要内容:

  • 安装项目
  • 生成输出头文件
  • 输出目标
  • 安装超级构建

前几章中,我们展示了如何使用CMake配置、构建和测试项目。安装项目是很重要的一部分,本章将演示如何实现这一点。

我们将指导完成各个步骤,直到完成安装一个简单的C++项目:从项目中构建的文件,并复制到正确的目录,确保其他项目使用CMake时可以找到该工程的输出目标。本章中的4个示例将建立在第1章第3节的示例基础上。之前,我们试图构建一个非常简单的库,并将其链接到一个可执行文件中。我们还展示了如何使用相同的源文件构建静态库和动态库。本章中,我们将会讨论安装时所发生的事情。

10.1 安装项目

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

第一个示例中,将介绍我们的小项目和一些基本概念,这些概念也将在后面的示例中使用。安装文件、库和可执行文件是一项非常基础的任务,但是也可能会带来一些缺陷。我们将带您了解这些问题,并展示如何使用CMake有效地避开这些缺陷。

准备工作

第1章第3节的示例,几乎复用:只添加对UUID库的依赖。这个依赖是有条件的,如果没有找到UUID库,我们将通过预处理程序排除使用UUID库的代码。项目布局如下:

1
2
3
4
5
6
7
8
9
.
├── CMakeLists.txt
├── src
│ ├── CMakeLists.txt
│ ├── hello-world.cpp
│ ├── Message.cpp
│ └── Message.hpp
└── tests
└── CMakeLists.txt

我们已经看到,有三个CMakeLists.txt,一个是主CMakeLists.txt,另一个是位于src目录下的,还有一个是位于test目录下的。

Message.hpp头文件包含以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#pragma once

#include <iosfwd>
#include <string>

class Message
{
public:
Message(const std::string &m) : message_(m) {}
friend std::ostream &operator<<(std::ostream &os, Message &obj)
{
return obj.printObject(os);
}

private:
std::string message_;
std::ostream &printObject(std::ostream &os);
};

std::string getUUID();

Message.cpp中有相应的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include "Message.hpp"
#include <iostream>
#include <string>
#ifdef HAVE_UUID
#include <uuid/uuid.h>
#endif
std::ostream &Message::printObject(std::ostream &os)
{
os << "This is my very nice message: " << std::endl;
os << message_ << std::endl;
os << "...and here is its UUID: " << getUUID();
return os;
}
#ifdef HAVE_UUID
std::string getUUID()
{
uuid_t uuid;
uuid_generate(uuid);
char uuid_str[37];
uuid_unparse_lower(uuid, uuid_str);
uuid_clear(uuid);
std::string uuid_cxx(uuid_str);
return uuid_cxx;
}
#else
std::string getUUID()
{
return "Ooooops, no UUID for you!";
}
#endif

最后,示例hello-world.cpp内容如下:

1
2
3
4
5
6
7
8
9
10
11
#include <cstdlib>
#include <iostream>
#include "Message.hpp"
int main()
{
Message say_hello("Hello, CMake World!");
std::cout << say_hello << std::endl;
Message say_goodbye("Goodbye, CMake World");
std::cout << say_goodbye << std::endl;
return EXIT_SUCCESS;
}

具体实施

我们先来看一下主CMakeLists.txt:

  1. 声明CMake最低版本,并定义一个C++11项目。请注意,我们已经为我们的项目设置了一个版本,在project中使用VERSION进行指定:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    # CMake 3.6 needed for IMPORTED_TARGET option
    # to pkg_search_module
    cmake_minimum_required(VERSION 3.6 FATAL_ERROR)
    project(recipe-01
    LANGUAGES CXX
    VERSION 1.0.0
    )
    # <<< General set up >>>
    set(CMAKE_CXX_STANDARD 11)
    set(CMAKE_CXX_EXTENSIONS OFF)
    set(CMAKE_CXX_STANDARD_REQUIRED ON)
  2. 用户可以通过CMAKE_INSTALL_PREFIX变量定义安装目录。CMake会给这个变量设置一个默认值:Windows上的C:\Program Files和Unix上的/usr/local。我们将会打印安装目录的信息:

    1
    message(STATUS "Project will be installed to ${CMAKE_INSTALL_PREFIX}")
  3. 默认情况下,我们更喜欢以Release的方式配置项目。用户可以通过CMAKE_BUILD_TYPE设置此变量,从而改变配置类型,我们将检查是否存在这种情况。如果没有,将设置为默认值:

    1
    2
    3
    4
    if(NOT CMAKE_BUILD_TYPE)
    set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE)
    endif()
    message(STATUS "Build type set to ${CMAKE_BUILD_TYPE}")
  4. 接下来,告诉CMake在何处构建可执行、静态和动态库目标。便于在用户不打算安装项目的情况下,访问这些构建目标。这里使用标准CMake的GNUInstallDirs.cmake模块。这将确保的项目布局的合理性和可移植性:

    1
    2
    3
    4
    5
    6
    7
    8
    include(GNUInstallDirs)

    set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY
    ${PROJECT_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR})
    set(CMAKE_LIBRARY_OUTPUT_DIRECTORY
    ${PROJECT_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR})
    set(CMAKE_RUNTIME_OUTPUT_DIRECTORY
    ${PROJECT_BINARY_DIR}/${CMAKE_INSTALL_BINDIR})
  5. 虽然,前面的命令配置了构建目录中输出的位置,但是需要下面的命令来配置可执行程序、库以及安装前缀中包含的文件的位置。它们大致遵循相同的布局,但是我们定义了新的INSTALL_LIBDIRINSTALL_BINDIRINSTALL_INCLUDEDIRINSTALL_CMAKEDIR变量。当然,也可以覆盖这些变量:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    # Offer the user the choice of overriding the installation directories
    set(INSTALL_LIBDIR ${CMAKE_INSTALL_LIBDIR} CACHE PATH "Installation directory for libraries")
    set(INSTALL_BINDIR ${CMAKE_INSTALL_BINDIR} CACHE PATH "Installation directory for executables")
    set(INSTALL_INCLUDEDIR ${CMAKE_INSTALL_INCLUDEDIR} CACHE PATH "Installation directory for header files")
    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")
  6. 报告组件安装的路径:

    1
    2
    3
    4
    5
    6
    # Report to user
    foreach(p LIB BIN INCLUDE CMAKE)
    file(TO_NATIVE_PATH ${CMAKE_INSTALL_PREFIX}/${INSTALL_${p}DIR} _path )
    message(STATUS "Installing ${p} components to ${_path}")
    unset(_path)
    endforeach()
  7. CMakeLists.txt文件中的最后一个指令添加src子目录,启用测试,并添加tests子目录:

    1
    2
    3
    add_subdirectory(src)
    enable_testing()
    add_subdirectory(tests)

现在我们继续分析src/CMakeLists.txt,其定义了构建的实际目标:

  1. 我们的项目依赖于UUID库:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    # 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. 我们希望建立一个动态库,将该目标声明为message-shared:

    1
    add_library(message-shared SHARED "")
  3. 这个目标由target_sources命令指定:

    1
    2
    3
    4
    target_sources(message-shared
    PRIVATE
    ${CMAKE_CURRENT_LIST_DIR}/Message.cpp
    )
  4. 我们为目标声明编译时定义和链接库。请注意,所有这些都是PUBLIC,以确保所有依赖的目标将正确继承它们:

    1
    2
    3
    4
    5
    6
    7
    8
      target_compile_definitions(message-shared
    PUBLIC
    $<$<BOOL:${UUID_FOUND}>:HAVE_UUID>
    )
    target_link_libraries(message-shared
    PUBLIC
    $<$<BOOL:${UUID_FOUND}>:PkgConfig::UUID>
    )
  5. 然后设置目标的附加属性:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    set_target_properties(message-shared
    PROPERTIES
    POSITION_INDEPENDENT_CODE 1
    SOVERSION ${PROJECT_VERSION_MAJOR}
    OUTPUT_NAME "message"
    DEBUG_POSTFIX "_d"
    PUBLIC_HEADER "Message.hpp"
    MACOSX_RPATH ON
    WINDOWS_EXPORT_ALL_SYMBOLS ON
    )
  6. 最后,为“Hello, world”程序添加可执行目标:

    1
    add_executable(hello-world_wDSO hello-world.cpp)
  7. hello-world_wDSO可执行目标,会链接到动态库:

    1
    2
    3
    4
    target_link_libraries(hello-world_wDSO
    PUBLIC
    message-shared
    )

src/CMakeLists.txt文件中,还包含安装指令。考虑这些之前,我们需要设置可执行文件的RPATH

  1. 使用CMake路径操作,我们可以设置message_RPATH变量。这将为GNU/Linux和macOS设置适当的RPATH:

    1
    2
    3
    4
    5
    6
    7
    8
    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)
  2. 现在,可以使用这个变量来设置可执行目标hello-world_wDSORPATH(通过目标属性实现)。我们也可以设置额外的属性,稍后会对此进行更多的讨论:

    1
    2
    3
    4
    5
    6
    7
    8
    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
    )
  3. 终于可以安装库、头文件和可执行文件了!使用CMake提供的install命令来指定安装位置。注意,路径是相对的,我们将在后续进一步讨论这一点:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    install(
    TARGETS
    message-shared
    hello-world_wDSO
    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
    )

tests目录中的CMakeLists.txt文件包含简单的指令,以确保“Hello, World”可执行文件能够正确运行:

1
2
3
4
add_test(
NAME test_shared
COMMAND $<TARGET_FILE:hello-world_wDSO>
)

现在让我们配置、构建和安装项目,并查看结果。添加安装指令时,CMake就会生成一个名为install的新目标,该目标将运行安装规则:

1
2
3
4
$ mkdir -p build
$ cd build
$ cmake -G"Unix Makefiles" -DCMAKE_INSTALL_PREFIX=$HOME/Software/recipe-01
$ cmake --build . --target install

GNU/Linux构建目录的内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
build
├── bin
│ └── hello-world_wDSO
├── CMakeCache.txt
├── CMakeFiles
├── cmake_install.cmake
├── CTestTestfile.cmake
├── install_manifest.txt
├── lib64
│ ├── libmessage.so -> libmessage.so.1
│ └── libmessage.so.1
├── Makefile
├── src
├── Testing
└── tests

另一方面,在安装位置,可以找到如下的目录结构:

1
2
3
4
5
6
7
8
9
$HOME/Software/recipe-01/
├── bin
│ └── hello-world_wDSO
├── include
│ └── message
│ └── Message.hpp
└── lib64
├── libmessage.so -> libmessage.so.1
└── libmessage.so.1

这意味着安装指令中给出的位置,是相对于用户给定的CMAKE_INSTALL_PREFIX路径。

工作原理

这个示例有三个要点我们需要更详细地讨论:

  • 使用GNUInstallDirs.cmake定义目标安装的标准位置
  • 在动态库和可执行目标上设置的属性,特别是RPATH的处理
  • 安装指令

###安装到标准位置

对于项目的安装来说,什么是好的布局呢?如果只有自己使用该项目,那就无所谓好或坏的布局。然而,一旦向外部发布产品,和他人共用该项目,就应该在安装项目时提供一个合理的布局。幸运的是,我们可以遵循一些标准,CMake可以帮助我们做到这一点。实际上,GNUInstallDirs.cmake模块所做的就是定义这样一组变量,这些变量是安装不同类型文件的子目录的名称。在例子中,使用了以下内容:

  • *CMAKE_INSTALL_BINDIR:这将用于定义用户可执行文件所在的子目录,即所选安装目录下的bin目录。
  • CMAKE_INSTALL_LIBDIR:这将扩展到目标代码库(即静态库和动态库)所在的子目录。在64位系统上,它是lib64,而在32位系统上,它只是lib
  • CMAKE_INSTALL_INCLUDEDIR:最后,我们使用这个变量为C头文件获取正确的子目录,该变量为include

然而,用户可能希望覆盖这些选项。我们允许在主CMakeLists.txt文件中使用以下方式覆盖选项:

1
2
3
4
5
6
7
8
# Offer the user the choice
of overriding the installation directories
set(INSTALL_LIBDIR ${CMAKE_INSTALL_LIBDIR} CACHE PATH
"Installation directory for libraries")
set(INSTALL_BINDIR ${CMAKE_INSTALL_BINDIR} CACHE PATH
"Installation directory for executables")
set(INSTALL_INCLUDEDIR ${CMAKE_INSTALL_INCLUDEDIR} CACHE
PATH "Installation directory for header files")

这重新定义了在项目中使用的INSTALL_BINDIRINSTALL_LIBDIRINSTALL_INCLUDEDIR变量。我们还定义了INSTALL_CMAKEDIR变量,但它所扮演的角色将在接下来的几个示例中详细讨论。

TIPS:GNUInstallDirs.cmake模块定义了额外的变量,这些变量将有助于,将已安装的文件放置到所选安装前缀的子目录中。请参考CMake在线文档:https://cmake.org/cmake/help/v3.6/module/GNUInstallDirs.html

目标属性和RPATH处理

让我们更仔细地看看在动态库目标上设置的属性,需要设置以下内容:

  • POSITION_INDEPENDENT_CODE 1:设置生成位置无关代码所需的编译器标志。有关更多信息,请参考https://en.wikipedia.org/wiki/position-independentent_code
  • SOVERSION ${PROJECT_VERSION_MAJOR} : 这是动态库提供的应用程序编程接口(API)版本。在设置语义版本之后,将其设置为与项目的主版本一致。CMake目标也有一个版本属性,可以用来指定目标的构建版本。注意,SOVERSIONVERSION有所不同:随着时间的推移,提供相同API的多个构建版本。本例中,我们不关心这种的粒度控制:仅使用SOVERSION属性设置API版本就足够了,CMake将为我们将VERSION设置为相同的值。相关详细信息,请参考官方文档:https://cmake.org/cmake/help/latest/prop_tgt/SOVERSION.html
  • OUTPUT_NAME "message":这告诉CMake库的名称message,而不是目标message-shared的名称,libmessage.so.1将在构建时生成。从前面给出的构建目录和安装目录的也可以看出,libmessage.so的符号链接也将生成。
  • DEBUG_POSTFIX "_d":这告诉CMake,如果我们以Debug配置构建项目,则将_d后缀添加到生成的动态库。
  • PUBLIC_HEADER "Message.hpp":我们使用这个属性来设置头文件列表(本例中只有一个头文件),声明提供的API函数。这主要用于macOS上的动态库目标,也可以用于其他操作系统和目标。有关详细信息,请参见官方文档:https://cmake.org/cmake/help/v3.6/prop_tgt/PUBLIC_HEADER.html
  • MACOSX_RPATH ON:这将动态库的install_name部分(目录)设置为macOS上的@rpath
  • WINDOWS_EXPORT_ALL_SYMBOLS ON:这将强制在Windows上编译以导出所有符号。注意,这通常不是一个好的方式,我们将在第2节中展示如何生成导出头文件,以及如何在不同的平台上保证符号的可见性。

现在讨论一下RPATH。我们将hello-world_wDSO可执行文件链接到libmessage.so.1,这意味着在执行时,将加载动态库。因此,有关库位置的信息需要在某个地方进行编码,以便加载程序能够成功地完成其工作。库的定位有两种方法:

  • 通过设置环境变量通知链接器:
    • GNU/Linux上,这需要将路径附加到LD_LIBRARY_PATH环境变量中。注意,这很可能会污染系统中所有应用程序的链接器路径,并可能导致符号冲突( https://gms.tf/ld_library_path-considered-harmful.htm )。
    • macOS上,可以设置DYLD_LIBRARY_PATH变量。这与GNU/Linux上的LD_LIBRARY_PATH有相同的问题,可以通过使用DYLD_FALLBACK_LIBRARY_PATH变量来(部分的)改善这种情况。请看下面的链接,获取相关例子: https://stackoverflow.com/a/3172515/2528668
  • 可被编码到可执行文件中,使用RPATH可以设置可执行文件的运行时搜索路径

后一种方法更健壮。但是,设置动态对象的RPATH时,应该选择哪个路径?我们需要确保可执行文件总是找到正确的动态库,不管它是在构建树中运行还是在安装树中运行。这需要通过设置hello-world_wDSO目标的RPATH相关属性来实现的,通过$ORIGIN(在GNU/Linux上)或@loader_path(在macOS上)变量来查找与可执行文件本身位置相关的路径:

1
2
3
4
5
6
7
8
# 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)

当设置了message_RPATH变量,目标属性将完成剩下的工作:

1
2
3
4
5
6
7
8
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
)

让我们详细研究一下这个命令:

  • SKIP_BUILD_RPATH OFF:告诉CMake生成适当的RPATH,以便能够在构建树中运行可执行文件。
  • UILD_WITH_INSTALL_RPATH OFF:关闭生成可执行目标,使其RPATH调整为与安装树的RPATH相同。在构建树中不运行可执行文件。
  • INSTALL_RPATH "${message_RPATH}":将已安装的可执行目标的RPATH设置为先前的路径。
  • INSTALL_RPATH_USE_LINK_PATH ON:告诉CMake将链接器搜索路径附加到可执行文件的RPATH中。

NOTE:加载器在Unix系统上如何工作的更多信息,可参见:http://longwei.github.io/rpath_origin/

安装指令

最后,看一下安装指令。我们需要安装一个可执行文件、一个库和一个头文件。可执行文件和库是构建目标,因此我们使用安装命令的TARGETS选项。可以同时设置多个目标的安装规则:CMake知道它们是什么类型的目标,无论其是可执行程序库、动态库,还是静态库:

1
2
3
4
install(
TARGETS
message-shared
hello-world_wDSO

可执行文件将安装在RUNTIME DESTINATION,将其设置为${INSTALL_BINDIR}。动态库安装到LIBRARY_DESTINATION,将其设置为${INSTALL_LIBDIR}。静态库将安装到ARCHIVE DESTINATION,将其设置为${INSTALL_LIBDIR}:

1
2
3
4
5
6
7
8
9
ARCHIVE
DESTINATION ${INSTALL_LIBDIR}
COMPONENT lib
RUNTIME
DESTINATION ${INSTALL_BINDIR}
COMPONENT bin
LIBRARY
DESTINATION ${INSTALL_LIBDIR}
COMPONENT lib

注意,这里不仅指定了DESTINATION,还指定了COMPONENT。使用cmake --build . --target install安装命令,所有组件会按预期安装完毕。然而,有时只安装其中一些可用的。这就是COMPONENT关键字帮助我们做的事情。例如,当只要求安装库,我们可以执行以下步骤:

1
$ cmake -D COMPONENT=lib -P cmake_install.cmake

自从Message.hpp头文件设置为项目的公共头文件,我们可以使用PUBLIC_HEADER关键字将其与其他目标安装到选择的目的地:${INSTALL_INCLUDEDIR}/message。库用户现在可以包含头文件:#include <message/Message.hpp>,这需要在编译时,使用-I选项将正确的头文件查找路径位置传递给编译器。

安装指令中的各种目标地址会被解释为相对路径,除非使用绝对路径。但是相对于哪里呢?根据不同的安装工具而不同,而CMake可以去计算目标地址的绝对路径。当使用cmake --build . --target install,路径将相对于CMAKE_INSTALL_PREFIX计算。但当使用CPack时,绝对路径将相对于CPACK_PACKAGING_INSTALL_PREFIX计算。CPack的用法将在第11章中介绍。

NOTE:Unix Makefile和Ninja生成器还提供了另一种机制:DESTDIR。可以在DESTDIR指定的目录下重新定位整个安装树。也就是说,env DESTDIR=/tmp/stage cmake --build . --target install将安装相对于CMAKE_INSTALL_PREFIX/tmp/stage目录。可以在这里阅读更多信息:https://www.gnu.org/prep/standards/html_node/DESTDIR.html

更多信息

正确设置RPATH可能相当麻烦,但这对于用户来说无法避免。默认情况下,CMake设置可执行程序的RPATH,假设它们将从构建树运行。但是,安装之后RPATH被清除,当用户想要运行hello-world_wDSO时,就会出现问题。使用Linux上的ldd工具,我们可以检查构建树中的hello-world_wDSO可执行文件,运行ldd hello-world_wDSO将得到以下结果:

1
libmessage.so.1 => /home/user/cmake-cookbook/chapter-10/recipe-01/cxx-example/build/lib64/libmessage.so.1(0x00007f7a92e44000)

在安装目录中运行ldd hello-world_wDSO将得到以下结果:

1
libmessage.so.1 => Not found

这显然是不行的。但是,总是硬编码RPATH来指向构建树或安装目录也是错误的:这两个位置中的任何一个都可能被删除,从而导致可执行文件的损坏。这里给出的解决方案为构建树和安装目录中的可执行文件设置了不同的RPATH,因此它总是指向“有意义”的位置;也就是说,尽可能接近可执行文件。在构建树中运行ldd显示相同的输出:

1
2
3
libmessage.so.1 => /home/roberto/Workspace/robertodr/cmake-
cookbook/chapter-10/recipe-01/cxx-example/build/lib64/libmessage.so.1
(0x00007f7a92e44000)

另外,在安装目录下,我们得到:

1
libmessage.so.1 => /home/roberto/Software/ch10r01/bin/../lib64/libmessage.so.1 (0x00007fbd2a725000)

我们使用了带有目标参数的CMake安装命令,因为我们需要安装构建目标。而该命令还有另外4个参数:

10.2 生成输出头文件

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

设想一下,当我们的小型库非常受欢迎时,许多人都在使用它。然而,一些客户希望在安装时使用静态库,而另一些客户也注意到所有符号在动态库中都是可见的。最佳方式是规定动态库只公开最小的符号,从而限制代码中定义的对象和函数对外的可见性。我们希望在默认情况下,动态库定义的所有符号都对外隐藏。这将使得项目的贡献者,能够清楚地划分库和外部代码之间的接口,因为他们必须显式地标记所有要在项目外部使用的符号。因此,我们需要完成以下工作:

  • 使用同一组源文件构建动态库和静态库
  • 确保正确分隔动态库中符号的可见性

第1章第3节中,已经展示了CMake提供了与平台无关的方式实现的功能。但是,没有处理符号可见性的问题。我们将用当前的配方重新讨论这两点。

准备工作

我们仍将使用与前一个示例中基本相同的代码,但是我们需要修改src/CMakeLists.txtMessage.hpp头文件。后者将包括新的、自动生成的头文件messageExport.h:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#pragma once

#include
#include

#include "messageExport.h"

class message_EXPORT Message
{
public:
Message(const std::string &m) : message_(m) {}

friend std::ostream &operator<<(std::ostream &os, Message &obj)
{
return obj.printObject(os);
}

private:
std::string message_;
std::ostream &printObject(std::ostream &os);
};

std::string getUUID();

Message类的声明中引入了message_EXPORT预处理器指令,这个指令将让编译器生成对库的用户可见的符号。

具体实施

除了项目的名称外,主CMakeLists.txt文件没有改变。首先,看看src子目录中的CMakeLists.txt文件,所有工作实际上都在这里进行。我们将重点展示对之前示例的修改之处:

  1. 为消息传递库声明SHARED库目标及其源。注意,编译定义和链接库没有改变:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    add_library(message-shared SHARED "")

    target_sources(message-shared
    PRIVATE
    ${CMAKE_CURRENT_LIST_DIR}/Message.cpp
    )

    target_compile_definitions(message-shared
    PUBLIC
    $<$<BOOL:${UUID_FOUND}>:HAVE_UUID>
    )

    target_link_libraries(message-shared
    PUBLIC
    $<$<BOOL:${UUID_FOUND}>:PkgConfig::UUID>
    )
  2. 设置目标属性。将${CMAKE_BINARY_DIR}/${INSTALL_INCLUDEDIR}/messageExport.h头文件添加到公共头列表中,作为PUBLIC_HEADER目标属性的参数。CXX_VISIBILITY_PRESET置和VISIBILITY_INLINES_HIDDEN属性将在下一节中讨论:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    set_target_properties(message-shared
    PROPERTIES
    POSITION_INDEPENDENT_CODE 1
    CXX_VISIBILITY_PRESET hidden
    VISIBILITY_INLINES_HIDDEN 1
    SOVERSION ${PROJECT_VERSION_MAJOR}
    OUTPUT_NAME "message"
    DEBUG_POSTFIX "_d"
    PUBLIC_HEADER "Message.hpp;${CMAKE_BINARY_DIR}/${INSTALL_INCLUDEDIR}/messageExport.h"
    MACOSX_RPATH ON
    )
  3. 包含GenerateExportHeader.cmake模块并调用generate_export_header函数,这将在构建目录的子目录中生成messageExport.h头文件。我们将稍后会详细讨论这个函数和生成的头文件:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    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
    )
  4. 当要更改符号的可见性(从其默认值-隐藏值)时,都应该包含导出头文件。我们已经在Message.hpp头文件例这样做了,因为想在库中公开一些符号。现在将${CMAKE_BINARY_DIR}/${INSTALL_INCLUDEDIR}目录作为message-shared目标的PUBLIC包含目录列出:

    1
    2
    3
    4
    target_include_directories(message-shared
    PUBLIC
    ${CMAKE_BINARY_DIR}/${INSTALL_INCLUDEDIR}
    )

现在,可以将注意力转向静态库的生成:

  1. 添加一个库目标来生成静态库。将编译与静态库相同的源文件,以获得此动态库目标:

    1
    2
    3
    4
    5
    add_library(message-static STATIC "")
    target_sources(message-static
    PRIVATE
    ${CMAKE_CURRENT_LIST_DIR}/Message.cpp
    )
  2. 设置编译器定义,包含目录和链接库,就像我们为动态库目标所做的一样。但请注意,我们添加了message_STATIC_DEFINE编译时宏定义,为了确保我们的符号可以适当地暴露:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    target_compile_definitions(message-static
    PUBLIC
    message_STATIC_DEFINE
    $<$<BOOL:${UUID_FOUND}>:HAVE_UUID>
    )

    target_include_directories(message-static
    PUBLIC
    ${CMAKE_BINARY_DIR}/${INSTALL_INCLUDEDIR}
    )

    target_link_libraries(message-static
    PUBLIC
    $<$<BOOL:${UUID_FOUND}>:PkgConfig::UUID>
    )
  3. 还设置了message-static目标的属性:

    1
    2
    3
    4
    5
    6
    7
    8
    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"
    )
  4. 除了链接到消息动态库目标的hello-world_wDSO可执行目标之外,还定义了另一个可执行目标hello-world_wAR,这个链接指向静态库:

    1
    2
    3
    4
    5
    6
    add_executable(hello-world_wAR hello-world.cpp)

    target_link_libraries(hello-world_wAR
    PUBLIC
    message-static
    )
  5. 安装指令现在多了message-statichello-world_wAR目标,其他没有改变:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    install(
    TARGETS
    message-shared
    message-static
    hello-world_wDSO
    hello-world_wAR
    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
    )

工作原理

此示例演示了,如何设置动态库的符号可见性。最好的方式是在默认情况下隐藏所有符号,显式地只公开那些需要使用的符号。这需要分为两步实现。首先,需要指示编译器隐藏符号。当然,不同的编译器将有不同的可用选项,并且直接在CMakeLists.txt中设置这些选项并不是是跨平台的。CMake通过在动态库目标上设置两个属性,提供了一种健壮的跨平台方法来设置符号的可见性:

  • CXX_VISIBILITY_PRESET hidden:这将隐藏所有符号,除非显式地标记了其他符号。当使用GNU编译器时,这将为目标添加-fvisibility=hidden标志。
  • VISIBILITY_INLINES_HIDDEN 1:这将隐藏内联函数的符号。如果使用GNU编译器,这对应于-fvisibility-inlines-hidden

Windows上,这都是默认行为。实际上,我们需要在前面的示例中通过设置WINDOWS_EXPORT_ALL_SYMBOLS属性为ON来覆盖它。

如何标记可见的符号?这由预处理器决定,因此需要提供相应的预处理宏,这些宏可以扩展到所选平台上,以便编译器能够理解可见性属性。CMake中有现成的GenerateExportHeader.cmake模块。这个模块定义了generate_export_header函数,我们调用它的过程如下:

1
2
3
4
5
6
7
8
9
10
11
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
)

该函数生成messageExport.h头文件,其中包含预处理器所需的宏。根据EXPORT_FILE_NAME选项的请求,在目录${CMAKE_BINARY_DIR}/${INSTALL_INCLUDEDIR}中生成该文件。如果该选项为空,则头文件将在当前二进制目录中生成。这个函数的第一个参数是现有的目标(示例中是message- shared),函数的基本调用只需要传递现有目标的名称即可。可选参数,用于细粒度的控制所有生成宏,也可以传递:

  • BASE_NAME:设置生成的头文件和宏的名称。
  • EXPORT_MACRO_NAME:设置导出宏的名称。
  • EXPORT_FILE_NAME:设置导出头文件的名称。
  • DEPRECATED_MACRO_NAME:设置弃用宏的名称。这是用来标记将要废弃的代码,如果客户使用该宏定义,编译器将发出一个将要废弃的警告。
  • NO_EXPORT_MACRO_NAME:设置不导出宏的名字。
  • STATIC_DEFINE:用于定义宏的名称,以便使用相同源编译静态库时使用。
  • NO_DEPRECATED_MACRO_NAME:设置宏的名称,在编译时将“将要废弃”的代码排除在外。
  • DEFINE_NO_DEPRECATED:指示CMake生成预处理器代码,以从编译中排除“将要废弃”的代码。

GNU/Linux上,使用GNU编译器,CMake将生成以下messageExport.h头文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#ifndef message_EXPORT_H
#define message_EXPORT_H
#ifdef message_STATIC_DEFINE
# define message_EXPORT
# define message_NO_EXPORT
#else
# ifndef message_EXPORT
# ifdef message_shared_EXPORTS
/* We are building this library */
# define message_EXPORT __attribute__((visibility("default")))
# else
/* We are using this library */
# define message_EXPORT __attribute__((visibility("default")))
# endif
# endif
# ifndef message_NO_EXPORT
# define message_NO_EXPORT __attribute__((visibility("hidden")))
# endif
#endif
#ifndef message_DEPRECATED
# define message_DEPRECATED __attribute__ ((__deprecated__))
#endif
#ifndef message_DEPRECATED_EXPORT
# define message_DEPRECATED_EXPORT message_EXPORT message_DEPRECATED
#endif
#ifndef message_DEPRECATED_NO_EXPORT
# define message_DEPRECATED_NO_EXPORT message_NO_EXPORT message_DEPRECATED
#endif
#if 1 /* DEFINE_NO_DEPRECATED */
# ifndef message_NO_DEPRECATED
# define message_NO_DEPRECATED
# endif
#endif
#endif

我们可以使用message_EXPORT宏,预先处理用户公开类和函数。弃用可以通过在前面加上message_DEPRECATED宏来实现。

messageExport.h头文件的内容可以看出,所有符号都应该在静态库中可见,这就是message_STATIC_DEFINE宏起了作用。当声明了目标,我们就将其设置为编译时定义。静态库的其他目标属性如下:

  • ARCHIVE_OUTPUT_NAME "message":这将确保库文件的名称是message,而不是message-static
  • DEBUG_POSTFIX "_sd":这将把给定的后缀附加到库名称中。当目标构建类型为Release时,为静态库添加”_sd”后缀。
  • RELEASE_POSTFIX "_s":这与前面的属性类似,当目标构建类型为Release时,为静态库添加后缀“_s”。

更多信息

构建动态库时,隐藏内部符号是一个很好的方式。这意味着库会缩小,因为向用户公开的内容要小于库中的内容。这定义了应用程序二进制接口(ABI),通常情况下应该与应用程序编程接口(API)一致。这分两个阶段进行:

  1. 使用适当的编译器标志。
  2. 使用预处理器变量(示例中是message_EXPORT)标记要导出的符号。编译时,将解除这些符号(类和函数)的隐藏。

静态库只是目标文件的归档。因此,可以将源代码编译成目标文件,然后归档器将它们捆绑到归档文件中。这时没有ABI的概念:所有符号在默认情况下都是可见的,编译器的可见标志不影响静态归档。但是,如果要从相同的源文件构建动态和静态库,则需要一种方法来赋予message_EXPORT预处理变量意义,这两种情况都会出现在代码中。这里使用GenerateExportHeader.cmake模块,它定义一个包含所有逻辑的头文件,用于给出这个预处理变量的正确定义。对于动态库,它将给定的平台与编译器相组合。注意,根据构建或使用动态库,宏定义也会发生变化。幸运的是,CMake为我们解决了这个问题。对于静态库,它将扩展为一个空字符串,执行我们期望的操作——什么也不做。

细心的读者会注意到,构建此处所示的静态和共享库实际上需要编译源代码两次。对于我们的简单示例来说,这不是一个很大的开销,但会显得相当麻烦,即使对于只比示例稍大一点的项目来说,也是如此。为什么我们选择这种方法,而不是使用第1章第3节的方式呢?OBJECT库负责编译库的第一步:从源文件到对象文件。该步骤中,预处理器将介入并计算message_EXPORT。由于对象库的编译只发生一次,message_EXPORT被计算为构建动态库库或静态库兼容的值。因此,为了避免歧义,我们选择了更健壮的方法,即编译两次,为的就是让预处理器正确地评估变量的可见性。

NOTE:有关动态共享对象、静态存档和符号可见性的更多细节,建议阅读:http://people.redhat.com/drepper/dsohowto.pdf

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的项目可以轻松地获取它们。

准备工作

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
.
├── 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库,可以重用之前示例中的代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    # 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. 接下来,设置动态库目标并生成导出头文件:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    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. 为目标设置了PUBLICINTERFACE编译定义。注意$<INSTALL_INTERFACE:...>生成器表达式的使用:

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    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. 可执行文件的生成,与前一个示例中使用的命令完全相同:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    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将为目标生成一个导出的目标文件:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    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文件中定义:

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

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

    1
    2
    3
    4
    5
    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文件:

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

    1
    2
    3
    4
    5
    6
    7
    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命令进行替换:

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

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

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

    1
    2
    3
    4
    5
    6
    if(NOT WIN32)
    if(NOT TARGET PkgConfig::UUID)
    find_package(PkgConfig REQUIRED QUIET)
    pkg_search_module(UUID REQUIRED uuid IMPORTED_TARGET)
    endif()
    endif()

测试一下:

1
2
3
4
$ mkdir -p build
$ cd build
$ cmake -DCMAKE_INSTALL_PREFIX=$HOME/Software/recipe-03 ..
$ cmake --build . --target install

安装树应该如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$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目录:

1
find_package(message 1 CONFIG REQUIRED)

工作原理

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

这个问题可以通过遵循message-staticmessage-sharedhello-world_wDSOhello-world_wAR目标概述的模式来解决。我们将单独分析message-shared目标的CMake命令,这里只是进行一般性讨论:

  1. 生成目标在项目构建中列出其依赖项。对UUID库的链接是 message-sharedPUBLIC需求,因为它将用于在项目中构建目标和在下游项目中构建目标。编译时宏定义和包含目录需要在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文件中设置的:

    1
    2
    3
    4
    5
    6
    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进行简单的修改,就能找到消息库:

1
find_package(message VERSION 1 REQUIRED)

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

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

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

1
2
3
4
5
tests/
├── CMakeLists.txt
└── use_target
├── CMakeLists.txt
└── use_message.cpp

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

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    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. 测试了小项目可以构建:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    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. 小项目的测试也会运行:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    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. 最后,我们拆除固件:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    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
    )

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

10.4 安装超级构建

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

我们的消息库取得了巨大的成功,许多其他程序员都使用它,并且非常满意。也希望在自己的项目中使用它,但是不确定如何正确地管理依赖关系。可以用自己的代码附带消息库的源代码,但是如果该库已经安装在系统上了应该怎么做呢?第8章,展示了超级构建的场景,但是不确定如何安装这样的项目。本示例将带您了解安装超级构建的安装细节。

准备工作

此示例将针对消息库,构建一个简单的可执行链接。项目布局如下:

1
2
3
4
5
6
7
8
9
10
11
12
├── cmake
│ ├── install_hook.cmake.in
│ └── print_rpath.py
├── CMakeLists.txt
├── external
│ └── upstream
│ ├── CMakeLists.txt
│ └── message
│ └── CMakeLists.txt
└── src
├── CMakeLists.txt
└── use_message.cpp

CMakeLists.txt文件配合超级构建,external子目录包含处理依赖项的CMake指令。cmake子目录包含一个Python脚本和一个模板CMake脚本。这些将用于安装方面的微调,CMake脚本首先进行配置,然后调用Python脚本打印use_message可执行文件的RPATH:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import shlex
import subprocess
import sys

def main():
patcher = sys.argv[1]
elfobj = sys.argv[2]
tools = {'patchelf': '--print-rpath', 'chrpath': '--list', 'otool': '-L'}
if patcher not in tools.keys():
raise RuntimeError('Unknown tool {}'.format(patcher))
cmd = shlex.split('{:s} {:s} {:s}'.format(patcher, tools[patcher], elfobj))
rpath = subprocess.run(
cmd,
bufsize=1,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=True)
print(rpath.stdout)

if __name__ == "__main__":
main()

使用平台原生工具可以轻松地打印RPATH,稍后我们将在本示例中讨论这些工具。

最后,src子目录包含项目的CMakeLists.txt和源文件。use_message.cpp源文件包含以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <cstdlib>
#include <iostream>

#ifdef USING_message
#include <message/Message.hpp>
void messaging()
{
Message say_hello("Hello, World! From a client of yours!");
std::cout << say_hello << std::endl;
Message say_goodbye("Goodbye, World! From a client of yours!");
std::cout << say_goodbye << std::endl;
}
#else
void messaging()
{
std::cout << "Hello, World! From a client of yours!" << std::endl;
std::cout << "Goodbye, World! From a client of yours!" << std::endl;
}
#endif

int main()
{
messaging();
return EXIT_SUCCESS;
}

具体实施

我们将从主CMakeLists.txt文件开始,它用来协调超级构建:

  1. 与之前的示例相同。首先声明一个C++11项目,设置了默认安装路径、构建类型、目标的输出目录,以及安装树中组件的布局:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    cmake_minimum_required(VERSION 3.6 FATAL_ERROR)

    project(recipe-04
    LANGUAGES CXX
    VERSION 1.0.0
    )

    # <<< General set up >>>

    set(CMAKE_CXX_STANDARD 11)
    set(CMAKE_CXX_EXTENSIONS OFF)
    set(CMAKE_CXX_STANDARD_REQUIRED ON)

    if(NOT CMAKE_BUILD_TYPE)
    set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE)
    endif()

    message(STATUS "Build type set to ${CMAKE_BUILD_TYPE}")

    message(STATUS "Project will be installed to ${CMAKE_INSTALL_PREFIX}")

    include(GNUInstallDirs)

    set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY
    ${PROJECT_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR})
    set(CMAKE_LIBRARY_OUTPUT_DIRECTORY
    ${PROJECT_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR})
    set(CMAKE_RUNTIME_OUTPUT_DIRECTORY
    ${PROJECT_BINARY_DIR}/${CMAKE_INSTALL_BINDIR})

    # Offer the user the choice of overriding the installation directories
    set(INSTALL_LIBDIR ${CMAKE_INSTALL_LIBDIR} CACHE PATH "Installation directory for libraries")
    set(INSTALL_BINDIR ${CMAKE_INSTALL_BINDIR} CACHE PATH "Installation directory for executables")
    set(INSTALL_INCLUDEDIR ${CMAKE_INSTALL_INCLUDEDIR} CACHE PATH "Installation directory for header files")
    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")

    # Report to user
    foreach(p LIB BIN INCLUDE CMAKE)
    file(TO_NATIVE_PATH ${CMAKE_INSTALL_PREFIX}/${INSTALL_${p}DIR} _path )
    message(STATUS "Installing ${p} components to ${_path}")
    unset(_path)
    endforeach()
  2. 设置了EP_BASE目录属性,这将为超构建中的子项目设置布局。所有子项目都将在CMAKE_BINARY_DIR的子项目文件夹下生成:

    1
    set_property(DIRECTORY PROPERTY EP_BASE ${CMAKE_BINARY_DIR}/subprojects)
  3. 然后,声明STAGED_INSTALL_PREFIX变量。这个变量指向构建目录下的stage子目录,项目将在构建期间安装在这里。这是一种沙箱安装过程,让我们有机会检查整个超级构建的布局:

    1
    2
    set(STAGED_INSTALL_PREFIX ${CMAKE_BINARY_DIR}/stage)
    message(STATUS "${PROJECT_NAME} staged install: ${STAGED_INSTALL_PREFIX}")
  4. 添加external/upstream子目录。其中包括使用CMake指令来管理我们的上游依赖关系,在我们的例子中,就是消息库:

    1
    add_subdirectory(external/upstream)
  5. 然后,包含ExternalProject.cmake标准模块:

    1
    include(ExternalProject)
  6. 将自己的项目作为外部项目添加,调用ExternalProject_Add命令。SOURCE_DIR用于指定源位于src子目录中。我们会选择适当的CMake参数来配置我们的项目。这里,使用STAGED_INSTALL_PREFIX作为子项目的安装目录:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    ExternalProject_Add(${PROJECT_NAME}_core
    DEPENDS
    message_external
    SOURCE_DIR
    ${CMAKE_CURRENT_SOURCE_DIR}/src
    CMAKE_ARGS
    -DCMAKE_INSTALL_PREFIX=${STAGED_INSTALL_PREFIX}
    -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE}
    -DCMAKE_CXX_COMPILER=${CMAKE_CXX_COMPILER}
    -DCMAKE_CXX_FLAGS=${CMAKE_CXX_FLAGS}
    -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_PREFIX_PATH:PATH=${CMAKE_PREFIX_PATH}
    BUILD_ALWAYS
    1
    )
  7. 现在,为use_message添加一个测试,并由recipe-04_core构建。这将运行use_message可执行文件的安装,即位于构建树中的安装:

    1
    2
    3
    4
    5
    6
    7
    8
    enable_testing()

    add_test(
    NAME
    check_use_message
    COMMAND
    ${STAGED_INSTALL_PREFIX}/${INSTALL_BINDIR}/use_message
    )
  8. 最后,可以声明安装规则。因为所需要的东西都已经安装在暂存区域中,我们只要将暂存区域的内容复制到安装目录即可:

    1
    2
    3
    4
    5
    6
    7
    install(
    DIRECTORY
    ${STAGED_INSTALL_PREFIX}/
    DESTINATION
    .
    USE_SOURCE_PERMISSIONS
    )
  9. 使用SCRIPT参数声明一个附加的安装规则。CMake脚本的install_hook.cmake将被执行,但只在GNU/Linux和macOS上执行。这个脚本将打印已安装的可执行文件的RPATH,并运行它。我们将在下一节详细地讨论这个问题:

    1
    2
    3
    4
    5
    6
    7
    8
    if(UNIX)
    set(PRINT_SCRIPT "${CMAKE_CURRENT_LIST_DIR}/cmake/print_rpath.py")
    configure_file(cmake/install_hook.cmake.in install_hook.cmake @ONLY)
    install(
    SCRIPT
    ${CMAKE_CURRENT_BINARY_DIR}/install_hook.cmake
    )
    endif()

-Dmessage_DIR=${message_DIR}已作为CMake参数传递给项目,这将正确设置消息库依赖项的位置。message_DIR的值在external/upstream/message目录下的CMakeLists.txt文件中定义。这个文件处理依赖于消息库,让我们看看是如何处理的:

  1. 首先,搜索并找到包。用户可能已经在系统的某个地方安装了,并在配置时传递了message_DIR:

    1
    find_package(message 1 CONFIG QUIET)
  2. 如果找到了消息库,我们将向用户报告目标的位置和版本,并添加一个虚拟的message_external目标。这里,需要虚拟目标来正确处理超构建的依赖关系:

    1
    2
    3
    4
    if(message_FOUND)
    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. 如果没有找到这个库,我们将把它添加为一个外部项目,从在线Git存储库下载它,然后编译它。安装路径、构建类型和安装目录布局都是由主CMakeLists.txt文件设置,C++编译器和标志也是如此。项目将安装到STAGED_INSTALL_PREFIX下,然后进行测试:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    else()
    include(ExternalProject)
    message(STATUS "Suitable message could not be located, Building message instead.")
    ExternalProject_Add(message_external
    GIT_REPOSITORY
    https://github.com/dev-cafe/message.git
    GIT_TAG
    master
    UPDATE_COMMAND
    ""
    CMAKE_ARGS
    -DCMAKE_INSTALL_PREFIX=${STAGED_INSTALL_PREFIX}
    -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE}
    -DCMAKE_CXX_COMPILER=${CMAKE_CXX_COMPILER}
    CMAKE_CACHE_ARGS
    -DCMAKE_CXX_FLAGS:STRING=${CMAKE_CXX_FLAGS}
    TEST_AFTER_INSTALL
    1
    DOWNLOAD_NO_PROGRESS
    1
    LOG_CONFIGURE
    1
    LOG_BUILD
    1
    LOG_INSTALL
    1
    )
  4. 最后,将message_DIR目录进行设置,为指向新构建的messageConfig.cmake文件指明安装路径。注意,这些路径被保存到CMakeCache中:

    1
    2
    3
    4
    5
    6
    7
    8
    9
      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)
    endif()

我们终于准备好编译我们自己的项目,并成功地将其链接到消息库(无论是系统上已有的消息库,还是新构建的消息库)。由于这是一个超级构建,src子目录下的代码是一个完全独立的CMake项目:

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    cmake_minimum_required(VERSION 3.6 FATAL_ERROR)

    project(recipe-04_core
    LANGUAGES CXX
    )

    set(CMAKE_CXX_STANDARD 11)
    set(CMAKE_CXX_EXTENSIONS OFF)
    set(CMAKE_CXX_STANDARD_REQUIRED ON)

    include(GNUInstallDirs)

    set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY
    ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR})
    set(CMAKE_LIBRARY_OUTPUT_DIRECTORY
    ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR})
    set(CMAKE_RUNTIME_OUTPUT_DIRECTORY
    ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_BINDIR})
  2. 尝试找到消息库。超级构建中,正确设置message_DIR:

    1
    2
    3
    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. 添加可执行目标use_message,该目标由use_message.cpp源文件创建,并连接到message::message-shared目标:

    1
    2
    3
    4
    5
    6
    add_executable(use_message use_message.cpp)

    target_link_libraries(use_message
    PUBLIC
    message::message-shared
    )
  4. use_message设置目标属性。再次对RPATH进行设置:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    # Prepare RPATH
    file(RELATIVE_PATH _rel ${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_BINDIR} ${CMAKE_INSTALL_PREFIX})
    if(APPLE)
    set(_rpath "@loader_path/${_rel}")
    else()
    set(_rpath "\$ORIGIN/${_rel}")
    endif()
    file(TO_NATIVE_PATH "${_rpath}/${CMAKE_INSTALL_LIBDIR}" use_message_RPATH)

    set_target_properties(use_message
    PROPERTIES
    MACOSX_RPATH ON
    SKIP_BUILD_RPATH OFF
    BUILD_WITH_INSTALL_RPATH OFF
    INSTALL_RPATH "${use_message_RPATH}"
    INSTALL_RPATH_USE_LINK_PATH ON
    )
  5. 最后,为use_message目标设置了安装规则:

    1
    2
    3
    4
    5
    6
    7
    install(
    TARGETS
    use_message
    RUNTIME
    DESTINATION ${CMAKE_INSTALL_BINDIR}
    COMPONENT bin
    )

现在瞧瞧CMake脚本模板install_hook.cmake.in的内容:

  1. CMake脚本在我们的主项目范围之外执行,因此没有定义变量或目标的概念。因此,需要设置变量来保存已安装的use_message可执行文件的完整路径。注意使用@INSTALL_BINDIR@,它将由configure_file解析:

    1
    set(_executable ${CMAKE_INSTALL_PREFIX}/@INSTALL_BINDIR@/use_message)
  2. 需要找到平台本机可执行工具,使用该工具打印已安装的可执行文件的RPATH。我们将搜索chrpathpatchelfotool。当找到已安装的程序时,向用户提供有用的状态信息,并且退出搜索:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    set(_patcher)
    list(APPEND _patchers chrpath patchelf otool)
    foreach(p IN LISTS _patchers)
    find_program(${p}_FOUND
    NAMES
    ${p}
    )
    if(${p}_FOUND)
    set(_patcher ${p})
    message(STATUS "ELF patching tool ${_patcher} FOUND")
    break()
    endif()
    endforeach()
  3. 检查_patcher变量是否为空,这意味着PatchELF工具是否可用。当为空时,我们要进行的操作将会失败,所以会发出一个致命错误,提醒用户需要安装PatchELF工具:

    1
    2
    if(NOT _patcher)
    message(FATAL_ERROR "ELF patching tool NOT FOUND!\nPlease install one of chrpath, patchelf or otool")
  4. 当PatchELF工具找到了,则继续。我们调用Python脚本print_rpath.py,将_executable变量作为参数传递给execute_process

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    find_package(PythonInterp REQUIRED QUIET)
    execute_process(
    COMMAND
    ${PYTHON_EXECUTABLE} @PRINT_SCRIPT@ "${_patcher}"
    "${_executable}"
    RESULT_VARIABLE _res
    OUTPUT_VARIABLE _out
    ERROR_VARIABLE _err
    OUTPUT_STRIP_TRAILING_WHITESPACE
    )
  5. 检查_res变量的返回代码。如果执行成功,将打印_out变量中捕获的标准输出流。否则,打印退出前捕获的标准输出和错误流:

    1
    2
    3
    4
    5
    6
    7
    8
    9
      if(_res EQUAL 0)
    message(STATUS "RPATH for ${_executable} is ${_out}")
    else()
    message(STATUS "Something went wrong!")
    message(STATUS "Standard output from print_rpath.py: ${_out}")
    message(STATUS "Standard error from print_rpath.py: ${_err}")
    message(FATAL_ERROR "${_patcher} could NOT obtain RPATH for ${_executable}")
    endif()
    endif()
  6. 再使用execute_process来运行已安装的use_message可执行目标:

    1
    2
    3
    4
    5
    6
    7
    execute_process(
    COMMAND ${_executable}
    RESULT_VARIABLE _res
    OUTPUT_VARIABLE _out
    ERROR_VARIABLE _err
    OUTPUT_STRIP_TRAILING_WHITESPACE
    )
  7. 最后,向用户报告execute_process的结果:

    1
    2
    3
    4
    5
    6
    7
    8
    if(_res EQUAL 0)
    message(STATUS "Running ${_executable}:\n ${_out}")
    else()
    message(STATUS "Something went wrong!")
    message(STATUS "Standard output from running ${_executable}:\n ${_out}")
    message(STATUS "Standard error from running ${_executable}:\n ${_err}")
    message(FATAL_ERROR "Something went wrong with ${_executable}")
    endif()

工作原理

CMake工具箱中,超级构建是非常有用的模式。它通过将复杂的项目划分为更小、更容易管理的子项目来管理它们。此外,可以使用CMake作为构建项目的包管理器。CMake可以搜索依赖项,如果在系统上找不到依赖项,则重新构建它们。这里需要三个CMakeLists.txt文件:

  • CMakeLists.txt文件包含项目和依赖项共享的设置,还包括我们自己的项目(作为外部项目)。本例中,我们选择的名称为${PROJECT_NAME}_core;也就是recipe-04_core,因为项目名称recipe-04用于超级构建。
  • 外部CMakeLists.txt文件将尝试查找上游依赖项,并在导入目标和构建目标之间进行切换,这取决于是否找到了依赖项。对于每个依赖项,最好有单独的子目录,其中包含一个CMakeLists.txt文件。
  • 最后,我们项目的CMakeLists.txt文件,可以构建一个独立的CMake项目。在原则上,我们可以自己配置和构建它,而不需要超级构建提供的依赖关系管理工具。

当对消息库的依赖关系未得到满足时,将首先考虑超级构建:

1
2
3
$ mkdir -p build
$ cd build
$ cmake -DCMAKE_INSTALL_PREFIX=$HOME/Software/recipe-04 ..

让CMake查找库,这是我们得到的输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
-- The CXX compiler identification is GNU 7.3.0
-- Check for working CXX compiler: /nix/store/gqg2vrcq7krqi9rrl6pphvsg81sb8pjw-gcc-wrapper-7.3.0/bin/g++
-- Check for working CXX compiler: /nix/store/gqg2vrcq7krqi9rrl6pphvsg81sb8pjw-gcc-wrapper-7.3.0/bin/g++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Project will be installed to /home/roberto/Software/recipe-04
-- Build type set to Release
-- Installing LIB components to /home/roberto/Software/recipe-04/lib64
-- Installing BIN components to /home/roberto/Software/recipe-04/bin
-- Installing INCLUDE components to /home/roberto/Software/recipe-04/include
-- Installing CMAKE components to /home/roberto/Software/recipe-04/share/cmake/recipe-04
-- recipe-04 staged install: /home/roberto/Workspace/robertodr/cmake-cookbook/chapter-10/recipe-04/cxx-example/build/stage
-- Suitable message could not be located, Building message instead.
-- Configuring done
-- Generating done
-- Build files have been written to: /home/roberto/Workspace/robertodr/cmake-cookbook/chapter-10/recipe-04/cxx-example/build

根据指令,CMake报告如下:

  • 安装将分阶段进入构建树。分阶段安装是对实际安装过程进行沙箱化的一种方法。作为开发人员,这对于在运行安装命令之前检查所有库、可执行程序和文件是否安装在正确的位置非常有用。对于用户来说,可在构建目录中给出了相同的结构。这样,即使没有运行正确的安装,我们的项目也可以立即使用。
  • 系统上没有找到合适的消息库。然后,CMake将运行在构建项目之前构建库所提供的命令,以满足这种依赖性。

如果库已经位于系统的已知位置,我们可以将-Dmessage_DIR选项传递给CMake:

1
$ cmake -DCMAKE_INSTALL_PREFIX=$HOME/Software/use_message -Dmessage_DIR=$HOME/Software/message/share/cmake/message ..

事实上,这个库已经找到并导入。我们对自己的项目进行建造操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
-- The CXX compiler identification is GNU 7.3.0
-- Check for working CXX compiler: /nix/store/gqg2vrcq7krqi9rrl6pphvsg81sb8pjw-gcc-wrapper-7.3.0/bin/g++
-- Check for working CXX compiler: /nix/store/gqg2vrcq7krqi9rrl6pphvsg81sb8pjw-gcc-wrapper-7.3.0/bin/g++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Project will be installed to /home/roberto/Software/recipe-04
-- Build type set to Release
-- Installing LIB components to /home/roberto/Software/recipe-04/lib64
-- Installing BIN components to /home/roberto/Software/recipe-04/bin
-- Installing INCLUDE components to /home/roberto/Software/recipe-04/include
-- Installing CMAKE components to /home/roberto/Software/recipe-04/share/cmake/recipe-04
-- recipe-04 staged install: /home/roberto/Workspace/robertodr/cmake-cookbook/chapter-10/recipe-04/cxx-example/build/stage
-- Checking for one of the modules 'uuid'
-- Found message: /home/roberto/Software/message/lib64/libmessage.so.1 (found version 1.0.0)
-- Configuring done
-- Generating done
-- Build files have been written to: /home/roberto/Workspace/robertodr/cmake-cookbook/chapter-10/recipe-04/cxx-example/build

项目的最终安装规则是,将安装文件复制到CMAKE_INSTALL_PREFIX:

1
2
3
4
5
6
7
install(
DIRECTORY
${STAGED_INSTALL_PREFIX}/
DESTINATION
.
USE_SOURCE_PERMISSIONS
)

注意使用.而不是绝对路径${CMAKE_INSTALL_PREFIX},这样CPack工具就可以正确理解该规则。CPack的用法将在第11章中介绍。

recipe-04_core项目构建一个简单的可执行目标,该目标链接到消息动态库。正如本章前几节所讨论,为了让可执行文件正确运行,需要正确设置RPATH。本章的第1节展示了,如何在CMake的帮助下实现这一点,同样的模式在CMakeLists.txt中被重用,用于创建use_message的可执行目标:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
file(RELATIVE_PATH _rel ${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_BINDIR} ${CMAKE_INSTALL_PREFIX})
if(APPLE)
set(_rpath "@loader_path/${_rel}")
else()
set(_rpath "\$ORIGIN/${_rel}")
endif()
file(TO_NATIVE_PATH "${_rpath}/${CMAKE_INSTALL_LIBDIR}" use_message_RPATH)

set_target_properties(use_message
PROPERTIES
MACOSX_RPATH ON
SKIP_BUILD_RPATH OFF
BUILD_WITH_INSTALL_RPATH OFF
INSTALL_RPATH "${use_message_RPATH}"
INSTALL_RPATH_USE_LINK_PATH ON
)

为了检查这是否合适,可以使用本机工具打印已安装的可执行文件的RPATH。我们将对该工具的调用,封装到Python脚本中,并将其进一步封装到CMake脚本中。最后,使用SCRIPT关键字将CMake脚本作为安装规则调用:

1
2
3
4
5
6
7
8
if(UNIX)
set(PRINT_SCRIPT "${CMAKE_CURRENT_LIST_DIR}/cmake/print_rpath.py")
configure_file(cmake/install_hook.cmake.in install_hook.cmake @ONLY)
install(
SCRIPT
${CMAKE_CURRENT_BINARY_DIR}/install_hook.cmake
)
endif()

脚本是在安装最后进行执行:

1
$ cmake --build build --target install

GNU/Linux系统上,我们将看到以下输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
Install the project...
-- Install configuration: "Release"
-- Installing: /home/roberto/Software/recipe-04/.
-- Installing: /home/roberto/Software/recipe-04/./lib64
-- Installing: /home/roberto/Software/recipe-04/./lib64/libmessage.so
-- Installing: /home/roberto/Software/recipe-04/./lib64/libmessage_s.a
-- Installing: /home/roberto/Software/recipe-04/./lib64/libmessage.so.1
-- Installing: /home/roberto/Software/recipe-04/./include
-- Installing: /home/roberto/Software/recipe-04/./include/message
-- Installing: /home/roberto/Software/recipe-04/./include/message/Message.hpp
-- Installing: /home/roberto/Software/recipe-04/./include/message/messageExport.h
-- Installing: /home/roberto/Software/recipe-04/./share
-- Installing: /home/roberto/Software/recipe-04/./share/cmake
-- Installing: /home/roberto/Software/recipe-04/./share/cmake/message
-- Installing: /home/roberto/Software/recipe-04/./share/cmake/message/messageTargets-release.cmake
-- Installing: /home/roberto/Software/recipe-04/./share/cmake/message/messageConfigVersion.cmake
-- Installing: /home/roberto/Software/recipe-04/./share/cmake/message/messageConfig.cmake
-- Installing: /home/roberto/Software/recipe-04/./share/cmake/message/messageTargets.cmake
-- Installing: /home/roberto/Software/recipe-04/./bin
-- Installing: /home/roberto/Software/recipe-04/./bin/hello-world_wAR
-- Installing: /home/roberto/Software/recipe-04/./bin/use_message
-- Installing: /home/roberto/Software/recipe-04/./bin/hello-world_wDSO
-- ELF patching tool chrpath FOUND
-- RPATH for /home/roberto/Software/recipe-04/bin/use_message is /home/roberto/Software/recipe-04/bin/use_message: RUNPATH=$ORIGIN/../lib64:/home/roberto/Workspace/robertodr/cmake-cookbook/chapter-10/recipe-04/cxx-example/build/stage/lib64:/nix/store/di389pfcw2krnmh8nmkn55d1rnzmba37-CMake-Cookbook/lib64:/nix/store/di389pfcw2krnmh8nmkn55d1rnzmba37-CMake-Cookbook/lib:/nix/store/mjs2b8mmid86lvbzibzdlz8w5yrjgcnf-util-linux-2.31.1/lib:/nix/store/2kcrj1ksd2a14bm5sky182fv2xwfhfap-glibc-2.26-131/lib:/nix/store/4zd34747fz0ggzzasy4icgn3lmy89pra-gcc-7.3.0-lib/lib
-- Running /home/roberto/Software/recipe-04/bin/use_message:
This is my very nice message:
Hello, World! From a client of yours!
...and here is its UUID: a8014bf7-5dfa-45e2-8408-12e9a5941825
This is my very nice message:
Goodbye, World! From a client of yours!
...and here is its UUID: ac971ef4-7606-460f-9144-1ad96f713647

NOTE:我们建议使用的工具是PatchELF (https://nixos.org/patchelf.html )、chrpath (https://linux.die.net/man/1/chrpath )和otool (http://www.manpagez.com/man/1/otool/ )。第一种方法适用于GNU/Linux和macOS,而chrpath和otool分别适用于GNU/Linux和macOS。