CMake 秘籍(六)(1)

简介: CMake 秘籍(六)

第十一章:编写安装程序

在本章中,我们将涵盖以下节:

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

引言

在前几章中,我们已经展示了如何使用 CMake 配置、构建和测试我们的项目。安装项目是开发者工具箱中同样重要的一部分,本章将展示如何实现这一点。本章的节涵盖了以下图中概述的安装时操作:

我们将引导你完成精简一个简单的 C++项目安装的各个步骤:从确保项目中构建的重要文件被复制到正确的目录,到确保依赖于你的工作的其他项目可以使用 CMake 检测到它。本章的四个节将基于第一章,从简单可执行文件到库,第三部分,构建和链接共享和静态库中给出的简单示例。在那里我们尝试构建一个非常简单的库并将其链接到一个可执行文件中。我们还展示了如何从相同的源文件构建静态和共享库。在本章中,我们将更深入地讨论并正式化安装时发生的事情。

安装你的项目

本节的代码可在github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-10/recipe-01找到,并包含一个 C++示例。本节适用于 CMake 版本 3.6(及更高版本),并在 GNU/Linux、macOS 和 Windows 上进行了测试。

在本节的第一节中,我们将介绍我们的小项目以及将在后续节中使用的一些基本概念。安装文件、库和可执行文件是一项非常基本的任务,但它可能会带来一些陷阱。我们将引导你了解这些陷阱,并展示如何使用 CMake 有效地避免其中的许多陷阱。

准备工作

来自第一章,从简单可执行文件到库,第三部分,构建和链接共享和静态库的代码几乎未作改动地被使用:我们仅添加了对 UUID 库的依赖。这种依赖是有条件的,如果找不到 UUID 库,我们将通过预处理器排除使用它的代码。代码被适当地组织到自己的src子目录中。项目的布局如下:

.
├── CMakeLists.txt
├── src
│   ├── CMakeLists.txt
│   ├── hello-world.cpp
│   ├── Message.cpp
│   └── Message.hpp
└── tests
    └── CMakeLists.txt

我们已经可以看到,我们有一个根CMakeLists.txt,在src子目录下有一个叶子,在tests子目录下有另一个叶子。

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

#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中相应的实现:

#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可执行文件如下:

#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 项目。请注意,我们已使用VERSION关键字为project命令设置了项目版本:
# 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)
  1. 用户可以通过CMAKE_INSTALL_PREFIX变量定义安装前缀。CMake 将为该变量设置一个合理的默认值:在 Unix 上是/usr/local,在 Windows 上是C:\Program Files。我们打印一条状态消息报告其值:
message(STATUS "Project will be installed to ${CMAKE_INSTALL_PREFIX}")
  1. 默认情况下,我们为项目首选Release配置。用户将能够使用CMAKE_BUILD_TYPE变量设置此项,我们检查是否是这种情况。如果不是,我们将其设置为我们自己的默认合理值:
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}")
  1. 接下来,我们告诉 CMake 在哪里构建可执行文件、静态库和共享库目标。这便于用户在不打算实际安装项目的情况下访问这些构建目标。我们使用标准的 CMake GNUInstallDirs.cmake模块。这将确保一个合理且可移植的项目布局:
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})
  1. 虽然之前的命令固定了构建输出在构建目录内的位置,但以下命令需要固定可执行文件、库和包含文件在安装前缀内的位置。这些将大致遵循相同的布局,但我们定义了新的INSTALL_LIBDIRINSTALL_BINDIRINSTALL_INCLUDEDIRINSTALL_CMAKEDIR变量,用户可以覆盖这些变量,如果他们愿意的话:
# 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")
  1. 我们向用户报告组件将被安装到的路径:
# 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()
  1. CMakeLists.txt文件中的最后指令添加了src子目录,启用了测试,并添加了tests子目录:
add_subdirectory(src)
enable_testing()
add_subdirectory(tests)

我们现在继续分析src/CMakeLists.txt叶文件。该文件定义了实际要构建的目标:

  1. 我们的项目依赖于 UUID 库。如第五章,配置时间和构建时间操作,配方 8,探测执行所示,我们可以使用以下代码片段找到它:
# 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()
  1. 我们希望从源代码构建一个共享库,并声明一个名为message-shared的目标:
add_library(message-shared SHARED "")
  1. 使用target_sources命令指定此目标的源:
target_sources(message-shared
  PRIVATE
    ${CMAKE_CURRENT_LIST_DIR}/Message.cpp
  )
  1. 我们为目标声明编译定义和链接库。请注意,所有这些都是PUBLIC,以确保所有依赖目标将正确继承它们:
