面向 C++ 的现代 CMake 教程(四)(3)https://developer.aliyun.com/article/1526941
在安装过程中调用脚本
如果你曾经在类 Unix 系统上安装过一个共享库,你可能记得在可以使用它之前,你可能需要告诉动态链接器扫描可信目录并调用ldconfig
(在进一步阅读部分可以看到参考文献)来构建其缓存。如果你想要使你的安装完全自动化,CMake 提供了install(SCRIPT|CODE)
命令来支持这类情况。以下是完整命令的签名:
install([[SCRIPT <file>] [CODE <code>]] [ALL_COMPONENTS | COMPONENT <component>] [EXCLUDE_FROM_ALL] [...])
你应该选择SCRIPT
或CODE
模式并提供适当的参数——要么是一个运行 CMake 脚本的路径,要么是在安装过程中执行的 CMake 代码片段。为了了解这是如何工作的,我们将修改02-install-targets
示例以构建一个共享库:
第十一章/05-install-code/src/CMakeLists.txt
add_library(calc SHARED calc.cpp) target_include_directories(calc INTERFACE include) set_target_properties(calc PROPERTIES PUBLIC_HEADER src/include/calc/calc.h )
我们需要在安装脚本中将 artifact 类型从 ARCHIVE
更改为 LIBRARY
以复制文件。然后,我们可以在之后添加运行 ldconfig
的逻辑:
第十一章/05-install-code/CMakeLists.txt(片段)
... install(TARGETS calc LIBRARY PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/calc ) if (UNIX) install(CODE "execute_process(COMMAND ldconfig)") endif()
if()
条件检查命令是否与操作系统匹配(在 Windows 或 macOS 上执行ldconfig
是不正确的)。当然,提供的代码必须具有有效的 CMake 语法才能工作(不过,在初始构建期间不会进行检查;任何失败都会在安装时显现)。
运行安装命令后,我们可以通过打印缓存中的库来确认它是否工作:
# cmake -S <source-tree> -B <build-tree> # cmake --build <build-tree> # cmake --install <build-tree> -- Install configuration: "" -- Installing: /usr/local/lib/libcalc.so -- Installing: /usr/local/include/calc/calc.h # ldconfig -p | grep libcalc libcalc.so (libc6,x86-64) => /usr/local/lib/libcalc.so
这两种模式都支持生成表达式,如果你需要的话。因此,这个命令和 CMake 本身一样多功能,可以用于所有 sorts of things:为用户打印消息,验证安装是否成功,进行详尽的配置,文件签名——你能想到的都有。
既然我们已经知道了将一组文件安装到系统上的所有不同方法,那么接下来让我们学习如何将它们转换为其他 CMake 项目可以原生使用的包。
创建可重用包
在之前的章节中,我们大量使用了find_package()
。我们看到了它有多方便,以及它是如何简化整个过程的。为了使我们的项目通过这个命令可用,我们需要完成几步,以便 CMake 可以将我们的项目视为一个连贯的包:
- 使我们的目标可移动。
- 将目标导出文件安装到标准位置。
- 为包创建配置文件和版本文件。
让我们从开头说起:为什么目标需要可移动,我们又该如何实现?
理解可移动目标的问题
安装解决了许多问题,但不幸的是,它也引入了一些复杂性:不仅CMAKE_INSTALL_PREFIX
是平台特定的,而且它还可以在安装阶段由用户使用--prefix
选项进行设置。然而,目标导出文件是在安装之前生成的,在构建阶段,此时我们不知道安装的工件将去哪里。请看下面的代码:
chapter-11/01-export/src/CMakeLists.txt
add_library(calc STATIC calc.cpp) target_include_directories(calc INTERFACE include)
在这个例子中,我们特别将包含目录添加到calc
的包含目录中。由于这是一个相对路径,CMake 生成的目标将隐式地将这个路径与CMAKE_CURRENT_SOURCE_DIR
变量的内容相结合,该变量指向这个列表文件所在的目录。
然而,这还不够。已安装的项目不应再需要源代码或构建树中的文件。一切(包括库头文件)都被复制到一个共享位置,如 Linux 上的/usr/lib/calc/
。由于这个片段中定义的目标的包含目录路径仍然指向其源树,所以我们不能在另一个项目中使用这个目标。
CMake 用两个生成表达式解决了这个问题,这些表达式将根据上下文过滤出表达式:
$
:这包括了常规构建的内容,但在安装时将其排除。$
:这包括了安装的内容,但排除了常规构建。
下面的代码展示了你如何实际上使用它们:
chapter-11/06-install-export/src/CMakeLists.txt
add_library(calc STATIC calc.cpp) target_include_directories(calc INTERFACE "$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>" "$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>" ) set_target_properties(calc PROPERTIES PUBLIC_HEADER src/include/calc/calc.h )
对于常规构建,calc
目标属性的INTERFACE_INCLUDE_DIRECTORIES
值将像这样扩展:
"/root/examples/chapter-11/05-package/src/include" ""
空的双引号意味着在INSTALL_INTERFACE
中提供的值被排除,并被评估为空字符串。另一方面,当我们安装时,该值将像这样扩展:
"" "/usr/lib/calc/include"
这次,在BUILD_INTERFACE
生成表达式中提供的值被评估为空字符串,我们留下了另一个生成表达式的值。
关于CMAKE_INSTALL_PREFIX
再说一句:这个变量不应该用作目标中指定路径的组件。它将在构建阶段进行评估,使路径成为绝对路径,并且不一定与在安装阶段提供的路径相同(因为用户可能使用--prefix
选项)。相反,请使用$
生成表达式:
target_include_directories(my_target PUBLIC $<INSTALL_INTERFACE:$<INSTALL_PREFIX>/include/MyTarget> )
或者,更好的做法是使用相对路径(它们会前缀正确的安装前缀):
target_include_directories(my_target PUBLIC $<INSTALL_INTERFACE:include/MyTarget> )
请参阅官方文档以获取更多示例和信息(可以在进阶阅读部分找到此链接)。
现在我们的目标已经是“安装兼容”的,我们可以安全地生成并安装它们的导出文件。
安装目标导出文件
我们在无需安装导出部分稍微讨论了目标导出文件。打算用于安装的目标导出文件非常相似,创建它们的命令签名也是如此:
install(EXPORT <export-name> DESTINATION <dir> [NAMESPACE <namespace>] [[FILE <name>.cmake]| [PERMISSIONS permissions...] [CONFIGURATIONS [Debug|Release|...]] [EXPORT_LINK_INTERFACE_LIBRARIES] [COMPONENT <component>] [EXCLUDE_FROM_ALL])
这是“普通”的export(EXPORT)
和其他install()
命令的组合(它的选项工作方式相同)。只需记住,它会创建并安装一个名为导出,必须使用install(TARGETS)
命令定义。这里需要注意的是,生成的导出文件将包含在INSTALL_INTERFACE
生成表达式中评估的目标路径,而不是BUILD_INTERFACE
,就像export(EXPORT)
一样。
在此示例中,我们将为chapter-11/06-install-export/src/CMakeLists.txt
中的目标生成并安装目标导出文件。为此,我们必须在顶层列表文件中调用install(EXPORT)
:
chapter-11/06-install-export/CMakeLists.txt
cmake_minimum_required(VERSION 3.20.0) project(InstallExport CXX) include(GNUInstallDirs) # so it's available in ./src/ add_subdirectory(src bin) install(TARGETS calc EXPORT CalcTargets ARCHIVE PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/calc ) install(EXPORT CalcTargets DESTINATION ${CMAKE_INSTALL_LIBDIR}/calc/cmake NAMESPACE Calc:: )
再次注意我们如何在install(EXPORT)
中引用CalcTargets
导出名称。
在构建树中运行cmake --install
将导致导出文件在指定目的地生成:
... -- Installing: /usr/local/lib/calc/cmake/CalcTargets.cmake -- Installing: /usr/local/lib/calc/cmake/CalcTargets-noconfig.cmake
如果出于某种原因,目标导出文件的默认重写名称(.cmake
)对您不起作用,您可以添加FILE new-name.cmake
参数来更改它(文件名必须以.cmake
结尾)。
不要被这个困惑 - 目标导出文件不是一个配置文件,所以您现在还不能使用find_package()
来消耗已安装的目标。然而,如果需要,您可以直接包含导出文件。那么,我们如何定义可以被其他项目消耗的包呢?让我们找出答案!
编写基本配置文件
一个完整的包定义包括目标导出文件、包的config 文件以及包的版本文件,但技术上来说,为了使find_package()
工作只需要一个 config-file。它被视为一个包定义,负责提供任何包函数和宏,检查要求,查找依赖项,并包含目标导出文件。
如我们之前提到的,用户可以使用以下命令将您的包安装到他们系统上的任何位置:
cmake --install <build tree> --prefix=<installation path>
这个前缀决定了安装文件将被复制到的位置。为了支持这一点,您至少必须确保以下几点:
- 目标属性中的路径可以移动(如理解可移动目标的问题部分所述)。
- 您 config-file 中使用的路径相对于它本身是相对的。
为了使用已安装在非默认位置的这类包,消费项目在配置阶段需要通过CMAKE_PREFIX_PATH
变量提供<安装路径>
。我们可以用以下命令来实现:
cmake -B <build tree> -DCMAKE_PREFIX_PATH=<installation path>
find_package()
命令将按照文档中概述的路径(进一步阅读部分的链接)以平台特定的方式扫描。在 Windows 和类 Unix 系统中检查的一个模式如下:
<prefix>/<name>*/(lib/<arch>|lib*|share)/<name>*/(cmake|CMake)
这告诉我们,将 config-file 安装在如lib/calc/cmake
的路径上应该完全没有问题。另外,重要的是要强调 config-files 必须命名为<包名>-config.cmake
或<包名>Config.cmake
才能被找到。
让我们将 config-file 的安装添加到06-install-export
示例中:
chapter-11/07-config-file/CMakeLists.txt(片段)
... install(EXPORT CalcTargets DESTINATION ${CMAKE_INSTALL_LIBDIR}/calc/cmake NAMESPACE Calc:: ) install(FILES "CalcConfig.cmake" DESTINATION ${CMAKE_INSTALL_LIBDIR}/calc/cmake )
此命令将从同一源目录(CMAKE_INSTALL_LIBDIR
将被评估为平台正确的lib
路径)安装CalcConfig.cmake
。
我们能够提供的最基本的 config-file 由一条包含目标导出文件的直线组成:
chapter-11/07-config-file/CalcConfig.cmake
include("${CMAKE_CURRENT_LIST_DIR}/CalcTargets.cmake")
CMAKE_CURRENT_LIST_DIR
变量指的是 config-file 所在的目录。因为在我们示例中CalcConfig.cmake
和CalcTargets.cmake
安装在同一个目录中(如install(EXPORT)
所设置),目标导出文件将被正确包含。
为了确保我们的包可以被使用,我们将创建一个简单的项目,仅包含一个 listfile:
chapter-11/08-find-package/CMakeLists.txt
cmake_minimum_required(VERSION 3.20.0) project(FindCalcPackage CXX) find_package(Calc REQUIRED) include(CMakePrintHelpers) message("CMAKE_PREFIX_PATH: ${CMAKE_PREFIX_PATH}") message("CALC_FOUND: ${Calc_FOUND}") cmake_print_properties(TARGETS "Calc::calc" PROPERTIES IMPORTED_CONFIGURATIONS INTERFACE_INCLUDE_DIRECTORIES )
为了在实际中测试这个,我们可以将07-config-file
示例构建并安装到一个目录中,然后在使用DCMAKE_PREFIX_PATH
参数引用它的情况下构建08-find-package
,如下所示:
# cmake -S <source-tree-of-07> -B <build-tree-of-07> # cmake --build <build-tree-of-07> # cmake --install <build-tree-of-07> # cmake -S <source-tree-of-08> -B <build-tree-of-08> -DCMAKE_PREFIX_PATH=<build-tree-of-07>
这将产生以下输出(所有<_tree-of_>
占位符都将被真实路径替换):
CMAKE_PREFIX_PATH: <build-tree-of-07> CALC_FOUND: 1 -- Properties for TARGET Calc::calc: Calc::calc.IMPORTED_CONFIGURATIONS = "NOCONFIG" Calc::calc.INTERFACE_INCLUDE_DIRECTORIES = "<build-tree-of-07>/include" -- Configuring done -- Generating done -- Build files have been written to: <build-tree-of-08>
找到了CalcTargets.cmake
文件,并正确地包含了它,*include 目录*
的路径设置为遵循所选的前缀。这对于一个非常基础的打包情况解决了打包问题。现在,让我们学习如何处理更高级的场景。
创建高级配置文件
如果你管理的不仅仅是单个目标导出文件,那么在配置文件中包含几个宏可能是有用的。CMakePackageConfigHelpers
工具模块让我们可以使用configure_package_config_file()
命令。使用它时,我们需要提供一个模板文件,这个文件会被 CMake 变量插值,以生成一个带有两个内嵌宏定义的配置文件:
set_and_check( )
: 这个命令类似于set()
,但它会检查是否存在,如果不存在则会导致
FATAL_ERROR
。建议在配置文件中使用它,以便尽早发现错误的路径。check_required_components()
: 这句话添加到配置文件的最后,将验证我们包中由用户在find_package( REQUIRED )
中 required 的所有组件是否已经被找到。这是通过检查__FOUND
变量是否为真来完成的。
可以在生成配置文件的同时为更复杂的目录树准备安装阶段的路径。看看以下的签名:
configure_package_config_file(<template> <output> INSTALL_DESTINATION <path> [PATH_VARS <var1> <var2> ... <varN>] [NO_SET_AND_CHECK_MACRO] [NO_CHECK_REQUIRED_COMPONENTS_MACRO] [INSTALL_PREFIX <path>] )
作为
面向 C++ 的现代 CMake 教程(四)(5)https://developer.aliyun.com/article/1526943