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

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

面向 C++ 的现代 CMake 教程(四)(4)https://developer.aliyun.com/article/1526942

生成包版本文件

随着您的包的增长,它将逐渐增加新功能,旧的将被标记为弃用,最终被移除。对于使用您的包的开发人员来说,保持这些修改的变更日志是很重要的。当需要特定功能时,开发者可以找到支持它的最低版本并将其用作find_package()的参数,如下所示:

find_package(Calc 1.2.3 REQUIRED)

然后,CMake 会在配置文件中搜索Calc,并检查是否有一个名为-version.cmakeVersion.cmake版本文件存在于同一目录中,即CalcConfigVersion.cmake。接下来,这个文件将被读取以获取其版本信息以及与其他版本的兼容性。例如,你可能没有安装所需的版本1.2.3,但你可能有1.3.5,它被标记为与任何旧版本“兼容”。CMake 会欣然接受这样的包,因为它知道包供应商提供了向后兼容性。

您可以使用CMakePackageConfigHelpers工具模块通过调用write_basic_package_version_file()生成包的版本文件

write_basic_package_version_file(<filename> [VERSION <ver>]
  COMPATIBILITY <AnyNewerVersion | SameMajorVersion | 
                 SameMinorVersion | ExactVersion>
  [ARCH_INDEPENDENT] 
)

首先,我们需要提供要创建的工件的属性;它必须遵循我们之前概述的规则。除此之外,请记住我们应该将所有生成的文件存储在构建树中。

可选地,我们可以传递一个显式的VERSION(这里支持常用的格式,major.minor.patch)。如果我们不这样做,将使用project()命令中提供的版本(如果您的项目没有指定,请期待一个错误)。

COMPATIBILITY关键词不言自明:

  • ExactVersion必须与版本的所有三个组件相匹配,并且不支持范围版本:find_package( 1.2.8...1.3.4)
  • SameMinorVersion如果前两个组件相同(忽略patch)则匹配。
  • SameMajorVersion如果第一个组件相同(忽略minorpatch)则匹配。
  • AnyNewerVersion似乎有一个反向的名字:它会匹配任何旧版本。换句话说,版本1.4.2将与find_package( 1.2.8)相匹配。

通常,所有包必须为与消费项目相同的架构构建(执行精确检查)。然而,对于不编译任何内容的包(仅头文件库、宏包等),您可以使用ARCH_INDEPENDENT关键词跳过此检查。

现在,是时候来一个实际例子了。以下代码展示了如何为我们在06-install-export示例中开始的项目提供版本文件

chapter-11/10-version-file/CMakeLists.txt(片段)

cmake_minimum_required(VERSION 3.20.0)
project(VersionFile VERSION 1.2.3 LANGUAGES CXX)
...
include(CMakePackageConfigHelpers)
write_basic_package_version_file(
  "${CMAKE_CURRENT_BINARY_DIR}/CalcConfigVersion.cmake"
  COMPATIBILITY AnyNewerVersion
)
install(FILES "CalcConfig.cmake"
  "${CMAKE_CURRENT_BINARY_DIR}/CalcConfigVersion.cmake"
  DESTINATION ${CMAKE_INSTALL_LIBDIR}/calc/cmake
)

为了方便,我们在文件的顶部,在project()命令中配置包的版本。这需要我们从简短的project( )语法切换到通过添加LANGUAGE关键词来使用显式、完整语法的语法。

在包含助手工具模块后,我们调用生成命令并将文件写入符合find_package()所需模式的构建树中。在这里,我们故意省略了VERSION关键词,以便从PROJECT_VERSION变量中读取版本。我们还标记我们的包为与COMPATIBILITY AnyNewerVersion完全向后兼容。之后,我们将包版本文件安装到与CalcConfig.cmake相同的目的地。就这样——我们的包已经完全配置好了。

在下一节中,我们将学习什么是组件以及如何将它们与包一起使用。

定义组件

我们将先讨论包组件,通过澄清一些关于find_package()术语可能的混淆:

find_package(<PackageName> [version] [EXACT] [QUIET]
[MODULE]
  [REQUIRED] [[COMPONENTS] [components...]]
             [OPTIONAL_COMPONENTS components...]
             [NO_POLICY_SCOPE])

这里提到的组件不应与在install()命令中使用的COMPONENT关键字混淆。它们是两个不同的概念,尽管它们共享相同的名字,但必须分别理解。我们将在下面的子节中更详细地讨论这一点。

如何在find_package()中使用组件