target_compile_definitions(message-shared
  PUBLIC
    $<$<BOOL:${UUID_FOUND}>:HAVE_UUID>
  )
target_link_libraries(message-shared
  PUBLIC
    $<$<BOOL:${UUID_FOUND}>:PkgConfig::UUID>
  )
  1. 然后我们设置目标的额外属性。我们将在稍后对此进行评论。
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
  )
  1. 最后,我们为我们的“Hello, world”程序添加一个可执行目标:
add_executable(hello-world_wDSO hello-world.cpp)
  1. hello-world_wDSO可执行目标与共享库链接:
target_link_libraries(hello-world_wDSO
  PUBLIC
    message-shared
  )

src/CMakeLists.txt文件也包含了安装指令。在考虑这些之前,我们需要为我们的可执行文件固定RPATH

  1. 通过 CMake 路径操作,我们设置了message_RPATH变量。这将适当地为 GNU/Linux 和 macOS 设置RPATH
# 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)
  1. 我们现在可以使用这个变量来为我们的可执行目标hello-world_wDSO修复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
  )
  1. 我们终于准备好安装我们的库、头文件和可执行文件了!我们使用 CMake 提供的安装命令来指定这些文件应该去哪里。请注意,路径是相对的;我们将在下面进一步详细说明这一点:
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
  )

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

add_test(
  NAME test_shared
  COMMAND $<TARGET_FILE:hello-world_wDSO>
  )

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

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

在 GNU/Linux 上,构建目录的内容将是以下内容:

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

另一方面,在安装前缀下,你可以找到以下结构:

$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文件中允许了以下节:

# 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变量,但其作用将在接下来的几个配方中详细讨论。

GNUInstallDirs.cmake模块定义了额外的变量,这些变量将帮助将安装的文件放置在所选安装前缀的预期子目录中。请咨询 CMake 在线文档:cmake.org/cmake/help/v3.6/module/GNUInstallDirs.html

目标属性和 RPATH 处理

让我们更仔细地看一下设置在共享库目标上的属性。我们必须设置以下内容:

  • POSITION_INDEPENDENT_CODE 1: 这设置了生成位置无关代码所需的编译器标志。有关更多详细信息,请咨询en.wikipedia.org/wiki/Position-independent_code
  • SOVERSION ${PROJECT_VERSION_MAJOR}: 这是我们的共享库提供的应用程序编程接口(API)的版本。遵循语义版本,我们决定将其设置为与项目的主要版本相同。CMake 目标也有一个VERSION属性。这可以用来指定目标的构建版本。注意SOVERSIONVERSION可能不同:我们可能希望随着时间的推移提供同一 API 的多个构建。在本示例中,我们不关心这种粒度控制:仅设置 API 版本与SOVERSION属性就足够了,CMake 将为我们设置VERSION为相同的值。有关更多详细信息,请参阅官方文档: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 上的框架共享库目标设计的,但它也可以用于其他操作系统和目标,正如我们目前所做的。有关更多详细信息,请参阅官方文档:cmake.org/cmake/help/v3.6/prop_tgt/PUBLIC_HEADER.html
  • MACOSX_RPATH ON: 这将在 macOS 上将共享库的“install_name”字段的目录部分设置为@rpath
  • WINDOWS_EXPORT_ALL_SYMBOLS ON:这将强制在 Windows 上编译时导出所有符号。请注意,这通常不是一种好的做法,我们将在第 2 个菜谱中展示,即“生成导出头文件”,如何在不同平台上处理符号可见性。

现在让我们讨论RPATH。我们正在将hello-world_wDSO可执行文件链接到libmessage.so.1。这意味着当调用可执行文件时,将加载共享库。因此,为了使加载器成功完成其工作,需要在某个地方编码有关库位置的信息。关于库位置有两种方法:

  • 可以通过设置环境变量让链接器知道:
  • 在 GNU/Linux 上,这需要将路径附加到LD_LIBRARY_PATH环境变量。请注意,这很可能会污染系统上所有应用程序的链接器路径,并可能导致符号冲突(gms.tf/ld_library_path-considered-harmful.html)。
  • 在 macOS 上,您可以同样设置DYLD_LIBRARY_PATH变量。这和 GNU/Linux 上的LD_LIBRARY_PATH有同样的缺点,但可以通过使用DYLD_FALLBACK_LIBRARY_PATH变量来部分缓解这种情况。请参阅以下链接中的示例:stackoverflow.com/a/3172515/2528668
  • 它可以被编码到可执行文件中,使用RPATH设置运行时搜索路径。

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

