cmk-bst-prac-2e-merge-0

CMake 最佳实践第二版(一)

原文:zh.annas-archive.org/md5/FFE1C97095FAEE9D7B23FBC1FEE1C179

译者:飞龙

协议:CC BY-NC-SA 4.0

序言

软件世界和我们用来创建软件的工具每天都在不断发展。CMake 也不例外;在过去的二十年里,它不断发展,现在已被认为是构建 C++ 应用程序的行业标准。尽管 CMake 拥有丰富的功能和全面的文档,但关于如何有效地将这些功能结合使用的真实世界示例和指南却很少。这正是《CMake 最佳实践》所填补的空白。

本书并未深入探讨 CMake 的每个细节和功能,而是通过示例说明如何有效地使用 CMake 完成各种软件构建任务,而不会覆盖每个极端案例——其他书籍可以满足这一需求。本书的目的是尽可能简化,同时覆盖完成任务的推荐最佳实践。这样做的原因是,您不需要了解 CMake 的所有功能就能完成日常任务。

我们尝试通过具体的示例来说明所有概念,您可以亲自尝试。由于本书的读者主要是工程师,我们已经根据这一点量身定制了内容。在编写本书时,我们首先是软件工程师,其次是作者。因此,内容更注重实用性而非理论性。这本书是经过精心挑选的、已被验证的技术的汇编,您可以在日常的 CMake 工作流中使用它们。

从工程师到工程师,我们希望您喜欢这本书。

第二版有什么新内容?

在本第二版中,我们基于读者反馈和 CMake 最新的进展做了几项重要的补充和改进:

  • 关于为 Apple 产品构建软件的新章节:我们增加了关于如何为 Apple 平台构建软件的全面内容,解决了在 Apple 封闭生态系统中软件处理的独特方式。

  • 深入讨论 CMake 预设:本版包含了比上一版更详细的关于 CMake 预设的讨论。这将帮助您简化开发流程。

  • 重做的依赖处理章节:关于依赖处理的章节已大幅重做,包含了如何使用新的 CMake 依赖提供者以及使用 Conan 2 版本的实用指南,使得在项目中管理依赖变得更加容易。

  • 更正勘误和读者反馈:我们已经彻底修订了内容,纠正了任何错误,并纳入了读者的宝贵反馈,以增强本书的清晰度和实用性。

我们相信这些更新将使《CMake 最佳实践》第二版对您更有价值,提供最新的技巧和见解,以改善您使用 CMake 构建软件的体验。

本书适合谁阅读

本书适用于那些经常使用 C 或 C++ 的软件工程师和构建系统维护人员,帮助他们利用 CMake 改善日常任务。基本的 C++ 和编程知识将帮助你更好地理解书中所涵盖的示例。

本书内容涵盖

第一章启动 CMake,简要介绍了 CMake 的基本概念,然后直接讲解如何安装 CMake 并使用 CMake 构建项目。你将学到如何手动安装最新的稳定版本,即使你的包管理器没有提供该版本。你还将了解 CMake 的基本概念,以及它为什么是一个构建系统生成器,而不是一个构建系统本身。学习它如何与现代 C++(以及 C)的软件开发结合。

第二章以最佳方式访问 CMake,展示了如何通过命令行、GUI 最有效地使用 CMake,以及 CMake 如何与一些常见的 IDE 和编辑器集成。

第三章创建一个 CMake 项目,带你完成为构建可执行文件、库并将两者链接在一起的项目设置。

第四章打包、部署和安装 CMake 项目,将教你如何创建一个可分发版本的软件项目。你将学到如何添加安装说明,并使用 CMake 和 CPack(CMake 的打包程序)来打包项目。

第五章集成第三方库和依赖管理,解释了如何将现有的第三方库集成到你的项目中。它还展示了如何添加已安装在系统中的库、外部 CMake 项目和非 CMake 项目。本章涵盖了 CMake 的依赖项提供者,并简要介绍了如何使用外部包管理器。

第六章自动生成文档,探索了如何在构建过程中使用 CMake 和 doxygen、dot(graphviz)、plantuml 从代码中生成文档。

第七章无缝集成代码质量工具与 CMake,将展示如何将单元测试、代码清理工具、静态代码分析和代码覆盖工具集成到你的项目中。你将学到如何使用 CMake 发现并执行测试。

第八章使用 CMake 执行自定义任务,将解释如何将几乎任何工具集成到构建过程中。你将学习如何将外部程序包装成自定义目标,或如何将它们挂钩到构建过程中以执行。我们将介绍如何使用自定义任务生成文件,以及如何让它们消耗其他目标生成的文件。你还将学习如何在 CMake 构建的配置过程中执行系统命令,以及如何使用 CMake 的 cscript 模式创建跨平台的命令。

第九章创建可重现的构建环境,展示了如何在各种机器之间(包括 CI/CD 流水线)构建便携环境。如何使用 Docker、sysroots 和 CMake 预设,让你的构建能在任何地方“开箱即用”。

第十章处理超构建中的分布式仓库和依赖项,简化了使用 CMake 管理分布在多个 Git 仓库中的项目。你将学习如何创建一个超构建,允许你构建特定版本以及最新的夜间构建。探索它所需的先决条件,以及如何将它们结合起来。

第十一章为 Apple 系统创建软件,由于封闭的生态系统,Apple 平台有一些独特的构建特点,尤其是对于具有图形用户界面和更复杂库框架的应用程序。对于这些情况,Apple 使用称为 app bundles 和 frameworks 的特定格式。在本章中,你将学习如何创建这些 Apple 特有的构建产物,并且如何对它们进行签名,以便在 App Store 中使用。

第十二章跨平台编译和自定义工具链,展示了如何使用预定义的跨平台工具链。你还将学习如何编写自己的工具链定义,并方便地在 CMake 中使用不同的工具链。

第十三章复用 CMake 代码,解释了 CMake 模块以及如何将你的 CMake 文件进行通用化。你将学习如何编写广泛使用的模块,并将它们从你的项目中单独分发。

第十四章优化和维护 CMake 项目,提供关于如何加快构建时间的提示,以及如何在长期使用中保持 CMake 项目整洁有序的技巧。

第十五章迁移到 CMake,解释了如何在不完全停止开发的情况下,将一个大型现有代码库迁移到 CMake 的高层策略。

附录, 贡献 CMake 和进一步阅读材料,提供有关如何贡献的提示、需要注意的事项以及基本的贡献指南。它还会指导您在哪里找到额外的深入信息或更具体的文献。

如何最大限度地利用本书

书中涉及的软件/硬件 操作系统要求
CMake 3.25 或更新版本 Linux, Windows, macOS
GCC, Clang 或 MSVC Linux, Windows, macOS
git Linux, Windows, macOS

如果您使用的是本书的数字版,我们建议您自己输入代码,或者从本书的 GitHub 仓库(下节中会提供链接)访问代码。这样可以帮助您避免因复制粘贴代码而可能出现的错误

下载示例代码文件

您可以从 GitHub 下载本书的示例代码文件,网址是 github.com/PacktPublishing/CMake-Best-Practices---2nd-Edition。如果代码有更新,它将在 GitHub 仓库中更新。

我们还有其他代码包,来自我们丰富的书籍和视频目录,您可以在 github.com/PacktPublishing/ 查看!

使用的约定

本书中使用了一些文本约定。

文本中的代码:表示文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 账号。例如:“将下载的 WebStorm-10*.dmg 磁盘镜像文件挂载为您系统中的另一个磁盘。”

代码块如下所示:

project( 
"chapter1" 
VERSION 1.0 
DESCRIPTION "A simple C++ project to demonstrate basic CMake usage" 
LANGUAGES CXX 
)

当我们希望特别指出代码块中的某一部分时,相关的行或项目会用粗体显示:

include(GenerateExportHeader) 
generate_export_header(hello 
EXPORT_FILE_NAME export/hello/ 
export_hello.hpp)
target_include_directories(hello PUBLIC "${CMAKE_CURRENT_BINARY_DIR} 
/export")

任何命令行输入或输出都会如下所示:

cmake -G "Unix Makefiles" -DCMAKE_CXX_COMPILER=/usr/bin/g++-12 -S . 
-B ./build 

粗体:表示新术语、重要单词或您在屏幕上看到的单词。例如,菜单或对话框中的单词会以粗体显示。以下是一个示例:“尽管 CMake 已与许多 IDE 和编辑器很好地集成,但它本质上是一个命令行工具,因此学习如何在命令行界面CLI)中使用 CMake 对于充分发挥其潜力至关重要。”

提示或重要注释

以这种方式显示。

联系我们

我们始终欢迎读者的反馈。

一般反馈:如果您对本书的任何方面有疑问,请通过电子邮件联系我们 customercare@packtpub.com,并在邮件主题中注明书名。

勘误:虽然我们已尽力确保内容的准确性,但错误仍然可能发生。如果您在本书中发现错误,我们将非常感激您能向我们报告。请访问 www.packtpub.com/support/errata 并填写表单。

盗版:如果你在互联网上发现我们作品的任何非法复制品,请提供相关地址或网站名称。请通过版权@packt.com 与我们联系,并附上相关链接。

如果你有兴趣成为作者:如果你在某个领域有专长,并且有兴趣写书或为书籍做贡献,请访问authors.packtpub.com

分享你的想法

一旦你读完了《CMake 最佳实践》,我们很想听听你的想法!请点击这里直接访问亚马逊书评页面,分享你的反馈。

你的评论对我们以及技术社区非常重要,将帮助我们确保提供优质的内容。

下载本书的免费 PDF 版本

感谢你购买本书!

你喜欢随时随地阅读,但又无法随身携带纸质书籍吗?

你的电子书购买与设备不兼容吗?

不用担心,现在每本 Packt 书籍都附赠一份免费的无 DRM PDF 版本。

随时随地、在任何设备上阅读。从你最喜欢的技术书籍中直接复制、粘贴代码到你的应用程序中。

福利不止这些,你还可以独享折扣、电子报以及每日发送的精彩免费内容。

按照以下简单步骤享受相关福利:

  1. 扫描二维码或访问下面的链接

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/cmk-bst-prac-2e/img/B30947_QR_Free_PDF.jpg

packt.link/free-ebook/978-1-83588-064-7

  1. 提交你的购买凭证

  2. 就是这样!我们会将免费的 PDF 及其他福利直接发送到你的电子邮箱。

第一部分 – 基础知识

在第一章,你将学习如何调用 CMake,并获得关于 CMake 基本概念的高层次概述,以及 CMake 语言的简要介绍。

第二章将介绍如何从命令行、GUI 或不同的 IDE 和编辑器中使用 CMake。它将演示如何更改各种配置选项以及如何选择不同的编译器。

在第三章,我们将介绍如何创建一个简单的 CMake 项目来构建可执行文件和库。

这一部分包含以下章节:

  • 第一章启动 CMake

  • 第二章以最佳方式访问 CMake

  • 第三章创建 CMake 项目

第一章:启动 CMake

如果你正在使用 C++ 或 C 开发软件,可能已经听说过 CMake。过去 20 年,CMake 已经发展成为构建 C++ 应用程序的行业标准。但 CMake 不仅仅是一个构建系统——它是一个构建系统生成器,这意味着它为其他构建系统(如 Makefile、Ninja、Visual Studio、QtCreator、Android Studio 和 XCode)生成指令。CMake 不止于构建软件——它还包括支持安装、打包和测试软件的功能。

作为事实上的行业标准,CMake 是每个 C++ 程序员必须了解的技术。

本章将为你提供 CMake 的高层次概述,并介绍构建你的第一个程序所需的基础知识。我们将了解 CMake 的构建过程,并概述如何使用 CMake 语言来配置构建过程。

本章我们将涵盖以下主题:

  • CMake 简介

  • 安装 CMake

  • CMake 构建过程

  • 编写 CMake 文件

  • 不同的工具链和构建配置

  • 单配置和多配置生成器

让我们开始吧!

技术要求

为了运行本章中的示例,你需要一个支持 C++17 的最新 C++ 编译器。尽管这些示例并不复杂到需要新标准的功能,但它们已经相应地设置好了。

我们建议使用这里列出的任何编译器来运行示例:

  • Linux: GCC 9 或更新版本,Clang 10 或更新版本

  • Windows: MSVC 19 或更新版本,或 MinGW 9.0.0 或更新版本

  • macOS: AppleClang 10 或更新版本

本章中使用的完整示例可以在 github.com/PacktPublishing/CMake-Best-Practices---2nd-Edition/tree/main/chapter01 找到

注意

为了尝试本书中的任何示例,我们提供了一个现成的 Docker 容器,包含所有必要的依赖。

你可以在 github.com/PacktPublishing/CMake-Best-Practices---2nd-Edition 找到它。

CMake 简介

CMake 是开源的,且可在多个平台上使用。它也是与编译器无关的,这使得它在构建和分发跨平台软件时非常强大。所有这些功能使它成为以现代方式构建软件的宝贵工具——即通过依赖构建自动化和内置质量门控。

CMake 包含三个命令行工具:

  • cmake: CMake 本身,用于生成构建指令

  • ctest: CMake 的测试工具,用于检测和运行测试

  • cpack: CMake 的打包工具,用于将软件打包成方便的安装程序,如 DEB、RPM 和自解压安装程序

还有两个互动工具:

  • cmake-gui: 一个图形界面前端,帮助配置项目

  • ccmake:用于配置 CMake 的交互式终端 UI

cmake-gui工具可以方便地配置 CMake 构建并选择要使用的编译器:

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/cmk-bst-prac-2e/img/B30947_01_01.jpg

图 1.1 – 配置项目后的 cmake-gui 界面

如果你在控制台工作,但仍希望拥有交互式配置 CMake 的功能,那么ccmake是合适的工具。虽然它没有cmake-gui那么方便,但提供了相同的功能。当你必须通过ssh shell 或类似方式远程配置 CMake 时,这尤其有用:

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/cmk-bst-prac-2e/img/B30947_01_02.jpg

图 1.2 – 使用 ccmake 配置项目

CMake 相比于常规构建系统有许多优势。首先是跨平台特性。使用 CMake,你可以更容易地为各种编译器和平台创建构建指令,而无需深入了解各自构建系统的具体细节。

另外,CMake 能够发现系统库和依赖,这大大减少了寻找正确库文件来构建软件的麻烦。额外的好处是,CMake 与包管理器如 Conan 和 vcpkg 的集成非常顺畅。

CMake 不仅具备为多个平台构建软件的能力,还原生支持软件的测试、安装和打包,这使得 CMake 在构建软件时比单一构建系统更具优势。能够在一个统一的地方定义从构建、过度测试到打包的所有内容,对于长期维护项目极为有帮助。

CMake 本身对系统的依赖非常少,且可以在命令行上无须用户交互地运行,这使得它非常适合用于 CI/CD 流水线中的构建系统自动化。

现在我们已经简要介绍了 CMake 的功能,接下来让我们学习如何安装 CMake。

安装 CMake

CMake 可以从cmake.org/download/免费下载。它提供了预编译的二进制文件或源代码。对于大多数使用场景,预编译的二进制文件已经完全足够,但由于 CMake 本身依赖很少,构建一个版本也是可能的。

任何主要的 Linux 发行版都提供了 CMake 的安装包。虽然预打包的 CMake 版本通常不是最新发布版本,但如果系统经常更新,这些安装包通常足以使用。另一种方便的安装方式是使用 Python 包管理器pip

注意

本书中示例所需使用的最低 CMake 版本为3.23。我们建议你手动下载适当版本的 CMake,以确保获得正确的版本。

从源代码构建 CMake

CMake 是用 C++ 编写的,并使用 Make 构建自身。从零开始构建 CMake 是可能的,但对于大多数使用场景,使用二进制下载版本就足够了。

cmake.org/download/ 下载源代码包后,将其解压到一个文件夹,并运行以下命令:

./configure make

如果你还想构建 cmake-gui,可以使用 --qt-gui 选项进行配置。这要求你安装 Qt。配置过程可能会花些时间,但完成后,你可以使用以下命令安装 CMake:

make install

为了测试安装是否成功,你可以执行以下命令:

cmake --version

这将打印出 CMake 的版本,类似于这样:

cmake version 3.23.2
CMake suite is maintained and supported by Kitware (kitware.com/cmake).

现在,CMake 已经安装在你的机器上,你可以开始构建你的第一个项目了。让我们开始吧!

构建你的第一个项目

现在,是时候动手看看你的安装是否成功了。我们提供了一个简单的 hello world 项目的示例,你可以立即下载并构建。打开一个控制台,输入以下命令,你就可以开始了:

git clone   https://github.com/PacktPublishing/CMake-Best-Practices---2nd-Edition.git
cd CMake-Best-Practices---2nd-Edition/chapter01/simple_executable
cmake -S . -B build
cmake -–build ./build

这将生成一个名为 ch_simple_executable 的可执行文件,在控制台上输出 Welcome to CMake Best Practices

让我们详细看看发生了什么:

首先,使用 Git 检出示例仓库。示例 CMake 项目位于 chapter01/simple_executable 子文件夹中,构建前的文件结构如下所示:

.
├── CMakeLists.txt
└── src
    └── main.cpp

除了包含源代码的文件夹外,还有一个名为 CMakeLists.txt 的文件。该文件包含了 CMake 如何为项目创建构建指令及如何构建它的指令。每个 CMake 项目在项目根目录下都有一个 CMakeLists.txt 文件,但在各个子文件夹中可能还有多个同名的文件。

  1. 克隆完仓库后,构建过程通过 cmake –S . -B build 命令启动。这告诉 CMake 使用当前目录作为源目录,并使用名为 build 的目录来存放构建产物。我们将在本章稍后详细讨论源目录和构建目录的概念。CMake 的构建过程是一个两阶段的过程。第一步,通常称为 配置,读取 CMakeLists.txt 文件并生成本地构建工具链的指令。第二步,执行这些构建指令,构建出可执行文件或库。

在配置步骤中,检查构建要求,解决依赖关系,并生成构建指令。

  1. 配置项目时还会创建一个名为 CMakeCache.txt 的文件,包含创建构建指令所需的所有信息。接下来执行 cmake --build ./build 命令时,会通过内部调用 CMake 来执行构建;如果你使用的是 Windows,它会通过调用 Visual Studio 编译器来完成。这个步骤就是实际的二进制文件编译过程。如果一切顺利,build 文件夹中应该会有一个名为 ch1_simple_executable 的可执行文件。

在前面的示例中,我们通过传递 -S-B 命令行选项显式指定了源代码和构建文件夹。这通常是与 CMake 一起工作的推荐方法。还有一种更简短的方法,使用相对路径工作,这种方法在在线教程中也经常见到:

mkdir build
cd build
cmake ..
cmake --build

在这里,我们首先创建了构建文件夹,然后cd 进入该文件夹,并使用带有相对路径的 cmake。默认情况下,CMake 会假设它在要创建二进制文件和构建工件的文件夹中启动。

显式传递构建目录和源目录在使用 CMake 进行持续集成时通常很有用,因为明确指定有助于维护。如果你想为不同的配置创建不同的构建目录(例如在构建跨平台软件时),这也很有帮助。

那么,CMake 如何知道编译哪些文件以及创建哪些二进制文件呢?为此,它使用包含构建指令的文本文件,通常称为CMakeLists.txt

一个最小的 CMakeLists.txt 文件

对于一个非常简单的 hello world 示例,CMakeLists.txt 文件只包含几行指令:

cmake_minimum_required(VERSION 3.23)
project(
  "chapter1"
  VERSION 1.0
  DESCRIPTION "A simple project to demonstrate basic CMake usage"
  LANGUAGES CXX)
add_executable(ch1_simple_executable)
target_sources(ch1_simple_executable PRIVATE src/main.cpp)

让我们更详细地理解这些指令:

第一行定义了构建此项目所需的 CMake 最低版本。每个 CMakeLists.txt 文件都以此指令开始。该指令用于提醒用户,如果项目使用了仅在某个版本及以上的 CMake 特性,这时就会显示警告。一般来说,我们建议将版本设置为支持项目中使用特性所需的最低版本。

下一个指令是要构建项目的名称、版本和描述,之后是项目中使用的编程语言。这里,我们使用 CXX 来标记这是一个 C++ 项目。

add_executable 指令告诉 CMake 我们要构建一个可执行文件(与库或自定义工件不同,后者我们将在本书稍后介绍)。

target_sources 语句告诉 CMake 在哪里查找名为 ch1_simple_executable 的可执行文件的源代码,并且源代码的可见性仅限于该可执行文件。我们将在本书稍后部分详细介绍单个命令的具体内容。

恭喜——你现在可以使用 CMake 创建软件程序了。但是,要了解命令背后发生了什么,我们接下来将详细了解 CMake 构建过程。

理解 CMake 构建过程

CMake 的构建过程分为两个步骤,如下图所示。首先,如果没有使用任何特殊标志调用,CMake 会在配置过程中扫描系统,查找可用的工具链,然后决定输出结果应该是什么。第二步是在调用 cmake --build 时,实际的编译和构建过程。

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/cmk-bst-prac-2e/img/B30947_01_03.jpg

图 1.3 – CMake 的两阶段构建过程

