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. ├── cmake
  2. ├── install_hook.cmake.in
  3. └── print_rpath.py
  4. ├── CMakeLists.txt
  5. ├── external
  6. └── upstream
  7. ├── CMakeLists.txt
  8. └── message
  9. └── CMakeLists.txt
  10. └── src
  11. ├── CMakeLists.txt
  12. └── use_message.cpp

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

  1. import shlex
  2. import subprocess
  3. import sys
  4. def main():
  5. patcher = sys.argv[1]
  6. elfobj = sys.argv[2]
  7. tools = {'patchelf': '--print-rpath', 'chrpath': '--list', 'otool': '-L'}
  8. if patcher not in tools.keys():
  9. raise RuntimeError('Unknown tool {}'.format(patcher))
  10. cmd = shlex.split('{:s} {:s} {:s}'.format(patcher, tools[patcher], elfobj))
  11. rpath = subprocess.run(
  12. cmd,
  13. bufsize=1,
  14. stdout=subprocess.PIPE,
  15. stderr=subprocess.PIPE,
  16. universal_newlines=True)
  17. print(rpath.stdout)
  18. if __name__ == "__main__":
  19. main()

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

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

  1. #include <cstdlib>
  2. #include <iostream>
  3. #ifdef USING_message
  4. #include <message/Message.hpp>
  5. void messaging()
  6. {
  7. Message say_hello("Hello, World! From a client of yours!");
  8. std::cout << say_hello << std::endl;
  9. Message say_goodbye("Goodbye, World! From a client of yours!");
  10. std::cout << say_goodbye << std::endl;
  11. }
  12. #else
  13. void messaging()
  14. {
  15. std::cout << "Hello, World! From a client of yours!" << std::endl;
  16. std::cout << "Goodbye, World! From a client of yours!" << std::endl;
  17. }
  18. #endif
  19. int main()
  20. {
  21. messaging();
  22. return EXIT_SUCCESS;
  23. }

具体实施

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

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

    1. cmake_minimum_required(VERSION 3.6 FATAL_ERROR)
    2. project(recipe-04
    3. LANGUAGES CXX
    4. VERSION 1.0.0
    5. )
    6. # <<< General set up >>>
    7. set(CMAKE_CXX_STANDARD 11)
    8. set(CMAKE_CXX_EXTENSIONS OFF)
    9. set(CMAKE_CXX_STANDARD_REQUIRED ON)
    10. if(NOT CMAKE_BUILD_TYPE)
    11. set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE)
    12. endif()
    13. message(STATUS "Build type set to ${CMAKE_BUILD_TYPE}")
    14. message(STATUS "Project will be installed to ${CMAKE_INSTALL_PREFIX}")
    15. include(GNUInstallDirs)
    16. set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY
    17. ${PROJECT_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR})
    18. set(CMAKE_LIBRARY_OUTPUT_DIRECTORY
    19. ${PROJECT_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR})
    20. set(CMAKE_RUNTIME_OUTPUT_DIRECTORY
    21. ${PROJECT_BINARY_DIR}/${CMAKE_INSTALL_BINDIR})
    22. # Offer the user the choice of overriding the installation directories
    23. set(INSTALL_LIBDIR ${CMAKE_INSTALL_LIBDIR} CACHE PATH "Installation directory for libraries")
    24. set(INSTALL_BINDIR ${CMAKE_INSTALL_BINDIR} CACHE PATH "Installation directory for executables")
    25. set(INSTALL_INCLUDEDIR ${CMAKE_INSTALL_INCLUDEDIR} CACHE PATH "Installation directory for header files")
    26. if(WIN32 AND NOT CYGWIN)
    27. set(DEF_INSTALL_CMAKEDIR CMake)
    28. else()
    29. set(DEF_INSTALL_CMAKEDIR share/cmake/${PROJECT_NAME})
    30. endif()
    31. set(INSTALL_CMAKEDIR ${DEF_INSTALL_CMAKEDIR} CACHE PATH "Installation directory for CMake files")
    32. # Report to user
    33. foreach(p LIB BIN INCLUDE CMAKE)
    34. file(TO_NATIVE_PATH ${CMAKE_INSTALL_PREFIX}/${INSTALL_${p}DIR} _path )
    35. message(STATUS "Installing ${p} components to ${_path}")
    36. unset(_path)
    37. endforeach()
  2. 设置了EP_BASE目录属性,这将为超构建中的子项目设置布局。所有子项目都将在CMAKE_BINARY_DIR的子项目文件夹下生成:

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

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

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

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

    1. if(UNIX)
    2. set(PRINT_SCRIPT "${CMAKE_CURRENT_LIST_DIR}/cmake/print_rpath.py")
    3. configure_file(cmake/install_hook.cmake.in install_hook.cmake @ONLY)
    4. install(
    5. SCRIPT
    6. ${CMAKE_CURRENT_BINARY_DIR}/install_hook.cmake
    7. )
    8. 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. if(message_FOUND)
    2. get_property(_loc TARGET message::message-shared PROPERTY LOCATION)
    3. message(STATUS "Found message: ${_loc} (found version ${message_VERSION})")
    4. add_library(message_external INTERFACE) # dummy
  3. 如果没有找到这个库,我们将把它添加为一个外部项目,从在线Git存储库下载它,然后编译它。安装路径、构建类型和安装目录布局都是由主CMakeLists.txt文件设置,C++编译器和标志也是如此。项目将安装到STAGED_INSTALL_PREFIX下,然后进行测试:

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

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

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

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

    1. cmake_minimum_required(VERSION 3.6 FATAL_ERROR)
    2. project(recipe-04_core
    3. LANGUAGES CXX
    4. )
    5. set(CMAKE_CXX_STANDARD 11)
    6. set(CMAKE_CXX_EXTENSIONS OFF)
    7. set(CMAKE_CXX_STANDARD_REQUIRED ON)
    8. include(GNUInstallDirs)
    9. set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY
    10. ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR})
    11. set(CMAKE_LIBRARY_OUTPUT_DIRECTORY
    12. ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR})
    13. set(CMAKE_RUNTIME_OUTPUT_DIRECTORY
    14. ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_BINDIR})
  2. 尝试找到消息库。超级构建中,正确设置message_DIR:

    1. find_package(message 1 CONFIG REQUIRED)
    2. get_property(_loc TARGET message::message-shared PROPERTY LOCATION)
    3. message(STATUS "Found message: ${_loc} (found version ${message_VERSION})")
  3. 添加可执行目标use_message,该目标由use_message.cpp源文件创建,并连接到message::message-shared目标:

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

    1. # Prepare RPATH
    2. file(RELATIVE_PATH _rel ${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_BINDIR} ${CMAKE_INSTALL_PREFIX})
    3. if(APPLE)
    4. set(_rpath "@loader_path/${_rel}")
    5. else()
    6. set(_rpath "\$ORIGIN/${_rel}")
    7. endif()
    8. file(TO_NATIVE_PATH "${_rpath}/${CMAKE_INSTALL_LIBDIR}" use_message_RPATH)
    9. set_target_properties(use_message
    10. PROPERTIES
    11. MACOSX_RPATH ON
    12. SKIP_BUILD_RPATH OFF
    13. BUILD_WITH_INSTALL_RPATH OFF
    14. INSTALL_RPATH "${use_message_RPATH}"
    15. INSTALL_RPATH_USE_LINK_PATH ON
    16. )
  5. 最后,为use_message目标设置了安装规则:

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

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

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

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

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

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

