面向 C++ 的现代 CMake 教程(四)(4)

简介: 面向 C++ 的现代 CMake 教程(四)

面向 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] [...])

你应该选择SCRIPTCODE模式并提供适当的参数——要么是一个运行 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.cmakeCalcTargets.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

相关文章
|
8天前
|
C++
Clion CMake C/C++程序输出乱码
Clion CMake C/C++程序输出乱码
10 0
|
9天前
|
存储 算法 编译器
C++ 函数式编程教程
C++ 函数式编程学习
|
9天前
|
存储 编译器 开发工具
C++语言教程分享
C++语言教程分享
|
9天前
|
存储 编译器 C++
|
1月前
|
C++ 存储 索引
面向 C++ 的现代 CMake 教程(一)(5)
面向 C++ 的现代 CMake 教程(一)
46 0
|
1月前
|
缓存 存储 C++
面向 C++ 的现代 CMake 教程(一)(4)
面向 C++ 的现代 CMake 教程(一)
45 0
|
1月前
|
C++ 缓存 存储
面向 C++ 的现代 CMake 教程(一)(3)
面向 C++ 的现代 CMake 教程(一)
44 0
|
1月前
|
缓存 C++ Windows
面向 C++ 的现代 CMake 教程(一)(2)
面向 C++ 的现代 CMake 教程(一)
57 0
|
1月前
|
C++ 容器 Docker
面向 C++ 的现代 CMake 教程(一)(1)
面向 C++ 的现代 CMake 教程(一)
67 0
|
Java 编译器 Linux
【CMake】CMake 引入 ( Android Studio 创建 Native C++ 工程 | C/C++ 源码编译过程 | Makefile 工具 | CMake 引入 )(二)
【CMake】CMake 引入 ( Android Studio 创建 Native C++ 工程 | C/C++ 源码编译过程 | Makefile 工具 | CMake 引入 )(二)
280 0
【CMake】CMake 引入 ( Android Studio 创建 Native C++ 工程 | C/C++ 源码编译过程 | Makefile 工具 | CMake 引入 )(二)