标准输出是 Unix Makefiles,除非唯一检测到的编译器是 Microsoft Visual Studio,在这种情况下将创建一个 Visual Studio 解决方案(.sln)。

要更改生成器,可以将 -G 选项传递给 CMake,像这样:

cmake .. -G Ninja

这将生成供 Ninja 使用的文件(ninja-build.org/),Ninja 是一种替代的构建生成器。CMake 有许多可用的生成器。可以在 CMake 的官方网站上找到支持的生成器列表:cmake.org/cmake/help/latest/manual/cmake-generators.7.html

生成器主要分为两类——一种是有多种 Makefile 版本和 Ninja 生成器,通常从命令行使用,另一种是为 Visual Studio 或 Xcode 等 IDE 创建构建文件。

CMake 区分 单配置生成器多配置生成器。使用单配置生成器时,每次更改配置时必须重写构建文件;而多配置构建系统可以在不需要重新生成的情况下管理不同的配置。尽管本书中的示例使用单配置生成器,但它们也适用于多配置生成器。对于大多数示例,选择的生成器无关紧要;如果有区别,会特别提到:

生成器 多配置
Makefiles(所有版本)
Ninja
Ninja-Multi
Xcode
Visual Studio

此外,还有一些额外的生成器,它们使用普通的生成器,但还会为编辑器或 IDE 生成项目信息,例如 Sublime Text 2、Kate 编辑器、CodeBlocks 或 Eclipse。对于每个生成器,你可以选择编辑器是否应该使用 Make 或 Ninja 来内部构建应用程序。

调用后,CMake 会在 build 文件夹中创建许多文件,其中最显著的是 CMakeCache.txt 文件。这里存储了所有检测到的配置。缓存配置的主要好处是,后续的 CMake 运行速度更快。请注意,当使用 cmake-gui 时,第一步被拆分为配置项目和生成构建文件。然而,当从命令行运行时,这些步骤合并为一个。一旦配置完成,所有构建命令都会从 build 文件夹执行。

源文件夹和构建文件夹

在 CMake 中,存在两个逻辑文件夹。一是 source 文件夹,包含一个分层的项目集合;另一个是 build 文件夹,包含构建指令、缓存以及所有生成的二进制文件和产物。

source 文件夹的根目录是顶级 CMakeLists.txt 文件所在的位置。build 文件夹可以放在 source 文件夹内,但有些人喜欢将其放在其他位置。两者都可以,值得注意的是,本书中的示例决定将 build 文件夹放在 source 文件夹内。build 文件夹通常仅称为 build,但它可以有任何名称,包括针对不同平台的前后缀。在源代码树中使用 build 文件夹时,建议将其添加到 .gitignore 中,以避免意外提交。

配置 CMake 项目时,source 文件夹的项目和文件夹结构会在 build 文件夹中重新创建,从而使所有构建产物都位于相同的位置。在每个映射的文件夹中,会有一个名为 CMakeFiles 的子文件夹,其中包含 CMake 配置步骤生成的所有信息。

以下代码展示了一个 CMake 项目的示例结构:

├── chapter_1
│   ├── CMakeLists.txt
│   └── src
│       └── main.cpp
├── CMakeLists.txt

执行 CMake 配置时,CMake 项目的文件结构会映射到 build 文件夹中。每个包含 CMakeLists.txt 文件的文件夹都会被映射,并会创建一个名为 CMakeFiles 的子文件夹,其中包含 CMake 用于构建的内部信息:

├── build
│   ├── chapter_1
│   │   └── CMakeFiles
│   │   ├── CMakeCache.txt
│   └── CMakeFiles

在处理构建文件夹和构建配置时,一个重要的区分是单配置生成器与多配置生成器之间的区别。

单配置生成器和多配置生成器

在 CMake 中,生成器负责根据项目构建的平台以及开发者的偏好,创建本地构建系统(例如 Makefile、Visual Studio 解决方案)。CMake 中的两种主要生成器类型是单配置生成器和多配置生成器。

主要区别在于单配置生成器生成构建系统信息,其中每个构建配置(例如调试或发布)对应一个单独的构建目录。关于不同构建类型的更多细节将在本章后面介绍。如果切换构建配置(例如从调试构建切换到发布构建),用户必须选择一个不同的构建目录,否则之前的信息将被覆盖。

对于多配置生成器,一个构建目录可以包含多个配置,选择哪个配置仅在构建步骤中指定。

是否选择单配置生成器或多配置生成器,通常取决于个人偏好或某个操作系统中工具的便捷性。作为一个经验法则,可以说单配置生成器在命令行和 CI 环境中稍微更容易使用,而多配置生成器可能在 IDE 中有更好的集成。目前,我们已经使用现有的CMakeLists.txt来了解 CMake 的构建过程。我们学习了配置和构建步骤,以及生成器,并了解到我们需要CMakeLists.txt文件将必要的信息传递给 CMake。那么,接下来让我们更进一步,看看CMakeLists.txt文件的样子以及 CMake 语言是如何工作的。

编写 CMake 文件

当你编写 CMake 文件时,有一些核心概念和语言特性是你需要了解的。我们在这里不会涵盖语言的每个细节,因为 CMake 的文档已经做得相当不错,尤其是在全面性方面。接下来的章节将提供核心概念和语言特性的概述,后续章节会深入探讨不同方面的细节。

语言的完整文档可以在cmake.org/cmake/help/latest/manual/cmake-language.7.html找到。

CMake 语言 – 一个 10000 英尺的概览

CMake 使用名为CMakeLists.txt的配置文件来确定构建规范。这些文件是用一种脚本语言编写的,通常也叫做 CMake。该语言本身简单,支持变量、字符串函数、宏、函数定义和导入其他 CMake 文件。

除了列表外,没有对结构体或类等数据结构的支持。但正是这种相对简单性,使得 CMake 项目在正确执行时本质上更容易维护。

语法基于关键字和空格分隔的参数。例如,以下命令告诉 CMake 哪些文件需要添加到库中:

target_sources(MyLibrary
                PUBLIC include/api.h
                PRIVATE src/internals.cpp src/foo.cpp)

PUBLICPRIVATE关键字表示文件在与此库链接时的可见性,并且充当文件列表之间的分隔符。

此外,CMake 语言还支持所谓的“生成器表达式”,这些表达式在构建系统生成时进行评估。它们通常用于在项目的配置阶段,为每个生成的构建配置指定特定信息。它们将在本章后面的生成器 表达式部分进行详细介绍。

项目

CMake 将各种构建产物(如库、可执行文件、测试和文档)组织成项目。总是有一个根项目,尽管这些项目可以彼此封装。原则上,每个 CMakeLists.txt 文件中应该只有一个项目,这意味着每个项目必须在源目录中有一个单独的文件夹。

项目描述如下:

 project(
"chapter1"
VERSION 1.0
DESCRIPTION "A simple C++ project to demonstrate basic CMake usage"
LANGUAGES CXX
)

当前正在解析的项目存储在 PROJECT_NAME 变量中。对于根项目,这个信息也存储在 CMAKE_PROJECT_NAME 变量中,这对于判断一个项目是独立的还是被封装在另一个项目中非常有用。自版本 3.21 起,还引入了 PROJECT_IS_TOP_LEVEL 变量,用于直接判断当前项目是否为顶级项目。此外,使用 <PROJECT-NAME>_IS_TOP_LEVEL,可以检测某个特定项目是否为顶级项目。

以下是一些与项目相关的附加变量。对于根项目,所有这些变量都可以以 CMAKE_ 为前缀。如果在 project() 指令中没有定义它们,则这些字符串为空:

  • PROJECT_DESCRIPTION:项目的描述字符串

  • PROJECT_HOMEPAGE_URL:项目的 URL 字符串

  • PROJECT_VERSION:赋予项目的完整版本号

  • PROJECT_VERSION_MAJOR:版本字符串中的第一个数字

  • PROJECT_VERSION_MINOR:版本字符串中的第二个数字

  • PROJECT_VERSION_PATCH:版本字符串中的第三个数字

  • PROJECT_VERSION_TWEAK:版本字符串中的第四个数字

每个项目都有一个源目录和一个二进制目录,它们可能会彼此封装。假设以下示例中的每个 CMakeFiles.txt 文件都定义了一个项目:

.
├── CMakeLists.txt #defines project("CMakeBestPractices
"...)
├── chapter_1
│   ├── CMakeLists.txt # defines project("Chapter 1"...)

当解析根文件夹中的 CMakeLists.txt 文件时,PROJECT_NAMECMAKE_PROJECT_NAME 都将是 CMakeBestPractices。当你解析 chapter_1/CMakeLists.txt 时,PROJECT_NAME 变量将更改为 "Chapter_1",但 CMAKE_PROJECT_NAME 仍然保持为 CMakeBestPractices,这是根文件夹中的设置。

尽管项目可以嵌套,但最好以独立的方式编写它们,使其能够单独工作。虽然它们可能依赖于文件层次结构中较低的其他项目,但不应将某个项目作为另一个项目的子项目。可以在同一个 CMakeLists.txt 文件中多次调用 project(),但我们不推荐这种做法,因为它往往会使项目变得混乱,难以维护。通常,更好的做法是为每个项目创建一个单独的 CMakeLists.txt 文件,并通过子文件夹组织结构。

本书的 GitHub 仓库,包含了本书中的示例,采用层次化的方式组织,其中每个章节都是一个独立的项目,可能包含更多的项目来处理不同的部分和示例。

虽然每个示例都可以单独构建,但你也可以从仓库的根目录构建整个书籍项目。

变量

变量是 CMake 语言的核心部分。可以使用set命令设置变量,使用unset命令删除变量。变量名是区分大小写的。下面的示例展示了如何设置一个名为MYVAR的变量并将值1234赋给它:

set(MYVAR "1234")

要删除MYVAR变量,可以使用unset

unset(MYVAR)

一般的代码约定是将变量写成全大写字母。内部变量始终表示为字符串。

你可以通过$符号和花括号访问变量的值:

message(STATUS "The content of MYVAR are ${MYVAR}")

变量引用甚至可以嵌套,并按内外顺序进行评估:

${outer_${inner_variable}_variable}

变量可能具有以下作用域:

  • 函数作用域:在函数内部设置的变量仅在该函数内可见。

  • 目录作用域:源树中的每个子目录都会绑定变量,并包括父目录中的任何变量绑定。

  • 持久缓存:缓存变量可以是系统定义的或用户定义的。这些变量会在多次运行中保持其值。

PARENT_SCOPE选项传递给set()会使变量在父作用域中可见。

CMake 提供了多种预定义变量。这些变量以CMAKE_为前缀。完整列表可在cmake.org/cmake/help/latest/manual/cmake-variables.7.html查看。

列表

尽管 CMake 内部将变量存储为字符串,但可以通过用分号分隔值来处理 CMake 中的列表。列表可以通过将多个未加引号的变量传递给set(),或者直接传递一个分号分隔的字符串来创建:

set(MYLIST abc def ghi)
 set(MYLIST "abc;def;ghi")

可以使用list命令来操作列表,修改其内容、重新排序或查找项。以下代码将查询MYLISTabc值的索引,并检索该值并将其存储在名为ABC的变量中:

list(FIND MYLIST abc ABC_INDEX)
list(GET MYLIST ${ABC_INDEX} ABC)

要向列表中添加一个值,可以使用APPEND关键字。这里,xyz值被追加到MYLIST中:

list(APPEND MYLIST "xyz")

缓存变量和选项

CMake 会缓存某些变量,以便在随后的构建中运行得更快。这些变量存储在CMakeCache.txt文件中。通常,你不需要手动编辑它们,但它们在调试行为异常的构建时非常有用。

所有用于配置构建的变量都会被缓存。要缓存一个名为ch1_MYVAR、值为foo的自定义变量,可以使用set命令,如下所示:

 set(ch1_MYVAR foo CACHE STRING "Variable foo that configures bar")

请注意,缓存变量必须具有类型和文档字符串,以提供其简要总结。

大多数自动生成的缓存变量都标记为高级,这意味着它们在cmake-guiccmake中默认是隐藏的。要使它们可见,必须显式切换它们。如果CMakeLists.txt文件生成了其他缓存变量,它们也可以通过调用mark_as_advanced(MYVAR)命令来隐藏:

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/cmk-bst-prac-2e/img/B30947_01_04.jpg

图 1.4 – 左侧 – cmake-gui 不显示标记为高级的变量。右侧 – 标记“高级”复选框会显示所有标记为高级的变量

一条经验法则是,任何用户不应更改的选项或变量应标记为高级。标记变量为高级的常见情况是在编写 CMake 模块或查找依赖项时,正如我们在第五章《集成第三方库和依赖管理》和第十三章《重用 CMake 代码》中所见。

对于简单的布尔缓存变量,CMake 还提供了 option 关键字,默认值为 OFF,除非另行指定。这些变量也可以通过 CMakeDependentOption 模块相互依赖:

