CMake 秘籍(六)(2)https://developer.aliyun.com/article/1525058
准备就绪
本食谱将构建一个简单的可执行文件,该文件链接到message
库。项目的布局如下:
├── 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
:
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
源文件包含以下内容:
#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
文件开始:
- 其序言与之前的食谱相比没有变化。我们首先声明一个 C++11 项目,设置一个合理的默认安装前缀、构建类型、目标的输出目录以及安装树中组件的布局:
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()
- 我们设置
EP_BASE
目录属性。这将设置超级构建中子项目的布局。所有子项目都将在CMAKE_BINARY_DIR
的subprojects
文件夹下检出和构建:
set_property(DIRECTORY PROPERTY EP_BASE ${CMAKE_BINARY_DIR}/subprojects)
- 然后我们声明
STAGED_INSTALL_PREFIX
变量。该变量指向构建目录下的stage
子目录。项目将在构建期间安装在这里。这是一种沙盒化安装过程的方法,并给我们一个机会来检查整个超级构建是否将按照正确的布局安装:
set(STAGED_INSTALL_PREFIX ${CMAKE_BINARY_DIR}/stage) message(STATUS "${PROJECT_NAME} staged install: ${STAGED_INSTALL_PREFIX}")
- 我们添加
external/upstream
子目录。这包含管理我们的上游依赖项的 CMake 指令,在我们的例子中,是message
库:
add_subdirectory(external/upstream)
- 然后我们包含
ExternalProject.cmake
标准模块:
include(ExternalProject)
- 我们将自己的项目作为外部项目添加,调用
ExternalProject_Add
命令。SOURCE_DIR
选项指定源代码位于src
子目录中。我们还传递了所有适当的 CMake 参数来配置我们的项目。注意使用STAGED_INSTALL_PREFIX
作为子项目的安装前缀:
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 )
- 现在我们为
recipe-04_core
目标构建的use_message
可执行文件添加一个测试。这将运行位于构建树内的use_message
可执行文件的临时安装:
enable_testing() add_test( NAME check_use_message COMMAND ${STAGED_INSTALL_PREFIX}/${INSTALL_BINDIR}/use_message )
- 最后,我们可以声明安装规则。这次它们相当简单。由于所需的一切都已按照正确的布局安装在临时区域中,我们只需要将临时区域的全部内容复制到安装前缀:
install( DIRECTORY ${STAGED_INSTALL_PREFIX}/ DESTINATION . USE_SOURCE_PERMISSIONS )
- 我们使用
SCRIPT
参数声明一个额外的安装规则。CMake 脚本install_hook.cmake
将被执行,但仅限于 GNU/Linux 和 macOS。该脚本将打印已安装可执行文件的RPATH
并运行它。我们将在下一节中详细讨论这一点:
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
文件中定义。该文件处理对message
库的依赖——让我们看看它是如何处理的:
- 我们首先尝试找到该软件包。可能用户已经在系统上的某个地方安装了它,并在配置时传递了
message_DIR
选项:
find_package(message 1 CONFIG QUIET)
- 如果情况确实如此,并且找到了
message
,我们向用户报告目标的位置和版本,并添加一个虚拟的message_external
目标。虚拟目标是正确处理超级构建依赖项所必需的:
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
- 如果未找到该库,我们将把它作为外部项目添加,从其在线 Git 存储库下载并编译它。安装前缀、构建类型和安装目录布局都是从根
CMakeLists.txt
文件设置的,C++编译器和标志也是如此。该项目将被安装到STAGED_INSTALL_PREFIX
,然后进行测试:
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 )
- 最后,我们将
message_DIR
目录设置为指向新构建的messageConfig.cmake
文件的位置。请注意,路径被保存到 CMake 缓存中:
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()
我们终于准备好编译我们自己的项目,并成功地将其链接到message
库,无论是系统上已有的还是为了这个目的新构建的。由于这是一个超级构建,位于src
子目录下的代码是一个完全独立的 CMake 项目:
- 我们声明一个 C++11 项目,一如既往:
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})
- 我们尝试查找
message
库。在我们的超级构建中,配置将正确设置message_DIR
:
find_package(message 1 CONFIG REQUIRED) get_property(_loc TARGET message::message-shared PROPERTY LOCATION) message(STATUS "Found message: ${_loc} (found version ${message_VERSION})")
- 我们准备好添加我们的可执行目标
use_message
。这是从use_message.cpp
源文件构建的,并链接了message::message-shared
目标:
add_executable(use_message use_message.cpp) target_link_libraries(use_message PUBLIC message::message-shared )
- 为目标属性设置
use_message
。再次注意RPATH
修复:
# 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 )
- 最后,我们为
use_message
目标设置安装规则:
install( TARGETS use_message RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT bin )
现在让我们看看install_hook.cmake.in
模板 CMake 脚本的内容:
- CMake 脚本在我们的主项目范围之外执行,因此对在那里定义的变量或目标没有任何概念。因此,我们设置一个变量,其中包含已安装的
use_message
可执行文件的完整路径。请注意使用@INSTALL_BINDIR@
,它将由configure_file
解析:
set(_executable ${CMAKE_INSTALL_PREFIX}/@INSTALL_BINDIR@/use_message)
- 我们需要找到用于打印已安装可执行文件的
RPATH
的平台原生工具的可执行文件。我们将搜索chrpath
、patchelf
和otool
。一旦找到其中一个已安装的工具,搜索就会退出,并向用户显示有帮助的状态消息:
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()
- 我们检查
_patcher
变量是否不为空。这意味着没有可用的 ELF 修补工具,我们想要执行的操作将会失败。我们发出致命错误,并通知用户需要安装其中一个 ELF 修补工具:
if(NOT _patcher) message(FATAL_ERROR "ELF patching tool NOT FOUND!\nPlease install one of chrpath, patchelf or otool")
- 如果找到了 ELF 修补工具之一,我们继续进行。我们调用
print_rpath.py
Python 脚本,将_executable
变量作为参数传递。我们为此目的使用execute_process
:
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 )
- 我们检查
_res
变量以获取返回代码。如果执行成功,我们打印在_out
变量中捕获的标准输出流。否则,我们在退出前打印捕获的标准输出和错误流,并显示致命错误:
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()
- 我们再次调用
execute_process
来运行已安装的use_message
可执行文件:
execute_process( COMMAND ${_executable} RESULT_VARIABLE _res OUTPUT_VARIABLE _out ERROR_VARIABLE _err OUTPUT_STRIP_TRAILING_WHITESPACE )
- 最后,我们向用户报告
execute_process
的结果:
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 项目文件,因为原则上,我们可以单独配置和构建它,而不需要超级构建提供的额外依赖管理设施。
首先,我们将考虑在message
库的依赖未得到满足时的超级构建配置:
$ mkdir -p build $ cd build $ cmake -DCMAKE_INSTALL_PREFIX=$HOME/Software/recipe-04 ..
我们将让 CMake 为我们找到库,这是我们得到的输出:
-- 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 报告以下内容:
- 安装将被分阶段到构建树中。分阶段安装是一种沙盒化实际安装过程的方法。作为开发者,这对于检查所有库、可执行文件和文件是否安装在正确位置之前运行安装命令很有用。对于用户来说,它提供了相同的最终结构,但在构建目录内。这样,即使没有运行适当的安装,我们的项目也可以立即使用。
- 系统上没有找到合适的
message
库。然后,CMake 将在构建我们的项目之前运行提供用于构建库的命令,以满足这个依赖。
如果库已经在系统上的已知位置,我们可以传递
CMake 的-Dmessage_DIR
选项:
$ cmake -DCMAKE_INSTALL_PREFIX=$HOME/Software/use_message -Dmessage_DIR=$HOME/Software/message/share/cmake/message ..
实际上,库已被找到并导入。只会执行我们自己项目的构建操作:
-- 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
:
install( DIRECTORY ${STAGED_INSTALL_PREFIX}/ DESTINATION . USE_SOURCE_PERMISSIONS )
注意使用.
而不是${CMAKE_INSTALL_PREFIX}
绝对路径,这样这个规则也可以被 CPack 工具正确理解。CPack 的使用将在第十一章,打包项目,第一部分,生成源代码和二进制包中展示。
recipe-04_core
项目构建一个简单的可执行目标,该目标链接到message
共享库。正如本章前面所讨论的,需要正确设置RPATH
,以便可执行文件能够正确运行。本章的第一部分展示了如何使用 CMake 实现这一点,同样的模式在这里被用于处理创建use_message
可执行文件的CMakeLists.txt
:
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 脚本中。最终,CMake 脚本作为安装规则使用SCRIPT
关键字被调用:
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()
这个额外的脚本在安装过程的最后执行:
$ cmake --build build --target install
在 GNU/Linux 系统上,我们将看到以下输出:
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
我们建议用于处理可执行和可链接格式(ELF)对象的工具包括 PatchELF(nixos.org/patchelf.html
)、chrpath(linux.die.net/man/1/chrpath
)和 otool(www.manpagez.com/man/1/otool/
)。第一个工具适用于 GNU/Linux 和 macOS,而 chrpath 和 otool 分别适用于 GNU/Linux 和 macOS。
第十二章:打包项目
在本章中,我们将涵盖以下食谱:
- 生成源代码和二进制包
- 通过 PyPI 分发使用 CMake/pybind11 构建的 C++/Python 项目
- 通过 PyPI 分发使用 CMake/CFFI 构建的 C/Fortran/Python 项目
- 将简单项目作为 Conda 包分发
- 将具有依赖项的项目作为 Conda 包分发
引言
到目前为止,我们已经从源代码编译并安装(示例)软件包——这意味着通过 Git 获取项目,并手动执行配置、构建、测试和安装步骤。然而,在实践中,软件包通常使用包管理器(如 Apt、DNF、Pacman、pip 和 Conda)进行安装。我们需要能够以各种格式分发我们的代码项目:作为源代码存档或作为二进制安装程序。
这就是我们在熟悉的 CMake 项目使用方案中提到的打包时间,显示了项目的各个阶段:
在本章中,我们将探讨不同的打包策略。我们将首先讨论使用 CMake 家族中的工具 CPack 进行打包。我们还将提供将 CMake 项目打包并上传到 Python Package Index(PyPI,[pypi.org
](https://pypi.org))和 Anaconda Cloud(https://anaconda.org)的食谱——这些都是通过包管理器 pip 和 Conda([conda.io/docs/
](https://conda.io/docs/))分发包的标准且流行的平台。对于 PyPI,我们将演示如何打包和分发混合 C++/Python 或 C/Fortran/Python 项目。对于 Conda,我们将展示如何打包依赖于其他库的 C++项目。
生成源代码和二进制包
本食谱的代码可在https://github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-11/recipe-01找到。该食谱适用于 CMake 版本 3.6(及以上),并在 GNU/Linux、macOS 和 Windows 上进行了测试。
如果您的代码是开源的,用户将期望能够下载您项目的源代码,并使用您精心定制的 CMake 脚本自行构建。当然,打包操作可以用脚本完成,但 CPack 提供了更紧凑和便携的替代方案。本食谱将指导您创建多种打包替代方案:
- 源代码存档:您可以使用这些格式直接将源代码作为您喜欢的格式的压缩存档发货。您的用户不必担心您的特定版本控制系统。
- 二进制存档:使用这些格式将新构建的目标打包成您喜欢的格式的压缩存档。这些可能非常有用,但可能不足以分发库和可执行文件。
- 平台原生二进制安装程序:CPack 能够生成多种不同格式的二进制安装程序,因此您可以将软件分发目标定位到许多不同的平台。特别是,我们将展示如何生成安装程序:
- 以
.deb
格式为 Debian 基础的 GNU/Linux 发行版:manpages.debian.org/unstable/dpkg-dev/deb.5.en.html
- 以
.rpm
格式为 Red Hat 基础的 GNU/Linux 发行版:rpm.org/
- 以
.dmg
格式为 macOS 捆绑包:developer.apple.com/library/archive/documentation/CoreFoundation/Conceptual/CFBundles/BundleTypes/BundleTypes.html
- 以 NSIS 格式为 Windows:
nsis.sourceforge.net/Main_Page
准备工作
我们将使用第十章[72e949cc-6881-4be1-9710-9ac706c14a4d.xhtml]中介绍的message
库的源代码,编写安装程序,第 3 个配方,导出目标。项目树由以下目录和文件组成:
. ├── cmake │ ├── coffee.icns │ ├── Info.plist.in │ └── messageConfig.cmake.in ├── CMakeCPack.cmake ├── CMakeLists.txt ├── INSTALL.md ├── LICENSE ├── src │ ├── CMakeLists.txt │ ├── hello-world.cpp │ ├── Message.cpp │ └── Message.hpp └── tests ├── CMakeLists.txt └── use_target ├── CMakeLists.txt └── use_message.cpp
由于本配方的重点将是有效使用 CPack,我们将不对源代码本身进行评论。我们只会在CMakeCPack.cmake
中添加打包指令,我们将在稍后讨论。此外,我们添加了INSTALL.md
和一个LICENSE
文件:它们包含项目安装说明和许可证,并且是打包指令所必需的。
如何操作
让我们看看需要添加到此项目的打包指令。我们将它们收集在CMakeCPack.cmake
中,该文件在CMakeLists.txt
的末尾使用include(CMakeCPack.cmake)
包含:
- 我们声明包的名称。这与项目名称相同,因此我们使用
PROJECT_NAME
CMake 变量:
set(CPACK_PACKAGE_NAME "${PROJECT_NAME}")
- 我们声明了包的供应商:
set(CPACK_PACKAGE_VENDOR "CMake Cookbook")
- 打包的源代码将包括一个描述文件。这是包含安装说明的纯文本文件:
set(CPACK_PACKAGE_DESCRIPTION_FILE "${PROJECT_SOURCE_DIR}/INSTALL.md")
- 我们还添加了包的简要概述:
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "message: a small messaging library")
- 许可证文件也将包含在包中:
set(CPACK_RESOURCE_FILE_LICENSE "${PROJECT_SOURCE_DIR}/LICENSE")
- 从分发的包中安装时,文件将被放置在
/opt/recipe-01
目录中:
set(CPACK_PACKAGING_INSTALL_PREFIX "/opt/${PROJECT_NAME}")
- 包的主版本、次版本和补丁版本设置为 CPack 的变量:
set(CPACK_PACKAGE_VERSION_MAJOR "${PROJECT_VERSION_MAJOR}") set(CPACK_PACKAGE_VERSION_MINOR "${PROJECT_VERSION_MINOR}") set(CPACK_PACKAGE_VERSION_PATCH "${PROJECT_VERSION_PATCH}")
- 我们设置了一组文件和目录,以在打包操作期间忽略:
set(CPACK_SOURCE_IGNORE_FILES "${PROJECT_BINARY_DIR};/.git/;.gitignore")
- 我们列出了源代码存档的打包生成器——在我们的例子中是
ZIP
,用于生成.zip
存档,以及TGZ
,用于.tar.gz
存档。
set(CPACK_SOURCE_GENERATOR "ZIP;TGZ")
- 我们还列出了二进制存档生成器:
set(CPACK_GENERATOR "ZIP;TGZ")
- 我们现在还声明了平台原生的二进制安装程序,从 DEB 和 RPM 包生成器开始,仅适用于 GNU/Linux:
if(UNIX) if(CMAKE_SYSTEM_NAME MATCHES Linux) list(APPEND CPACK_GENERATOR "DEB") set(CPACK_DEBIAN_PACKAGE_MAINTAINER "robertodr") set(CPACK_DEBIAN_PACKAGE_SECTION "devel") set(CPACK_DEBIAN_PACKAGE_DEPENDS "uuid-dev") list(APPEND CPACK_GENERATOR "RPM") set(CPACK_RPM_PACKAGE_RELEASE "1") set(CPACK_RPM_PACKAGE_LICENSE "MIT") set(CPACK_RPM_PACKAGE_REQUIRES "uuid-devel") endif() endif()
- 如果我们使用的是 Windows,我们将希望生成一个 NSIS 安装程序:
if(WIN32 OR MINGW) list(APPEND CPACK_GENERATOR "NSIS") set(CPACK_NSIS_PACKAGE_NAME "message") set(CPACK_NSIS_CONTACT "robertdr") set(CPACK_NSIS_ENABLE_UNINSTALL_BEFORE_INSTALL ON) endif()
- 另一方面,在 macOS 上,捆绑包是我们的首选安装程序:
if(APPLE) list(APPEND CPACK_GENERATOR "Bundle") set(CPACK_BUNDLE_NAME "message") configure_file(${PROJECT_SOURCE_DIR}/cmake/Info.plist.in Info.plist @ONLY) set(CPACK_BUNDLE_PLIST ${CMAKE_CURRENT_BINARY_DIR}/Info.plist) set(CPACK_BUNDLE_ICON ${PROJECT_SOURCE_DIR}/cmake/coffee.icns) endif()
- 我们向用户打印有关当前系统上可用的包装生成器的信息性消息:
message(STATUS "CPack generators: ${CPACK_GENERATOR}")
- 最后,我们包含了
CPack.cmake
标准模块。这将向构建系统添加一个package
和一个package_source
目标:
include(CPack)
我们现在可以像往常一样配置项目:
$ mkdir -p build $ cd build $ cmake ..
使用以下命令,我们可以列出可用的目标(示例输出是在使用 Unix Makefiles 作为生成器的 GNU/Linux 系统上获得的):
$ cmake --build . --target help The following are some of the valid targets for this Makefile: ... all (the default if no target is provided) ... clean ... depend ... install/strip ... install ... package_source ... package ... install/local ... test ... list_install_components ... edit_cache ... rebuild_cache ... hello-world ... message
我们可以看到package
和package_source
目标可用。源包可以通过以下命令生成:
$ cmake --build . --target package_source Run CPack packaging tool for source... CPack: Create package using ZIP CPack: Install projects CPack: - Install directory: /home/user/cmake-cookbook/chapter-11/recipe-01/cxx-example CPack: Create package CPack: - package: /home/user/cmake-cookbook/chapter-11/recipe-01/cxx-example/build/recipe-01-1.0.0-Source.zip generated. CPack: Create package using TGZ CPack: Install projects CPack: - Install directory: /home/user/cmake-cookbook/chapter-11/recipe-01/cxx-example CPack: Create package CPack: - package: /home/user/cmake-cookbook/chapter-11/recipe-01/cxx-example/build/recipe-01-1.0.0-Source.tar.gz generated.
同样,我们可以构建二进制包:
$ cmake --build . --target package
在我们的例子中,我们获得了以下二进制包列表:
message-1.0.0-Linux.deb message-1.0.0-Linux.rpm message-1.0.0-Linux.tar.gz message-1.0.0-Linux.zip
CMake 秘籍(六)(4)https://developer.aliyun.com/article/1525061