当我们调用find_package()并带有COMPONENTSOPTIONAL_COMPONENTS列表时,我们告诉 CMake 我们只对提供这些组件的包感兴趣。然而,重要的是要意识到,是否有必要检查这一要求取决于包本身,如果包的供应商没有在创建高级 config 文件小节中提到的 config 文件中添加必要的检查,那么什么也不会发生。

请求的组件将通过_FIND_COMPONENTS变量传递给 config 文件(可选和非可选都有)。此外,对于每个非可选组件,将设置一个_FIND_REQUIRED_。作为包的作者,我们可以编写一个宏来扫描这个列表并检查我们是否提供了所有必需的组件。但我们不需要这样做——这正是check_required_components()所做的。要使用它,config 文件应在找到必要的组件时设置__FOUND变量。文件末尾的宏将检查是否设置了所有必需的变量。

如何在install()命令中使用组件

一些生成的工件可能不需要在所有场景中都进行安装。例如,一个项目可能为了开发目的安装静态库和公共头文件,但默认情况下,它只需安装共享库以供运行时使用。为了实现这种行为的双重性,我们可以使用在所有install()命令中可用的COMPONENT关键字来将工件分组,用户如果对限制安装到特定组件感兴趣,可以通过运行以下命令(组件名称区分大小写)来显式请求:

cmake --install <build tree> --component=<component name>

COMPONENT关键字标记一个工件并不意味着它不会被默认安装。为了防止这种情况发生,我们必须添加EXCLUDE_FROM_ALL关键字。

让我们通过一个代码示例来探索这些组件:

chapter-11/11-components/CMakeLists.txt(片段)

...
install(TARGETS calc EXPORT CalcTargets
  ARCHIVE
    COMPONENT lib
  PUBLIC_HEADER
    DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/calc
    COMPONENT headers
)
install(EXPORT CalcTargets
  DESTINATION ${CMAKE_INSTALL_LIBDIR}/calc/cmake
  NAMESPACE Calc::
  COMPONENT lib
)
install(CODE "MESSAGE(\"Installing 'extra' component\")"
  COMPONENT extra
  EXCLUDE_FROM_ALL
)
...

这些安装命令定义了以下组件:

  • lib:这包含静态库和目标导出文件。它默认安装。
  • headers:包含公共头文件。它默认安装。
  • extra:通过打印一条消息执行一段代码。它不会被默认安装。

让我们重申这一点:

  • 不带--component参数的cmake --install将安装libheaders组件。
  • cmake --install --component headers将只安装公共头文件。
  • cmake --install --component extra将打印一条在其他情况下无法访问的消息(因为EXCLUDE_FROM_ALL关键字)。

如果安装的工件没有指定COMPONENT关键字,它将从CMAKE_INSTALL_DEFAULT_COMPONENT_NAME变量中获得默认值Unspecified

注意

由于没有简单的方法从cmake命令行列出所有可用的组件,您的包的用户将受益于详尽的文档,列出您的包的组件。也许在INSTALL文件中提到这一点是个好主意。

如果调用cmake时为不存在的一个组件提供了--component参数,那么该命令将成功执行,不带任何警告或错误。它只是不会安装任何东西。

将我们的安装划分为组件使得用户能够挑选他们想要安装的内容。我们 mostly 讨论了将安装文件划分为组件,但还有些程序步骤,比如install(SCRIPT|CODE)或为共享库创建符号链接。

管理版本化共享库的符号链接

您的安装目标平台可能使用符号链接来帮助链接器发现当前安装的共享库版本。在创建一个指向lib.so.1文件的lib.so符号链接之后,可以通过向链接器传递-l参数来链接这个库。当需要时,此类符号链接由 CMake 的install(TARGETS  LIBRARY)块处理。

然而,我们可能决定将这个步骤移到另一个install()命令中,通过在这个块中添加NAMELINK_SKIP来实现:

install(TARGETS <target> LIBRARY COMPONENT cmp
  NAMELINK_SKIP)

要将符号链接分配给另一个组件(而不是完全禁用它),我们可以为同一目标重复install()命令,指定不同的组件,然后是NAMELINK_ONLY关键字:

install(TARGETS <target> LIBRARY COMPONENT lnk
  NAMELINK_ONLY)

同样,可以使用NAMELINK_COMPONENT关键字实现:

install(TARGETS <target> LIBRARY 
  COMPONENT cmp NAMELINK_COMPONENT lnk)

如今我们已经配置了自动安装,我们可以使用随 CMake 提供的 CPack 工具为我们的用户提供预先构建的工件。

使用 CPack 进行打包