option(CHAPTER1_PRINT_LANGUAGE_EXAMPLES "Print examples for each
  language" OFF)
include(CMakeDependentOption)
cmake_dependent_option(CHAPTER1_PRINT_HELLO_WORLD "print a greeting
  from chapter1 " ON CHAPTER1_PRINT_LANGUAGE_EXAMPLES ON)

选项通常是指定简单项目配置的便捷方式。它们是 bool 类型的缓存变量。如果已经存在与选项同名的变量,则调用 option 不会执行任何操作。

属性

CMake 中的属性是附加到特定对象或 CMake 范围的值,如文件、目标、目录或测试用例。可以通过使用 set_property 函数来设置或更改属性。要读取属性的值,可以使用 get_property 函数,它遵循类似的模式。默认情况下,set_property 会覆盖已经存储在属性中的值。可以通过将 APPENDAPPEND_STRING 传递给 set_property 来将值添加到当前值中。

完整的函数签名如下:

set_property(<Scope> <EntityName>
              [APPEND] [APPEND_STRING]
              PROPERTY <propertyName> [<values>])

范围说明符可以具有以下值:

  • GLOBAL:影响整个构建过程的全局属性。

  • DIRECTORY <dir>:绑定到当前目录或 <dir> 中指定目录的属性。也可以通过使用 set_directory_properties 命令直接设置。

  • TARGET <targets>:特定目标的属性。也可以通过使用 set_target_properties 函数来设置。

  • SOURCE <files>:将一个属性应用于源文件列表。也可以通过使用 set_source_files_properties 直接设置。此外,还有 SOURCE DIRECTORYSOURCE TARGET_DIRECTORY 扩展选项:

    • DIRECTORY <dirs>:为目录范围内的源文件设置属性。该目录必须已经由 CMake 解析,或者是当前目录,或者是通过 add_subdirectory 添加的。

    • TARGET_DIRECTORY <targets>:将属性设置为指定目标所在的目录。同样,目标必须在设置属性时已经存在。

  • INSTALL <files>:为已安装的文件设置属性。这些可以用于控制 cpack 的行为。

  • TEST <tests>:设置测试的属性。也可以通过set_test_properties直接设置。

  • CACHE <entry>:设置缓存变量的属性。最常见的包括将变量设置为高级选项或为其添加文档字符串。

支持的属性完整列表,按其不同实体排序,可以在cmake.org/cmake/help/latest/manual/cmake-properties.7.html找到。

在修改属性时,使用诸如set_target_propertiesset_test_properties等直接函数比使用更通用的set_property命令更好。使用显式命令可以避免错误和属性名称混淆,通常更具可读性。还有define_property函数,它创建一个没有设置值的属性。我们建议你不要使用它,因为属性应该始终有一个合理的默认值。

循环和条件

像任何编程语言一样,CMake 支持条件和循环块。条件块位于if()elseif()else()endif()语句之间。条件使用各种关键字表达。

一元关键字位于值之前,如下所示:

 if(DEFINED MY_VAR)

用于条件的一元关键字如下:

  • COMMAND:如果提供的值是命令,则为true

  • DEFINED:如果值是已定义的变量,则为true

此外,还有一元文件系统条件:

  • EXISTS:如果传递的文件或目录存在,则为true

  • IS_DIRECTORY:检查提供的路径是否是一个目录

  • IS_SYMLINK:如果提供的路径是符号链接,则为true

  • IS_ABSOLUTE:检查提供的路径是否是绝对路径

二元测试比较两个值,并将它们放在需要比较的值之间,如下所示:

if(MYVAR STREQUAL "FOO")

二元运算符如下:

  • LESSGREATEREQUALLESS_EQUALGREATER_EQUAL:这些比较数值。

  • STRLESSSTREQUALSTRGREATERSTRLESS_EQUALSTRGREATER_EQUAL:这些按字典顺序比较字符串。

  • VERSION_LESSVERSION_EQUALVERSION_GREATERVERSION_LESS_EQUALVERSION_GREATER_EQUAL:这些比较版本字符串。

  • MATCHES:这些与正则表达式进行比较。

  • IS_NEWER_THAN:检查传递的两个文件中哪个文件最近被修改。不幸的是,这并不十分精确,因为如果两个文件有相同的时间戳,它也会返回true。还有更多的混淆,因为如果任一文件缺失,结果也会是true

最后,还有布尔运算符ORANDNOT

循环可以通过while()endwhile()foreach()endforeach()实现。循环可以通过break()终止;continue()会中止当前的迭代并立即开始下一次迭代。

while循环的条件与if语句相同。下面的例子在MYVAR小于5时循环。请注意,为了增加变量值,我们使用了math()函数:

 set(MYVAR 0)
while(MYVAR LESS "5")
  message(STATUS "Chapter1: MYVAR is '${MYVAR}'")
  math(EXPR MYVAR "${MYVAR}+1")
endwhile()

除了while循环,CMake 还提供了用于遍历列表或范围的循环:

foreach(ITEM IN LISTS MYLIST)
# do something with ${ITEM}
endforeach()

for循环可以通过使用RANGE关键字在特定范围内创建:

foreach(ITEM RANGE 0 10)
# do something with ${ITEM}
endforeach()

尽管foreach()RANGE版本只需要一个stop变量就能工作,但最好还是始终指定起始和结束值。

函数

函数由function()/endfunction()定义。函数为变量开启了新的作用域,因此在函数内部定义的所有变量在外部不可访问,除非将PARENT_SCOPE选项传递给set()

函数不区分大小写,通过调用function的名称并加上圆括号来调用:

function(foo ARG1)
# do something
endfunction()
# invoke foo with parameter bar
foo("bar")

函数是让你的 CMake 部分可重用的好方法,通常在处理较大项目时非常有用。

CMake 宏通过macro()/endmacro()命令定义。它们有点像函数,不同之处在于函数中的参数是真正的变量,而宏中的参数是字符串替换。这意味着宏的所有参数必须使用花括号访问。

另一个区别是通过调用函数,控制权会转移到函数中。宏的执行方式像是将宏的主体粘贴到调用位置一样。这意味着宏不会创建与变量和控制流相关的作用域。因此,强烈建议避免在宏中调用return(),因为这会阻止宏调用位置的作用域执行。

目标

CMake 的构建系统是作为一组逻辑目标组织的,这些目标对应于可执行文件、库或自定义命令或工件,例如文档或类似的内容。

在 CMake 中有三种主要方式来创建目标——add_executableadd_libraryadd_custom_target。前两个用于创建可执行文件和静态或共享库,而第三个则可以包含几乎任何自定义命令来执行。

目标可以相互依赖,以确保一个目标在另一个目标之前构建。

在设置构建配置或编译器选项的属性时,最好使用目标而不是全局变量。一些目标属性有可见性修饰符,如PRIVATEPUBLICINTERFACE,用以表示哪些要求是传递性的——也就是说,哪些属性必须被依赖的目标“继承”。

生成器表达式

生成器表达式是构建配置阶段评估的小语句。大多数函数允许使用生成器表达式,但也有少数例外。生成器表达式的形式为$<OPERATOR:VALUE>,其中OPERATOR应用或比较VALUE。你可以将生成器表达式看作是小型的内联if语句。

在下面的示例中,使用生成器表达式根据构建配置是调试还是发布,设置my_target的不同编译定义:

target_compile_definitions(my_target PRIVATE
    $<$<CONFIG:Debug>:MY_DEBUG_FLAG>
    $<$<CONFIG:Release>:MY_RELEASE_FLAG>
)

这个示例告诉 CMake 评估CONFIG变量,值可以是DebugRelease,如果匹配其中一个,则为my_target目标定义MY_DEBUG_FLAGMY_RELEASE_FLAG。生成器表达式在编写平台和编译器独立的 CMake 文件时非常有用。除了查询值之外,生成器表达式还可以用来转换字符串和列表:

$<LOWER_CASE:CMake>

这将输出cmake

你可以在cmake.org/cmake/help/latest/manual/cmake-generator-expressions.7.html了解更多关于生成器表达式的信息。

大多数 CMake 命令都能处理生成器表达式,但有一些显著的例外,如用于文件操作的file()命令和在配置步骤中调用第三方程序的execute_process()命令。

另一个需要注意的事项是,在配置或生成步骤的哪个阶段可以使用哪些生成器表达式。例如,对于多配置生成器,$<CONFIG:...>在配置步骤期间可能未设置,因为构建配置通常只在构建步骤期间传递。在下一节中,我们将学习如何告诉 CMake 使用哪个工具链,以及如何配置不同的构建类型,如调试或发布。

CMake 策略

对于顶层的CMakeLists.txt文件,cmake_minimum_required必须在任何项目调用之前调用,因为它还设置了用于构建项目的 CMake 内部策略。

策略用于在多个 CMake 版本之间保持向后兼容性。它们可以配置为使用OLD行为,这意味着cmake表现出向后兼容性,或者使用NEW,这意味着新策略生效。由于每个新版本都会引入新规则和新特性,策略将用于警告你可能存在的向后兼容性问题。策略可以通过cmake_policy调用进行禁用或启用。

在下面的示例中,CMP0121策略已设置为向后兼容的值。CMP0121是在 CMake 3.21 版本中引入的,它检查list()命令的索引变量是否符合有效格式——即它们是否为整数:

cmake_minimum_required(VERSION 3.21)
cmake_policy(SET CMP0121 OLD)
list(APPEND MYLIST "abc;def;ghi")
list(GET MYLIST "any" OUT_VAR)

通过设置cmake_policy(SET CMP0121 OLD),启用了向后兼容性,尽管前面的代码访问了MYLIST"any"索引(不是整数),但不会产生警告。

将策略设置为NEW将在 CMake 配置步骤中抛出一个错误——[build] list index: any is not a valid index

除非你正在包含遗留项目,否则避免设置单一策略。

通常,应该通过设置 cmake_minimum_required 命令来控制策略,而不是通过更改单个策略。更改单个策略的最常见使用场景是将遗留项目作为子文件夹包含时。

到目前为止,我们已经介绍了 CMake 语言背后的基本概念,它用于配置构建系统。CMake 用于为不同类型的构建和语言生成构建指令。在下一节中,我们将学习如何指定要使用的编译器,以及如何配置构建。

不同的工具链和构建类型

CMake 的强大之处在于,你可以使用相同的构建规范——即 CMakeLists.txt——来适配不同的编译器工具链,而无需重写任何内容。一个工具链通常由一系列程序组成,能够编译和链接二进制文件,创建归档文件等。

CMake 支持多种语言,可以为其配置工具链。本书将重点讲解 C++。为不同编程语言配置工具链的方法是,将以下变量中的 CXX 部分替换为相应的语言标签:

  • C

  • CXX – C++

  • CUDA

  • OBJC – Objective C

  • OBJCXX – Objective C++

  • Fortran

  • HIP – NVIDIA 和 AMD GPU 的 HIP C++ 运行时 API

  • ISPC – 基于 C 的 SPMD 编程语言

  • ASM – 汇编器

如果项目没有指定语言,则默认假设使用 C 和 CXX。

CMake 会通过检查系统自动检测要使用的工具链,但如果需要,可以通过环境变量进行配置,或者在交叉编译的情况下,通过提供工具链文件来配置。这个工具链会存储在缓存中,因此如果工具链发生变化,必须删除并重新构建缓存。如果安装了多个编译器,可以通过在调用 CMake 前设置环境变量(如 CC 用于 C 编译器,CXX 用于 C++ 编译器)来指定非默认的编译器。在这里,我们使用 CXX 环境变量来覆盖 CMake 使用的默认编译器:

CXX=g++-7 cmake /path/to/the/source

或者,你可以通过传递相应的 cmake 变量并使用 -D 来覆盖使用的 C++ 编译器,如下所示:

cmake -D CMAKE_CXX_COMPILER=g++-7 /path/to/source

这两种方法都确保 CMake 使用 GCC 版本 7 来构建,而不是系统中可用的任何默认编译器。避免在 CMakeLists.txt 文件中设置编译器工具链,因为这与 CMake 文件应该是平台和编译器无关的理念相冲突。

默认情况下,链接器会由所选的编译器自动选择,但也可以通过传递链接器可执行文件的路径,并使用 CMAKE_CXX_LINKER 变量来选择不同的链接器。

构建类型

在构建 C++ 应用程序时,通常会有多种构建类型,例如包含所有调试符号的调试构建和经过优化的发布构建。

CMake 原生提供四种构建类型:

  • Debug:未优化,包含所有调试符号,并启用所有断言。与在 GCC 和 Clang 中设置 -O0 -g 相同。

  • Release:针对速度进行了优化,不包含调试符号,且禁用断言。通常,这就是发布时使用的构建类型。等同于 -O3 -DNDEBUG

  • RelWithDebInfo:提供优化后的代码,并包含调试符号,但禁用断言,等同于 -O2 -g -DNDEBUG

  • MinSizeRel:与 Release 相同,但针对小二进制文件大小进行了优化,而不是优化速度,这通常对应 -Os -DNDEBUG。注意,并非所有生成器在所有平台上都支持此配置。

请注意,构建类型必须在配置阶段传递给单配置生成器,例如 CMake 或 Ninja。对于多目标生成器,如 MSVC,它们不会在配置步骤中使用,而是在构建步骤中指定。也可以创建自定义构建类型,这可以方便地用于指定生成代码覆盖的构建,但通常并非所有编译器都能支持,因此需要一些谨慎。有关自定义构建类型的示例,请参见 第七章无缝集成代码质量工具与 CMake。由于 CMake 支持各种各样的工具链、生成器和语言,一个常见的问题是如何找到并维护这些选项的有效组合。在这里,预设可以提供帮助。

使用预设保持良好的构建配置

在使用 CMake 构建软件时,一个常见的问题是如何共享良好的或可用的配置来构建项目。通常,团队和个人都有偏好的构建产物存放位置、使用哪种生成器在某个平台上构建,或者希望 CI 环境使用与本地相同的设置进行构建。自 2020 年 12 月 CMake 3.19 发布以来,这些信息可以存储在 CMakePresets.json 文件中,该文件位于项目的根目录。此外,每个用户还可以通过 CMakeUserPresets.json 文件覆盖自己的配置。基本的预设通常会放在版本控制下,但用户的预设不会被提交到版本系统中。两个文件都遵循相同的 JSON 格式,顶层结构如下:

{
"version": 6,
"cmakeMinimumRequired": {
"major": 3,
"minor": 23,
"patch": 0
},
"configurePresets": [...],
"buildPresets": [...],
"testPresets": [...],
"packagePresets ": [...],
"workflowPresets": [...],}

第一行,"version": 6,表示 JSON 文件的架构版本。CMake 3.23 支持最多版本六,但预计新的版本发布将带来新的架构版本。

接下来,cmakeMinimumRequired{...} 指定了需要使用的 CMake 版本。虽然这是可选的,但最好将其包含在内,并确保版本与 CMakeLists.txt 文件中指定的版本匹配。

此后,可以通过 configurePresetsbuildPresetstestPresets 添加不同构建阶段的各种预设。如其名所示,configurePresets 适用于 CMake 构建过程的配置阶段,而其他两个则用于构建和测试阶段。构建和测试预设可以继承一个或多个配置预设。如果没有指定继承关系,它们将应用于所有先前的步骤。

要查看项目中已配置的预设,可以运行 cmake --list-presets 查看可用预设的列表。要使用预设进行构建,运行 cmake --build --``preset name

要查看 JSON 模式的完整规格,请访问 cmake.org/cmake/help/v3.21/manual/cmake-presets.7.html

预设是共享如何以非常明确的方式构建项目知识的好方法。撰写本文时,越来越多的 IDE 和编辑器原生支持 CMake 预设,尤其是在处理跨编译和工具链时。在这里,我们只为你提供了 CMake 预设的简要概述;它们将在 第九章 中更深入地介绍,创建可重复的 构建 环境

总结

在本章中,我们为你提供了 CMake 的简要概述。首先,你学习了如何安装并运行一个简单的构建。接着,你了解了 CMake 的两阶段构建过程以及单配置生成器和多配置生成器。

到现在为止,你应该能够构建本书 GitHub 仓库中提供的示例:github.com/PacktPublishing/CMake-Best-Practices---2nd-Edition。你学习了 CMake 语言的核心功能,如变量、目标和策略。我们简要介绍了函数和宏,以及用于流程控制的条件语句和循环。随着你继续阅读本书,你将运用到目前为止所学的内容,探索更多好的实践和技巧,将从简单的单目标项目过渡到复杂的软件项目,并通过良好的 CMake 配置保持其可维护性。

在下一章中,我们将学习如何执行 CMake 中一些最常见的任务,以及 CMake 如何与各种 IDE 配合工作。

进一步阅读

要了解更多关于本章中讨论的主题,请参考以下资源:

问题

回答以下问题,以测试你对本章内容的理解:

  1. 如何启动 CMake 的配置步骤?

  2. 单配置生成器和多配置生成器之间有什么区别?

  3. 如何启动 CMake 的构建步骤?

  4. 哪个 CMake 可执行文件可以用于运行测试?

  5. 哪个 CMake 可执行文件用于打包?

  6. CMake 中的目标是什么?

  7. 属性和变量有什么区别?

  8. CMake 预设的用途是什么?

答案

以下是前面问题的答案:

  1. 你可以通过以下命令开始 CMake 的配置步骤:

    cmake -S /path/to/source -``B /path/to/build

  2. 单配置生成器只会生成一个类型构建的构建文件,例如调试或发布构建。多配置生成器会一次性生成所有可用构建类型的构建指令。

  3. 你可以使用以下命令开始 CMake 的构建步骤:

    cmake -build /path/to/build
    
  4. 以下是可以用于运行测试的 CMake 可执行文件:

    ctest
    
  5. 以下是可以用于打包的 CMake 可执行文件:

    cpack
    
  6. CMake 中的目标是围绕构建组织的逻辑单元。它们可以是可执行文件、库,或包含自定义命令。

  7. 与变量不同,属性是附加到特定对象或作用域上的。

  8. CMake 预设用于共享构建的工作配置。

第二章:以最佳方式访问 CMake

在上一章中,我们已经了解了 CMake 并学习了它的基本概念。现在,我们将学习如何与它交互。学习如何与 CMake 交互非常重要。在开始使用 CMake 构建你的软件项目之前,你必须先学会如何配置、构建和安装现有项目。这将使你能够与 CMake 项目进行交互。

本章将探讨 CMake 作为一个界面所提供的功能,并检查一些流行的 IDE 和编辑器集成。本章将涵盖以下内容:

  • 通过命令行界面使用 CMake

  • 使用 cmake-guiccmake 界面

  • IDE 和编辑器集成(Visual Studio、Visual Studio CodeVSCode)和 Qt Creator)

由于我们有很多内容要讲解,因此不要浪费时间,直接开始技术要求。

技术要求

在深入详细内容之前,有一些要求需要满足,才能跟上示例的步伐:

  • CMake 最佳实践库:这是包含本书所有示例内容的主要库。可以在线访问:github.com/PacktPublishing/CMake-Best-Practices---2nd-Edition/

  • 后续章节中关于打包和依赖管理的一些示例使用了 OpenSSL 来说明如何与第三方库一起工作。要安装它,可以使用操作系统提供的包管理器,例如 apt-getchocolateybrew,或者从 OpenSSL wiki 中提供的任何链接下载:wiki.openssl.org/index.php/Binaries。选择 OpenSSL 是因为它可以在多种平台上免费使用,并且易于安装。

通过命令行界面使用 CMake

尽管 CMake 已经很好地集成到许多 IDE 和编辑器中,但它本质上是一个 命令行 工具,因此学习如何在 命令行界面 (CLI) 中使用 CMake 是充分发挥其潜力的关键。通过命令行使用 CMake 还可以帮助理解 CMake 的内部工作原理和概念。在本节中,我们将学习如何使用 CLI 执行最基本的 CMake 操作。

与 CMake CLI 的交互可以通过在操作系统终端中输入 cmake 命令来完成,前提是已安装 CMake 且 cmake 可执行文件已包含在系统的 PATH 变量(或等效项)中。你可以通过在终端中输入 cmake 而不带任何参数来验证这一点,如下图所示:

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/cmk-bst-prac-2e/img/B30947_02_01.jpg

图 2.1 – 调用 cmake 命令

如果您的终端提示缺少命令,您应该安装 CMake(在 第一章启动 CMake 中有详细说明),或通过将其添加到系统的 PATH 变量中使其可被发现。请参考您的操作系统指南,了解如何将路径添加到系统的 PATH 变量。

安装 CMake 并将其添加到 PATH 变量中(如果需要),之后您应该测试 CMake 是否可用。您可以在命令行中执行的最基本命令是 cmake --version,该命令可以让您检查 CMake 的版本:

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/cmk-bst-prac-2e/img/B30947_02_02.jpg

图 2.2 – 在终端中检查 CMake 版本

CMake 将以 cmake version <maj.min.rev> 的形式输出版本字符串。您应该看到一个包含您安装的 CMake 版本号的输出。

注意

如果版本与已安装的版本不匹配,可能是您的系统上安装了多个 CMake 版本。由于本书中的示例是为 CMake 版本 3.23 及以上编写的,建议在继续之前先解决该问题。

安装 CMake 后,您还应该安装构建系统和编译器。对于 Debian 类操作系统(例如 Debian 和 Ubuntu),可以通过执行 sudo apt install build-essential 命令轻松完成。此软件包本质上包含 gccg++make

CLI 的使用将在 Ubuntu 22.04 环境中进行演示。除了少数边缘情况外,其他环境中的使用方法相同。那些边缘情况将在后续中提到。

学习 CMake CLI 基础知识

你应该学习的关于使用 CMake CLI 的三个基本知识点如下:

  • 配置 CMake 项目

  • 构建 CMake 项目

  • 安装 CMake 项目

学习基础知识后,您将能够构建并安装任何您选择的 CMake 项目。让我们从配置开始。

通过 CLI 配置项目

要通过命令行配置 CMake 项目,您可以使用 cmake -G "Unix Makefiles" -S <project_root> -B <output_directory> 结构。-S 参数用于指定要配置的 CMake 项目,而 -B 指定 配置 输出目录。最后,-G 参数允许我们指定用于生成构建系统的生成器。配置过程的结果将写入 <output_directory>

作为示例,让我们将本书的示例项目配置到项目根目录 build 目录中:

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/cmk-bst-prac-2e/img/B30947_02_03.jpg

图 2.3 – 克隆示例代码库

重要提示

项目必须已经存在于您的环境中。如果没有,请在终端中执行 git clone https://github.com/PacktPublishing/CMake-Best-Practices---2nd-Edition.git 通过 Git 克隆该项目。

现在进入 CMake-Best-Practices---2nd-Edition/chapter02/simple_example 目录并执行 cmake -G "Unix Makefiles" -S . -B ./build,如以下截图所示:

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/cmk-bst-prac-2e/img/B30947_02_04.jpg

图 2.4 – 使用 CMake 配置示例代码

这个命令就像是对 CMake 说,使用“Unix Makefiles”(-G "Unix Makefiles") 生成器在当前目录(-S .)为 CMake 项目生成构建系统,并将其输出到构建(-B ./build)目录

CMake 将把当前文件夹中的项目配置到 build 文件夹中。由于我们省略了构建类型,CMake 使用了 Debug 构建类型(这是项目的默认 CMAKE_BUILD_TYPE 值)。

在接下来的部分中,我们将了解在配置步骤中使用的基本设置。

更改构建类型

CMake 默认情况下不会假定任何构建类型。要设置构建类型,必须向 configure 命令提供一个名为 CMAKE_BUILD_TYPE 的额外变量。要提供额外的变量,变量必须以 -D 为前缀。

要获取 Release 构建而不是 Debug,请在 configure 命令中添加 CMAKE_BUILD_TYPE 变量,如前所述:cmake -G "Unix Makefiles" -S . -B ./build

注意

CMAKE_BUILD_TYPE 变量仅适用于单配置生成器,例如 Unix Makefiles 和 Ninja。在多配置生成器中,如 Visual Studio,构建类型是一个构建时参数,而不是配置时参数。因此,不能通过使用 CMAKE_BUILD_TYPE 参数来配置。请参见 为多配置生成器安装特定配置 部分,了解如何在这些生成器中更改构建类型。

更改生成器类型

根据环境,CMake 默认尝试选择合适的生成器。要显式指定生成器,必须提供 -G 参数,并指定一个有效的生成器名称。例如,如果您想使用 Ninja 作为构建系统而不是 make,可以按如下方式更改:

cmake -G "Ninja" -DCMAKE_BUILD_TYPE=Debug -S . -B ./build

输出应与以下图中所示的命令输出相似:

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/cmk-bst-prac-2e/img/B30947_02_05.jpg

图 2.5 – 检查 CMake 的 Ninja 生成器输出

这将导致 CMake 生成 Ninja 构建文件,而不是 Makefiles。

为了查看您环境中所有可用的生成器类型,可以执行 cmake --help 命令。可用的生成器将在 Help text generators 部分的末尾列出,如下所示:

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/cmk-bst-prac-2e/img/B30947_02_06.jpg

图 2.6 – 帮助中可用生成器的列表

带有星号的生成器是您当前环境的默认生成器。

更改编译器

在 CMake 中,要使用的编译器是通过每种语言的 CMAKE_<LANG>_COMPILER 变量来指定的。为了更改某种语言的编译器,必须将 CMAKE_<LANG>_COMPILER 参数传递给 Configure 命令。对于 C/C++ 项目,通常被覆盖的变量是 CMAKE_C_COMPILER(C 编译器)和 CMAKE_CXX_COMPILER(C++ 编译器)。编译器标志同样由 CMAKE_<LANG>_FLAGS 变量控制。此变量可用于存储与配置无关的编译器标志。

作为示例,让我们尝试在一个 g++-12 不是默认编译器的环境中使用它作为 C++ 编译器:

cmake -G "Unix Makefiles" -DCMAKE_CXX_COMPILER=/usr/bin/g++-12 -S .
   -B ./build

在这里,我们可以看到使用的是 g++-12,而不是系统默认的编译器 g++-11

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/cmk-bst-prac-2e/img/B30947_02_07.jpg

图 2.7 – 使用不同编译器(g++-10)配置项目

如果没有指定编译器,CMake 会优先在此环境中使用 g++-9

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/cmk-bst-prac-2e/img/B30947_02_08.jpg

图 2.8 – 无编译器偏好配置行为

将标志传递给编译器

为了说明如何指定编译器标志,假设你想启用所有警告并将其视为错误。这些行为分别通过 gcc 工具链中的 -Wall-Werror 编译器标志进行控制;因此,我们需要将这些标志传递给 C++ 编译器。以下代码说明了如何实现:

cmake -G "Unix Makefiles" -DCMAKE_CXX_FLAGS="-Wall -Werror"
-S . -B ./build

我们可以看到,在下面的示例中,命令中指定的标志(-Wall-Werror)被传递给了编译器:

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/cmk-bst-prac-2e/img/B30947_02_09.jpg

图 2.9 – 将标志传递给 C++ 编译器

构建标志可以通过在其后添加大写的构建类型字符串来为每种构建类型定制。以下列出了四个不同构建类型的四个变量。它们在根据编译器标志指定构建类型时非常有用。仅当配置的构建类型匹配时,指定在这些变量中的标志才有效:

  • CMAKE_<LANG>_FLAGS_DEBUG

  • CMAKE_<LANG>_FLAGS_RELEASE

  • CMAKE_<LANG>_FLAGS_RELWITHDEBINFO

  • CMAKE_<LANG>_FLAGS_MINSIZEREL

除了前面的示例,如果你只想在 Release 构建中将警告视为错误,构建类型特定的编译器标志可以让你做到这一点。

这是一个说明如何使用构建类型特定编译器标志的示例:

cmake -G "Unix Makefiles" -DCMAKE_CXX_FLAGS="-Wall -Werror" -DCMAKE_CXX_FLAGS_RELEASE="-fpermissive" -DCMAKE_BUILD_TYPE=Debug -S . -B ./build

请注意,在前面的命令中存在一个额外的 CMAKE_CXX_FLAGS_RELEASE 参数。只有在构建类型为 Release 时,这个变量中的内容才会被传递给编译器。由于构建类型被指定为 Debug,我们可以看到传递给编译器的标志中没有 -fpermissive 标志,如下图所示:

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/cmk-bst-prac-2e/img/B30947_02_10.jpg

图 2.10 – 基于构建类型指定标志;在 Debug 构建中缺少 -fpermissive 标志

图 2.10 中,注意到 -fpermissive 标志在构建命令中没有出现,而且 grep 的结果为空。这证实了 CMAKE_CXX_FLAGS_RELEASE 变量在 Debug 构建类型中没有被使用。当构建类型指定为 Release 时,我们可以看到 -O3 标志存在:

cmake -G "Unix Makefiles" -DCMAKE_CXX_FLAGS="-Wall -Werror" -
  DCMAKE_CXX_FLAGS_RELEASE="-fpermissive" -DCMAKE_BUILD_TYPE=
    Release -S . -B ./build

在这一行中,你告诉 CMake,在当前目录中配置 CMake 项目并将其构建到 build/ 文件夹,使用 “Unix Makefiles” 生成器。对于所有构建类型,毫不犹豫地将 -Wall 和 –Werror 标志传递给编译器。如果构建类型是 Release,还需要传递 -fpermissive 标志。

这是当构建类型设置为 Release 时命令的输出:

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/cmk-bst-prac-2e/img/B30947_02_11.jpg

图 2.11 – 基于构建类型指定标志;在 Release 构建中存在 -fpermissive 标志

图 2.11 中,我们可以确认 -fpermissive 标志也传递给了编译器。请注意,尽管 RelWithDebInfoMinSizeRel 也是 Release 构建,但它们与 Release 构建类型是不同的,因此在 CMAKE_<LANG>_FLAGS_RELEASE 变量中指定的标志不会应用到它们。

快捷方式 – 使用 CMake 预设

在命令行中使用 CMake 提供了大量的配置选项,这给予了对构建过程的很多控制。然而,它也可能变得相当困难,因为需要跟踪项目的各种配置所需的标志和参数组合。在 CMake 3.21 引入 CMake 预设 之前,跟踪构建项目所需的所有不同标志可能是一个相当大的挑战。但幸运的是,CMake 预设简化了很多繁琐的工作,因为几乎所有通过命令行传递给 CMake 的选项都可以在预设中表示。这就是为什么它们成为预先配置各种 CMake 选项组合的好方法。我们将在 第九章 中深入探讨 CMake 预设,创建可复现的构建环境,但如今越来越多的项目已经预先提供了预设。

要列出所有可用的预设,请使用以下命令:

cmake --list-presets

要使用预设调用来配置项目,请使用以下命令:

cmake --preset my-preset-name

提示

一旦你熟悉了如何配置 CMake 的基本选项,我们强烈建议使用 CMake 预设,以便轻松管理所有不同的构建配置、编译器标志等。

列出缓存变量

你可以通过执行 cmake -L ./build/ 命令列出所有缓存变量(见 图 2.12)。默认情况下,这不会显示与每个变量相关的高级变量和帮助字符串。如果你想同时显示它们,请改用 cmake -LAH ./build/ 命令。

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/cmk-bst-prac-2e/img/B30947_02_12.jpg

图 2.12 – CMake 导出的缓存变量列表

通过 CLI 构建配置好的项目

要构建配置好的项目,执行 cmake --build ./build 命令。

此命令告诉 CMake 构建已经在 构建文件夹中配置好的 CMake 项目

您也可以等效地执行 cd build && make。使用 cmake --build 的好处是它使您无需调用特定于构建系统的命令。当构建 CI 流水线或构建脚本时,它尤其有用。通过这种方式,您可以更改构建系统生成器,而不必更改构建命令。

您可以在以下示例中看到 cmake --build ./build 命令的输出示例:

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/cmk-bst-prac-2e/img/B30947_02_13.jpg

图 2.13 – 构建配置好的项目

并行构建

在执行构建命令时,您还可以自定义构建时间的细节。最显著的构建时间配置是用于构建项目的作业数量。要指定作业数,可以将 --parallel <job_count> 添加到您的 cmake --build 命令中。

要进行并行构建,执行 cmake --build ./build --parallel 2,其中数字 2 表示作业数。构建系统推荐的作业数量是最多每个硬件线程一个作业。在多核系统中,还建议使用比可用硬件线程数少至少一个作业数,以避免在构建过程中影响系统的响应能力。

注意

通常,您可以在每个硬件线程上使用多个作业并获得更快的构建时间,因为构建过程大多数是 I/O 限制的,但效果可能因人而异。请进行实验并观察。

此外,一些构建系统,如 Ninja,将尽量利用系统中可用的所有硬件线程,因此,如果您的目标是使用系统中的所有硬件线程,则为这些构建系统指定作业数是多余的。您可以通过在 Linux 环境中执行 nproc 命令来获取硬件线程数。

在期望在不同环境中调用的命令中,最好不要为依赖环境的变量使用固定值,例如 CI/CD 脚本和构建脚本。下面是一个示例 build 命令,利用 nproc 动态确定并行作业的数量:

cmake --build ./build/ --parallel $(($(nproc)-1))

让我们观察不同的作业数量如何影响构建时间。我们将使用 time 工具来测量每次命令执行的时间。环境详情如下:

  • 操作系统: Ubuntu 22.04

  • CPU: 第 11 代 Intel i9-11900H @2.5GHz

  • 内存: 32 GB

使用一个作业(--parallel 1),构建时间结果如下:

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/cmk-bst-prac-2e/img/B30947_02_14.jpg

图 2.14 – 使用一个作业的并行构建时间结果

使用两个作业(--parallel 4)的构建时间结果如下:

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/cmk-bst-prac-2e/img/B30947_02_15.jpg

图 2.15 – 使用两个任务的并行构建时间结果

即使是在一个非常简单的项目中,我们也能清楚地看到额外的任务如何帮助加快构建时间。

仅构建特定目标

默认情况下,CMake 会构建所有已配置的可用目标。由于构建所有目标并不总是理想的,CMake 允许通过 --target 子选项来构建目标的子集。该子选项可以多次指定,如下所示:

cmake --build ./build/ --target "ch2_framework_component1" --target
  "ch2_framework_component2"

此命令将构建范围限制为仅包括 ch2_framework_component1ch2_framework_component2 目标。如果这些目标还依赖于其他目标,它们也将被构建。

在构建之前删除之前的构建产物

如果你想执行一次干净的构建,可能需要先删除之前运行时生成的产物。为此,可以使用 --clean-first 子选项。这个子选项会调用一个特殊的目标,清除构建过程中生成的所有产物(执行 make clean)。

这里是一个如何为名为 build 的构建文件夹执行此操作的示例:

cmake --build ./build/ --clean-first

调试你的构建过程

正如我们在前面 传递标志给编译器 部分所做的那样,你可能希望检查在构建过程中哪些命令被调用,以及它们使用了哪些参数。--verbose 子命令指示 CMake 在支持详细模式的命令下,以详细模式调用所有构建命令。这使我们能够轻松调查棘手的编译和链接错误。

要以详细模式构建名为 build 的文件夹,请按如下示例调用 --build

cmake --build ./build/ --verbose

向构建工具传递命令行参数

如果你需要将参数传递给底层构建工具,可以在命令末尾添加 -- 并写下将要传递的参数:

cmake --build ./build/ -- --trace

在前述的情况下,--trace 将直接传递给构建工具,在我们例子中是 make。这将使 make 打印每个构建配方的追踪信息。

通过命令行接口安装项目

如果需要,CMake 本身允许将产物安装到环境中。为了做到这一点,CMake 代码必须已经使用 CMake install() 指令指定在调用 cmake --install(或构建系统等效命令)时要安装的内容。chapter_2 的内容已经以这种方式配置,以展示该命令。

我们将在 第四章中学习如何使 CMake 目标可安装,打包、部署和安装 CMake 项目

cmake --install 命令需要一个已经配置并构建过的项目。如果你还没有配置并构建 CMake 项目,请先配置并构建它。然后,发出 cmake --install <project_binary_dir> 命令来安装 CMake 项目。由于在我们的示例中 build 用作项目的二进制目录,<project_binary_dir> 将被替换为 build

以下图展示了 install 命令的示例:

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/cmk-bst-prac-2e/img/B30947_02_16.jpg

图 2.16 – 安装项目

默认安装目录在不同环境之间有所不同。在类 Unix 环境中,默认安装目录为 /usr/local,而在 Windows 环境中,默认安装目录为 C:/Program Files

提示

请记住,在尝试安装项目之前,项目必须已经构建完成。

为了能够成功安装项目,您必须具有适当的权限/许可,以便写入安装目标目录。

更改默认安装路径

要更改默认安装目录,您可以指定额外的 --prefix 参数,如此处所示,以更改安装目录:

cmake --install build --prefix /tmp/example

以下图展示了在调用 cmake --install 并使用 /tmp/example 前缀后,/tmp/example 文件夹的内容:

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/cmk-bst-prac-2e/img/B30947_02_17.jpg

图 2.17 – 将项目安装到不同路径

如此处所示,安装根目录已成功更改为 /tmp/example

安装时剥离二进制文件

在软件世界中,构建工件通常会捆绑一些额外的信息,例如调试所需的符号表。这些信息对于执行最终产品可能并不必要,并且可能大幅增加二进制文件的大小。如果您希望减少最终产品的存储占用,剥离二进制文件可能是一个不错的选择。剥离的另一个额外好处是,它使得逆向工程二进制文件变得更加困难,因为二进制文件中的关键信息符号被剥离掉了。

CMake 的 --install 命令允许在安装操作时剥离二进制文件。可以通过在 --install 命令中指定额外的 --strip 选项来启用此功能,如下所示:

cmake --install build --strip

在下面的示例中,您可以观察到未剥离和剥离二进制文件之间的大小差异。请注意,剥离静态库有其自身的限制,并且 CMake 默认情况下不会执行此操作。您可以在此图中看到未剥离二进制文件的大小:

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/cmk-bst-prac-2e/img/B30947_02_18.jpg

图 2.18 – 工件大小(未剥离)

使用剥离过的 (cmake –install build --strip) 二进制文件,大小差异如下图所示:

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/cmk-bst-prac-2e/img/B30947_02_19.jpg

图 2.19 – 工件大小(剥离)

仅安装特定组件(基于组件的安装)

如果项目在 install() 命令中使用了 CMake 的 COMPONENT 功能,您可以通过指定组件名称来安装特定组件。COMPONENT 功能允许将安装过程分为多个子部分。为了说明这个功能,chapter_2 示例被结构化为两个组件,分别命名为 librariesexecutables

要安装特定组件,需要在 cmake --install 命令中添加一个额外的 --component 参数:

cmake --install build --component ch2.executables

这是一个示例调用:

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/cmk-bst-prac-2e/img/B30947_02_20.jpg

图 2.20 – 仅安装特定组件

安装特定配置(仅适用于多配置生成器)

一些生成器支持相同构建配置的多种配置(例如,Visual Studio)。对于这种生成器,--install 选项提供了一个额外的 --config 参数,用于指定要安装的二进制文件的配置。

这是一个示例:

cmake --install build --config Debug

注意

正如你可能注意到的,示例中使用的命令参数非常长且明确。这是故意的。明确指定参数可以确保我们每次执行时都能得到一致的结果,无论在哪个环境下运行我们的命令。例如,如果没有 -G 参数,CMake 会默认使用环境中的首选构建系统生成器,这可能不是我们想要的结果。我们的座右铭是,明确总比隐含好。明确指定参数可以使我们的意图更清晰,并自然地使得在 CI 系统/脚本中编写更具未来兼容性和可维护性的 CMake 代码。

我们已经讲解了 CMake 命令行用法的基础知识。接下来让我们继续学习 CMake 的另一种可用界面形式——CMake 的图形界面。

使用 CMake-GUI 和 ccmake 进行高级配置

虽然它们看起来不同,但大多数界面做的事情基本相同;因此,我们在上一部分已经覆盖的内容在这里同样有效。记住,我们将改变的是交互的形式,而不是我们实际交互的工具。

注意

在继续之前,请检查你的终端中是否可以使用 ccmake 命令。如果不能,请确认你的 PATH 变量是否设置正确,并检查你的安装情况。

学习如何使用 ccmake(CMake curses GUI)

ccmake 是基于终端的 ncurses

由于 ccmake 并不是默认安装的 CMake 包的一部分,它需要单独安装,可以通过操作系统的包管理器安装,或者从 CMake 官方网站下载并安装。使用 ccmake 和在 CLI 中使用 CMake 完全相同,只是它无法调用构建和安装步骤。主要的区别是,ccmake 会显示一个基于终端的图形界面,便于交互式地编辑缓存的 CMake 变量。当你在尝试设置时,这是一个非常方便的工具。ccmake 的状态栏会显示每个设置项的描述及其可能的值。

要开始使用 ccmake,在项目配置步骤中使用 ccmake 代替 cmake。在我们的示例中,我们将完全复制之前在 通过 CLI 配置项目 部分中讲解的命令行示例:

ccmake -S . -B ./build

以下是前面命令的示例输出:

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/cmk-bst-prac-2e/img/B30947_02_21.jpg

图 2.21 – ccmake 主屏幕

执行命令后,将出现基于终端的 UI。初始页面是主页面,可以在其中编辑 CMake 变量。EMPTY CACHE表示没有进行过先前的配置,CMake 缓存文件(CMakeCache.txt)目前为空。要开始编辑变量,必须首先进行项目配置。按键盘上的C键即可进行配置,如Keys:部分所示。

按下C键后,将执行 CMake 配置步骤,并显示带有配置输出的日志输出屏幕:

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/cmk-bst-prac-2e/img/B30947_02_22.jpg

图 2.22 – 配置后 ccmake 日志屏幕

要关闭日志输出屏幕并返回主屏幕,请按E键。返回后,你会发现EMPTY CACHE已被CMakeCache.txt文件中的变量名替换。要选择一个变量,使用键盘上的上下箭头键。当前选中的变量会以白色高亮显示,如下图所示:

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/cmk-bst-prac-2e/img/B30947_02_23.jpg

图 2.23 – 配置后 ccmake 主屏幕

在前面的截图中,选择了CMAKE_BUILD_TYPE变量。在右侧,显示了 CMake 变量的当前值。对于CMAKE_BUILD_TYPE,目前它是空的。变量值旁边的星号表示该变量的值在先前的配置中刚刚发生了变化。你可以按Enter键编辑它,或者按键盘上的D键删除它。下图展示了更改变量后的ccmake主屏幕:

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/cmk-bst-prac-2e/img/B30947_02_24.jpg

图 2.24 – 变量更改后的 ccmake 主屏幕

让我们将CMAKE_BUILD_TYPE设置为Release并重新配置:

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/cmk-bst-prac-2e/img/B30947_02_25.jpg

图 2.25 – ccmake 配置输出(Release)

我们可以观察到,构建类型现在已设置为Release。返回上一屏幕,按下g(生成)按钮以保存更改。按下q(不生成,退出)按钮可以丢弃更改。

要编辑其他变量,例如CMAKE_CXX_COMPILERCMAKE_CXX_FLAGS,需要启用高级模式。通过调用mark_as_advanced() CMake 函数,这些变量默认被标记为高级标志,因此它们在图形界面中默认是隐藏的。在主屏幕上,按t键切换到高级模式:

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/cmk-bst-prac-2e/img/B30947_02_26.jpg

图 2.26 – 高级模式下的 ccmake

启用高级模式后,一整套新选项会变得可见。你可以像普通变量一样观察和修改它们的值。你可能已经注意到,之前隐藏的变量CHAPTER2_BUILD_DRIVER_APPLICATION现在出现了。这是一个用户定义的 CMake 变量。该变量定义如下:

# Option to exclude driver application from build.
set(CHAPTER2_BUILD_DRIVER_APPLICATION TRUE CACHE BOOL "Whether to
ccmak  include driver application in build. Default: True")
# Hide this option from GUI's by default.
mark_as_advanced(CHAPTER2_BUILD_DRIVER_APPLICATION)

CHAPTER2_BUILD_DRIVER_APPLICATION变量被定义为布尔类型的缓存变量,默认值为true。它被标记为高级选项,因此在非高级模式下不会显示。

通过 cmake-gui 使用 CMake

如果你是那种觉得命令行界面(CLI)不直观,或者你更喜欢 GUI 而不是 CLI 的人,CMake 也提供了一个跨平台的 GUI。与ccmake相比,cmake-gui提供了更多功能,如环境编辑器正则表达式资源管理器

CMake GUI 并不总是默认包含在 CMake 安装中;根据使用的操作系统,它可能需要单独安装。它的主要目的是允许用户配置 CMake 项目。要启动cmake-gui,可以在终端中输入cmake-gui命令。在 Windows 上,它也可以从开始菜单找到。如果这些方法都无法工作,请进入 CMake 安装路径,它应该位于bin\目录中。

注意

如果你在 Windows 环境下启动cmake-gui,并打算使用 Visual Studio 提供的工具链,请从 IDE 的相应“本地工具命令提示符”启动cmake-gui。如果你有多个版本的 IDE,请确保使用正确的本地工具命令提示符。否则,CMake 可能无法找到所需的工具(如编译器),或者可能会找到错误的工具。有关详细信息,请参考docs.microsoft.com/en-us/visualstudio/ide/reference/command-prompt-powershell?view=vs-2019

这是 CMake GUI 的主窗口:

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/cmk-bst-prac-2e/img/B30947_02_27.jpg

图 2.27 – CMake GUI 主窗口

CMake GUI 的主屏幕基本包含以下内容:

  • 源代码路径字段

  • 输出路径字段

  • 预设选择列表

  • 配置生成按钮

  • 缓存变量列表

这些是我们将要交互的四个基本内容。要开始配置项目,请通过点击浏览源代码...按钮选择项目的根目录。然后,通过点击浏览构建...按钮选择项目的输出目录。此路径将是通过所选生成器生成的输出文件的路径。

如果项目包含 CMake 预设,可以从预设列表中选择预设。任何由预设修改的缓存变量将显示在缓存变量列表中。在以下示例中,选择了一个配置 Clang 13 作为编译器,并将调试作为构建类型的预设:

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/cmk-bst-prac-2e/img/B30947_02_28.jpg

图 2.28 – 从 CMake GUI 选择预设

设置源路径和输出路径后,点击配置以开始配置选定的项目。CMake GUI 将允许你选择生成器、平台选择(如果生成器支持)、工具集和编译器等详细信息,如下图所示:

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/cmk-bst-prac-2e/img/B30947_02_29.jpg

图 2.29 – CMake GUI 生成器选择界面

根据你的环境填写这些详细信息后,点击完成继续。CMake GUI 将开始使用给定的详细信息配置你的项目,并在日志区域报告输出。成功配置后,你还应该能在缓存变量列表区域看到缓存变量:

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/cmk-bst-prac-2e/img/B30947_02_30.jpg

图 2.30 – 配置后的 CMake GUI

如果一切正常,点击.sln.cxxproj以及其他文件。生成项目后,makefiles),然后会显示生成的文件。之后,你可以使用 IDE 来构建项目。

重要提示

请注意,生成的项目只是生成器的产物,对生成的项目文件(.sln.cxxproj)所做的更改不会被保存,并将在下次生成时丢失。修改CMakeLists.txt文件或编辑CMakeCache.txt文件(无论是直接还是间接)时,别忘了重新生成项目文件。对于版本控制,应该将生成的项目文件视为构建产物,不应将其添加到版本控制中。你可以通过适当的生成器在 CMake 中重新生成项目,随时从头开始获取它们。

有时,项目可能需要调整某些缓存变量,或者你可能决定使用不同的构建类型。例如,要更改任何缓存变量,点击所需缓存变量的值;它应变为可编辑。根据变量类型,可能会显示复选框而不是字符串。如果所需变量未在列表中显示,它可能是高级变量,只有在cmake-gui处于高级模式时才能看到。

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/cmk-bst-prac-2e/img/B30947_02_31.jpg

图 2.31 – cmake-gui 高级模式

调整任何缓存值后,点击配置,然后点击生成以应用更改。

提示

另一个有用的功能是分组功能,它允许将缓存变量根据其公共前缀进行分组(如果存在)。组名由变量名的第一部分决定,直到第一个下划线为止。

我们已经涵盖了 cmake-gui 的最基本功能。在继续学习其他杂项内容之前,如果你需要重新加载缓存值或删除缓存并从头开始,你可以在文件菜单中找到重新加载缓存删除缓存菜单项。

调整环境变量

CMake GUI 提供了一个便捷的环境变量编辑器,允许对环境变量执行增、删、改、查操作。要访问它,只需点击主屏幕上的环境变量…按钮。点击后,环境变量编辑器窗口将弹出,如下图所示:

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/cmk-bst-prac-2e/img/B30947_02_32.jpg

图 2.32 – CMake GUI 环境变量编辑器

环境变量编辑器窗口包含当前环境中存在的环境变量列表。要编辑环境变量,只需双击表格中所需环境变量的值字段。该窗口还允许使用添加条目删除条目按钮来添加和删除信息。

使用 CMake 评估正则表达式

你是否曾经想过,CMake 是如何评估正则表达式的,它到底会给出什么结果?如果是的话,你可能以前通过 message() 手动调试它,打印正则表达式匹配结果变量。那如果我告诉你有一种更好的方法呢?让我向你介绍 CMake GUI 中的正则表达式浏览器工具:

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/cmk-bst-prac-2e/img/B30947_02_33.jpg

图 2.33 – CMake GUI 正则表达式浏览器

这个隐藏的宝藏让你可以使用 CMake 的正则表达式引擎调试正则表达式。它位于工具菜单中,名称为正则表达式浏览器…。使用起来非常简单:

  1. 将表达式输入到正则表达式字段中。

    该工具将检查表达式是否有效。如果有效,屏幕上的有效文本将显示为绿色。如果 CMake 的正则表达式引擎不喜欢你给出的表达式,它将变为红色。

  2. 将测试字符串输入到输入文本字段中。正则表达式将与此文本进行匹配。

  3. 如果有任何匹配,窗口上的匹配字样将从红色变为绿色。匹配的字符串将显示在完全匹配字段中。

  4. 匹配时,捕获组将分别分配给匹配 1匹配 2、… 匹配 N,如果有的话。

在本节中,我们学习了如何使用 CMake 的本地图形界面。接下来,我们将继续通过了解一些 CMake 的 IDE 和编辑器集成来学习如何使用 CMake。

在 Visual Studio、VSCode 和 Qt Creator 中使用 CMake

作为软件开发中的常用工具,CMake 与各种 IDE 和源代码编辑器都有集成。在使用 IDE 或编辑器时,利用这些集成可能对用户来说更加方便。本节将介绍 CMake 如何与一些流行的 IDE 和编辑器集成。

如果你期待的是如何使用 IDE 或编辑器的指南,那么这一部分不涉及这方面的内容。本节的重点是探索并了解 CMake 与这些工具的集成。假设你已经具备与将要交互的 IDE/编辑器的使用经验。

让我们从 Visual Studio 开始。

Visual Studio

Visual Studio是支持 CMake 的后来的参与者之一。与其他流行的 IDE 不同,Visual Studio 直到 2017 年才开始原生支持 CMake。在那一年,微软决定行动,推出了内置支持 CMake 项目的功能,并随 Visual Studio 2017 一起发布。从那时起,这成为了 Visual Studio IDE 的一个重要功能。

要开始使用,请获取 Visual Studio 2017 或更高版本的副本。对于旧版本的 Visual Studio,这个功能完全不可用。在我们的示例中,我们将使用 Visual Studio 2022 社区版。

从头开始创建 CMake 项目

Visual Studio 的项目创建功能基于项目模板。从 Visual Studio 2017 及以后版本,项目模板中也包含了 CMake 项目模板。我们将学习如何使用这个模板来创建新的 CMake 项目。

要使用 Visual Studio 创建一个新的 CMake 项目,请点击欢迎页面上的创建新项目按钮。或者,你也可以通过点击文件 | 新建 | 项目来访问,或者使用Ctrl + Shift + N新建项目)快捷键。Visual Studio 2022 的欢迎屏幕如下所示:

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/cmk-bst-prac-2e/img/B30947_02_34.jpg

