目录
第二部分:实用 CMake (Practical CMake – Getting Your Hands Dirty with CMake)
2.0 第二部分概括及章节内容(Part II Summary and Chapter Contents)
2.1 打包、部署和安装 CMake 项目(Packaging, Deploying, and Installing a CMake Project)
2.1.1 本章介绍(Introduction to this chapter)
2.1.2 使 CMake 目标可安装(Making CMake targets installable)
2.1.3 安装文件和目录(Installing files and directories)
2.1.4 install() 命令的其他常用参数(Other common parameters of the install() command)
2.1.5 提供配置信息使其他人使用您的项目(Supplying configuration information for others using your project)
2.1.6 使用 CPack 创建可安装包(Creating an installable package with CPack)
2.1.7 总结及问题(Summary and Questions)
1.引言
在这一部分中,您将能够以适合绝大多数用例的方式使用 CMake 设置软件项目。第 4 章和第 5 章将介绍安装和打包项目、依赖管理以及包管理器的使用等方面。第 6、7 和 8 章将介绍将外部工具集成到 CMake 项目中以创建文档、确保代码质量或执行构建软件所需的几乎任何任意任务。第 9 章和第 10 章将介绍为构建项目创建可重用的环境,以及处理来自分布式存储库的大项目。最后,第 11 章将向您展示如何使用 CMake 进行模糊测试。
2.章节内容
• 第 4 章,打包、部署和安装 CMake 项目
• 第 5 章,集成第三方库和依赖管理
• 第 6 章,自动生成文档
• 第 7 章,将代码质量工具与 CMake 无缝集成
• 第 8 章,使用 CMake 执行自定义任务
• 第 9 章,创建可重现的构建环境
• 第 10 章,在超级构建中处理大型项目和分布式存储库
• 第 11 章,使用 CMake 进行自动模糊测试
1.本章主要主题
构建一个软件项目只是工程的一半。另一半是关于向您的导师或者老板交付和展示软件。您的导师或者老板是任何项目的最大利益相关者,即使您正在为自己编写一个爱好项目。
这些消费者可能有各种各样的经历和目的。他们可能是开发人员、包维护人员、高级用户或普通的用户。了解他们的用例、场景和要求很重要。由于软件大多是抽象的,让我们假设您的项目是烤豆——它可能很美味,在工厂里可能闻起来很香,但包装不当会缩短其保质期,使其难以运输或消费。这将使您的产品不太可能被消费者所需要。即使您的产品一开始就很棒,但消费者不会注意到它,因为由于包装不好,他们对您的产品的体验很糟糕。因此,重要的是从一开始就做好这些事情。请记住,快乐的消费者会给产品带来价值。 CMake 具有良好的内部支持和工具,使安装和打包变得容易。这样做的好处是 CMake 利用现有的项目代码来做这些事情。因此,使项目可安装或打包项目不会导致高昂的维护成本。在本章中,我们将学习如何利用 CMake 现有的安装和打包部署功能。
在本章中,我们将介绍以下主题:
• 使 CMake 目标可安装
• 为使用您的项目的其他人提供配置信息
• 使用 CPack 创建可安装包
2.本章资源文件与先决知识
在深入本章之前,您应该很好地掌握 CMake 中的目标(在第 1 章,Kickstarting CMake 和第 3 章,创建 CMake 项目中进行了简要介绍)。
本章将建立在这些知识之上。
请从本章的 GitHub 存储库中获取本章的示例,网址为: https://github.com/PacktPublishing/CMake-Best-Practices。本章的示例内容将在 chapter_4/ 子文件夹中提供。
在项目中支持部署的最原始方式是使其可安装。此处的可安装一词并不是指安装软件的预制包。相反,最终用户仍然需要获取项目的源代码并从头开始构建它。可安装项目具有额外的构建系统代码,用于在系统上安装运行时或开发工件。构建系统将在此处执行安装操作,前提是它有正确的操作说明。由于我们使用 CMake 生成构建系统文件,因此 CMake 必须生成相关的安装代码。在本节中,我们将深入探讨如何指示 CMake 为 CMake 目标生成此类代码的基础知识。
1. install() 命令
install(...) 命令是一个内置的 CMake 命令,它允许您生成用于安装目标、文件、目录等的构建系统指令。CMake 不会生成安装指令,除非明确告知这样做。因此,安装的内容始终在您的控制之下。我们来看看它的基本用法。
2. 安装 CMake 目标
Ⅰ.TARGETS 参数简介
要使 CMake 目标可安装,必须使用至少一个参数指定 TARGETS 参数。此用法的命令签名如下:
install(TARGETS <target>... [...])
TARGETS 参数表示 install 将接受一组 CMake 目标来为其生成安装代码。
在这种形式中,只会安装目标的输出工件。目标最常见的输出工件定义如下:
• ARCHIVE(静态库、DLL 导入库和链接器导入文件):
• macOS 中标记为 FRAMEWORK 的目标除外
• LIBRARY(共享库):
• macOS 中标记为 FRAMEWORK 的目标除外
• DLL 除外(在 Windows 中)
• RUNTIME(可执行文件和 DLL):
• macOS 中标记为 MACOSX_BUNDLE 的目标除外
在使目标可安装后,CMake 将生成必要的安装代码来安装将为目标生成的输出工件。
Ⅱ.用cmake安装可执行文件
为了说明这一点,让我们制作一个可一起安装的基本可执行目标。要查看运行中的 install(...) 命令,让我们检查第 4 章示例 1 的 CMakeLists.txt 文件,该文件可以在 chapter4/ex01_executable 文件夹中找到:
cmake_minimum_required(VERSION 3.21) project( ch4_ex01_executable VERSION 1.0 DESCRIPTION "Chapter 4 Example 01, installable executable project" LANGUAGES CXX) # Define an executable target named `ch4_ex01_executable` add_executable(ch4_ex01_executable) # Specify source files for target named `ch4_ex01_executable` target_sources(ch4_ex01_executable PRIVATE src/main.cpp) # Request compile features for target named `ch4_ex01_executable`. # Requested `cxx_std_11` feature determines the minimum C++ standard required # to build this target. It's PRIVATE, since it is a build-time requirement only. target_compile_features(ch4_ex01_executable PRIVATE cxx_std_11) # Make executable target `ch4_ex01_executable` installable. As mentioned before # in Chapter 4 content, this will only install the output artifacts produced by the target. install(TARGETS ch4_ex01_executable)经过前三章的学习,您应该读这样的cmakelists.txt文件像吃小菜一样吧!
在前面的代码中,定义了一个名为 ch4_ex01_executable 的可执行目标,并在随后的两行中填充了它的属性。最后一行 install(...) 是我们感兴趣的行。它告诉 CMake 为 ch4_ex01_executable 创建所需的安装代码。要检查 ch4_ex01_executable 是否可以安装,让我们构建项目并通过 CLI 安装它:
ch4_ex02_static
Ⅲ.让我们检查一下 cmake --install 命令做了什么及安装位置
在上述输出中,我们可以看到 ch4_ex01_executable 目标的输出工件——即安装了 ch4_ex01_executable 二进制文件。由于这是 ch4_ex01_executable 目标具有的唯一输出工件,我们可以得出结论,我们的目标确实可以安装。
请注意,ch4_ex01_executable 并未直接安装在 /tmp/install 测试(前缀)目录中。相反,安装命令将它放在 bin/ 子目录中。这是因为 CMake 很聪明地知道应该把什么样的工件放在哪里。在传统的 UNIX 系统中,二进制文件进入 /usr/bin,而库文件进入 /usr/lib。
CMake 知道 add_executable() 命令会生成一个可执行的二进制工件并将其放入 /bin 子目录中。这些目录默认由 CMake 提供,具体取决于目标类型。提供默认安装路径信息的 CMake 模块称为 GNUInstallDirs 模块。 GNUInstallDirs 模块在包含时定义了各种 CMAKE_INSTALL_ 路径。下表显示了目标的默认安装目录:
要覆盖内置默认值,在 install(...) 命令中需要一个额外的 <TARGET_TYPE> DESTINATION 参数。为了说明这一点,让我们尝试将默认的 RUNTIME 安装目录更改为 qbin 而不是 bin。这样做只需要我们对原来的 install(...) 命令做一个小的修改:
cmake_minimum_required(VERSION 3.21) project( ch4_ex01_executable VERSION 1.0 DESCRIPTION "Chapter 4 Example 01, installable executable project" LANGUAGES CXX) # Define an executable target named `ch4_ex01_executable` add_executable(ch4_ex01_executable) # Specify source files for target named `ch4_ex01_executable` target_sources(ch4_ex01_executable PRIVATE src/main.cpp) # Request compile features for target named `ch4_ex01_executable`. # Requested `cxx_std_11` feature determines the minimum C++ standard required # to build this target. It's PRIVATE, since it is a build-time requirement only. target_compile_features(ch4_ex01_executable PRIVATE cxx_std_11) # Make executable target `ch4_ex01_executable` installable. As mentioned before # in Chapter 4 content, this will only install the output artifacts produced by the target. install(TARGETS ch4_ex01_executable RUNTIME DESTINATION qbin)进行此更改后,我们可以重新运行配置、构建和安装命令。
我们可以通过检查 cmake --install 命令的输出来确认 RUNTIME 目标已更改。与第一次不同,我们可以观察到 ch4_ex01_executable 二进制文件被放入 qbin 而不是默认的(bin)目录:
Ⅳ.安装静态库
现在,让我们看另一个例子。这次我们将安装一个 STATIC 库。让我们看一下第 4 章示例 2 的 CMakeLists.txt 文件,该文件可以在 chapter4/ex02_static 文件夹中找到。让我们开始检查文件:
cmake_minimum_required(VERSION 3.21) project( ch4_ex02_static VERSION 1.0 DESCRIPTION "Chapter 4 Example 02, installable static library project" LANGUAGES CXX) # Define an executable target named `ch4_ex02_static` add_library(ch4_ex02_static STATIC) # Specify source files for target named `ch4_ex02_static` target_sources(ch4_ex02_static PRIVATE src/lib.cpp) # Specify the include directories for the target named `ch4_ex02_static` 为名为 `ch4_ex02_static` 的目标指定包含目录 target_include_directories(ch4_ex02_static PUBLIC include) # Request compile features for target named `ch4_ex02_static`. # Requested `cxx_std_11` feature determines the minimum C++ standard required # to build this target. It's PRIVATE, since it is a build-time requirement only. target_compile_features(ch4_ex02_static PRIVATE cxx_std_11) # Defines the ${CMAKE_INSTALL_INCLUDEDIR} variable. include(GNUInstallDirs) # Make executable target `ch4_ex02_static` installable. As mentioned before in Chapter 4 content, this will only install the output artifacts produced by the target. install(TARGETS ch4_ex02_static) # Install the header files. Since header files are not listed as output artifacts, they have to be installed separately. install ( DIRECTORY include/ DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}" )我们来解释一下代码:
这个项目名称为ch4_ex02_static,要创立一个名字为ch4_ex02_static的静态库。这个库功能实现的源文件在src/lib.cpp中。名为 `ch4_ex02_static` 的目标指定包含目录为include文件夹。下面的就是安装命令了。然后使可执行目标 `ch4_ex02_static` 可安装。正如前面第 4 章内容中提到的,这只会安装目标生成的输出工件。再安装头文件。由于头文件未作为输出工件列出,因此必须单独安装它们。
您可能已经注意到,它与我们之前的示例有些不同。首先,有一个带有 DIRECTORY 参数的附加 install(...) 命令。这是使静态库的头文件可安装所必需的。这样做的原因是 CMake 不会安装任何不是输出工件的文件,并且 STATIC 库目标仅生成二进制文件作为输出工件。头文件不被视为输出工件,应单独安装。
DIRECTORY 参数中的尾部斜杠导致 CMake 复制文件夹的内容,而不是按名称复制文件夹。库文件所在目录:发现静态库被成功安装。这是因为 CMake 很聪明地知道应该把什么样的工件放在哪里。在传统的 UNIX 系统中,二进制文件进入 /usr/bin,而库文件进入 /usr/lib。(前文提过)
/usr/local/include所在目录树:我们发现通过cmakelists.txt最后一行指令,将安装路径下的/include所有文件全部拷贝到了 /usr/local/include目录下。
正如我们在上一节中看到的,我们要安装的东西并不总是目标输出工件的一部分。它们可能是目标的运行时依赖项,例如图像、资产、资源文件、脚本和配置文件。 CMake 提供了 install(FILES...) 和 install(DIRECTORY...) 命令来安装任何特定的文件或目录。让我们从安装文件开始。
1.安装文件:命令简介
install(FILES...) 命令接受一个或多个文件作为参数。它还需要一个额外的 TYPE 或 DESTINATION 参数。这两个参数都用于确定指定文件的目标目录。
TYPE 参数用于指示哪些文件将使用该文件类型的默认路径作为安装目录。可以通过设置相关的 GNUInstallDirs 变量来覆盖默认值。下表显示了有效的 TYPE 值及其目录映射:
如果您不想使用 TYPE 参数,可以使用 DESTINATION 参数。它允许您为 install(...) 命令中的指定文件提供自定义目标。
install(FILES...) 的另一种形式是 install(PROGRAMS.CMAKE_INSTALL_PREFIX..),它与 install(FILES...) 相同,只是它还设置已安装文件的 OWNER_EXECUTE、GROUP_EXECUTE 和 WORLD_EXECUTE 权限。这对于必须由最终用户执行的二进制文件或脚本文件是有意义的。
2.安装文件:示例
为了理解 install(FILES|PROGRAMS...),我们来看一个例子。我们要研究的示例是第 4 章示例 3 (chapter_4/ex03_file)。它本质上包含三个文件:chapter4_greeter_content(txt文件)、chapter4_greeter.py(python文件) 和 CMakeLists.txt。首先,我们看一下它的 CMakeLists.txt 文件:
cmake_minimum_required(VERSION 3.21) project( ch4_ex03_file VERSION 1.0 DESCRIPTION "Chapter 4 Example 03, install file/program" LANGUAGES CXX) include(GNUInstallDirs) install(FILES "${CMAKE_CURRENT_LIST_DIR}/chapter4_greeter_content" DESTINATION "${CMAKE_INSTALL_BINDIR}") install(PROGRAMS "${CMAKE_CURRENT_LIST_DIR}/chapter4_greeter.py" DESTINATION "${CMAKE_INSTALL_BINDIR}" RENAME chapter4_greeter)让我们说明我们所看到的:
在第一个 install(...) 命令中,我们告诉 CMake 将 chapter4_greeter_content 文件(CMAKE_CURRENT_LIST_DIR 指向当前工作路径)安装在系统默认 BIN 目录中(CMAKE_INSTALL_BINDIR 指向bin目录)的当前 CMakeLists.txt 目录 (chapter4/ex03_file) 中。
在第二个 install(...) 命令中,我们告诉 CMake 将 chapter4_greeter.py 安装在名为 chapter4_greeter 的默认 BIN 目录中。
RENAME 参数仅对单文件 install(...) 调用有效。使用这些 install(...) 指令,CMake 应该在 ${CMAKE_INSTALL_PREFIX}/bin 目录中安装 chapter4_greeter.py 和 chapter4_greeter_content 文件。让我们通过 CLI 构建和安装项目:
让我们看看 cmake --install 命令做了什么:
上述输出确认 CMake 为 chapter4_greeter_content 和 chapter4_greeter.py 文件生成了所需的安装代码。最后,让我们检查一下 chapter4_greeter 文件是否可以执行,因为我们使用了 PROGRAMS 参数来安装它:
至此,我们结束了 install(FILES|PROGRAMS...) 部分。让我们继续学习安装目录。
2.安装 directories
Ⅰ.全部安装
install(DIRECTORY...) 命令对于安装目录很有用。目录的结构将按原样复制到目标。目录可以作为一个整体安装,也可以有选择地安装。让我们从最基本的目录安装示例开始:
install(DIRECTORY dir1 dir2 dir3 TYPE LOCALSTATE)
将在 ${CMAKE_INSTALL_PREFIX}/var 目录中安装 dir1 和 dir2 目录,以及它们的所有子文件夹和文件。有时,不希望安装文件夹的全部内容。幸运的是,CMake 允许 install 命令根据通配模式和正则表达式包含或排除目录内容。下面让我们选择性地安装dir1、dir2和dir3。
我们先来运行这一次的结果看一看效果:将dir1 dir2 dir3 全部文件全部安装了。
我们看/usr/local/var的目录树:
再看/home/liuhongwei/桌面/chapter04/ex04_directory除了build以外的目录树:
Ⅱ.部分安装
cmake_minimum_required(VERSION 3.21) project( ch4_ex04_directory VERSION 1.0 DESCRIPTION "Chapter 4 Example 04, install directory" LANGUAGES CXX) include(GNUInstallDirs) # Defines the ${CMAKE_INSTALL_INCLUDEDIR} variable. install(DIRECTORY dir1 DESTINATION ${CMAKE_INSTALL_LOCALSTATEDIR} FILES_MATCHING PATTERN "*.x") install(DIRECTORY dir2 DESTINATION ${CMAKE_INSTALL_LOCALSTATEDIR} FILES_MATCHING PATTERN "*.hpp" EXCLUDE PATTERN "*") install(DIRECTORY dir3 DESTINATION ${CMAKE_INSTALL_LOCALSTATEDIR} PATTERN "bin" EXCLUDE)在这个示例中,我们使用 FILES_MATCHING 参数来定义文件选择的标准。 FILES_MATCHING 后面可以跟 PATTERN 或 REGEX 参数。
PATTERN 允许您定义通配模式,而 REGEX 允许您定义正则表达式。默认情况下,这些表达式用于包含文件。如果要排除符合条件的文件,可以将 EXCLUDE 参数附加到模式。
请注意,由于 FILES_MATCHING 参数,这些过滤器不适用于子目录名称。我们还在最后一个 install(...) 命令中使用了 PATTERN,没有 FILES_MATCHING 前缀,这允许我们过滤子目录而不是文件。
这次只安装dir1中扩展名为.x的文件,dir2中没有.hpp扩展名的文件,以及dir3中除了bin文件夹之外的所有内容。此示例在第 4 章示例 4 中位于 chapter4/ex04_directory 文件夹中。让我们编译并安装它,看看它是否正确:
cmake --install 的输出应如下所示:
liuhongwei@liuhongwei-Lenovo-Legion-R9000P2021H:~/桌面/chapter04/ex04_directory$ sudo cmake --install ./build/ [sudo] liuhongwei 的密码: -- Install configuration: "" -- Installing: /usr/local/var/dir1 -- Installing: /usr/local/var/dir1/asset1.x -- Installing: /usr/local/var/dir1/subdir -- Installing: /usr/local/var/dir1/subdir/asset5.x -- Installing: /usr/local/var/dir2 -- Installing: /usr/local/var/dir2/chapter4_hello.dat -- Installing: /usr/local/var/dir3 -- Installing: /usr/local/var/dir3/asset4FILES_MATCHING 不能在 PATTERN 或 REGEX 之后使用,但反之亦然。
在输出中,我们可以看到只有扩展名为 .x 的文件是从 dir1 中挑选出来的。这是因为第一个 install(...) 命令中的 FILES_MATCHING PATTERN "*.x" 参数导致asset2 文件无法安装。另外,请注意 dir2/chapter4_hello.dat 文件已安装,而 dir2/chapter4_hello.hpp 文件已被跳过。这是由于第二个 install(...) 命令中的 FILES_MATCHING PATTERN "*.hpp" EXCLUDE PATTERN "*" 参数造成的。最后,我们可以看到安装了 dir3/asset4 文件,并且完全跳过了 dir3/bin 目录,因为在最后一个 install(...) 命令中指定了 PATTERN "bin" EXCLUDE 参数。
/home/liuhongwei/桌面/chapter04/ex04_directory目录树如下:
/usr/local/var目录树如下:
我们已经介绍了 install(...) 命令的基础知识。让我们继续 install(...) 命令的其他常用参数。
正如我们所见, install() 命令的第一个参数指示要安装的内容。还有其他参数允许我们自定义安装。一起来看看一些常用的参数:
1.目的地参数
此参数允许您为 install(...) 命令中指定的文件指定目标目录。目录路径可以是相对的或绝对的。相对路径将相对于 CMAKE_INSTALL_PREFIX 变量。
建议使用相对路径使安装可重定位。此外,使用相对路径进行打包也很重要,因为 cpack 要求安装路径是相对的。使用以相关 GNUInstallDirs 变量开头的路径是一种很好的做法,这样包维护人员可以在需要时覆盖安装目标。 DESTINATION 参数可以与 TARGETS、FILES、IMPORTED_RUNTIME_ARTIFACTS、EXPORT 和 DIRECTORY 安装类型一起使用。
2.权限参数
此参数允许您在支持的平台上更改已安装文件的权限。可用权限为 OWNER_READ、OWNER_WRITE、OWNER_EXECUTE、GROUP_READ、GROUP_WRITE、GROUP_EXECUTE、WORLD_READ、WORLD_WRITE、WORLD_EXECUTE、SETUID 和 SETGID。 PERMISSIONS 参数可以与 TARGETS、FILES、IMPORTED_RUNTIME_ARTIFACTS、EXPORT 和 DIRECTORY 安装类型一起使用。
3.配置参数
这允许您在指定特定构建配置时限制要应用的一组参数。
4.可选参数
此参数使要安装的文件是可选的,以便在文件不存在时安装不会失败。 OPTIONAL 参数可以与 TARGETS、FILES、IMPORTED_RUNTIME_ARTIFACTS 和 DIRECTORY 安装类型一起使用。
在本节中,我们学习了如何使目标、文件和目录可安装。在下一节中,我们将学习如何生成配置信息,以便我们可以将 CMake 项目直接导入另一个 CMake 项目。
在上一节中,我们学习了如何使我们的项目可安装,以便其他人可以通过将其安装在他们的系统上来使用我们的项目。但有时,交付工件是不够的。例如,如果您要交付一个库,那么将其导入项目(尤其是 CMake 项目)也必须很容易。在本节中,我们将学习如何简化其他 CMake 项目的导入过程 。。
鉴于要导入的项目具有正确的配置文件,有多种导入库的便捷方法。这样做的主要方法之一是利用 find_package() 方法(我们将在第 5 章,集成第三方库和依赖管理中介绍)。如果您有消费者在他们的工作流程中使用 CMake,如果他们可以编写 find_package(your_ project_name) 并开始使用您的代码,他们会很高兴。在本节中,我们将学习如何生成所需的配置文件以使 find_package() 为您的项目工作。 CMake 使用依赖项的首选方式是通过包。包传达基于 CMake 的构建系统的依赖信息。包的形式可以是配置文件包、查找模块包或 pkg-config 包。所有的包类型都可以通过 find_package() 找到和使用。为了篇幅和简单起见,我们将在本节中仅介绍配置文件包。其余的方法大多是缺少配置文件的解决方法,所以最好留在过去。让我们开始发现配置文件包。
1.进入 CMake 包的世界——配置文件包
Ⅰ.包配置文件
配置文件包基于包含包内容信息的配置文件。此信息指示包的内容位置,因此 CMake 读取此文件并使用包。因此,仅发现包配置文件就足以使用包。
有两种类型的配置文件——一个包配置文件和一个可选的包版本文件。这两个文件都必须具有特定的命名约定。
包配置文件可以命名为 <ProjectName>Config.cmake 或 <projectname>-config.cmake,取决于个人喜好。
CMake 将在 find_package(ProjectName)/find_package(projectname) 调用中选择这两种表示法。包配置文件的内容类似于以下内容:
set(Foo_INCLUDE_DIRS ${PREFIX}/include/foo-1.2) set(Foo_LIBRARIES ${PREFIX}/lib/foo-1.2/libfoo.a)${PREFIX} 是项目的安装前缀。它是一个变量,因为安装前缀可以根据系统类型更改,也可以由用户更改。
Ⅱ.包版本文件
与包配置文件类似,包版本文件也可以命名为 <ProjectName>ConfigVersion.cmake 或 <projectname>-config-version.cmake。
CMake 期望包配置和包版本文件出现在 find_package(...) 搜索路径中。您可以在 CMake 的帮助下创建这些文件。 <CMAKE_PREFIX_PATH>/cmake 目录是 find_package(...) 在搜索包时查找的众多位置之一。在我们的示例中,我们将把我们的 Config-file 包配置文件放到这个文件夹中。
要创建配置文件包,我们需要学习一些额外的东西,例如导出目标和 CmakePackageConfigHelpers 模块。要了解这些内容,让我们开始深入研究一个实际示例。
Ⅲ.案例
我们将按照第 4 章的示例 5 学习如何构建 CMake 项目以使其成为配置文件包。它位于 chapter4/ex05_config_file_package 文件夹中。让我们首先检查 chapter4/ex05_config_file_package 目录中的 CMakeLists.txt 文件(注释和项目命令已被省略以支持空间;另外,请注意,与主题无关的行将不予提及):
include(GNUInstallDirs) #这是配置文件包 cmake 文件的默认安装目录,它是相对于安装前缀的。 set(ch4_ex05_lib_INSTALL_CMAKEDIR cmake CACHE PATH "Installation directory for config-file package cmake files") /*…*/CMakeLists.txt 文件与 chapter4/ex02_static 非常相似。这是因为它是相同的示例,只是它支持配置文件打包。第一行 include(GNUInstallDirs) 用于包含 GNUInstallDirs 模块。这提供了 CMAKE_INSTALL_INCLUDEDIR 变量,稍后将使用该变量。 set(ch4_ex05_lib_INSTALL_CMAKEDIR...) 是用户自定义变量,用于设置config-file打包配置文件的目标安装目录。它是应该在 install(...) 指令中使用的相对路径,因此它隐含地相对于 CMAKE_INSTALL_PREFIX:
/*…*/ #为名为 `ch4_ex05_lib` 的目标指定包含目录 target_include_directories(ch4_ex05_lib PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include> ) target_compile_features(ch4_ex05_lib PUBLIC cxx_std_11) /*…*/target_include_directories(...) 调用与通常的调用完全不同。它使用生成器表达式来区分构建时包含目录和安装时包含目录,因为当目标导入另一个项目时,构建时包含路径将不存在。以下命令集将使目标可安装:
/*…*/ install(TARGETS ch4_ex05_lib EXPORT ch4_ex05_lib_export INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} ) install ( DIRECTORY ${PROJECT_SOURCE_DIR}/include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} ) /*…*/install(TARGETS...) 也与平时有所不同。它包含一个额外的 EXPORT 参数。
此 EXPORT 参数用于从给定的 install(...) 目标创建导出名称。然后可以使用此导出名称导出这些目标。使用 INCLUDES DESTINATION 参数指定的路径将用于填充导出目标的 INTERFACE_INCLUDE_DIRECTORIES 属性,并将自动以安装前缀路径作为前缀。这里,install(DIRECTORY...) 命令用于安装目标的头文件,位于 ${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_INCLUDEDIR} 目录中的 ${PROJECT_SOURCE_DIR}/include/。 ${CMAKE_INSTALL_INCLUDEDIR} 变量用于使消费者能够覆盖此安装的包含目录。现在,让我们从前面示例中创建的导出名称创建一个导出文件:
/*…*/ install(EXPORT ch4_ex05_lib_export FILE ch4_ex05_lib-config.cmake NAMESPACE ch4_ex05_lib:: DESTINATION ${ch4_ex05_lib_INSTALL_CMAKEDIR} ) /*…*/install(EXPORT...) 可能是这个文件中最重要的一段代码。它是执行实际目标导出的代码。它会生成一个 CMake 文件,其中包含给定导出名称中的所有导出目标。
EXPORT 参数接受现有的导出名称来执行导出。它指的是我们在前面的 install(TARGETS...) 调用中创建的 ch4_ex05_lib_export 导出名称。 FILE 参数用于确定导出的文件名,并设置为 ch4_ex05_lib-config.cmake。 NAMESPACE 参数用于为所有导出的目标添加一个命名空间的前缀。这允许您在一个公共命名空间下连接所有导出的目标,并避免与具有相似目标名称的包发生冲突。最后,DESTINATION 参数确定生成的导出文件的安装路径。这设置为 ${ch4_ex05_lib_INSTALL_CMAKEDIR} 以允许 find_package() 发现它。
由于我们没有提供除导出目标之外的任何附加功能,因此导出文件的名称是 ch4_ex05_lib-config.cmake。这是此包所需的包配置文件名。我们这样做是因为示例项目不需要首先满足任何额外的依赖项,并且可以直接按原样导入。如果需要任何额外的操作,建议有一个满足这些依赖关系的中间包配置文件,并在之后包含导出的文件。使用 install(EXPORT...) 命令,我们获得了 ch4_ex05_lib-config.cmake 文件。这意味着我们的目标可以通过 find_package(..) 来使用。要获得对 find_package(...) 的完全支持,还需要一个额外的步骤,即获取 ch4_ex05_lib-config-version.cmake 文件:
/*…*/ include(CMakePackageConfigHelpers) write_basic_package_version_file( "ch4_ex05_lib-config-version.cmake" # Package compatibility strategy. SameMajorVersion is essentially 'semantic versioning'. COMPATIBILITY SameMajorVersion ) install(FILES "${CMAKE_CURRENT_BINARY_DIR}/ch4_ex05_lib-config-version. cmake" DESTINATION "${ch4_ex05_lib_INSTALL_CMAKEDIR}" ) /* end of the file */在最后几行中,您可以找到生成和安装 ch4_ex05_lib-config-version.cmake 文件所需的代码。
使用 include(CMakePackageConfigHelpers) 行,CMakePackageConfigHelpers 模块被导入,该模块提供 write_basic_package_version_file(…) 函数。 write_basic_package_version_file(…) 用于根据给定的参数自动生成包版本文件。第一个位置参数是输出的文件名。 VERSION 参数用于指定我们以major.minor.patch 形式生成的包的版本。选择退出以允许 write_basic_package_version_file 自动从项目版本中获取它。 COMPATIBILITY 参数允许您指定兼容性策略,具体取决于版本的值。 SameMajorVersion 表示此包与具有相同主版本值的任何版本兼容。其他可能的值是 AnyNewerVersion、SameMinorVersion 和 ExactVersion。
现在,让我们测试一下这是否有效。要测试包配置,我们必须定期安装项目:
cmake –S . -B ./build cmake --build ./build cmake --install ./build --prefix /tmp/install-testcmake --install 命令的输出应如下所示:
liuhongwei@liuhongwei-Lenovo-Legion-R9000P2021H:~/桌面/chapter04/ex05_config_fil e_package$ cmake --install ./build/ --prefix /tmp/install-test -- Install configuration: "" -- Installing: /tmp/install-test/lib/libch4_ex05_lib.a -- Installing: /tmp/install-test/include -- Installing: /tmp/install-test/include/chapter4 -- Installing: /tmp/install-test/include/chapter4/ex05 -- Installing: /tmp/install-test/include/chapter4/ex05/lib.hpp -- Installing: /tmp/install-test/cmake/ch4_ex05_lib-config.cmake -- Installing: /tmp/install-test/cmake/ch4_ex05_lib-config-noconfig.cmake -- Installing: /tmp/install-test/cmake/ch4_ex05_lib-config-version.cmake在这里,我们可以看到我们的包配置文件已经成功安装在/tmp/install-test/cmake目录下。我将把检查这些文件的内容留给你作为练习。所以,我们手里有一个消耗品包。让我们换边尝试食用我们刚出炉的包装。为此,我们将查看 chapter4/ex05_consumer 示例。让我们一起检查 CMakeLists.txt 文件:
if(NOT PROJECT_IS_TOP_LEVEL) message(FATAL_ERROR "The chapter-4, ex05_consumer project is intended to be a standalone, top-level project. Do not include this directory.") endif() find_package(ch4_ex05_lib 1 CONFIG REQUIRED) add_executable(ch4_ex05_consumer src/main.cpp) target_compile_features(ch4_ex05_consumer PRIVATE cxx_std_11) target_link_libraries(ch4_ex05_consumer ch4_ex05_lib::ch4_ex05_ lib)在前几行中,我们可以看到项目是否是顶级项目的验证。由于此示例旨在成为外部应用程序,因此不应成为根示例项目的一部分。因此,我们可以保证我们将使用包导出的目标,而不是根项目的目标。根项目也不包括 ex05_consumer 文件夹。接下来,有一个 find_package(…) 调用,其中 ch4_ex05_lib 作为包名给出。还明确要求包的主版本为 1; find_package(...) 必须只考虑 CONFIG 包,并且此 find_package(...) 调用中指定的包是必需的。在随后的行中,定义了一个名为 ch4_ex05_consumer 的常规可执行文件,它与 ch4_ex05_lib 命名空间 (ch4_ex05_lib::ch4_ex05_lib) 中的 ch4_ex05_lib 链接。 ch4_ex05_lib::ch4_ex05_lib 是我们在包中定义的实际目标。让我们看一下源文件,src/main.cpp:
#include <chapter4/ex05/lib.hpp> int main(void){ chapter4::ex05::greeter g; g.greet(); }这是一个简单的应用程序,它包含chapter4/ex05/lib.hpp,创建一个greeter 类的实例,并调用greet() 函数。让我们尝试编译并运行应用程序:
cd chapter_4/ex05_consumer cmake -S . -B build/ -DCMAKE_PREFIX_PATH:STRING=/tmp/installtest cmake --build build/ ./build/ch4_ex05_consumer由于我们使用自定义前缀 (/tmp/install-test) 安装了包,我们可以通过设置 CMAKE_PREFIX_PATH 变量来指示这一点。这会导致 find_package(...) 也在 /tmp/install-test 中搜索包。对于默认前缀安装,不需要此参数设置。我们应该看到臭名昭著的Hello, world!如果一切顺利,消息:
./build/ch4_ex05_consumer Hello, world!在这里,我们的消费者可以使用我们的小迎宾员,每个人都很开心。现在,让我们通过学习如何使用 CPack 打包来结束本节。
到目前为止,我们已经了解了 CMake 如何构建软件项目。虽然 CMake 是节目的明星,但 CMake 也有一些强大的朋友。是时候给大家介绍一下CMake的打包工具CPack了。
默认情况下,它随 CMake 安装一起提供。它允许您利用现有的 CMake 代码来生成特定于平台的安装和包。 CPack 在概念上类似于 CMake。它基于生成包而不是构建系统文件的生成器。下表显示了可用的 CPack 生成器类型,从 CPack 版本 3.21.3 开始:
CPack 使用 CMake 的安装机制来填充包的内容。 CPack 使用 CPackConfig.cmake 和 CPackSourceConfig.cmake 文件中存在的配置详细信息来生成包。这些文件可以手动填充,也可以由 CMake 在 CPack 模块的帮助下自动生成。在现有 CMake 项目上使用 CPack 就像包含 CPack 模块一样简单,因为该项目已经具有正确的 install(...) 命令。包含 CPack 模块会导致 CMake 生成 CPackConfig.cmake 和 CPackSourceConfig.cmake 文件,它们是打包项目所需的 CPack 配置。此外,额外的包目标将可用于构建步骤。此步骤将构建项目并运行 CPack 以便开始打包。当 CMake 或用户正确填充 CPack 配置文件时,可以使用 CPack。 CPack 模块允许您自定义打包过程。
可以设置大量的 CPack 变量。这些变量分为两组——公共变量和生成器特定变量。公共变量影响所有的包生成器,而特定于生成器的变量只影响特定类型的生成器。我们将检查最基本和最突出的变量,我们将主要处理常见变量。下表显示了我们将在示例中使用的最常见的 CPack 变量:
必须在包含 CPack 模块之前对变量进行任何更改。否则,将使用默认值。让我们通过一个示例来了解 CPack 的实际应用。我们将遵循第 4 章示例 6 (chapter4/ex06_pack) 示例。此示例被构造为独立项目,不是根示例项目的一部分。它是一个常规项目,有两个子目录,名为可执行文件和库。可执行目录的 CMakeLists.txt 文件如下所示:
add_executable(ch4_ex06_executable src/main.cpp) target_compile_features(ch4_ex06_executable PRIVATE cxx_std_11) target_link_libraries(ch4_ex06_executable PRIVATE ch4_ex06_ library) install(TARGETS ch4_ex06_executable)库目录的 CMakeLists.txt 文件如下所示:
add_library(ch4_ex06_library STATIC src/lib.cpp) target_compile_features(ch4_ex06_library PRIVATE cxx_std_11) target_include_directories(ch4_ex06_library PUBLIC include) set_target_properties(ch4_ex06_library PROPERTIES PUBLIC_HEADER include/chapter4/ex06/lib.hpp) include(GNUInstallDirs) # Defines the ${CMAKE_INSTALL_ INCLUDEDIR} variable. install(TARGETS ch4_ex06_library) install ( DIRECTORY ${PROJECT_SOURCE_DIR}/include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} )这些文件夹的 CMakeLists.txt 文件不包含任何异常。它们包含常规的、可安装的 CMake 目标,并且没有声明任何关于 CPack 的内容。让我们也看看顶层 CMakeLists.txt 文件:
cmake_minimum_required(VERSION 3.21) project( ch4_ex06_pack VERSION 1.0 DESCRIPTION "Chapter 4 Example 06, Packaging with CPack" LANGUAGES CXX) if(NOT PROJECT_IS_TOP_LEVEL) message(FATAL_ERROR "The chapter-4, ex06_pack project is intended to be a standalone, top-level project. Do not include this directory.") endif() add_subdirectory(executable) add_subdirectory(library) set(CPACK_PACKAGE_VENDOR "CTT Authors") set(CPACK_GENERATOR "DEB;RPM;TBZ2") set(CPACK_THREADS 0) set(CPACK_DEBIAN_PACKAGE_MAINTAINER "CTT Authors") include(CPack)顶级 CMakeLists.txt 文件几乎是一个常规的顶级 CMakeLists.txt 文件,除了最后四行。它设置三个与 CPack 相关的变量,然后包含 CPack 模块。这四行足以提供基本的 CPack 支持。 CPACK_PACKAGE_NAME 和 CPACK_PACKAGE_VERSION_* 变量未设置为让 CPack 从顶级项目的名称和版本参数中推断出它们。让我们配置项目,看看它是否有效:
cd chapter_4/ex06_pack cmake –S . -B build/配置项目后,CpackConfig.cmake 和 CpackConfigSource。 cmake 文件应该由 CPack 模块生成到 build/CPack* 目录。让我们检查它们是否存在:
$ ls build/CPack* build/CPackConfig.cmake build/CPackSourceConfig.cmake在这里,我们可以看到 CPack 配置文件是自动生成的。让我们构建它并尝试使用 CPack 打包项目:
cmake --build build/ cpack --config build/CPackConfig.cmake -B build/--config 参数是 CPack 命令的主要输入。 -B 参数覆盖 CPack 将其工件写入的默认包目录。让我们看看 CPack 的输出:
CPack: Create package using DEB /*…*/ CPack: - package: /home/user/workspace/personal/CMake-BestPractices/chapter_4/ex06_pack/build/ch4_ex06_pack-1.0-Linux.deb generated. CPack: Create package using RPM /*…*/ CPack: - package: /home/user/workspace/personal/CMake-BestPractices/chapter_4/ex06_pack/build/ch4_ex06_pack-1.0-Linux.rpm generated. CPack: Create package using TBZ2 /*…*/ CPack: - package: /home/user/workspace/personal/CMake-BestPractices/chapter_4/ex06_pack/build/ch4_ex06_pack-1.0-Linux.tar. bz2 generated.在这里,我们可以看到 CPack 使用 DEB、RPM 和 TBZ2 生成器分别生成了 ch4_ex06_pack-1.0-Linux.deb、ch4_ex06_pack-1.0-Linux.rpm 和 ch4_ex06_pack-1.0-Linux.tar.bz2 包。让我们尝试在 Debian 环境中安装生成的 Debian 软件包。
如果打包正确,我们应该可以直接在命令行调用 ch4_ex06_executable:
13:38 $ ch4_ex06_executable Hello, world!成功!作为练习,尝试安装 RPM 和 tar.bz2 软件包。在本节中,我们学习了如何使用 CPack 来打包我们的项目。这绝不是详尽的指南。 CPack 本身值得几章来详细介绍。至此,我们成功地走到了本章的结尾。
1.总结
在本章中,我们学习了使目标可安装的基础知识,以及如何为开发和消费者环境打包项目。部署是专业软件项目的一个非常重要的方面,借助我们在本章中介绍的内容,您将能够轻松解决此类部署需求。在下一章中,我们将学习如何将第三方库集成到 CMake 项目中。
2.问题
回答以下问题以测试您对本章的了解:
1. 我们如何指导 CMake 使 CMake 目标可安装?
2. 通过 install(TARGETS) 命令安装目标时会安装哪些文件?
3. 对于库目标,是否通过 install(TARGETS) 命令安装了头文件?为什么?如果没有,还可以做些什么来安装它们?
4. GNUInstallDirs CMake 模块提供什么?
5. 如何有选择地将目录的内容安装到目标目录中?
6. 为什么在指定安装目标目录时要使用相对路径?
7. 配置文件包需要哪些基本文件?
8. 导出目标是什么意思?
9. 如何使 CMake 项目可以用 CPack 打包?