# 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变量,目标属性将完成剩余的工作:

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,以便能够在构建树内运行可执行文件。
  • BUILD_WITH_INSTALL_RPATH OFF:关闭生成具有与安装树相同的RPATH的可执行目标。这将阻止我们在构建树内运行可执行文件。
  • INSTALL_RPATH "${message_RPATH}":将安装的可执行目标的RPATH设置为先前计算的路径。
  • INSTALL_RPATH_USE_LINK_PATH ON:告诉 CMake 将链接器搜索路径附加到可执行文件的RPATH

关于加载器在 Unix 系统上如何工作的更多信息,可以在这篇博客文章中找到:longwei.github.io/rpath_origin/

安装指令

最后,让我们考虑安装指令。我们需要安装一个可执行文件、一个库和一个头文件。可执行文件和库是构建目标,因此我们使用install命令的TARGETS选项。可以一次性设置多个目标的安装规则:CMake 知道它们是什么类型的目标;也就是说,它们是可执行文件、共享库还是静态库:

install(
  TARGETS
    message-shared
    hello-world_wDSO

可执行文件将被安装在RUNTIME DESTINATION,我们将其设置为 ${INSTALL_BINDIR}。共享库被安装到LIBRARY DESTINATION,我们将其设置为 ${INSTALL_LIBDIR}。静态库将被安装到ARCHIVE DESTINATION,我们也将其设置为 ${INSTALL_LIBDIR}

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关键字可以帮助我们的地方。例如,要仅安装库,我们可以运行以下命令:

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

由于Message.hpp头文件被设置为项目的公共头文件,我们可以使用PUBLIC_HEADER关键字将其与其他目标一起安装到选定的目的地: ${INSTALL_INCLUDEDIR}/message。库的用户现在可以通过以下方式包含头文件:#include ,前提是正确的位置通过-I选项传递给编译器。

安装指令中的各种目的地被解释为相对路径,除非使用绝对路径。但是相对于什么?CMake 可以根据触发安装的工具以不同的方式计算绝对路径。当我们使用cmake --build . --target install时,正如我们所做的那样,路径将相对于CMAKE_INSTALL_PREFIX计算。然而,当使用 CPack 时,绝对路径将相对于CPACK_PACKAGING_INSTALL_PREFIX计算。CPack 的使用将在第十一章,打包项目,第 1 个配方,生成源代码和二进制包中展示。

另一种机制在 Unix Makefiles 和 Ninja 生成器中可用:DESTDIR。可以将整个安装树重新定位到由DESTDIR指定的目录下。也就是说,env DESTDIR=/tmp/stage cmake --build . --target install将相对于CMAKE_INSTALL_PREFIX安装项目,并在/tmp/stage目录下。您可以在这里了解更多信息:www.gnu.org/prep/standards/html_node/DESTDIR.html

还有更多

正确设置 RPATH 可能相当棘手,但对于第三方用户来说至关重要。默认情况下,CMake 设置可执行文件的 RPATH,假设它们将从构建树中运行。然而,在安装时,RPATH 被清除,导致用户想要运行 hello-world_wDSO 时出现问题。在 Linux 上使用 ldd 工具,我们可以检查构建树中的 hello-world_wDSO 可执行文件,以查看加载器将在哪里查找 libmessage.so

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

在安装前缀中运行 ldd hello-world_wDSO 将导致以下结果:

libmessage.so.1 => Not found

这显然是错误的。然而,始终将 RPATH 硬编码指向构建树或安装前缀也同样错误:这两个位置中的任何一个都可能被删除,导致可执行文件损坏。这里提出的解决方案为构建树中的可执行文件和安装前缀中的可执行文件设置了不同的 RPATH,以便它总是指向“有意义”的地方;也就是说,尽可能靠近可执行文件。在构建树中运行 ldd 显示相同的结果:

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

另一方面,在安装前缀中,我们现在得到以下结果:

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

我们已经使用带有 TARGETS 签名的 CMake 安装命令,因为我们需要安装构建目标。但是,该命令还有四个额外的签名:

生成导出头文件

本节的代码可在github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-10/recipe-02找到,并包含一个 C++示例。本节适用于 CMake 版本 3.6(及以上),并在 GNU/Linux、macOS 和 Windows 上进行了测试。

让我们设想一下,我们介绍的小型库已经变得非常流行,许多人都在使用它。然而,一些客户也希望在安装时提供一个静态库。其他客户注意到,共享库中的所有符号都是可见的。最佳实践规定,共享库只应公开最小数量的符号,从而限制代码中定义的对象和函数对外界的可见性。我们希望确保默认情况下,我们共享库中定义的所有符号对库外都是隐藏的。这将迫使项目贡献者明确界定库与外部代码之间的接口,因为他们必须明确标记那些也打算在项目外部使用的符号。因此,我们希望做以下事情:

  • 从同一组源文件构建共享和静态库。
  • 确保只有共享库中的符号可见性得到适当界定。

第三部分,构建和链接静态和共享库,在第一章,从简单的可执行文件到库,已经展示了 CMake 提供了实现第一点的平台无关功能。然而,我们没有解决符号可见性的问题。我们将使用当前的节重新审视这两点。

准备工作

我们仍将主要使用与上一节相同的代码,但我们需要修改src/CMakeLists.txtMessage.hpp头文件。后者将包含新的自动生成的头文件messageExport.h

#pragma once
#include <iosfwd>
#include <string>
#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_EXPORT预处理器指令在Message类的声明中被引入。这个指令将允许编译器生成对库用户可见的符号。

如何操作

除了项目名称之外,根目录的CMakeLists.txt文件保持不变。让我们首先看一下src子目录中的CMakeLists.txt文件,所有额外的工作实际上都在这里进行。我们将根据上一节中的文件来突出显示更改:

  1. 我们声明了我们的SHARED库目标及其消息库的源文件。请注意,编译定义和链接库保持不变:
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>
  )
  1. 我们还设置了目标属性。我们在PUBLIC_HEADER目标属性的参数中添加了${CMAKE_BINARY_DIR}/${INSTALL_INCLUDEDIR}/messageExport.h头文件。CXX_VISIBILITY_PRESETVISIBILITY_INLINES_HIDDEN属性将在下一节讨论:
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
  )
  1. 我们包含了标准的 CMake 模块GenerateExportHeader.cmake,并调用了generate_export_header函数。这将生成位于构建目录子目录中的messageExport.h头文件。我们很快将详细讨论这个函数和生成的头文件:
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
  )
  1. 每当需要将符号的可见性从默认的隐藏值更改时,都应该包含导出头文件。我们在Message.hpp头文件中做到了这一点,因为我们希望在库中暴露一些符号。现在我们将${CMAKE_BINARY_DIR}/${INSTALL_INCLUDEDIR}目录列为message-shared目标的PUBLIC包含目录:
target_include_directories(message-shared
  PUBLIC
    ${CMAKE_BINARY_DIR}/${INSTALL_INCLUDEDIR}
  ) 

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

  1. 我们添加了一个库目标来生成静态库。将使用与共享库相同的源代码编译来获得这个目标:
add_library(message-static STATIC "")
target_sources(message-static
  PRIVATE
    ${CMAKE_CURRENT_LIST_DIR}/Message.cpp
  )
  1. 我们设置了编译定义、包含目录和链接库,就像我们为共享库目标所做的那样。然而,请注意,我们添加了message_STATIC_DEFINE编译定义。这是为了确保我们的符号被正确暴露:
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>
  )
  1. 我们还为message-static目标设置了属性。这些将在下一节中讨论:
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"
  )
  1. 除了链接message-shared库目标的hello-world_wDSO可执行目标之外,我们还定义了另一个可执行目标hello-world_wAR。这个目标链接的是静态库:
add_executable(hello-world_wAR hello-world.cpp)
target_link_libraries(hello-world_wAR
  PUBLIC
    message-static
  )
  1. 安装指令现在列出了额外的message-statichello-world_wAR目标,但其他方面没有变化:
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
  )

CMake 秘籍(六)(2)https://developer.aliyun.com/article/1525058

相关文章
|
2月前
|
编译器 Shell 开发工具
CMake 秘籍(八)(5)
CMake 秘籍(八)
21 2
|
2月前
|
编译器 Linux C语言
CMake 秘籍(二)(2)
CMake 秘籍(二)
27 2
|
2月前
|
编译器 Shell
CMake 秘籍(八)(3)
CMake 秘籍(八)
24 2
|
2月前
|
Linux iOS开发 C++
CMake 秘籍(六)(3)
CMake 秘籍(六)
21 1
|
2月前
|
编译器 Linux C++
CMake 秘籍(六)(5)
CMake 秘籍(六)
19 1
|
2月前
|
Shell Linux C++
CMake 秘籍(六)(4)
CMake 秘籍(六)
18 1
|
2月前
|
Linux C++ iOS开发
CMake 秘籍(三)(4)
CMake 秘籍(三)
13 1
|
2月前
|
编译器 开发工具
CMake 秘籍(八)(2)
CMake 秘籍(八)
17 0
|
2月前
|
编译器 Linux C++
CMake 秘籍(六)(2)
CMake 秘籍(六)
21 0
|
2月前
|
并行计算 关系型数据库 编译器
CMake 秘籍(七)(3)
CMake 秘籍(七)
32 0

相关实验场景

更多