图 2.34 – Visual Studio 2022 欢迎屏幕

创建新项目屏幕上,双击项目模板列表中的CMake 项目。你可以通过使用位于列表顶部的搜索栏来筛选项目模板:

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/cmk-bst-prac-2e/img/B30947_02_35.jpg

图 2.35 – Visual Studio 2022 创建新项目屏幕

点击CMakeProject1之后。

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/cmk-bst-prac-2e/img/B30947_02_36.jpg

图 2.36 – Visual Studio 2022 新项目配置屏幕

填写完详细信息后,点击CMakeLists.txt文件、C++源文件和一个 C++头文件,文件名与选择的项目名称相同。新创建的项目布局如下图所示:

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/cmk-bst-prac-2e/img/B30947_02_37.jpg

图 2.37 – 使用 Visual Studio 创建新 CMake 项目后的第一印象

打开现有 CMake 项目

要打开一个现有的 CMake 项目,请转到项目的 CMakeLists.txt 文件。下图显示了 Open 菜单的样子:

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/cmk-bst-prac-2e/img/B30947_02_38.jpg

图 2.38 – CMake 项目打开菜单

接下来,让我们看看如何配置和构建 CMake 项目。

配置和构建 CMake 项目

要在 Visual Studio 中构建 CMake 项目,请进入 configure 步骤并生成所需的构建系统文件。配置完成后,点击 Build | Build All 来构建项目。你也可以通过使用 F7 快捷键来触发 Build All