从源代码构建项目有其优点,但它可能需要很长时间并引入很多复杂性。这并不是终端用户所期望的最佳体验,尤其是如果他们自己不是开发者的话。对于终端用户来说,一种更加便捷的软件分发方式是使用包含编译工件和其他运行时所需静态文件的二进制包。CMake 通过名为cpack的命令行工具支持生成多种此类包。

以下表格列出了可用的包生成器:

这些生成器中的大多数都有广泛的配置。深入了解所有它们的细节超出了本书的范围,所以一定要查看完整的文档,您可以在“进一步阅读”部分找到。相反,我们将关注一般使用案例。

注意

包生成器不应该与构建系统生成器(Unix Makefiles,Visual Studio 等)混淆。

要使用 CPack,我们需要正确配置项目的安装,并使用必要的install()命令构建项目。在我们构建树中生成的cmake_install.cmake将用于cpack根据配置文件(CPackConfig.cmake)准备二进制包。虽然可以手动创建此文件,但使用include(CPack)更容易地在项目的列表文件中包含实用模块。它将在项目的构建树中生成配置,并在需要的地方提供所有默认值。

让我们看看如何扩展示例11-components,使其可以与 CPack 一起工作:

chapter-11/12-cpack/CMakeLists.txt (片段)

cmake_minimum_required(VERSION 3.20.0)
project(CPackPackage VERSION 1.2.3 LANGUAGES CXX)
include(GNUInstallDirs)
add_subdirectory(src bin)
install(...)
install(...)
install(...)
set(CPACK_PACKAGE_VENDOR "Rafal Swidzinski")
set(CPACK_PACKAGE_CONTACT "email@example.com")
set(CPACK_PACKAGE_DESCRIPTION "Simple Calculator")
include(CPack)

代码相当直观,所以我们不会过多地解释(请参考模块文档,可以在进一步阅读部分找到)。这里值得注意的一点是,CPack模块将从project()命令中推断出一些值:

  • CPACK_PACKAGE_NAME
  • CPACK_PACKAGE_VERSION
  • CPACK_PACKAGE_FILE_NAME

最后一个值将用于生成输出包。其结构如下:

$CPACK_PACKAGE_NAME-$CPACK_PACKAGE_VERSION-$CPACK_SYSTEM_NAME

在这里,CPACK_SYSTEM_NAME是目标操作系统的名称;例如,Linuxwin32。例如,通过在 Debian 上执行 ZIP 生成器,CPack 将生成一个名为CPackPackage-1.2.3-Linux.zip的文件。

在我们构建项目之后,我们可以在构建树中运行cpack二进制文件来生成实际的包:

cpack [<options>]

从技术上讲,CPack 能够读取放置在当前工作目录中的所有配置文件选项,但你也可以选择从命令行覆盖这些设置:

  • -G :这是一个由分号分隔的包生成器列表。默认值可以在CPackConfig.cmake中的CPACK_GENERATOR变量中指定。
  • -C :这是一个由分号分隔的构建配置(调试、发布)列表,用于生成包(对于多配置构建系统生成器,这是必需的)。
  • -D =: 这个选项会覆盖CPackConfig.cmake文件中设置的变量,以为准。
  • --config : 这是你应该使用的配置文件,而不是默认的CPackConfig.cmake
  • --verbose, -V: 提供详细输出。
  • -P : 覆盖包名称。
  • -R : 覆盖包版本。
  • --vendor : 覆盖包供应商。
  • -B : 为cpack指定输出目录(默认情况下,这将是目前的工作目录)。

让我们尝试为我们的12-cpack输出生成包。我们将使用 ZIP、7Z 和 Debian 包生成器:

cpack -G "ZIP;7Z;DEB" -B packages

以下应该生成以下包:

  • CPackPackage-1.2.3-Linux.7z
  • CPackPackage-1.2.3-Linux.deb
  • CPackPackage-1.2.3-Linux.zip

在这种格式中,二进制包准备好发布在我们项目的网站上,在 GitHub 发行版中,或发送到包仓库,供最终用户享用。

摘要

在没有像 CMake 这样的工具的情况下,以跨平台方式编写安装脚本是一项极其复杂的任务。虽然设置还需要一点工作,但它是一个更加流畅的过程,紧密地与本书到目前为止使用的所有其他概念和技术相关联。

首先,我们学习了如何从项目中导出 CMake 目标,以便它们可以在不安装它们的情况下被其他项目消费。然后,我们学习了如何安装已经为此目的配置好的项目。

