返回介绍

10.1 安装项目

发布于 2025-05-06 21:45:59 字数 18655 浏览 0 评论 0 收藏

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 库的代码。项目布局如下:

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

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

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 项目。请注意,我们已经为我们的项目设置了一个版本,在 project 中使用 VERSION 进行指定:
    # 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 。我们将会打印安装目录的信息:
    message(STATUS "Project will be installed to ${CMAKE_INSTALL_PREFIX}")
  3. 默认情况下,我们更喜欢以 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}")
  4. 接下来,告诉 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})
  5. 虽然,前面的命令配置了构建目录中输出的位置,但是需要下面的命令来配置可执行程序、库以及安装前缀中包含的文件的位置。它们大致遵循相同的布局,但是我们定义了新的 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")
  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 子目录:
    add_subdirectory(src)
    enable_testing()
    add_subdirectory(tests)

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

  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. 我们希望建立一个动态库,将该目标声明为 message-shared :
    add_library(message-shared SHARED "")
  3. 这个目标由 target_sources 命令指定:
    target_sources(message-shared
      PRIVATE
          ${CMAKE_CURRENT_LIST_DIR}/Message.cpp
      )
  4. 我们为目标声明编译时定义和链接库。请注意,所有这些都是 PUBLIC ,以确保所有依赖的目标将正确继承它们:
      target_compile_definitions(message-shared
      PUBLIC
          $<$<BOOL:${UUID_FOUND}>:HAVE_UUID>
      )
    target_link_libraries(message-shared
      PUBLIC
          $<$<BOOL:${UUID_FOUND}>:PkgConfig::UUID>
      )
  5. 然后设置目标的附加属性:
    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”程序添加可执行目标:
    add_executable(hello-world_wDSO hello-world.cpp)
  7. hello-world_wDSO 可执行目标,会链接到动态库:
    target_link_libraries(hello-world_wDSO
      PUBLIC
          message-shared
      )

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

  1. 使用 CMake 路径操作,我们可以设置 message_RPATH 变量。这将为 GNU/Linux 和 macOS 设置适当的 RPATH :
    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 (通过目标属性实现)。我们也可以设置额外的属性,稍后会对此进行更多的讨论:
    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 命令来指定安装位置。注意,路径是相对的,我们将在后续进一步讨论这一点:
    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”可执行文件能够正确运行:

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 变量,但它所扮演的角色将在接下来的几个示例中详细讨论。

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 上) 变量来查找与可执行文件本身位置相关的路径:

# 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 ,以便能够在构建树中运行可执行文件。
  • 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 知道它们是什么类型的目标,无论其是可执行程序库、动态库,还是静态库:

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 <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 将得到以下结果:

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)

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

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。