请注意,每当你保存 CMakeLists.txt 文件时,Visual Studio 会自动调用 configure,该文件是项目的一部分。

执行 CMake 目标上的常见操作

Visual Studio 使用 启动目标 概念来进行需要目标的操作,如构建、调试和启动。要将 CMake 目标设置为启动目标,请使用工具栏上的 Select Startup Target 下拉框。Visual Studio 会在配置时自动将 CMake 目标填充到这个下拉框中:

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/cmk-bst-prac-2e/img/B30947_02_39.jpg

图 2.39 – 启动目标选择下拉菜单

设置启动目标后,你可以像在 Visual Studio 中一样调用调试、构建或启动等操作:

  1. 要进行调试,首先点击 Debug | Startup Target,然后点击 Debug | Start Debugging 或使用 F5 快捷键。

  2. 要在不调试的情况下启动,请点击 Start without debug 或使用 Ctrl + F5 快捷键。

  3. 要进行构建,点击 Build,点击 Build | Build ,或使用 Ctrl + B 快捷键。

    按钮位置如下面的图所示:

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/cmk-bst-prac-2e/img/B30947_02_40.jpg

图 2.40 – 工具栏按钮位置

本节我们已经介绍了 Visual Studio CMake 集成的基础知识。在下一节中,我们将继续学习另一个 Microsoft 产品——VSCode。

Visual Studio Code

VSCode 是微软开发的开源代码编辑器。它不是一个 IDE,但通过扩展可以变得强大并拥有类似 IDE 的功能。扩展市场有各种各样的附加内容,从主题到语言服务器。你几乎可以找到任何东西的扩展,这使得 VSCode 既强大又受到广泛用户的喜爱。毫不奇怪,VSCode 也有官方的 CMake 扩展。该扩展最初由 Colby Pike(也被称为 vector-of-bool)开发,但现在由 Microsoft 官方维护。

本节我们将学习如何安装扩展并使用它执行基本的 CMake 任务。

在继续之前,VSCode 必须已经安装在你的环境中。如果没有,请访问 code.visualstudio.com/learn/get-started/basics 获取下载和安装的详细信息。

同时,我们将频繁访问命令面板。强烈建议经常使用它,以便熟悉它。对于那些问“命令面板到底是什么?”的人,下面是一个截图:

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/cmk-bst-prac-2e/img/B30947_02_41.jpg

图 2.41 – VSCode 命令面板

是的,就是那个东西。说实话,直到现在我才知道它有个名字。访问命令面板的快捷键是F1Ctrl + Shift + P。命令面板是 VSCode 的核心,它能加速 VSCode 的工作流程。

安装扩展

安装扩展是相当简单的。你可以通过 CLI 安装,使用以下命令(如果你使用的是 Insiders 版本,请将code替换为code-insiders):

code --install-extension ms-vscode.cmake-tools

另外,你也可以通过 VSCode 的图形界面做同样的操作。打开 VSCode 并在扩展搜索框中输入CMake Tools,然后选择CMake Tools(由Microsoft提供)。要小心不要与 CMake 扩展混淆。点击安装按钮来安装:

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/cmk-bst-prac-2e/img/B30947_02_42.jpg

图 2.42 – VSCode 扩展市场

安装完成后,扩展就可以使用了。

快速开始项目

VSCode CMake Tools 扩展提供了一个cmake quick start。选择CMake: Quick Start并按下键盘上的Enter键。

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/cmk-bst-prac-2e/img/B30947_02_43.jpg

图 2.43 – 命令面板 – 定位 CMake: Quick Start

首先,扩展会询问使用哪个工具链。选择适合你新项目的工具链。关于工具链的更多信息将在处理工具链部分中讨论。

在选择好工具链后,系统会提示你输入项目名称。这将是你顶级 CMake 项目的名称。输入你选择的名称。

最后,将显示一个示例应用程序代码的选择。在此选择中,你将被要求创建一个可执行应用程序项目或一个库项目。选择其中之一,瞧!你就拥有了一个工作中的 CMake 项目。选择后,CMakeLists.txtmain.cpp文件将被生成。这些文件的内容在可执行文件和库的选择之间稍有不同。

打开现有项目

在 VSCode 中打开 CMake 项目并没有什么特别的。只需打开包含项目顶级CMakeLists.txt文件的文件夹。CMake Tools 扩展将自动识别该文件夹为 CMake 项目,所有与 CMake 相关的命令将会在 VSCode 的命令面板上可用。打开现有项目时,系统会询问是否配置该项目。

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/cmk-bst-prac-2e/img/B30947_02_44.jpg

图 2.44 – VSCode 询问是否配置现有项目

如果项目支持 CMake 预设,你将自动被询问选择哪个预设。如果项目不支持预设,那么你将被要求选择一个编译器工具链,稍后在本章的处理 kits 部分将对此进行解释。

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/cmk-bst-prac-2e/img/B30947_02_45.jpg

图 2.45 – 在 VSCode 中选择 CMake 预设

至此,我们已经准备好配置项目。

配置、构建和清理项目

要配置一个 CMake 项目,从命令面板中选择CMake: Configure菜单项。要构建项目,选择构建目标,点击CMake: Set Build Target菜单项。这将让你选择在触发构建时将构建哪个目标。最后,选择CMake: Build以构建选定的构建目标。如果要在不将其设置为构建目标的情况下构建特定目标,可以使用CMake: Build Target菜单项。

要清理构建产物,请使用clean目标并删除所有构建产物。

调试目标

要调试一个目标,选择CMake: Set Debug Target菜单项,从命令面板中选择调试目标。你将看到列出所有可调试的目标:

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/cmk-bst-prac-2e/img/B30947_02_46.jpg

图 2.46 – 调试目标选择

选择目标并从命令面板中选择CMake: DebugCtrl + F5)。选定的目标将在调试器下启动。

如果你想在不使用调试器的情况下运行选定的目标,请选择CMake: Run Without DebuggingShift + F5)。

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/cmk-bst-prac-2e/img/B30947_02_47.jpg

图 2.47 – 正在调试的可执行 Chapter1 目标

在下一节中,我们将讨论如何为调试目标提供参数。

向调试目标传递参数

你要调试的目标可能需要命令行参数。要向调试目标传递命令行参数,请打开 VSCode 的 settings.json 并追加以下行:

"cmake.debugConfig": {
        "args": [
            "<argument1>",
            "<argument2>"
        ]
    }

args JSON 数组中,你可以放置目标所需的任何数量的参数。这些参数将无条件地传递给所有未来的调试目标。如果你想对参数进行精细控制,最好还是定义一个 launch.json 文件。

处理 kits

CMake Tools 扩展中的 kit 代表了一组可用于构建项目的工具组合;因此,kit 这个术语几乎可以视为工具链的同义词。Kit 使得在多编译器环境中工作变得更加简便,允许用户选择使用哪种编译器。Kit 可以通过扩展自动发现,或者通过工具链文件读取,或由用户手动定义。

要查看项目的可用 kits,请从命令面板中选择CMake: Select a Kit菜单项(F1Ctrl + Shift + P)。

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/cmk-bst-prac-2e/img/B30947_02_48.jpg

图 2.48 – Kit 选择列表

选择的工具包将用于配置 CMake 项目,这意味着工具包中定义的工具将用于编译该项目。选择工具包将自动触发 CMake 配置。

默认情况下,扩展会自动扫描工具包。因此,发现的工具链会作为选项列在工具包选择菜单中。如果您的工具链没有显示在这里,这意味着 CMake Tools 未能发现它。在这种情况下,首先尝试重新扫描工具包。如果仍然没有显示,您可以手动将其添加到用户本地的 cmake-tools-kits.json (1) 文件中来定义额外的工具包。

通常不需要添加新的工具包,因为扩展可以很好地自动发现工具链。如果遇到异常情况失败,这里有一个工具包模板,您可以自定义并将其附加到用户本地的 cmake-tools-kits.json 文件中,以定义一个新的工具包。要打开用户本地的工具包文件,请从命令面板中选择 CMake: 编辑用户本地 CMake 工具包 菜单项:

  {
    "name":"<name of the kit>",
    "compilers" {
      "CXX":"<absolute-path-to-c++-compiler>",
      "C": "<absolute-path-to-c-compiler>"
    }
  }

注意

在较旧版本的 CMake Tools 扩展中,cmake-tools-kits.json 文件可能被命名为 cmake-kits.json

请记住,如果您的工具包名称与 CMake Tools 自动生成的名称冲突,CMake Tools 在扫描时会覆盖您的条目。因此,请始终为您的工具包定义提供唯一的名称。

有关工具包的更多信息,请参阅 github.com/microsoft/vscode-cmake-tools/blob/dev/gcampbell/KitCmakePath/docs/kits.md

Qt Creator

Qt Creator 是另一个支持 CMake 项目的 IDE。CMake 支持相当不错,并且默认提供,无需额外的插件。在本节中,我们将快速了解 Qt Creator 对 CMake 的支持。

和往常一样,确保您的 IDE 已正确安装并在环境中配置好。

本示例使用的是 Qt Creator 版本 5.0.1。

添加您的 CMake 安装

为了在 Qt Creator 中使用 CMake,必须在 Qt Creator 中定义 CMake 的路径。要查看和定义 CMake 路径,请导航至 编辑 | 首选项 | CMake

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/cmk-bst-prac-2e/img/B30947_02_49.jpg

图 2.49 – Qt Creator CMake 路径设置

根据 CMake 的安装方式,Qt Creator 可能能够自动检测到正确的版本。如果没有,您可以手动配置它。要选择在 Qt Creator 中运行的 CMake 可执行文件,请选择所需的条目并点击 设置为默认 按钮。

要添加新的 CMake 可执行文件,点击 添加。这将把一个新的条目添加到 手动 部分,并弹出一个窗口,您可以在其中填写新条目的详细信息:

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/cmk-bst-prac-2e/img/B30947_02_50.jpg

图 2.50 – 添加新的 CMake 可执行文件

该窗口中的字段在这里有详细描述:

  • 名称:用于区分新 CMake 可执行文件条目的唯一名称。

  • cmake/cmake.exe

  • 版本:CMake 的版本(由 Qt Creator 推测)。

  • 帮助文件:可选的 Qt Creator 帮助文件,用于该可执行文件。这样在按下 F1 时,CMake 帮助文件会显示出来。

  • CMakeLists.txt 文件的更改。

填写完详细信息后,点击 应用 将新的 CMake 可执行文件添加到 Qt Creator 中。如果你希望 Qt Creator 使用它,别忘了将其设置为默认。

创建 CMake 项目

在 Qt Creator 中创建 CMake 项目遵循与创建常规项目相同的步骤。Qt Creator 不将 CMake 视为外部构建系统生成器。相反,它允许用户在三种构建系统生成器之间进行选择,分别是 qmakecmakeqbs。任何类型的 Qt 项目都可以通过这些构建系统生成器中的任意一种从头开始创建。

要在 Qt Creator 中创建 CMake 项目,请点击 文件 | 新建文件或项目... (Ctrl + N),然后在 新建文件或项目 窗口中选择项目类型。我们以 Qt Widgets 应用程序 作为示例。

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/cmk-bst-prac-2e/img/B30947_02_51.jpg

图 2.51 – Qt Creator 新建文件或项目窗口

选择后,项目创建向导将出现。根据需要填写详细信息。在 定义构建系统 步骤中选择 CMake,如以下截图所示:

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/cmk-bst-prac-2e/img/B30947_02_52.jpg

图 2.52 – Qt Creator 新建项目向导中的构建系统选择

就是这样!你已经创建了一个带有 CMake 构建系统的 Qt 应用程序。

下图展示了一个新创建的 CMake 项目:

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/cmk-bst-prac-2e/img/B30947_02_53.jpg

图 2.53 – 生成的基于 CMake 的 Qt 小部件应用程序项目

打开现有的 CMake 项目

要在 Qt Creator 中打开现有的 CMake 项目,请点击 文件 | 打开文件或项目... (Ctrl + O) 菜单项。选择项目的顶层 CMakeLists.txt 文件,然后点击 打开。Qt Creator 会提示你选择一个工具链(kit)来构建项目。选择你首选的工具链后,点击 配置项目 按钮。项目将被打开,并且 CMake 配置步骤会使用所选工具链执行。

例如,以下图所示的是使用 Qt Creator 打开的 CMake Best Practices 项目:

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/cmk-bst-prac-2e/img/B30947_02_54.jpg

图 2.54 – 在 Qt Creator 中查看 CMake Best Practices 示例项目

第一次打开 CMake 项目时,Qt Creator 会在项目的根目录中创建一个名为 CMakeLists.txt.user 的文件。该文件包含一些 Qt 特有的细节,这些细节不能存储在 CMakeLists.txt 文件中,例如工具链信息和编辑器设置。

配置和构建

在大多数情况下(例如,打开项目并保存对 CMakeLists.txt 的更改),Qt Creator 会自动运行 CMake 配置,而无需手动执行。若要手动运行 CMake 配置,请点击 Build | Run CMake 菜单项。

配置完成后,点击最左侧的锤子图标以构建项目。或者,可以使用 Ctrl + B 快捷键。这将构建整个 CMake 项目。若要仅构建特定的 CMake 目标,请使用位于 cm 旁边的定位器,然后按下空格键。

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/cmk-bst-prac-2e/img/B30947_02_55.jpg

图 2.55 – Qt Creator 定位器建议

定位器将显示可构建的 CMake 目标。可以通过高亮选择目标并按 Enter 键,或直接用鼠标点击目标来选择。

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/cmk-bst-prac-2e/img/B30947_02_56.jpg

图 2.56 – 定位器中显示的可用 CMake 构建目标

选择的 CMake 目标(以及其依赖项)将被构建。

运行和调试

要运行或调试一个 CMake 目标,请按 Kit 选择器按钮(左侧导航栏中的计算机图标),并选择 CMake 目标。然后,点击运行按钮(Kit 选择器下方的 播放图标)来运行,或者点击调试按钮(带有错误的 播放图标)来调试。

下图显示了 Kit 选择器菜单的内容:

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/cmk-bst-prac-2e/img/B30947_02_57.jpg

图 2.57 – 显示 CMake 目标的 Kit 选择器

在这里,我们结束了使用 CMake 和 Qt Creator 的基础内容。有关更高级的主题,您可以参考 进一步阅读 部分提供的资源。

总结

在本章中,我们介绍了与 CMake 交互的基本方法,包括 CLI 和 GUI。我们还讨论了各种 IDE 和编辑器的集成,它们对于日常工作流程至关重要。使用任何工具都需要了解如何与其交互。学习交互方式使我们能够更好地利用工具本身,也能帮助我们更轻松地达成目标。

在下一章中,我们将讨论 CMake 项目的构建块,这将使你能够从零开始创建一个结构良好、适合生产的 CMake 项目。

问题

为了巩固你在本章学到的内容,试着回答以下问题。如果你在回答时遇到困难,请返回相关章节并重新阅读该主题:

  1. 描述如何通过命令行接口(CLI)将 CMake 项目配置到项目根目录下的构建文件夹,涉及以下各项:

    1. 另一个 C++ 编译器,位于 /usr/bin/clang++

    2. Ninja 生成器

    3. -Wall 编译器标志,用于 Debug 构建类型

  2. 描述如何使用命令行和 CMake 构建之前在 Q1 中配置的项目,涉及以下各项:

    1. 八个并行任务

    2. Unix Makefiles 生成器中的 --trace 选项

  3. 描述如何使用 directory/opt/project 命令行通过 CMake 安装之前在 Q1 中构建的项目?

  4. 假设 CMake-Best-Practices 项目已经配置并构建完成,必须执行哪个命令来仅安装 ch2.libraries 组件?

  5. CMake 中的高级变量是什么?

答案

  1. 下面是答案:

    1. cmake –S . -B ./build -DCMAKE_CXX_COMPILER:STRING= "/``usr/bin/clang++ "

    2. cmake –S . -B ./build -``G "Ninja"

    3. cmake –S . -B ./build -``DCMAKE_BUILD_FLAGS_DEBUG:STRING= "-Wall"

  2. 在 Q1 中之前配置的项目可以通过以下命令使用 CMake 在命令行中构建:

    1. cmake --build ./build --``parallel 8

    2. cmake --build ./build -- VERBOSE=1

  3. cmake --install ./``build --prefix=/opt/project

  4. cmake --install ./build --``component ch2.libraries

  5. 它是一个 CMake 缓存变量,标记为 高级,通过 mark_as_advanced() 函数使其在图形界面中隐藏。

进一步阅读

本章讨论的主题有很多相关的指南和文档。你可以在这里找到一份不完全的推荐阅读材料清单:

第三章:创建一个 CMake 项目

到现在为止,你应该已经熟悉了如何使用 CMake 及其基本概念,如两阶段构建。目前,我们只讨论了如何使用 CMake 与现有代码配合,但更有趣的部分是如何使用 CMake 构建应用程序。在本章中,你将学习如何构建可执行文件和库,并学习如何将它们一起使用。我们将深入探讨创建不同类型的库,并展示一些关于如何构建 CMake 项目的好实践。由于库通常伴随着多种编译器设置,我们将学习如何设置它们,并在必要时将这些设置传递给依赖库。由于项目中的依赖关系可能变得相当复杂,我们还将学习如何可视化不同目标之间的依赖关系。

本章将涵盖以下主题:

  • 设置项目

  • 创建一个“hello world”可执行文件

  • 创建一个简单的库

  • 整合它们

技术要求

和之前的章节一样,所有示例都已使用 CMake 3.21 测试,并在以下编译器之一上运行:

  • GCC 9 或更新版本

  • Clang 12 或更新版本

  • MSVC 19 或更新版本

本章的所有示例和源代码可以在本书的 GitHub 仓库中找到,github.com/PacktPublishing/CMake-Best-Practices---2nd-Edition,在 chapter03 子文件夹中。