在那之后,我们开始探索安装的基础知识,从最重要的主题开始:安装 CMake 目标。我们现在知道 CMake 如何处理各种工件类型的不同目的地以及如何处理 somewhat special 的公共头文件。为了在较低级别管理这些安装步骤,我们讨论了install()命令的其他模式,包括安装文件、程序和目录以及在安装过程中调用脚本。

在解释了如何编码安装步骤之后,我们学习了 CMake 的可重用包。具体来说,我们学习了如何使项目中的目标可移动,以便包可以在用户希望安装的任何地方进行安装。然后,我们专注于形成一个完全定义的包,它可以通过find_package()被其他项目消费,这需要准备目标导出文件、配置文件以及版本文件

认识到不同用户可能需要我们包的不同部分,我们发现了如何将工件和动作分组在安装组件中,以及它们与 CMake 包组件的区别。

最后,我们提到了 CPack,并学习了如何准备基本的二进制包,以使用预编译的形式分发我们的软件。

要完全掌握安装和打包的所有细节和复杂性还有很长的路要走,但本章为我们提供了坚实的基础,以处理最常见的情况并自信地进一步探索它们。

在下一章中,我们将把我们到目前为止所学的所有内容付诸实践,通过创建一个连贯、专业的项目。

进一步阅读

要了解更多关于本章涵盖的主题,请查看以下资源:

首先,我们学习了如何从项目中导出 CMake 目标,以便它们可以在不安装它们的情况下被其他项目消费。然后,我们学习了如何安装已经为此目的配置好的项目。

在那之后,我们开始探索安装的基础知识,从最重要的主题开始:安装 CMake 目标。我们现在知道 CMake 如何处理各种工件类型的不同目的地以及如何处理 somewhat special 的公共头文件。为了在较低级别管理这些安装步骤,我们讨论了install()命令的其他模式,包括安装文件、程序和目录以及在安装过程中调用脚本。

在解释了如何编码安装步骤之后,我们学习了 CMake 的可重用包。具体来说,我们学习了如何使项目中的目标可移动,以便包可以在用户希望安装的任何地方进行安装。然后,我们专注于形成一个完全定义的包,它可以通过find_package()被其他项目消费,这需要准备目标导出文件、配置文件以及版本文件

认识到不同用户可能需要我们包的不同部分,我们发现了如何将工件和动作分组在安装组件中,以及它们与 CMake 包组件的区别。

最后,我们提到了 CPack,并学习了如何准备基本的二进制包,以使用预编译的形式分发我们的软件。

要完全掌握安装和打包的所有细节和复杂性还有很长的路要走,但本章为我们提供了坚实的基础,以处理最常见的情况并自信地进一步探索它们。

在下一章中,我们将把我们到目前为止所学的所有内容付诸实践,通过创建一个连贯、专业的项目。

进一步阅读

要了解更多关于本章涵盖的主题,请查看以下资源:

相关文章
|
10天前
|
C++
Clion CMake C/C++程序输出乱码
Clion CMake C/C++程序输出乱码
10 0
|
12天前
|
存储 算法 编译器
C++ 函数式编程教程
C++ 函数式编程学习
|
12天前
|
存储 编译器 开发工具
C++语言教程分享
C++语言教程分享
|
12天前
|
存储 编译器 C++
|
1月前
|
C++ 存储 索引
面向 C++ 的现代 CMake 教程(一)(5)
面向 C++ 的现代 CMake 教程(一)
46 0
|
1月前
|
缓存 存储 C++
面向 C++ 的现代 CMake 教程(一)(4)
面向 C++ 的现代 CMake 教程(一)
46 0
|
1月前
|
C++ 缓存 存储
面向 C++ 的现代 CMake 教程(一)(3)
面向 C++ 的现代 CMake 教程(一)
45 0
|
1月前
|
缓存 C++ Windows
面向 C++ 的现代 CMake 教程(一)(2)
面向 C++ 的现代 CMake 教程(一)
58 0
|
1月前
|
C++ 容器 Docker
面向 C++ 的现代 CMake 教程(一)(1)
面向 C++ 的现代 CMake 教程(一)
71 0
|
3天前
|
编译器 C++
【C++】string类的使用④(字符串操作String operations )
这篇博客探讨了C++ STL中`std::string`的几个关键操作,如`c_str()`和`data()`,它们分别返回指向字符串的const char*指针,前者保证以&#39;\0&#39;结尾,后者不保证。`get_allocator()`返回内存分配器,通常不直接使用。`copy()`函数用于将字符串部分复制到字符数组,不添加&#39;\0&#39;。`find()`和`rfind()`用于向前和向后搜索子串或字符。`npos`是string类中的一个常量,表示找不到匹配项时的返回值。博客通过实例展示了这些函数的用法。