工作原理

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

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

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

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

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

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

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

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

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

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

  1. file(RELATIVE_PATH _rel ${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_BINDIR} ${CMAKE_INSTALL_PREFIX})
  2. if(APPLE)
  3. set(_rpath "@loader_path/${_rel}")
  4. else()
  5. set(_rpath "\$ORIGIN/${_rel}")
  6. endif()
  7. file(TO_NATIVE_PATH "${_rpath}/${CMAKE_INSTALL_LIBDIR}" use_message_RPATH)
  8. set_target_properties(use_message
  9. PROPERTIES
  10. MACOSX_RPATH ON
  11. SKIP_BUILD_RPATH OFF
  12. BUILD_WITH_INSTALL_RPATH OFF
  13. INSTALL_RPATH "${use_message_RPATH}"
  14. INSTALL_RPATH_USE_LINK_PATH ON
  15. )

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

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

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

  1. $ cmake --build build --target install

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

  1. Install the project...
  2. -- Install configuration: "Release"
  3. -- Installing: /home/roberto/Software/recipe-04/.
  4. -- Installing: /home/roberto/Software/recipe-04/./lib64
  5. -- Installing: /home/roberto/Software/recipe-04/./lib64/libmessage.so
  6. -- Installing: /home/roberto/Software/recipe-04/./lib64/libmessage_s.a
  7. -- Installing: /home/roberto/Software/recipe-04/./lib64/libmessage.so.1
  8. -- Installing: /home/roberto/Software/recipe-04/./include
  9. -- Installing: /home/roberto/Software/recipe-04/./include/message
  10. -- Installing: /home/roberto/Software/recipe-04/./include/message/Message.hpp
  11. -- Installing: /home/roberto/Software/recipe-04/./include/message/messageExport.h
  12. -- Installing: /home/roberto/Software/recipe-04/./share
  13. -- Installing: /home/roberto/Software/recipe-04/./share/cmake
  14. -- Installing: /home/roberto/Software/recipe-04/./share/cmake/message
  15. -- Installing: /home/roberto/Software/recipe-04/./share/cmake/message/messageTargets-release.cmake
  16. -- Installing: /home/roberto/Software/recipe-04/./share/cmake/message/messageConfigVersion.cmake
  17. -- Installing: /home/roberto/Software/recipe-04/./share/cmake/message/messageConfig.cmake
  18. -- Installing: /home/roberto/Software/recipe-04/./share/cmake/message/messageTargets.cmake
  19. -- Installing: /home/roberto/Software/recipe-04/./bin
  20. -- Installing: /home/roberto/Software/recipe-04/./bin/hello-world_wAR
  21. -- Installing: /home/roberto/Software/recipe-04/./bin/use_message
  22. -- Installing: /home/roberto/Software/recipe-04/./bin/hello-world_wDSO
  23. -- ELF patching tool chrpath FOUND
  24. -- 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
  25. -- Running /home/roberto/Software/recipe-04/bin/use_message:
  26. This is my very nice message:
  27. Hello, World! From a client of yours!
  28. ...and here is its UUID: a8014bf7-5dfa-45e2-8408-12e9a5941825
  29. This is my very nice message:
  30. Goodbye, World! From a client of yours!
  31. ...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。