设置项目

虽然 CMake 可以处理几乎任何项目文件结构,但有一些关于如何组织文件的好实践。本书中的示例遵循以下常见模式:

├── CMakeLists.txt
├── build
├── include/project_name
└── src

在一个最小化项目结构中,包含三个文件夹和一个文件。它们如下:

  • build:存放build文件和二进制文件的文件夹。当克隆一个新项目时,通常不会看到 build 文件夹,因为它将由 CMake 生成。它通常被命名为 build,但也可以有任何名称。

  • include/project_name:该文件夹包含所有外部可访问的公共头文件。添加一个包含项目名称的子文件夹是有帮助的,因为头文件的引用通常是通过 <project_name/somefile.h> 完成的,这样更容易判断头文件来自哪个库。

  • src:这个文件夹包含所有私有的源文件和头文件。

  • CMakeLists.txt:这是根 CMake 文件。build 文件夹几乎可以放在任何位置。将其放在项目根目录下是非常方便的。然而,我们强烈建议避免选择任何非空文件夹作为 build 文件夹。特别是,将构建文件放入 includesrc 中被认为是一个不好的做法。通常会有 testdoc 等附加文件夹,用于组织测试和文档页面。

使用嵌套项目

当你将项目嵌套在彼此内部时,每个项目应当映射上面的文件结构,并且每个CMakeLists.txt文件应编写成使子项目能够独立构建。这意味着每个子项目的CMakeLists.txt文件应该指定cmake_minimum_required,并可选择性地定义项目。我们将在第十章中深入讨论大型项目和超级构建,处理分布式仓库和依赖关系的超级构建

嵌套项目看起来像这样:

├── CMakeLists.txt
├── build
├── include/project_name
├── src
└── subproject
    ├── CMakeLists.txt
    ├── include
    │   └── subproject
    └── src

在这里,文件夹结构在subproject文件夹中得到了重复。坚持这种文件夹结构并使子项目能够独立构建,可以更容易地移动项目。这也允许开发人员只构建项目的一部分,这在大型项目中尤为有用,因为在这些项目中,构建时间可能会相当长。

现在我们已经完成了文件结构的设置,接下来让我们从创建一个简单的独立可执行文件开始,不涉及任何特殊的依赖项。本章后面,我们将创建各种类型的库并将它们组合在一起。

创建一个“hello world”可执行文件

首先,我们将从一个简单的 hello world C++程序创建一个简单的可执行文件。下面的 C++程序将打印出Welcome to CMakeBest Practices

#include <iostream>
int main(int, char **) {
  std::cout << "Welcome to CMake Best Practices\n";
  return 0;
}

要构建这个,我们需要编译它并给可执行文件命名。让我们看看用来构建这个可执行文件的CMakeLists.txt文件长什么样:

cmake_minimum_required(VERSION 3.21)
project(
    hello_world_standalone
    VERSION 1.0
    DESCRIPTION "A simple C++ project"
    HOMEPAGE_URL  https://github.com/PacktPublishing/CMake-Best-Practices---2nd-Edition
    LANGUAGES CXX
)
add_executable(hello_world)
target_sources(hello_world PRIVATE src/main.cpp)

在第一行cmake_minimum_required(VERSION 3.21)中,我们告诉 CMake 预期使用的 CMake 版本以及 CMake 将启用哪些功能。如果尝试使用低于指定版本的 CMake 构建这个项目,会导致错误。在本书中,我们使用 CMake 3.21 进行所有示例,但为了兼容性,你可以选择较低的版本。

对于这个例子,版本 3.1 将是绝对的最低要求,因为在此之前,target_sources命令是不可用的。一个好的做法是将cmake_minimum_required命令放在每个CMakeLists.txt文件的顶部。

接下来,使用project()命令设置项目。第一个参数是项目的名称——在我们的例子中是"hello_world_standalone"

接下来,项目的版本设置为 1.0。接下来是简短的描述和主页的 URL。最后,LANGUAGES CXX属性指定我们正在构建一个 C++项目。除了项目名称,所有其他参数都是可选的。

调用add_executable(hello_world)命令会创建一个名为hello_world的目标。这也将是由这个目标创建的可执行文件的名称。

现在目标已经创建,使用target_sources将 C++源文件添加到目标中。在这种情况下,hello_world是目标名称,如add_executable中所指定。PRIVATE定义指定源文件仅用于构建此目标,并且对任何依赖的目标不可见。在作用域说明符之后,是一组相对于当前CMakeLists.txt文件路径的源文件列表。如果需要,可以通过CMAKE_CURRENT_SOURCE_DIR变量访问当前正在处理的CMakeLists.txt文件的位置。

源文件可以直接添加到add_executable函数中,或者使用target_sources函数单独添加。通过target_sources添加可以显式地定义源文件的使用范围,使用PRIVATEPUBLICINTERFACE。然而,除PRIVATE外的任何指定仅对库目标有意义。当源文件直接添加到add_executable命令时,它们默认是PRIVATE的。

一个常见的模式是将项目的主可执行文件命名为项目的名称,可以通过使用PROJECT_NAME变量来实现,例如:

project(hello_world
...
)
add_executable(${PROJECT_NAME})

尽管乍一看这似乎很方便,但这不是一个好的做法。项目的名称和目标承载着不同的语义,因此应该视为两个独立的事物,因此应避免使用PROJECT_NAME作为目标的名称。

可执行文件很重要,而且相对容易创建,但除非你正在构建一个巨大的整体应用,否则库是模块化和分发代码的好方式。在下一节中,我们将学习如何构建库以及如何处理不同的链接方法。

创建一个简单的库

创建库的过程与创建可执行文件相似,尽管由于库目标通常会被其他目标使用(无论是同一项目中的目标,还是其他项目中的目标),因此需要考虑一些额外的因素。由于库通常有一个内部部分和一个公开的 API,我们在将文件添加到项目时必须考虑这一点。

一个简单的库项目将是这样的:

cmake_minimum_required(VERSION 3.21)
project(
  ch3_hello
  VERSION 1.0
  DESCRIPTION
    "A simple C++ project to demonstrate creating executables and
      libraries in CMake"
  LANGUAGES CXX)
add_library(hello)
add_library(ch3_hello::hello ALIAS hello)
target_sources(
  hello
  PRIVATE src/hello.cpp src/internal.cpp)
target_compile_features(hello PUBLIC cxx_std_17)
target_include_directories(
  hello
  PRIVATE src/hello
  PUBLIC include)

同样,文件以设置cmake_minimum_required和项目信息开始,你现在应该已经很熟悉这些内容了。

接下来,使用add_library创建库的目标——在这种情况下,库的类型未被确定。我们可以传递STATICSHARED来显式地确定库的链接类型。如果省略这一部分,我们允许库的任何下游使用者选择如何构建和链接它。通常,静态库最容易处理,但在编译时间和模块化分发方面有一些缺点。有关构建共享库的更多信息,请参见共享库中的符号可见性子部分。

如果省略库的类型,BUILD_SHARED_LIBS变量决定默认情况下库是作为共享库还是静态库构建。此变量不应在项目的CMakeLists.txt文件中无条件设置;它应始终由用户传递。

除了定义库目标外,良好的实践是还定义一个库别名,可以通过以下代码实现:

add_library(ch3_hello::hello ALIAS hello)

这会创建一个别名,名为ch3_hello::hello,它指代hello目标。

接下来,使用target_sources添加库的源文件。第一个参数是目标名称,后面是由PRIVATEPUBLICINTERFACE关键字分隔的源文件。在实际操作中,源文件几乎总是使用PRIVATE修饰符添加。PRIVATEPUBLIC关键字指定了源文件在哪些地方用于编译。指定PRIVATE意味着源文件仅会在hello目标本身中使用。如果使用PUBLIC,则源文件会添加到hello以及任何链接到hello的目标中。如前所述,这通常不是期望的行为。INTERFACE关键字意味着源文件不会添加到hello中,但会添加到任何与hello链接的目标中。这通常只适用于头文件,而不适用于源文件。一般来说,任何指定为PRIVATE的目标都可以视为该目标的构建要求。标记为PUBLIC的源文件是构建和接口要求,而标记为INTERFACE的源文件仅为接口要求。最后,使用target_include_directories设置库的include目录。通过此命令指定的文件夹中的所有文件可以通过#include <file.hpp>(使用尖括号)而非#include ""来访问,尽管使用引号的版本仍然有效。include目录在PRIVATEPUBLICINTERFACE的语义上与源文件类似。

PRIVATE包含的路径不会被包括在目标属性INTERFACE_INCLUDE_DIRECTORIES中。当目标依赖于库时,CMake 会读取此属性,以确定哪些include目录对被依赖目标可见。

由于库的 C++代码使用了与现代 C++版本相关的特性,例如 C++11/14/17/20 或即将发布的 C++23,我们必须设置cxx_std_17属性。由于此标准对于编译库本身以及与库的接口都是必要的,因此它设置为PUBLIC。只有当头文件中包含需要特定标准的代码时,才有必要将其设置为PUBLICINTERFACE。如果仅内部代码依赖于某个标准,则更倾向于将其设置为PRIVATE。通常,尽量将公共 C++标准设置为能正常工作的最低版本。也可以只启用某个现代 C++标准的特定特性,但这相对较少见。

可用的编译特性完整列表可以在cmake.org/cmake/help/latest/prop_gbl/CMAKE_CXX_KNOWN_FEATURES.html找到。

库别名

库别名是一种在不创建新构建目标的情况下引用库的方式,有时也被称为命名空间。一个常见的模式是为从项目安装的每个库创建一个形如MyProject::Library的库别名。

它们可以用于语义上将多个目标分组。它们还可以帮助避免命名冲突,特别是当项目中包含常见的目标(例如名为utilshelpers等的库)时。一个好的做法是将同一项目的所有目标放在同一个命名空间下。当你从其他项目链接库时,包含命名空间可以防止你不小心链接错误的库。被认为是好习惯的是,为所有库创建一个带有命名空间的别名,将它们分组,以便可以通过命名空间引用它们:

add_library(ch3_hello::hello ALIAS hello)
...
target_link_libraries(SomeLibrary PRIVATE ch3_hello::hello)

除了帮助确定目标的来源外,CMake 还使用命名空间来识别导入的目标,并创建更好的诊断消息,正如我们在安装和打包部分中看到的,在第四章中,CMake 项目的打包、部署和安装,以及在第五章中,集成第三方库和依赖管理,我们将讲解依赖管理时也会涉及此内容。

始终使用命名空间

作为好习惯,始终使用命名空间别名目标,并通过namespace::前缀引用它们。

通常,当你从项目外部引用目标时,使用包含命名空间的完整名称并通过target_link_library添加它们。虽然别名是语义化命名 CMake 构建目标的一种方式,但它们对构建后实际生成的库文件名称的影响有限。不过,CMake 提供了方便的函数来控制命名并确保库符合不同操作系统的命名约定。

命名库

当你使用add_library(<name>)创建库时,库的名称必须在项目内全局唯一,因为名称冲突会导致错误。默认情况下,库的实际文件名是根据平台的约定构造的,例如 Linux 上的lib<name>.so,以及 Windows 上的<name>.lib<name>.dll。通过设置目标的OUTPUT_NAME属性,可以更改文件名的默认行为。以下示例中,输出文件的名称已从ch3_hello更改为hello

add_library(ch3_hello)
set_target_properties(
   ch3_hello
   PROPERTIES OUTPUT_NAME hello
)

避免使用以lib为前缀或后缀的库名,因为 CMake 可能会根据平台自动在文件名的前面或后面附加适当的字符串。

共享库常用的命名约定是将版本添加到文件名中,以指定构建版本和 API 版本。通过为库目标指定VERSIONSOVERSION属性,CMake 将在构建和安装库时创建必要的文件名和符号链接:

set_target_properties(
    hello
    PROPERTIES VERSION ${PROJECT_VERSION} # Contains 1.2.3
    SOVERSION ${PROJECT_VERSION_MAJOR} # Contains only 1
)

在 Linux 上,以上示例将生成名为libhello.so.1.0.0的文件,并且从libhello.solibhello.so.1到实际库文件的符号链接也会创建。以下截图显示了生成的文件和指向它的符号链接:

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/cmk-bst-prac-2e/img/B30947_03_01.jpg

图 3.1 – 构建时带有 SOVERSION 属性的库文件和生成的符号链接

在项目中常见的一种约定是为不同的构建配置添加不同的文件名后缀。CMake 通过设置CMAKE_<CONFIG>_POSTFIX全局变量或者添加<CONFIG>_POSTFIX属性到目标来处理这一点。如果设置了这个变量,后缀会自动添加到非可执行目标上。与大多数全局变量一样,它们应通过命令行或预设传递给 CMake,而不是硬编码在CMakeLists.txt文件中。

调试库的后缀也可以明确地为单个目标设置,如下例所示:

set_target_properties(
hello
PROPERTIES DEBUG_POSTFIX d)

这样,在调试配置下构建时,库文件和符号链接将被命名为libhellod.so。由于在 CMake 中,库链接是通过目标而非文件名进行的,选择正确的文件名会自动完成,因此我们无需手动跟踪。然而,在链接共享库时需要注意的一点是符号的可见性。我们将在下一节中讨论这个问题。

共享库中的符号可见性

要链接共享库,链接器必须知道哪些符号可以从库外部使用。这些符号可以是类、函数、类型等,公开它们的过程称为导出。

编译器在指定符号可见性时有不同的方式和默认行为,这使得以平台无关的方式指定这一点变得有些麻烦。首先是编译器的默认可见性;GCC 和 Clang 默认假定所有符号都是可见的,而 Visual Studio 编译器默认隐藏所有符号,除非显式导出。通过设置CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS,可以改变 MSVC 的默认行为,但这是一个粗暴的解决方法,只能在库的所有符号都应该导出的情况下使用。

虽然将所有符号设置为公开可见是确保链接顺利的简单方法,但它也有一些缺点。

通过导出所有内容,无法防止依赖目标使用内部代码。

由于每个符号都可以被外部代码使用,链接器无法丢弃死代码,因此生成的库往往会变得臃肿。如果库包含模板,这一点尤其明显,因为模板会显著增加符号的数量。

由于每个符号都被导出,唯一可以判断哪些符号应该被视为隐藏或内部的线索只能来自文档。

暴露库的内部符号可能会暴露应该保持隐藏的内容。

设置所有符号为可见

当你设置共享库中的所有符号都可见时,尤其是在关注安全问题或二进制大小很重要的情况下,要小心。

更改默认可见性

要更改符号的默认可见性,将<LANG>_VISIBILITY_PRESET属性设置为HIDDEN。此属性可以全局设置,也可以针对单个库目标进行设置。<LANG>会替换为库所使用的编程语言,例如 C++使用CXX,C 语言使用C。如果所有符号都应该是隐藏的并且要导出,它们必须在代码中特别标记。最常见的做法是指定一个预处理器定义来决定一个符号是否可见:

class HELLO_EXPORT Hello {
…
};

HELLO_EXPORT定义将包含关于符号在库编译时是否会导出的信息,或者在链接库时是否应当导入。GCC 和 Clang 使用__attribute__(…)关键字来确定此行为,而在 Windows 上使用的是_declspec(…)。编写能够跨平台处理的头文件并不是一件容易的事,特别是当你还需要考虑库可能被构建为静态库和对象库时。幸运的是,CMake 提供了generate_export_header宏,它由GenerateExportHeader模块导入,以简化这一过程。

在以下示例中,hello库的符号默认设置为隐藏。然后,使用GenerateExportHeader模块导入的generate_export_header宏单独启用它们。此外,示例还将VISIBILITY_INLINES_HIDDEN属性设置为TRUE,以通过隐藏内联类成员函数来进一步减少导出符号表。设置内联符号的可见性并非严格必要,但通常在设置默认可见性时会这样做:

add_library(hello SHARED)
set_property(TARGET hello PROPERTY CXX_VISIBILITY_PRESET "hidden")
set_property(TARGET hello PROPERTY VISIBILITY_INLINES_HIDDEN TRUE)
include(GenerateExportHeader)
generate_export_header(hello EXPORT_FILE_NAME export/hello/
  export_hello.hpp)
target_include_directories(hello PUBLIC "${CMAKE_CURRENT_BINARY_DIR}
  /export")

调用generate_export_header会在CMAKE_CURRENT_BINARY_DIR/export/hello目录下创建一个名为export_hello.hpp的文件,该文件可以在库的其他文件中包含。将这些生成的文件放在构建目录的子文件夹中是一种好的做法,这样只有部分目录会被添加到include路径中。生成的文件的include结构应该与库其他部分的include结构保持一致。所以,在这个例子中,如果所有公共头文件都是通过#include <hello/a_public_header.h>方式包含的,那么导出头文件也应该放在名为hello的文件夹中。生成的文件还必须添加到安装指令中,正如在第四章中所解释的那样,打包、部署和安装 CMake 项目。此外,为了生成导出文件,必须为目标设置必要的编译器标志来导出符号。

由于生成的头文件必须包含在声明要导出的类、函数和类型的文件中,因此CMAKE_CURRENT_BINARY_DIR/export/被添加到target_include_directories中。请注意,这必须是PUBLIC,以便依赖的库也能够找到该文件。

关于generate_export_header宏还有许多其他选项,但我们在本节中所看到的已经涵盖了大部分常见用例。有关设置符号可见性的更多信息,请参阅官方 CMake 文档:cmake.org/cmake/help/latest/module/GenerateExportHeader.html

接口库或头文件库

头文件库有点特殊,因为它们不需要编译;相反,它们导出自己的头文件,以便可以直接在其他库中包含。在大多数方面,头文件库的工作方式与普通库相似,但它们的头文件是通过INTERFACE关键字公开的,而不是通过PUBLIC关键字。

由于头文件库不需要编译,它们不会将源代码添加到目标中。以下代码创建了一个最小的头文件库:

project(
  ch3_hello_header_only
  VERSION 1.0
  DESCRIPTION "Chapter 3 header-only example"
  LANGUAGES CXX)
add_library(hello_header_only INTERFACE)
target_include_directories(hello_header_only INTERFACE include/)
target_compile_features( hello_header_only INTERFACE cxx_std_17)

还值得注意的是,在 CMake 版本3.19之前,INTERFACE库不能添加任何target_sources。现在,头文件库可以列出源代码,但这种用法很少见。

对象库——仅供内部使用

有时,你可能想要拆分代码,以便某些部分可以被重用,而无需创建完整的库。一个常见的做法是,当你想在可执行文件和单元测试中使用一些代码,而无需重新编译所有内容两次时。

为此,CMake 提供了对象库,其中源代码会被编译,但不会被归档或链接。通过调用add_library(MyLibrary OBJECT)来创建一个对象库。

自 CMake 3.12 以来,这些目标可以像普通库一样通过将它们添加到target_link_libraries函数中来使用。在 3.12 版本之前,目标库需要通过生成表达式添加,即$<TARGET_OBJECTS:MyLibrary>。这将在构建系统生成期间扩展为一个对象列表。虽然这仍然可以做到,但不再推荐这样做,因为它会迅速变得难以维护,尤其是在项目中有多个目标库的情况下。

何时使用目标库

目标库有助于加速构建和模块化代码,而无需将模块公开。

使用目标库时,所有不同类型的库都被涵盖。库本身编写和维护都很有趣,但除非它们集成到更大的项目中,否则它们什么也做不了。所以,让我们看看到目前为止我们定义的所有库如何在可执行文件中使用。

汇总 - 使用你的库

到目前为止,我们已经创建了三种不同的库——一个二进制库,可以静态或动态链接,一个接口或仅头文件库,以及一个已预编译但未链接的目标库。

让我们学习如何在共享项目中将它们用于可执行文件。将它们作为系统库安装或作为外部依赖项使用将在第五章集成第三方库依赖管理中讨论。

所以,我们可以将add_library调用放在同一个CMakeLists.txt文件中,或者通过使用add_subdirectory将它们集成在一起。两者都是有效的选项,具体取决于项目的设置,如本章的设置项目处理嵌套项目部分所述。

在下面的示例中,我们假设在hello_libhello_header_onlyhello_object目录中已定义了三个带有CMakeLists.txt文件的库。这些库可以通过add_subdirectory命令包含进来。在这里,我们创建了一个名为chapter3的新目标,即我们的可执行文件。然后,通过target_link_libraries将这些库添加到可执行文件中:

add_subdirectory(hello_lib)
add_subdirectory(hello_header_only)
add_subdirectory(hello_object)
add_executable(chapter3)
target_sources(chapter3 PRIVATE src/main.cpp)
target_link_libraries(chapter3 PRIVATE hello_header_only hello_lib
  hello_object)

target_link_libraries的目标可以是一个可执行文件,也可以是另一个库。同样,库是通过访问说明符进行链接的,访问说明符可以是以下之一:

  • PRIVATE:该库用于链接,但它不是公共接口的一部分。链接的库只有在构建目标时才是必需的。

  • INTERFACE:该库不会被链接,但它是公共接口的一部分。当你在其他地方使用该目标时,链接的库是必需的。这通常只在你链接其他只包含头文件的库时使用。

  • PUBLIC:该库被链接,并且它是公共接口的一部分。因此,该库既是构建依赖项,也是使用依赖项。

注意 – 不良做法

本书的作者强烈不推荐以下做法,因为它们往往会创建难以维护的项目,并使得在不同的构建环境之间移植变得困难。不过,我们将其包括在内以确保内容的完整性。

PUBLICPRIVATEINTERFACE 后面传递另一个目标时,您还可以传递库的完整路径或库的文件名,例如 /usr/share/lib/mylib.so 或仅 mylib.so。这些做法是可以实现的,但不推荐使用,因为它们会使 CMake 项目变得不易移植。此外,您还可以通过传递类似 -nolibc 这样的内容来传递链接器标志,尽管同样不推荐这样做。如果所有目标都需要特殊的链接器标志,最好通过命令行传递它们。如果单个库需要特殊的标志,则使用 target_link_options 是推荐的做法,最好与命令行上设置的选项结合使用。

在下一节中,我们将讨论如何设置编译器和链接器选项。

设置编译器和链接器选项

C++ 编译器有很多选项,涉及一些常见的标志设置,同时从外部设置预处理器定义也是一种常见做法。在 CMake 中,这些选项是通过 target_compile_options 命令传递的。更改链接器行为则通过 target_link_options 命令实现。不幸的是,编译器和链接器可能在设置标志的方式上有所不同。例如,在 GCC 和 Clang 中,选项是通过连字符(-)传递的,而 Microsoft 编译器则使用斜杠(/)作为选项的前缀。但通过使用生成器表达式(在第一章《启动 CMake》中介绍过),可以方便地在 CMake 中处理这些差异,以下是一个示例:

target_compile_options(
  hello
  PRIVATE $<$<CXX_COMPILER_ID:MSVC>:/SomeOption>
          $<$<CXX_COMPILER_ID:GNU,Clang,AppleClang>:-someOption>
)

让我们详细看看这个生成器表达式。

$<$<CXX_COMPILER_ID:MSVC>:/SomeOption> 是一个嵌套的生成器表达式,按从内到外的顺序进行求值。生成器表达式在构建系统生成期间进行求值。首先,$<CXX_COMPILER_ID:MSVC> 如果 C++ 编译器为 MSVC,则求值为 true。如果是这种情况,那么外部表达式将返回 /SomeOption,然后传递给编译器。如果内部表达式求值为 false,则什么都不会传递。

$<$<CXX_COMPILER_ID:GNU,Clang,AppleClang>:-fopenmp> 类似地工作,但它不仅检查单一的值,而是传递一个包含 GNU,Clang,AppleClang 的列表。如果 CXX_COMPILER_ID 与这些值中的任何一个匹配,则内部表达式求值为 true,并将 someOption 传递给编译器。

将编译器或链接器选项传递为 PRIVATE 标记它们是此目标的构建要求,而不需要在库接口中使用。如果将 PRIVATE 替换为 PUBLIC,则编译选项也会成为一个使用要求,所有依赖于原始目标的目标将使用相同的编译选项。将编译器选项暴露给依赖目标需要谨慎处理。如果某个编译选项仅用于使用目标,但不用于构建目标,则可以使用 keyword INTERFACE。这通常出现在构建仅包含头文件的库时。

编译选项的特殊情况是预处理器或编译定义,这些定义会传递给底层程序。它们是通过 target_compile_definitions 函数传递的。

使用编译数据库调试编译选项

要查看所有编译选项,可以查看生成的构建文件,例如 Makefile 或 Visual Studio 项目。更方便的方法是让 CMake 将所有编译命令导出为 JSON 编译数据库。

通过启用 CMAKE_EXPORT_COMPILE_COMMANDS 变量,CMake 会在 build 文件夹中创建一个名为 compile_commands.json 的文件,里面包含完整的编译命令。

启用此选项并运行 CMake 后,将生成类似以下内容的结果:

{
  "directory": "/workspaces/CMake-Best-Pracitces/build",
  "command": "/usr/bin/g++ -I/workspaces/CMake-Best-Practices/
  chapter_3/hello_header_only/include -I/workspaces/CMake-Tips-and-
  Tricks/chapter_3/hello_lib/include -I/workspaces/CMake-Tips-and-
  Tricks/chapter_3/hello_object_lib/include -g -fopenmp -o
  chapter_3/CMakeFiles/chapter3.dir/src/main.cpp.o -c /workspaces
  /CMake-Best-Practices/chapter_3/src/main.cpp",
  "file": "/workspaces/CMake-Best-Practices/chapter_3/src/main.cpp"
},

注意从之前示例中手动指定的 -fopenMP 标志的添加。compile_commands.json 可以作为一种与构建系统无关的方式来加载命令。一些 IDE,如 VS Code 和 Clion,能够解析该 JSON 文件并自动生成项目信息。

编译命令数据库的完整规范可以在 clang.llvm.org/docs/JSONCompilationDatabase.html 找到。

目前,越来越多的工具使用 compile_commands.json 数据库来确定传递的确切编译选项,因此许多项目默认启用此功能。特别是,大多数来自 LLVM 的工具,例如 clang-tidy 静态分析工具或用于代码补全的 clangd,都能从访问编译数据库中获益匪浅。如果编译选项出现问题,也可以通过该数据库方便地调试编译选项:clang.llvm.org/docs/JSONCompilationDatabase.html

总结

现在你已经完成了本章的内容,准备好使用 CMake 创建应用程序和库,并开始构建比“hello world”更复杂的项目。你已经学会了如何将不同的目标链接在一起,以及如何将编译器和链接器选项传递给目标。我们还讨论了仅供内部使用的对象库,并讲解了共享库的符号可见性。最后,你学会了如何自动化文档化这些依赖关系,以便对大型项目有一个概览。

在下一章中,你将学习如何在不同平台上打包和安装你的应用程序和库。

问题

请回答以下问题,测试你对本章的理解:

  1. 创建可执行目标的 CMake 命令是什么?

  2. 创建库目标的 CMake 命令是什么?

  3. 如何指定一个库是静态链接还是动态链接?

  4. 对象库有什么特别之处,在哪里使用它们最为方便?

  5. 如何指定共享库的默认符号可见性?

  6. 如何为目标指定编译器选项,如何查看编译命令?

答案

  1. 创建可执行目标的 CMake 命令是:

    add_executable

  2. 创建库目标的 CMake 命令是:

    add_library

  3. 通过添加 SHAREDSTATIC 关键字,或者设置 BUILD_SHARED_LIBS 全局变量

  4. 对象库是已编译但未链接的库。它们用于在内部分离代码,并减少编译时间。

  5. 通过全局设置 <LANG>_VISIBILITY_PRESET 属性

  6. 通过调用 target_compile_options 函数。编译选项可以在 compile_commands.json 文件中查看,该文件会在将 CMAKE_EXPORT_COMPILE_COMMANDS 变量设置为 true 时生成。

第二部分 – 实用 CMake – 亲自动手使用 CMake

在这一部分中,你将能够以适应大多数用例的方式使用 CMake 设置软件项目。第 45 章将涵盖项目安装和打包、依赖管理以及包管理器的使用。

第 6、78 章将介绍如何将外部工具集成到 CMake 项目中,以生成文档、确保代码质量或执行几乎任何构建软件所需的任务。第 910 章将讲解如何为构建项目创建可重用的环境,并处理来自分布式仓库的大型项目。

最后,第十一章将稍微介绍一下 macOS 生态系统,并覆盖一些特定的构建和部署 Apple 系统软件的要求。

本部分包含以下章节:

  • 第四章**,打包、部署和 安装 CMake 项目

  • 第五章**, 集成第三方库和依赖管理

  • 第六章**,自动生成文档

  • 第七章**,无缝集成代码质量工具与 CMake

  • 第八章**,使用 CMake 执行自定义任务

  • 第九章**,创建可重现的构建环境

  • 第十章**, 在超级构建中处理分布式仓库和依赖关系

  • 第十一章**,为 Apple 系统创建软件

第四章:打包、部署和安装 CMake 项目

正确地打包软件往往被编写和构建软件的过程所掩盖,然而它通常是确保任何软件项目成功和持久性的一个重要因素。打包是开发者创作与最终用户体验之间的桥梁,涵盖了从分发到安装和维护的方方面面。打包得当的软件不仅简化了部署过程,还增强了用户满意度、可靠性,并且便于无缝更新和修复漏洞。

确保软件以与这些不同环境兼容的方式打包,对其可用性和可访问性至关重要。此外,用户的技术能力跨度广泛,从经验丰富的专业人士到新手不等。因此,打包必须迎合这一范围,为经验较少的用户提供直观的安装过程,同时为技术熟练的用户提供高级选项。此外,遵守安装标准对于用户的便利性和系统完整性都至关重要。通过遵循既定的打包规范,开发者可以减少在目标系统中堆积不必要的文件或冲突的依赖关系,从而促进系统的稳定性和整洁性。归根结底,软件打包是将原始代码转化为精致、可访问产品的关键最后一步,和开发过程本身一样至关重要。

CMake 内部有良好的支持和工具,使得安装和打包变得简单。这一点的好处在于,CMake 利用现有的项目代码来实现这些功能。因此,使项目可安装或打包项目不会带来沉重的维护成本。本章中,我们将学习如何利用 CMake 在安装和打包方面的现有能力,来支持部署工作。

本章将涵盖以下主题:

  • 使 CMake 目标可安装

  • 使用你的项目为他人提供配置信息

  • 使用 CPack 创建可安装包

技术要求

在深入本章之前,你应该对 CMake 中的目标有一个良好的理解(在第一章《启动 CMake》和第三章《创建 CMake 项目》中简要介绍,详细内容见其中)。本章将基于这些知识进行扩展。

请从本书的 GitHub 仓库获取本章的示例,地址为 github.com/PacktPublishing/CMake-Best-Practices---2nd-Edition。本章的示例内容位于 chapter04/ 子文件夹中。

使 CMake 目标可安装

在 CMake 的上下文中,安装打包软件是两个不同的概念。安装软件涉及将可执行文件、库和其他必要资源复制到预定的位置。而打包则是将所有必要的文件和依赖项捆绑成一个可分发格式(例如 tarball、ZIP 压缩包或安装程序包),以便于在其他系统上进行分发和安装。CMake 的打包机制是先将项目安装到临时位置,然后将安装的文件打包成适当的格式。

支持项目部署的最原始方式是将其设置为可安装。相反,最终用户仍然必须获取项目的源代码并从头开始构建它。一个可安装的项目会有额外的构建系统代码,用于在系统上安装运行时或开发工件。如果有适当的安装指令,构建系统将在这里执行安装操作。由于我们使用 CMake 生成构建系统文件,因此 CMake 必须生成相关的安装代码。在这一节中,我们将深入探讨如何指导 CMake 为 CMake 目标生成此类代码的基础知识。

install()命令

install(...)命令是一个内置的 CMake 命令,允许您生成安装目标、文件、目录等的构建系统指令。CMake 不会生成安装指令,除非明确告诉它这么做。因此,什么内容被安装始终在您的控制之下。让我们来看一下它的基本用法。

安装 CMake 目标

要使 CMake 目标可安装,必须指定TARGETS参数,并提供至少一个参数。该用法的命令签名如下:

install(TARGETS <target>... [...])

TARGETS参数表示install将接受一组 CMake 目标,生成安装代码。在这种形式下,只有目标的输出工件会被安装。目标的最常见输出工件定义如下:

  • ARCHIVE(静态库、DLL 导入库和链接器导入文件):

    • 除了在 macOS 中标记为FRAMEWORK的目标
  • LIBRARY(共享库):

    • 除了在 macOS 中标记为FRAMEWORK的目标

    • 除了 DLL(在 Windows 中)

  • RUNTIME(可执行文件和 DLL):

    • 除了在 macOS 中标记为MACOSX_BUNDLE的目标

在将目标设置为可安装后,CMake 会生成必要的安装代码,以便安装为该目标生成的输出工件。为了说明这一点,让我们一起将一个基本的可执行目标设置为可安装。要查看install(...)命令的实际操作,我们可以查看位于chapter04/ex01_executable文件夹中的Chapter 4example 1CMakeLists.txt文件:

add_executable(ch4_ex01_executable)
target_sources(ch4_ex01_executable src/main.cpp)
target_compile_features(ch4_ex01_executable PRIVATE cxx_std_11)
install(TARGETS ch4_ex01_executable)

在前面的代码中,定义了一个名为ch4_ex01_executable的可执行目标,并在接下来的两行中填充了它的属性。最后一行install(...)是我们感兴趣的部分,它告诉 CMake 为ch4_ex01_executable创建所需的安装代码。

为了检查ch4_ex01_executable是否可以被安装,让我们在chapter 4的根文件夹中通过 CLI 构建并安装该项目:

cmake -S . -B ./build -DCMAKE_BUILD_TYPE="Release"
cmake --build ./build
cmake --install ./build --prefix /tmp/install-test

注意

与其为cmake --install指定--prefix参数,你也可以使用CMAKE_INSTALL_PREFIX变量来提供非默认的install前缀。

在使用 CMake 与多配置生成器(如 Ninja 多配置和 Visual Studio)时,请为cmake --buildcmake --install命令指定--config参数:

# For multi-config generators:
cmake --build ./build --config Release
cmake --install ./build --prefix /tmp/install-test --config Debug

让我们检查一下cmake --install命令的作用:

-- Install configuration: "Release"
-- Installing: /tmp/install-test/lib/libch2.framework.component1.a
-- Installing: /tmp/install-test/lib/libch2.framework.component2.so
-- Installing: /tmp/install-test/bin/ch2.driver_application
-- Set runtime path of "/tmp/install-test/bin/
    ch2.driver_application" to ""
-- Installing: /tmp/install-test/bin/ch4_ex01_executable

在前面输出的最后一行中,我们可以看到ch4_ex01_executable目标的输出工件——也就是说,ch4_ex01_executable二进制文件已经被安装。由于这是ch4_ex01_executable目标的唯一输出工件,我们可以得出结论,目标确实已经变得可以安装了。

请注意,ch4_ex01_executable并没有直接安装到/tmp/install-test(前缀)目录中。相反,install命令将它放入了bin/子目录。这是因为 CMake 智能地判断了应该将什么类型的工件放到哪里。在传统的 UNIX 系统中,二进制文件通常放在/usr/bin,而库文件放在/usr/lib。CMake 知道add_executable()命令会生成一个可执行的二进制工件,并将其放入/bin子目录。这些目录是 CMake 默认提供的,具体取决于目标类型。提供默认安装路径信息的 CMake 模块被称为GNUInstallDirs模块。GNUInstallDirs模块在被包含时定义了各种CMAKE_INSTALL_路径。下表显示了各个目标的默认安装目录:

目标类型 GNUInstallDirs 变量 内置默认值
RUNTIME $ bin
LIBRARY $ lib
ARCHIVE $ lib
PRIVATE_HEADER $ include
PUBLIC_HEADER $ include

为了覆盖内置的默认值,install(...)命令中需要一个额外的<TARGET_TYPE> DESTINATION参数。为了说明这一点,假设我们要将默认的RUNTIME安装目录更改为qbin,而不是bin。这样做只需要对原始的install(...)命令做一个小的修改:

# …
install(TARGETS ch4_ex01_executable
        RUNTIME DESTINATION qbin
)

做出此更改后,我们可以重新运行 configurebuildinstall 命令。我们可以通过检查 cmake --install 命令的输出确认 RUNTIME 目标已经更改。与第一次不同,我们可以观察到 ch4_ex01_executable 二进制文件被放入 qbin 而不是默认的 (bin) 目录:

# ...
-- Installing: /tmp/install-test/qbin/ch4_ex01_executable

现在,让我们看另一个示例。这次我们将安装一个 STATIC 库。让我们看看 第四章 中的 CMakeLists.txt 文件,示例 2,它位于 chapter04/ex02_static 文件夹中。由于篇幅原因,注释和 project(...) 命令已被省略。让我们开始检查文件:

add_library(ch4_ex02_static STATIC)
target_sources(ch4_ex02_static PRIVATE src/lib.cpp)
target_include_directories(ch4_ex02_static PUBLIC include)
target_compile_features(ch4_ex02_static PRIVATE cxx_std_11)
include(GNUInstallDirs)
install(TARGETS ch4_ex02_static)
install (
     DIRECTORY include/
     DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}"
)

如你所见,它与我们之前的示例稍有不同。首先,新增了一个带有 DIRECTORY 参数的 install(...) 命令。这是为了使静态库的头文件可以被安装。原因是 CMake 不会安装任何不是 输出产物 的文件,而 STATIC 库目标只会生成一个二进制文件作为 输出产物。头文件不是 输出产物,应单独安装。

注意

DIRECTORY 参数中的尾随斜杠会导致 CMake 复制文件夹内容,而不是按名称复制文件夹。CMake 处理尾随斜杠的方式与 Linux 的 rsync 命令相同。

安装文件和目录

如我们在前一节中看到的,我们打算安装的内容并不总是目标的 输出产物。它们可能是目标的运行时依赖项,例如图像、资源、配置文件、脚本和资源文件。CMake 提供了 install(FILES...)install(DIRECTORY...) 命令,用于安装任何特定的文件或目录。让我们从安装文件开始。

安装文件

install(FILES...) 命令接受一个或多个文件作为参数。它还需要一个额外的 TYPEDESTINATION 参数。这两个参数用于确定指定文件的目标目录。TYPE 参数用于指示哪些文件将使用该文件类型的默认路径作为安装目录。通过设置相关的 GNUInstallDirs 变量可以覆盖默认值。以下表格显示了有效的 TYPE 值及其目录映射:

类型 GNUInstallDirs 变量 内置默认值
BIN $ bin
SBIN $ sbin
LIB $ lib
INCLUDE $ include
SYSCONF $ etc
SHAREDSTATE $ com
LOCALSTATE $ var
RUNSTATE $ /run
DATA $
INFO $ /info
LOCALE $ /locale
MAN $ /man
DOC $ /doc

如果你不想使用 TYPE 参数,可以改用 DESTINATION 参数。它允许你为 install(...) 命令中指定的文件提供自定义的目标位置。

install(FILES...) 的一种替代形式是 install(PROGRAMS...),它与 install(FILES...) 相同,区别在于它还为已安装的文件设置了 OWNER_EXECUTEGROUP_EXECUTEWORLD_EXECUTE 权限。对于必须由最终用户执行的二进制文件或脚本文件来说,这样做是有意义的。

要理解 install(FILES|PROGRAMS...),让我们看一个例子。我们将要查看的例子是 第四章**,示例 3chapter04/ex03_file)。它实际上包含了三个文件:chapter04_greeter_contentchapter04_greeter.pyCMakeLists.txt。首先,让我们看看它的 CMakeLists.txt 文件:

install(FILES "${CMAKE_CURRENT_LIST_DIR}/chapter04_greeter_content"
  DESTINATION "${CMAKE_INSTALL_BINDIR}")
install(PROGRAMS "${CMAKE_CURRENT_LIST_DIR}/chapter04_greeter.py"
  DESTINATION "${CMAKE_INSTALL_BINDIR}" RENAME chapter04_greeter)

让我们消化一下我们所看到的内容;在第一个 install(...) 命令中,我们告诉 CMake 将 chapter04_greeter_content 文件安装到当前 CMakeLists.txt 目录(chapter04/ex03_file)的系统默认 BIN 目录中。在第二个 install(...) 命令中,我们告诉 CMake 将 chapter04_greeter.py 文件安装到默认的 BIN 目录,并且文件名为 chapter04_greeter

注意

RENAME 参数仅在单文件 install(...) 调用时有效。

使用这些 install(...) 指令,CMake 应该会将 chapter04_greeter.pychapter04_greeter_content 文件安装到 ${CMAKE_INSTALL_PREFIX}/bin 目录。让我们通过 CLI 构建并安装项目:

cmake -S . -B ./build
cmake --build ./build
cmake --install ./build --prefix /tmp/install-test

让我们看看 cmake --install 命令做了什么:

/* … */
-- Installing: /tmp/install-test/bin/chapter04_greeter_content
-- Installing: /tmp/install-test/bin/chapter04_greeter

上面的输出确认了 CMake 为 chapter04_greeter_contentchapter04_greeter.py 文件生成了所需的安装代码。最后,让我们检查一下 chapter04_greeter 文件是否能够执行,因为我们使用了 PROGRAMS 参数来安装它:

15:01 $ /tmp/install-test/bin/chapter04_greeter
['Hello from installed file!']

这样,我们就完成了 install(FILES|PROGRAMS...) 部分的内容。接下来,让我们继续安装目录。

安装目录

install(DIRECTORY...) 命令对于安装目录非常有用。目录的结构将会被原样复制到目标位置。目录可以作为整体安装,也可以选择性地安装。让我们先从最基本的目录安装示例开始:

install(DIRECTORY dir1 dir2 dir3 TYPE LOCALSTATE)

上面的例子将会把 dir1dir2 目录安装到 ${CMAKE_INSTALL_PREFIX}/var 目录中,并且连同它们的所有子文件夹和文件一起原样安装。有时候,安装文件夹的全部内容并不理想。幸运的是,CMake 允许 install 命令根据通配符模式和正则表达式来包含或排除目录内容。让我们这次选择性地安装 dir1dir2dir3

include(GNUInstallDirs)
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后面可以跟PATTERNREGEX参数。PATTERN允许您定义一个通配符模式,而REGEX允许您定义一个正则表达式。默认情况下,这些表达式用于包含文件。如果要排除符合标准的文件,可以在模式后添加EXCLUDE参数。请注意,这些过滤器不会应用于子目录名称,因为FILES_MATCHING参数的限制。我们还在最后一个install(...)命令中使用了PATTERN而没有加上FILES_MATCHING,这使得我们可以过滤子目录而非文件。这一次,只有dir1中扩展名为.x的文件、dir2中没有.hpp扩展名的文件以及dir3中除bin文件夹外的所有内容将被安装。这个示例可以在chapter04/ex04_directory文件夹中的Chapter 4**,示例 4中找到。让我们编译并安装它,看看它是否执行了正确的操作:

cmake -S . -B ./build
cmake -- build ./build
cmake -- install ./build –prefix /tmp/install-test

cmake --install的输出应该如下所示:

-- Installing: /tmp/install-test/var/dir1
-- Installing: /tmp/install-test/var/dir1/subdir
-- Installing: /tmp/install-test/var/dir1/subdir/asset5.x
-- Installing: /tmp/install-test/var/dir1/asset1.x
-- Installing: /tmp/install-test/var/dir2
-- Installing: /tmp/install-test/var/dir2/chapter04_hello.dat
-- Installing: /tmp/install-test/var/dir3
-- Installing: /tmp/install-test/var/dir3/asset4

注意

FILES_MATCHING不能在PATTERNREGEX之后使用,但可以反过来使用。

在输出中,我们可以看到只有扩展名为.x的文件被从dir1中选取。这是因为在第一个install(...)命令中使用了FILES_MATCHING PATTERN "*.x"参数,导致asset2文件没有被安装。同时,注意到dir2/chapter04_hello.dat文件被安装,而dir2/chapter04_hello.hpp文件被跳过。这是因为第二个install(…)命令中的FILES_MATCHING PATTERN "*.hpp" EXCLUDE PATTERN "*"参数所致。最后,我们看到dir3/asset4文件被安装,而dir3/bin目录被完全跳过,因为在最后一个install(...)命令中指定了PATTERN "bin" EXCLUDE参数。

使用install(DIRECTORY...)时,我们已经涵盖了install(...)命令的基础知识。接下来,让我们继续了解install(…)命令的其他常见参数。

install()命令的其他常见参数

如我们所见,install()命令的第一个参数指示要安装的内容。还有一些额外的参数可以让我们定制安装过程。让我们一起查看一些常见的参数。

DESTINATION 参数

该参数允许你为 install(...) 命令中指定的文件指定目标目录。目录路径可以是相对路径或绝对路径。相对路径将相对于 CMAKE_INSTALL_PREFIX 变量。建议使用相对路径以使安装可重定位。此外,为了打包,使用相对路径也很重要,因为 cpack 要求安装路径必须是相对的。最好使用以相关的 GNUInstallDirs 变量开头的路径,这样包维护者可以根据需要覆盖安装目标位置。DESTINATION 参数可以与 TARGETSFILESIMPORTED_RUNTIME_ARTIFACTSEXPORTDIRECTORY 安装类型一起使用。

PERMISSIONS 参数

该参数允许你在支持的平台上更改已安装文件的权限。可用的权限有 OWNER_READOWNER_WRITEOWNER_EXECUTEGROUP_READGROUP_WRITEGROUP_EXECUTEWORLD_READWORLD_WRITEWORLD_EXECUTESETUIDSETGIDPERMISSIONS 参数可以与 TARGETSFILESIMPORTED_RUNTIME_ARTIFACTSEXPORTDIRECTORY 安装类型一起使用。

CONFIGURATIONS 参数

这允许你在指定特定构建配置时限制应用的参数集。

OPTIONAL 参数

该参数使得文件的安装变为可选,这样当文件不存在时,安装不会失败。OPTIONAL 参数可以与 TARGETSFILESIMPORTED_RUNTIME_ARTIFACTSDIRECTORY 安装类型一起使用。

在本节中,我们学习了如何使目标、文件和目录可安装。在下一节中,我们将学习如何生成配置信息,以便可以直接将 CMake 项目导入到另一个 CMake 项目中。

为他人提供项目的配置信息

在上一节中,我们学习了如何使我们的项目可安装,以便他人可以通过安装它到他们的系统中来使用我们的项目。但有时候,仅仅交付制品并不足够。例如,如果你交付的是一个库,它必须也能方便地导入到另一个项目中——尤其是 CMake 项目中。在本节中,我们将学习如何让其他 CMake 项目更容易导入你的项目。

如果被导入的项目具有适当的配置文件,则有一些便捷的方法可以导入库。一个突出的方式是利用 find_package() 方法(我们将在第五章中讲解,集成第三方库依赖管理)。如果你的消费者在工作流程中使用 CMake,他们会很高兴能够直接写 find_package(your_project_name),并开始使用你的代码。在本节中,我们将学习如何生成所需的配置文件,以使 find_package() 能在你的项目中正常工作。

CMake 推荐的依赖管理方式是通过包(packages)。包用于传递 CMake 基于构建系统的依赖信息。包可以是 Config-file 包、Find-module 包或 pkg-config 包的形式。所有这些包类型都可以通过 find_package() 查找并使用。为了提高效率并遵循最佳实践,本节将仅关注 Config-file 包。其他方法,如 find-modulespkg-config 包,通常被视为过时的变通方法,主要在没有配置文件的情况下使用,通常不推荐使用。让我们深入了解 Config-file 包,理解它们的优点和实现方式。

进入 CMake 包的世界 —— Config-file

Config-file 包基于包含包内容信息的配置文件。这些信息指示包的内容位置,因此 CMake 会读取此文件并使用该包。因此,仅发现包的配置文件就足够使用该包了。

配置文件有两种类型 —— 包配置文件和可选的包版本文件。两个文件都必须遵循特定的命名约定。包配置文件可以命名为 <ProjectName>Config.cmake<projectname>-config.cmake,具体取决于个人偏好。在 find_package(ProjectName)/find_package(projectname) 调用时,CMake 会自动识别这两种命名方式。包配置文件的内容大致如下:

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 的帮助下创建这些文件。find_package(...) 在查找包时会检查多个位置,其中之一就是 <CMAKE_PREFIX_PATH>/cmake 目录。在我们的例子中,我们将把 config-file 包配置文件放到这个文件夹中。

为了创建 config-file 包,我们需要了解一些额外的内容,例如 CmakePackageConfigHelpers 模块。为了了解这些内容,让我们开始深入探讨一个实际的例子。我们将跟随 第四章**,示例 5 来学习如何构建一个 CMake 项目,将其组织成 chapter04/ex05_config_file_package 文件夹。首先,让我们检查 chapter04/ex05_config_file_package 目录中的 CMakeLists.txt 文件(注释和项目命令已省略以节省空间;另外,请注意,所有与主题无关的行将不被提及):

include(GNUInstallDirs)
set(ch4_ex05_lib_INSTALL_CMAKEDIR cmake CACHE PATH "Installation
  directory for config-file package cmake files")Is

CMakeLists.txt 文件与 chapter04/ex02_static 非常相似。这是因为它是同一个示例,只是它支持 config-file 包。第一行 include(GNUInstallDirs) 用于包含 GNUInstallDirs 模块。这个模块提供了 CMAKE_INSTALL_INCLUDEDIR 变量,稍后会用到。set(ch4_ex05_lib_INSTALL_CMAKEDIR...) 是一个用户定义的变量,用于设置 config-file 打包配置文件的目标安装目录。它是一个相对路径,应在 install(…) 指令中使用,因此它隐式地是相对于 CMAKE_INSTALL_PREFIX 的:

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(...) 调用与通常的调用非常不同。它使用了 generator expressions 来区分构建时的 include 目录和安装时的 include 目录,因为构建时的 include 路径在目标被导入到另一个项目时将不存在。以下一组命令将使目标可安装:

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...) 命令用于安装目标的头文件,这些文件位于 ${PROJECT_SOURCE_DIR}/include/,并安装到 ${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_INCLUDEDIR} 目录下。${CMAKE_INSTALL_INCLUDEDIR} 变量用于为用户提供覆盖此安装的 include 目录的能力。现在,让我们从之前示例中创建一个导出文件:

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.cmakeNAMESPACE 参数用于给所有导出的目标添加前缀命名空间。这使得你可以将所有导出的目标放在一个公共的命名空间下,避免与其他有相似目标名称的包发生冲突。最后,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表示该包与任何具有相同主版本号的版本兼容。其他可能的值包括AnyNewerVersionSameMinorVersionExactVersion

现在,让我们测试一下这个是否有效。为了测试包配置,我们必须以常规方式安装项目:

cmake -S . -B ./build
cmake --build ./build
cmake --install ./build --prefix /tmp/install-test

cmake --install命令的输出应如下所示:

-- 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目录中。检查这些文件的内容作为练习留给您自己。所以,现在我们手头有一个可消费的包。让我们换个角度,尝试消费我们新创建的包。为此,我们将查看chapter04/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_libch4_ex05_lib::ch4_ex05_lib)。ch4_ex05_lib::ch4_ex05_lib就是我们在软件包中定义的实际目标。让我们来看一下源文件src/main.cpp

#include <chapter04/ex05/lib.hpp>
int main(void){
    chapter04::ex05::greeter g;
    g.greet();
}

这是一个简单的应用程序,它包括chapter04/ex05/lib.hpp,创建一个greeter类的实例,并调用greet()函数。让我们尝试编译并运行该应用程序:

cd chapter04/ex05_consumer
cmake -S . -B build/ -DCMAKE_PREFIX_PATH:STRING=/tmp/install-test
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打包来结束这一部分。

使用 CPack 创建可安装软件包

到目前为止,我们已经看到了 CMake 如何构建软件项目。尽管 CMake 是这场演出的主角,但它也有一些强大的朋友。现在是时候向你介绍 CPack——CMake 的打包工具了。它默认与 CMake 一起安装。它允许你利用现有的 CMake 代码生成特定平台的安装包。CPack 的概念类似于 CMake。它基于生成器,这些生成器生成的是软件包而非构建系统文件。下表展示了截至版本 3.21.3 的可用 CPack 生成器类型:

生成器名称 描述
7Z 7-zip 压缩档案
DEB Debian 软件包
External CPack 外部软件包
IFW Qt 安装程序框架
NSIS Null Soft 安装程序
NSIS64 Null Soft 安装程序(64 位)
NuGet NuGet 软件包
RPM RPM 软件包
STGZ 自解压 TAR gzip 压缩档案
TBZ2 Tar BZip2 压缩档案
TGZ Tar GZip 压缩档案
TXZ Tar XZ 压缩档案
TZ Tar 压缩档案
TZST Tar Zstandard 压缩档案
ZIP Zip 压缩档案

CPack 使用 CMake 的安装机制来填充包的内容。CPack 使用位于CPackConfig.cmakeCPackSourceConfig.cmake文件中的配置详情来生成包。这些文件可以手动填充,也可以通过 CMake 配合 CPack 模块自动生成。对于一个已有的 CMake 项目,使用 CPack 非常简单,只需要包含 CPack 模块,前提是项目已经有正确的install(…)命令。包含 CPack 模块会使 CMake 生成CPackConfig.cmakeCPackSourceConfig.cmake文件,这些文件是打包项目所需的 CPack 配置。此外,还会生成一个额外的package目标,用于构建步骤。这个步骤会构建项目并运行 CPack,从而开始打包。当 CPack 配置文件已经正确填充时,无论是通过 CMake 还是用户,CPack 都可以使用。CPack 模块允许你自定义打包过程。可以设置大量的 CPack 变量,这些变量分为两类——通用变量和生成器特定变量。通用变量影响所有包生成器,而生成器特定变量仅影响特定类型的生成器。我们将检查最基本和最显著的变量,主要处理通用变量。以下表格展示了我们将在示例中使用的最常见的 CPack 变量:

变量名 描述 默认值
CPACK_PACKAGE_NAME 包名 项目名
CPACK_PACKAGE_VENDOR 包的供应商名称 “Humanity”
CPACK_PACKAGE_VERSION_MAJOR 包的主版本 项目的主版本
CPACK_PACKAGE_VERSION_MINOR 包的次版本 项目的次版本
CPACK_PACKAGE_VERSION_PATCH 包的补丁版本 项目的补丁版本
CPACK_GENERATOR 使用的 CPack 生成器列表
CPACK_THREADS 支持并行时使用的线程数 1

必须在包含 CPack 模块之前修改变量,否则将使用默认值。让我们通过一个例子来深入了解 CPack 的实际操作。我们将跟随第四章示例 6chapter04/ex06_pack)进行。这个示例是一个独立的项目,不是根项目的一部分。它是一个常规项目,包含名为executablelibrary的两个子目录。executable目录的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)

library目录的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/chapter04/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 chapter04, 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 "CBP Authors")
set(CPACK_GENERATOR "DEB;RPM;TBZ2")
set(CPACK_THREADS 0)
set(CPACK_DEBIAN_PACKAGE_MAINTAINER "CBP Authors")
include(CPack)

顶层的 CMakeLists.txt 文件几乎是一个常规的顶层 CMakeLists.txt 文件,唯一不同的是最后四行。它设置了三个与 CPack 相关的变量,并引入了 CPack 模块。这四行足以提供基本的 CPack 支持。CPACK_PACKAGE_NAMECPACK_PACKAGE_VERSION_* 变量没有被设置,让 CPack 从顶层项目的名称和版本参数中推导出来。让我们配置一下项目,看看是否有效:

cd chapter04/ex06_pack
cmake –S . -B build/

配置项目后,CpackConfig.cmakeCpackConfigSource.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-Best-Practices/chapter04/ex06_pack/build/ch4_ex06_pack-1.0-Linux.deb
generated.
CPack: Create package using RPM
/*…*/
CPack: - package: /home/user/workspace/personal/CMake-Best-Practices/chapter04/ex06_pack/build/ch4_ex06_pack-1.0-Linux.rpm
generated.
CPack: Create package using TBZ2
/*…*/
CPack: - package: /home/user/workspace/personal/CMake-Best-Practices/chapter04/ex06_pack/build/ch4_ex06_pack-1.0-Linux.tar.bz2
generated.

在这里,我们可以看到 CPack 使用了 DEBRPMTBZ2 生成器分别生成了 ch4_ex06_pack-1.0-Linux.debch4_ex06_pack-1.0-Linux.rpmch4_ex06_pack-1.0-Linux.tar.bz2 包。我们来尝试在 Debian 环境中安装生成的 Debian 包:

sudo dpkg -i build/ch4_ex06_pack-1.0-Linux.deb

如果打包正确,我们应该能够在命令行中直接调用 ch4_ex06_executable

13:38 $ ch4_ex06_executable
Hello, world!

成功了!作为练习,试着安装 RPMtar.bz2 包。处理包文件通常有两种方式。一种是创建小型包,依赖其他包来安装所需的依赖项;另一种方式是创建包含所有必要库的独立安装包,以便独立运行。通常,Linux 发行版自带包管理器来处理这些依赖项,而 Windows 和 macOS 默认依赖独立的安装程序。虽然近年来,Windows 上的 Chocolatey 和 macOS 上的 Homebrew 已成为支持依赖包的流行包管理器,但 CPack 目前(尚未)支持它们。到目前为止,我们只看过需要用户自行安装所有依赖项的简单包。接下来,我们来看一下如何为 Windows 构建一个便于分发的独立包。

为 Windows 创建独立安装程序

由于 Windows 并没有自带标准的包管理器,软件的安装程序通常需要包含所有必要的库。一种做法是将预制的安装程序打包成 NSIS 或 WIX 安装包,但这并非总是可行的,所以我们来看一下如何查找依赖文件。为此,CMake 提供了 install 命令的可选 RUNTIME_DEPENDENCIES 标志和 InstallRequiredSystemLibraries 模块,用于查找打包所需的依赖项。

它们的使用方式如下:

if(WIN32)
  if(CMAKE_BUILD_TYPE STREQUAL "Debug")
    set(CMAKE_INSTALL_DEBUG_LIBRARIES TRUE)
  endif()
  include(InstallRequiredSystemLibraries)
endif()

在前面的示例中,包含了 InstallRequiredSystemLibraries 模块。该模块是针对 Windows 进行定制的。包含该模块会创建安装编译器提供的库的指令,例如 MSVC 提供的 Visual Studio C++ 运行时库。通过将 CMAKE_INSTALL_DEBUG_LIBRARIES 变量设置为 true(如前面示例中所做),可以配置为包括库的调试版本。还有更多选项可以指示 CMake 安装额外的库,例如 Windows MFC 库、OpenMP 或用于在 Windows XP 或更早版本的 Windows 上进行应用本地部署的 Microsoft Universal CRT 库。

模块的完整文档可以在这里找到:cmake.org/cmake/help/latest/module/InstallRequiredSystemLibraries.html

包括编译器提供的库是一回事,但通常软件项目还会依赖其他库。如果这些库需要与项目一起打包,可以通过 install() 命令的 RUNTIME_DEPENDENCIES 选项来包含它们,如下所示:

# this includes the runtime directories of the executable and the library
install(TARGETS ch4_ex07_executable
        RUNTIME_DEPENDENCIES
        PRE_EXCLUDE_REGEXES "api-ms-.*" "ext-ms-.*"
        POST_EXCLUDE_REGEXES ".*system32/.*\\.dll"
        )

这将尝试找出目标指定依赖的任何共享库。由于 Windows 处理 DLL 解析的方式,这很可能会找到比实际需要更多的库。具体来说,它很可能会找到以api-msext-ms开头的库,这些库是为了兼容性原因存在的,并且并不需要。可以通过 PRE_EXCLUDE_REGEXES 选项将这些库过滤掉,该选项会在包含库之前进行过滤。任何与这些正则表达式匹配的文件路径都将在确定运行时依赖时被排除在考虑范围之外。或者,也可以使用 POST_EXCLUDE_REGEXES 选项,在找到文件之后对其进行过滤。如果你想排除来自某个特定位置的文件,这个选项很有用。在前面的示例中,它被用来排除来自 32 位 system32 文件夹的 DLL 文件。

在本节中,我们学习了如何使用 CPack 打包我们的项目。这不是一本详尽的指南。有关完整指南,官方的 CPack 文档提供了大量的信息。至此,我们成功完成了本章内容。

总结

在本章中,我们学习了如何使目标可安装的基础知识,以及如何为开发和消费者环境打包项目。部署是专业软件项目中的一个重要方面,借助我们在本章中覆盖的内容,你可以轻松处理这些部署需求。

在下一章中,我们将学习如何将第三方库集成到 CMake 项目中。

问题

请回答以下问题,测试你对本章内容的理解:

  1. 我们如何指示 CMake 使 CMake 目标可安装?

  2. 通过 install(TARGETS) 命令安装时,哪些文件会被安装?

  3. 对于库目标,install(TARGETS) 命令是否会安装头文件?为什么?如果没有,如何安装头文件?

  4. GNUInstallDirs CMake 模块提供了什么?

  5. 如何选择性地将一个目录的内容安装到目标目录中?

  6. 为什么在指定安装目标目录时应该使用相对路径?

  7. config-file 包所需的基本文件是什么?

  8. 导出一个目标是什么意思?

  9. 如何使 CMake 项目能够通过 CPack 打包?

答案

以下是上述问题的答案:

  1. 这可以通过 install(TARGETS <target_name>) 命令实现。

  2. 指定目标的输出工件。

  3. 不会,因为头文件不被视为目标的输出工件。它们必须通过 install(DIRECTORY) 命令单独安装。

  4. GNUInstallDirs CMake 模块提供了系统特定的默认安装路径,例如 binlibinclude

  5. 通过 install(DIRECTORY) 命令的 PATTERNFILES_MATCHING 参数的帮助。

  6. 为了使安装可迁移,用户可以通过指定安装前缀来更改安装目录。

  7. <package-name>-config.cmake<package-name>Config.cmake 文件,另可选择包含 <package-name>-config-version.cmake<package-name>ConfigVersion.cmake 文件。

  8. 导出一个目标意味着创建所需的 CMake 代码,以便将其导入到另一个 CMake 项目中。

  9. 通过包含 CPack 模块可以实现。

posted @   绝不原创的飞龙  阅读(10)  评论(0编辑  收藏  举报
编辑推荐:
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
阅读排行:
· 终于写完轮子一部分:tcp代理 了,记录一下
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
历史上的今天:
2024-03-03 OpenDocCN 20240303 更新
2024-03-03 笨办法学 Python3 第五版(预览)(三)
2024-03-03 笨办法学 Python3 第五版(预览)(二)
2024-03-03 笨办法学 Python3 第五版(预览)(一)
2023-03-03 PyTorch 1.0 中文官方教程:用 numpy 和 scipy 创建扩展
点击右上角即可分享
微信分享提示