CMake-最简指南-全-

CMake 最简指南(全)

原文:zh.annas-archive.org/md5/24d02b6780ccd97288f1c67371ea0295

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

我们非常高兴你决定选择Minimal CMake,无论是为了首次了解 CMake,还是为了扩展你今天对 CMake 的理解。如果你有兴趣了解 CMake 如何帮助你创建库和应用程序、与世界级开源软件集成,或者它如何帮助你与他人共享你的创作,那么你来对地方了。

本书的标题有些俏皮,但其核心思想是尽可能快地深入了解 CMake 的精髓,跳过一些部分,完全避免其他部分。CMake 最擅长的就是做你需要它做的事,然后给你留出空间。你不需要成为 CMake 专家就能高效使用它,无论你是 CMake 新手,还是离开一段时间后回来看它有什么变化,本书都将为你提供有价值的内容。

本书的一个重要特点是它专注于实际的示例。它将一步一步地引导你从一个简单的控制台应用程序开始,一直到一个完整的窗口化应用程序,这个应用程序可以在 macOS、Windows 和 Linux 上运行。每一章都建立在上一章的基础上,并且每一章都伴随有源代码,源代码被拆分成多个部分,每一部分都在前面的基础上逐步展开。你将亲眼目睹整个过程是如何展开的,以及每个变化背后的推理和取舍。

第一部分中,我们将确保每个人都具备设置 CMake 所需的工具。然后我们将通过一些简单的 CMake 脚本,帮助大家熟悉最基本的 CMake 命令。接着,我们将重点介绍 CMake 中一项相对较新且功能强大的补充,它使得集成外部库变得非常简单。除了展示如何集成外部代码外,我们还将展示如何通过创建可重用的库使自己的代码能够共享。

第二部分建立在第一部分的基础上。我们将首先介绍近年来 CMake 增加的一些极其有用的生活质量改进。这些改进消除了许多繁琐的命令,同时保持了 CMake 脚本的简洁与清晰。我们还将开始介绍更大的依赖项,并理解如何处理它们的策略,以及如何创建它们。最后,我们将展示如何创建一个统一的构建系统,使用一个命令即可配置并构建我们的应用程序、库和依赖项。

第三部分中,我们将介绍一些 CMake 能帮助我们构建和共享更好软件的其他重要领域。我们将看到如何为我们的库和应用程序添加多种类型的测试,以及如何借助配套工具 CTest 将它们结合起来。我们还将深入了解 CMake 如何帮助我们打包应用程序,使其不仅能在我们的机器上运行,还能在任何地方运行。最后,我们将回顾一下其他一些可以简化 CMake 使用的优秀工具,并了解接下来在哪里可以继续扩展你的 CMake 知识。

为什么选择 CMake?

CMake 是那种不幸地(虽然不一定是不值得的)有些不太好的声誉的工具。许多开发者曾经被 CMake 烧过。他们可能在一个遗留代码库中被迫与它打交道,不得不处理复杂性、大量的 CMake 脚本和可疑的做法。或者,他们可能尝试将其用于新项目,但最终因为难以让事情正常工作而感到困惑和沮丧。由于 CMake 存在已久,许多过时的示例和资源引用了旧版本的 CMake,缺少所有最新功能(以及对什么有效、什么不能做的经验教训)。就像任何成功的框架或语言一样,CMake 也带有一定的包袱,这是不可避免的。

话虽如此,好消息是,在过去的几年里,CMake 经历了一次复兴,从 3.0 版本开始(对于死忠用户来说,技术上是 2.8 版本),通过从目标生成复杂项目的新功能,带来了巨大的变化,自那时以来,它的受欢迎程度逐渐上升。

CMake 日益流行的原因之一是一个至关重要的细节,这一点再强调也不为过。如果你使用 CMake 描述你的构建,默认情况下,你将拥有一个可以在 Windows、Linux 和 macOS 上构建的项目。如果你在 Windows 上使用 Visual Studio、在 macOS 上使用 Xcode 或在 Linux 上使用 Make 文件启动项目,迁移到其他平台会更难,且不同平台上的其他人也更难参与进来。将构建设置和选项存储在版本化的、易于阅读的脚本中是非常有用的(再也不需要在 IDE 中翻找嵌套的选项卡和窗口来找到要更改的值)。

另一个有趣的发展是,随着 CMake 的普及,它现在已经成为 C 和 C++ 构建系统的通用语言。如果一个 C 或 C++ 项目托管在 GitHub 上, chances are 它正在使用 CMake,即使不是,通过极少的努力,也可以编写一个简单的集成或包装器,使得该库能被 CMake 使用。这解锁了巨大的潜力,因为为项目添加依赖关系以提供改进,突然间变成了几行代码,而不是一个痛苦、繁重且耗时的过程,通常需要将其他库的代码嵌入到你的项目中,或者下载并编写代码来构建那些库。

使用 CMake 构建你的项目并且掌握足够的 CMake 知识以应付日常需求(你完全不需要成为专家)将使你编写应用程序的速度更快、更加易于维护,并且更具协作性。这也会让使用 C 或 C++ 更加有趣(我们保证),不仅仅是 C 和 C++,在本文发布时,CMake 还支持 C#、Fortran、CUDA、Objective-C 和 Swift。

CMake 绝非完美,但今天,它在 C 和 C++ 领域无处不在,并且由于 Kitware 开发者和许多具有影响力的开源贡献者的出色工作,它不断发展和改进。我们确信,无论你是拥有多年经验的专业 C++ 开发者,还是刚刚入门、正在学习或重新熟悉 CMake 的学生或爱好者,掌握 CMake 都会让你成为一个更快乐、更高效的开发者。你在依赖管理和项目结构等方面获得的经验,未来也会为你带来好处。

现在让我们将注意力转向你认为最适合的群体,以帮助你从这本书中获得最大收益。

这本书适合谁

我们理解,可能有广泛的读者群体对阅读本书感兴趣,因此我们希望为每种类型的读者提供一些具体细节,以帮助你更好地准备好学习本书的内容。

学生

如果你是计算机科学或软件工程专业的学生,那么这本书可能会对你有帮助,不仅能帮助你入门 CMake,还能让你了解跨平台开发、静态库与共享库、代码结构和测试等内容。书中的信息将为你开发自己的桌面应用程序提供坚实的基础,无论是游戏、工具还是模拟。某些主题可能对你来说不太熟悉,但通过研究代码并跟随推荐的链接,你很快就能掌握内容。

经验丰富的 C/C++ 开发者

如果你带着丰富的构建 C/C++ 库和/或应用程序的经验来阅读本书,那么很多基础内容可能会很熟悉,但你如何使用 CMake 来高效地配置和设置将会引起你的兴趣(例如,涉及动态库加载和 RPATH 处理等主题)。示例项目还使用了一些有趣的库(特别是图形库 bgfx 和用户界面库 Dear ImGui),这些也可能会引起你的兴趣,同时它们在 CMake 中的处理方法也是如此。CMake 如何处理安装和打包也可能与你相关(特别是如何使用 CMake 正确配置安装程序/磁盘镜像)。

有经验的开发者(其他语言)

如果你是一个熟悉其他编程语言的有经验的开发者(例如 JavaScript/TypeScript、Java、C#、Python、Rust),那么你一定能够轻松跟上本书的内容。希望 CMake 与你所使用的现有语言/框架中的构建系统之间的相似之处,能使你更容易理解这些概念(尤其是在依赖管理方面)。如果你是 C/C++ 新手(或久未接触),那么这些示例也可以作为 C/C++ 的复习材料。

爱好者

如果你是一个业余开发者,想要构建你的第一个游戏或工具,与朋友、家人或同事分享,那么本书应该能为你提供所需的一切,帮助你入门。如果你想构建一个窗口化的应用程序或坚持运行终端中的应用,我们也为你提供了相关内容。本书中介绍如何集成第三方库的信息,尤其有助于帮助你在短时间内提高生产力。

本书内容概览

第一章入门,涵盖了无论你是使用 Windows、macOS 还是 Linux,都能启动和运行 CMake 所需的一切。

第二章你好,CMake!,带你快速浏览 CMake,介绍了一些最基本的概念以及我们将在本书中构建的应用程序的核心。

第三章使用 FetchContent 与外部依赖,展示了如何引入我们的第一个外部依赖,以最小的努力增强应用程序。

第四章为 FetchContent 创建库,换个角度,展示了如何创建一个库,之后可以被FetchContent使用。

第五章简化 CMake 配置,转向关注如何设置 CMake,以尽可能高效地工作,并消除冗长的命令。

第六章安装依赖与 ExternalProject_Add,在第五章的基础上,向你展示了如何最好地处理项目中的更大依赖。

第七章为你的库添加安装支持,详细讲解了如何使你的库可安装,以便像我们在第六章中探讨的依赖一样使用。

第八章使用超级构建简化入门,回到简化项目的主题,演示了如何通过一个命令构建你的项目和多个外部依赖。

第九章为项目编写测试,探讨了测试的重要性,并概述了 CTest 如何帮助巩固各种类型的测试。

第十章为共享打包项目,讲解了在将你的项目准备好与他人共享时,解决最后一个难题。

第十一章支持工具与下一步,探讨了更广泛的 CMake 生态系统,并介绍了一系列能够与 CMake 本身互补的工具,还分享了一些关于未来学习 CMake 的主题和资源。

充分利用本书

如果你在以下内容中有一些经验,跟随学习会更加容易:

  • 基本的 C/C++ 知识(或类似的过程式语言,如 Java 或 C#)

  • 熟悉终端/命令行

  • 使用代码编辑器的经验(例如 Visual Studio Code)

  • 基本的图形编程概念的意识(加分项;这有助于理解后面的某些示例,但并非必需)

我们希望这本书能帮助你理解和欣赏 CMake 在简化应用构建方面的作用。它将为你提供一个坚实的基础,帮助你构建和组织新项目,让使用他人的代码以及分享自己的代码变得更加简单。

书中涵盖的 软件/硬件 操作系统 要求
C, C++, CMake, CTest, CPack, Visual Studio Code Windows, macOS, Linux

我们建议你克隆本书附带的仓库,浏览代码并运行示例,以更深入地了解事物的运作方式。我们还建议使用文本比较工具(diff 工具)来比较章节内容,随着时间的推移,库和应用程序会发生变化。

下载示例代码文件

你可以从 github.com/PacktPublishing/Minimal-CMake 下载本书的示例代码文件。如果代码有更新,它会在 GitHub 仓库中进行更新。

我们还提供了其他来自我们丰富书籍和视频目录的代码包,详情请访问 github.com/PacktPublishing/。快来看看吧!

使用的约定

本书中使用了若干文本约定。

文本中的代码:表示文本中的代码词汇、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名。以下是一个示例:“我们将介绍我们需要在 CMakeLists.txt 文件中进行的更改,以及创建软件包所需的命令。”

一段代码的设置如下:

cmake_minimum_required(VERSION 3.28)
project(mc-array LANGUAGES C)

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

Shaders not found. Have you built them using compile-shader-<platform>.sh/bat script?

粗体:表示新术语、重要词汇或屏幕上显示的词语。例如,菜单或对话框中的文字会以粗体显示。以下是一个示例:“完成后,关闭并重新打开终端,返回到 VS 2022 的开发者命令提示符。”

提示或重要说明

如下所示:

联系我们

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

一般反馈:如果你对本书的任何部分有疑问,请通过电子邮件联系 customercare@packtpub.com,并在邮件主题中提到书名。

勘误表:尽管我们已尽最大努力确保内容的准确性,错误还是有可能发生。如果你发现书中有错误,我们非常感激你能将其报告给我们。请访问 www.packtpub.com/support/errata 并填写表格。

盗版:如果你在互联网上发现任何我们作品的非法副本,无论形式如何,我们将非常感激你提供相关位置或网站名称。请通过版权@packtpub.com 与我们联系,并附上相关材料的链接。

如果你有兴趣成为作者:如果你在某个主题上有专业知识,并且有意编写或贡献书籍,请访问authors.packtpub.com

分享你的想法

阅读完Minimal Cmake后,我们很希望听到你的想法!请点击这里直接进入本书的亚马逊评论页面,分享你的反馈。

你的评论对我们和技术社区至关重要,将帮助我们确保提供优质内容。

下载本书的免费 PDF 副本

感谢你购买本书!

你喜欢在移动中阅读,但又无法随身携带纸质书籍吗?

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

别担心,现在每本 Packt 书籍都附赠免费的 DRM 免保护 PDF 版本。

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

优惠不仅仅于此,你还可以独享折扣、新闻通讯以及每日送到你邮箱的优质免费内容。

按照这些简单步骤获取优惠:

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

packt.link/free-ebook/9781835087312

  1. 提交你的购买凭证

  2. 就这些!我们将直接通过电子邮件发送你的免费 PDF 及其他优惠。

第一部分:启动

本书的第一部分通过确保你拥有开始使用 CMake 所需的一切来开始,无论你是在 Windows、macOS 还是 Linux 上。一旦你的环境设置好并运行起来,我们将介绍 CMake 本身,从我们项目的基础开始,这些基础将随着本书的进展而不断发展。随着我们的简单应用程序投入使用,我们将转向如何借助 CMake 使用他人的代码,并展示如何以简洁的方式轻松引入第三方依赖。在进入本书的第二部分之前,我们将展示如何将我们的应用程序的一部分提取到一个可重用的库中,该库可以与他人共享并被我们的主应用程序使用。

本部分包括以下章节:

  • 第一章开始使用

  • 第二章你好,CMake!

  • 第三章使用 FetchContent 与外部依赖

  • 第四章为 FetchContent 创建库

第一章:入门

Minimal CMake 的目标是引导你完成应用程序的开发过程,从一个简单的控制台应用程序开始,直到一个完整的窗口化应用程序,你可以向朋友演示,并将其作为未来项目的模板。

我们将看到 CMake 如何帮助整个过程。或许 CMake 提供的最大好处是,它能够轻松地将现有的开源软件集成进来,以增强你应用的功能。

在开始使用 CMake 创建应用之前,我们需要确保我们的开发环境已设置并准备好。根据你选择的平台(Windows、macOS 或 Linux),设置过程会有所不同。我们将在这里讨论每个平台。这将为我们介绍 CMake 并开始构建应用程序的核心提供一个良好的起点。

在本章中,我们将讨论以下主题:

  • 在 Windows 上安装 CMake

  • 在 macOS 上安装 CMake

  • 在 Linux(Ubuntu)上安装 CMake

  • 安装 Git

  • Visual Studio Code 设置(可选)

技术要求

为了充分利用本书,我们建议你在本地运行示例。为此,你需要以下内容:

  • 一台运行最新 操作系统 (OS) 的 Windows、Mac 或 Linux 计算机

  • 一个工作中的 C/C++ 编译器(如果你还没有安装,建议使用每个平台的系统默认编译器)

本章中的代码示例可以通过以下链接找到:github.com/PacktPublishing/Minimal-CMake

CMake 版本

本书中的所有示例都已经在 CMake 3.28.1 版本下进行过测试。早期版本无法保证兼容。升级到较新的版本应该是安全的,但可能会存在差异。如果有疑问,建议在运行本书中的示例时使用 CMake 3.28.1。

在 Windows 上安装 CMake

在本节中,我们将介绍如何安装你在 Windows 上开始使用 CMake 构建应用所需的所有内容。

首先,你将需要一个 C/C++ 编译器。如果你还没有安装编译器,推荐使用 Visual Studio(可以从 visualstudio.microsoft.com/vs/community/ 下载 Visual Studio 2022 Community Edition)。

Visual Studio 是一个集成开发环境,附带微软的 C++ 编译器用于 Windows(cl.exe)。我们不会直接讨论 Visual Studio,尽管如果你更喜欢,也可以使用它(见 第十一章**,支持工具与后续步骤,其中有简要总结)。我们将讨论如何生成 Visual Studio 解决方案文件,并调用 MSBuild 构建项目。为了保持尽可能的一致性,我们将使用 Visual Studio Code 来展示大多数示例。这更多是出于方便的考虑,如果你更习惯使用其他工具,完全可以选择使用它。随着 CMake 的流行,Visual Studio 对 CMake 的支持大大增强,如果你主要在 Windows 上开发,值得了解一下。

Visual Studio 与 Visual Studio Code

虽然它们听起来相似,但 Visual Studio 和 Visual Studio Code 是两个截然不同的应用程序。Visual Studio 是微软的集成开发环境,主要运行在 Windows 上(令人困惑的是,macOS 上也有一个版本的 Visual Studio,与 Windows 版本大不相同)。Visual Studio 用于构建 C++ 或 .NET(C#、F# 和 Visual Basic)应用程序。另一方面,Visual Studio Code 是一个跨平台的代码编辑器,支持 Windows、macOS 和 Linux。它拥有广泛的扩展库,可以与许多不同的编程语言一起使用。它在 Web 开发中非常受欢迎,对 TypeScript 和 JavaScript 支持良好,尽管通过微软的 C/C++ 扩展,它对 C++ 也有强大的支持。我们将在本书中使用 Visual Studio Code。

打开 Visual Studio 安装程序并选择Visual Studio Community 2022(如果你在阅读本书时有更新的版本,随时可以选择那个版本)。

图 1.1:Visual Studio 安装程序版本选择器

图 1.1:Visual Studio 安装程序版本选择器

选择Visual Studio Community 2022后,系统会显示一个新面板。工作负载标签页让你选择一个选项来包含一组合理的默认设置。向下滚动并选择C++ 桌面开发

图 1.2:Visual Studio 安装程序工作负载选择器

图 1.2:Visual Studio 安装程序工作负载选择器

右侧有几个可选的组件默认被选中。保持这些选项选中也不会有问题。如果你愿意,可以取消选择某些功能,例如图像和 3D 模型编辑器Boost/Google.Test 测试适配器

确认选择后,点击窗口右下角的安装按钮。

安装完成后,打开 Windows 开始菜单并按照以下步骤操作:

图 1.3:Windows 11 任务栏搜索框

图 1.3:Windows 11 任务栏搜索框

  1. 搜索终端

图 1.4: Windows 11 应用搜索结果

图 1.4: Windows 11 应用搜索结果

  1. 打开 Terminal 应用。然后,从顶部栏点击下拉菜单,选择 VS 2022 的开发者命令提示符

图 1.5: Microsoft Terminal 新标签页选择器

图 1.5: Microsoft Terminal 新标签页选择器

自定义命令提示符

在指定主机和目标架构的情况下,修改默认的 VsDevCmd.bat 是可能的。为此,进入 profiles 部分,找到 Command Prompt 项,在 list 下修改 commandLine 属性,包含 VsDevCmd.bat 的路径和所需的架构(例如,"commandline": "%SystemRoot%\\System32\\cmd.exe /k \"C:\\Program Files\\Microsoft Visual Studio\\2022\\Community\\Common7\\Tools\\VsDevCmd.bat\" -arch=x64 -host_arch=x64")。也可以在 Windows Terminal 打开 Git Bash 时调用 VsDevCmd.bat(如果你还没有安装 Git,请参阅 安装 Git 部分)。为此,找到 "commandLine": "\"C:\\Program Files\\Microsoft Visual Studio\\2022\\Community\\Common7\\Tools\\VsDevCmd.bat\" -arch=x64 -host_arch=x64 && \"%PROGRAMFILES%/Git/bin/bash.exe\" -i -l"

  1. 为了验证 Microsoft 编译器是否按预期工作,运行 cl.exe。你应该能看到以下输出(架构会根据你使用的机器而有所不同):

图 1.6: 从开发者命令提示符运行 cl.exe

图 1.6: 从开发者命令提示符运行 cl.exe

CMake 和 Visual Studio

Visual Studio 自带了自己的 CMake 版本,你可以依赖这个版本并跳过接下来的两步。它位于 C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin。运行 cmake --version 会显示 cmake version <version>-msvc1,这表示该版本与普通的 CMake 版本不同。

  1. 如果你的系统上尚未安装 CMake(或者安装的是相当旧的版本),请访问 cmake.org/download/ 获取最新版本(截至本文写作时,版本是 3.28.1)。

    最简单的选项是下载 Windows x64 安装程序 (cmake-3.28.1-windows-x86_64.msi),然后按照标准安装说明进行操作。

图 1.7: CMake Windows 安装程序

图 1.7: CMake Windows 安装程序

  1. 确保你选择了 将 CMake 添加到系统 PATH 环境变量中的当前用户

图 1.8: CMake 安装程序 PATH 选项

图 1.8: CMake 安装程序 PATH 选项

  1. 按照剩余的安装说明进行操作,等待 CMake 安装完成。一旦完成,关闭并重新打开 Terminal,然后返回到 cmakecmake --version,你应该能看到以下内容:

图 1.9: 从开发者命令提示符运行 cmake.exe

图 1.9: 从开发者命令提示符运行 cmake.exe

有了这些,我们就可以开始使用 CMake 构建了。

在 macOS 上安装 CMake

在本节中,我们将介绍如何安装所有你需要的工具,以便在 macOS 上开始构建应用程序。

首先,你需要一个 C/C++ 编译器。如果你还没有安装编译器,最安全的选择是安装 Xcode,它可以从 App Store 下载:

  1. 通过点击 macOS 菜单栏上的放大镜图标,进入Spotlight 搜索

图 1.10:macOS 菜单栏上的 Spotlight 搜索选项

图 1.10:macOS 菜单栏上的 Spotlight 搜索选项

  1. 搜索 App Store

图 1.11:从 Spotlight 搜索中查找 App Store

图 1.11:从 Spotlight 搜索中查找 App Store

  1. App Store 中搜索 Xcode

图 1.12:来自 App Store 的 Xcode 搜索结果

图 1.12:来自 App Store 的 Xcode 搜索结果

  1. 点击 获取 然后点击 安装 按钮。

图 1.13:Xcode 应用程序安装

图 1.13:Xcode 应用程序安装

也可以从 developer.apple.com 安装Xcode命令行工具,特别是从 developer.apple.com/download/all/,该链接也包含了我们与 CMake 一起使用所需的核心工具。要访问 Apple Developer 网站,需要一个 Apple Developer 账户(你可以在这里了解更多:developer.apple.com/account)。

  1. 一旦打开 Terminal,再次输入以下命令:
% clang --version

你应该看到类似以下的消息:

Apple clang version 15.0.0 (clang-1500.3.9.4)
...

这确认了我们有一个有效的编译器,并且现在可以安装 CMake 来与其一起使用。

  1. 如果你当前没有在系统上安装 CMake(或者安装的是一个相当旧的版本),请访问 cmake.org/download/ 获取最新版本(截至本文撰写时为 3.28.1)。

    最简单的选项是获取适用于 macOS 10.13 或更高版本的磁盘镜像(.dmg 文件)(cmake-3.28.1-macos-universal.dmg),并按照标准安装说明进行操作。

图 1.14:CMake macOS 安装

图 1.14:CMake macOS 安装

  1. CMake 拖动到 应用程序 文件夹中。

    现在,CMake GUI 已经可以在系统中使用,但尚未能从 Terminal 使用 CMake。

  2. 为了能够从 Terminal 运行 CMake 命令,打开 CMake(在 应用程序 文件夹中),暂时忽略弹出的 UI,接着进入 CMake macOS 菜单栏并点击 工具| 如何安装用于命令行使用

图 1.15:macOS 菜单栏上的 CMake 命令行安装选项

图 1.15:macOS 菜单栏上的 CMake 命令行安装选项

  1. 点击此项后,会弹出一个包含多个选项的窗口。最不干扰的选项可能是第一个,第二个选项也是不错的选择。

图 1.16:CMake 命令行安装选项面板

图 1.16:CMake 命令行安装选项面板

  1. 为了使路径选项持久化,我们需要更新我们的.zshrc文件。复制以下行:
PATH="/Applications/CMake.app/Contents/bin":"$PATH"
  1. 从终端确保你在主目录(cd ~)中,然后打开你的.zshrc文件(你可以使用你喜欢的文本编辑器,或者在终端中输入nano .zshrc)。

图 1.17:从终端用 nano 打开.zshrc

图 1.17:从终端用 nano 打开.zshrc

  1. 粘贴之前的命令并保存文件。

图 1.18:在终端内用 nano 修改.zshrc

图 1.18:在终端内用 nano 修改.zshrc

  1. 为了重新加载 Zsh 配置文件并更新PATH变量,运行source .zshrc

图 1.19:通过再次执行.zshrc 来刷新终端环境

图 1.19:通过再次执行.zshrc 来刷新终端环境

  1. 最后,从终端运行cmake来验证是否能够找到它。

图 1.20:从终端运行 cmake

图 1.20:从终端运行 cmake

你也可以使用where cmakecmake --version来验证是否安装了正确版本。

有了这些,我们就可以开始使用 CMake 进行构建了。

在 Linux(Ubuntu)上安装 CMake

在这一节中,我们将介绍如何获取你在 Linux(Ubuntu)上构建应用所需的一切。

首先,你需要一个 C/C++编译器。如果你还没有安装编译器,一个很好的选择是使用 GCC。可以通过标准的 Ubuntu 包管理器apt来安装:

  1. 使用桌面上的显示应用程序打开终端

图 1.21:Ubuntu 显示应用菜单选项

图 1.21:Ubuntu 显示应用菜单选项

  1. 运行sudo apt update,然后运行sudo apt install build-essential(你的 Ubuntu 版本可能已经安装了这个,但最好检查一下)。

图 1.22:从终端安装 build-essential

图 1.22:从终端安装 build-essential

  1. 运行gcc --version来验证编译器是否能够找到并正常工作。你应该看到类似以下的输出:
gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0 ...

图 1.23:从终端运行 gcc --version

图 1.23:从终端运行 gcc --version

  1. 接下来,我们需要安装 CMake。这可以通过包管理器(例如apt)完成,但我们在这里直接进行安装,以指定精确的版本。访问cmake.org/download/,并向下滚动找到二进制发行版部分。根据你的架构,选择 Linux x86_64(Intel)(cmake-3.28.1-linux-x86_64.tar.gz)或 Linux aarch64(ARM)(cmake-3.28.1-linux-aarch64.tar.gz)。

  2. 从你下载 CMake 的文件夹运行此命令,提取并安装 CMake 到你的/opt文件夹:

sudo tar -C /opt -xzf cmake-3.28.1-linux-aarch64.tar.gz

(将文件提取到本地文件夹并更新 PATH 变量以指向 bin 文件夹是完全合理的做法。安装到 /opt 是一种常见的方法)。

  1. 你也可以直接双击 tar.gz 文件并使用 归档管理器提取 选项:

    1. 点击 提取 选项,然后转到 其他位置 | 计算机,选择 opt 文件夹。

    2. 然后再次点击右上角的 提取

图 1.24:Ubuntu 归档管理器提取对话框

图 1.24:Ubuntu 归档管理器提取对话框

  1. 转到你的主目录(cd ~),然后输入 nano .bashrc

图 1.25:从终端使用 nano 打开 .bashrc

图 1.25:从终端使用 nano 打开 .bashrc

  1. 将你在 /opt 文件夹中提取的目录里的 bin 子文件夹添加到 PATH 变量中,使用以下命令:

    cmake-3.28.1-linux-x86_64 instead of cmake-3.28.1-linux-aarch64).
    

图 1.26:在终端内使用 nano 修改 .bashrc

图 1.26:在终端内使用 nano 修改 .bashrc

  1. 保存文件并关闭 nano(Ctrl+O, Ctrl+X)。然后运行 source .bashrc 来重新加载 .bashrc 文件并更新当前终端会话中的 PATH 变量。

  2. 最后,输入 cmake 并按回车键,确认一切按预期工作。你应该会看到以下内容输出:

图 1.27:从终端运行 cmake

图 1.27:从终端运行 cmake

  1. 最后一步,运行 sudo apt-get install libgles2-mesa-dev 来确保你已经安装了运行书中后续示例所需的依赖项。

至此,我们已经准备好开始使用 CMake 构建项目了。

安装 Git

为了跟随本书每章提供的示例并获取书中的源代码(可从书籍网站 github.com/PacktPublishing/Minimal-CMake 获取),建议在你的系统上安装 Git。

最简单的做法是访问 git-scm.com/downloads,并根据你选择的平台下载 Git,如果你还没有安装的话。

在 macOS 上,Git 是作为我们在 macOS 上安装 CMake 一文中介绍的 Xcode 安装的一部分。 在 Windows 上,下载 64 位安装程序并运行安装。在 Linux(Ubuntu)上,运行 sudo apt-get install git 来安装 Git。

在命令行中输入 git 以验证该工具是否可用。

Visual Studio Code 设置(可选)

为了确保全书体验的一致性,将使用 Visual Studio Code 和本地终端来演示代码示例,无论是在 Windows、macOS 还是 Linux 上。以下部分概述了如何设置 Visual Studio Code 并配置开发环境。如果你更倾向于使用其他编辑器,也是可以的。跟随本书所需的只需一个 C/C++ 编译器和 CMake。Visual Studio Code 只是作为一个跨平台编辑器使用(它还提供了很棒的 CMake 支持,详细内容可见 第十一章**,支持工具与下一步)。

要安装 Visual Studio Code,请访问 code.visualstudio.com/Download。那里有适用于 Windows、Linux 和 macOS 的下载链接。按照你选择的平台的安装说明进行操作。在 Windows 上,选择 用户安装程序,并按照设置说明进行操作。

在 Linux 上,可以下载 .deb 包并使用 code-stable-...tar.gz 文件,并将其解压到 /opt,就像我们解压 CMake 一样(例如,sudo tar -C /opt -xzf code-stable-arm64-1702460949.tar.gz)。解压后,通过再次更新 .bashrc 文件,将 /opt/VSCode-linux-<arch>/bin 添加到你的路径中。

在 Mac 上,下载 .zip 文件,解压后,将 Visual Studio Code 应用程序拖放到 应用程序 文件夹中(可以通过 Finder 完成)。

需要提到的一点是,确保将 Visual Studio Code 添加到你的 PATH 中,以便可以从命令行轻松打开(使用 code . 从你的项目或工作区文件夹)。这可以在 Windows 的安装向导中完成,或通过在 Linux 上更新 .bashrc 来完成。在 macOS 上,可以通过 Visual Studio Code 内部完成此操作。打开 Visual Studio Code,按下 F1Shift + Cmd + P(macOS),或按下 Shift + Ctrl + P(Windows 或 Linux)。另外,你也可以从菜单栏点击 shell。然后执行 code 动作。

一旦安装并启动 Visual Studio Code,导航到 C/C++ 扩展包

图 1.28:Visual Studio Code 的扩展视图

图 1.28:Visual Studio Code 的扩展视图

C/C++ 扩展包扩展包括 C/C++ 扩展,提供 IntelliSense 和调试功能。该扩展包还包括 CMake 语言支持和 CMake Tools,这是 Visual Studio Code 的 CMake 集成工具。

现在我们已经安装了 Visual Studio Code,确保在所有平台上开发时都能获得一致的体验。使用 Visual Studio Code 完全是可选的,但强烈推荐使用。在 第十一章**,支持工具与下一步,我们将展示 CMake 和 Visual Studio Code 如何相辅相成。

总结

在本章中,我们介绍了开始使用 CMake 开发所需的一切。我们在 Windows、macOS 和 Linux 上安装了 C/C++ 编译器,并在每个平台上安装了 CMake。我们了解了如何安装 Git,并演示了如何安装 Visual Studio Code 以及启用一些有用的扩展。正确配置我们的开发环境非常重要,以确保后续的示例能够按预期运行。现在,我们已经具备了开始使用 CMake 的所有条件,并可以开始开发项目,了解 CMake 如何加速我们的软件构建过程。

在下一章中,我们将介绍 CMake,并查看你将在终端中常用的命令。我们还将查看构成 CMake 脚本的一些核心命令。我们将搭建一个基本的应用程序,并学习生成器、构建类型等内容。

第二章:Hello, CMake!

我们现在开始使用 CMake。首先,我们将介绍在终端中频繁使用的命令,然后是我们在 CMake 脚本中编写的命令。我们将通过启动一个Hello, CMake! 应用程序(回顾每个人最喜欢的 Hello, World! 程序),并用一个最小的 CMake 脚本进行引导,深入探讨我们使用的每个 CMake 命令。很快,这些命令将成为你的第二天性,让你轻松构建代码。

CMake 拥有丰富的功能集,但幸运的是,开始时只需要学习很少的内容就能提高生产力。它有很多选项可以处理复杂的使用场景;不过幸运的是,暂时我们不需要担心这些。知道它们在那儿就好,但不要觉得一开始就需要了解所有有关命令或 CMake 语言的知识。随着项目的推进,你将有足够的时间去学习这些。

本章将涵盖以下主题:

  • 从命令行使用 CMake

  • 检查我们的第一个 CMakeLists.txt 文件

  • CMake 生成器

  • 项目下一步

  • 添加另一个文件

技术要求

为了跟上进度,请确保你已满足第一章《入门》的要求。包括以下内容:

  • 一个具有最新 操作系统 (OS) 的 Windows、Mac 或 Linux 机器

  • 一个工作中的 C/C++ 编译器(如果你还没有,建议使用每个平台的系统默认编译器)

本章中的代码示例可以通过以下链接找到:github.com/PacktPublishing/Minimal-CMake。

从命令行使用 CMake

在深入了解第一个 CMake 脚本的内容之前,先克隆书中代码示例的仓库。可以通过打开终端并运行以下命令来执行此操作。

Linux/macOS

如果你在 Linux/macOS 上工作,请运行以下命令:

cd ~ # User's home directory on Linux/macOS (feel free to pick another location)
mkdir minimal-cmake
cd minimal-cmake
git clone https://github.com/PacktPublishing/Minimal-CMake.git .

现在你已经准备好在 macOS 或 Linux 上探索书中的代码仓库。

Windows

如果你在 Windows 上工作,请运行以下命令:

cd C:\Users\%USERNAME% # User's home directory on Windows (feel free to pick another location)
mkdir minimal-cmake
cd minimal-cmake
git clone https://github.com/PacktPublishing/Minimal-CMake.git .

现在你已经准备好在 Windows 上探索书中的代码仓库。

探索仓库

克隆仓库后,导航到第二章的第一个代码示例:

cd ch2/part-1

从这里开始,输入 ls(如果你在 Windows 上且没有使用 Git Bash 或类似工具,请将 ls 替换为 dir)。显示的文件夹内容如下:

CMakeLists.txt
main.c

CMakeLists.txt 文件显示我们处于 CMake 项目的根目录。所有 CMake 项目在其根目录都有这个文件,正是从这里我们可以要求 CMake 为我们的平台生成构建文件。

调用 CMake

让我们运行第一个 CMake 命令:

cmake -B build

这是你将会学会并喜爱的最重要的 CMake 命令之一。它通常是在克隆一个使用 CMake 的仓库后你第一个运行的命令。运行这个命令时,你应该看到类似以下的输出(下面是 macOS 输出):

-- The C compiler identification is AppleClang 15.0.0.15000100
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info – done
-- Check for working C compiler: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/cc - skipped
-- Detecting C compile features
-- Detecting C compile features – done
-- Configuring done (3.4s)
-- Generating done (0.0s)
-- Build files have been written to: /path/to/minimal-cmake/ch2/part-1/build

让我们简要分析一下我们使用的命令(cmake -B build)。命令的第一部分(cmake)是 CMake 可执行文件。如果我们在没有任何参数的情况下调用它,CMake 无法获取足够的信息来知道我们想要做什么;我们只会看到用法说明:

Usage
  cmake [options] <path-to-source>
  cmake [options] <path-to-existing-build>
  cmake [options] -S <path-to-source> -B <path-to-build>
Specify a source directory to (re-)generate a build system for it in the current working directory. Specify an existing build directory to re-generate its build system.

在我们的情况下,我们希望 CMake 为我们的目标平台生成构建文件。为此,我们使用 -B 选项来指定一个文件夹来存放构建文件。该文件夹的名称和位置是任意的(我们可以写 cmake -B my-built-filescmake -B ../../build-output),但通常会使用位于项目根目录下的 build 文件夹作为约定。

由于我们不想将这些文件提交到源代码控制中,通常会在 .gitignore 文件中添加某种形式的 build,这样我们就不会不小心开始跟踪这些文件(有些项目选择使用 bin 代替;不过,这种做法相对较少见)。这种做法是从源代码文件夹中使用 cmake . 的变体。如果这样做,增加了构建文件被意外添加到源代码控制中的风险,并且使得管理不同的构建类型变得繁琐。

显式指定源目录

如果 CMake 不是从与 CMakeLists.txt 文件相同的文件夹中调用的,可以提供一个单独的命令行参数 -S,并指定该文件所在的路径(当从构建自动化脚本如 GitHub Actions 调用 CMake 时,这一点尤其有用,这样就不需要切换目录)。如果在相同的文件夹中,您可以通过使用 cmake -S . -B build 来显式指定,但这在技术上是多余的,省略它是完全可以的。

CMake 的一大优点和一大缺点是,它在幕后为我们做出了很多猜测和假设,这些假设在没有仔细检查的情况下并不显而易见。稍后在本章中,我们将介绍更重要的选项,但可以简单地说,CMake 选择了一些合理的默认设置,我们可能需要稍后进行调整。

使用 CMake 构建

我们现在已经生成了一些构建文件(具体细节不重要),但还没有进行构建。就我们当前需要理解的部分,使用 CMake 是一个两步过程(第一步严格来说可以分解为两个进一步的步骤,称为配置生成,但这两个步骤都会在运行 cmake -B build 时完成,所以我们现在可以将它们视为一个步骤)。构建步骤需要一个新命令:

cmake --build build

前面的命令处理了在第一步中由 CMake 调用的底层构建系统。我们使用 --build 作为命令,而 build 只是我们在先前命令中指定的文件夹。

构建系统可以被看作是一种软件,它协调多个低级应用程序(例如编译器和链接器)在目标平台上生成某种输出(通常是应用程序或库)。在 macOS 和 Linux 的情况下,默认的底层构建系统将是 Make。

CMakeLists.txt)并将其映射到 Make 命令(以及我们即将学习的其他许多构建系统)。

直接调用构建系统

如果你知道底层的构建系统是 Make,你可以选择运行 make -C build,它的效果与 cmake --build build 相同。不幸的是,这并不具有可移植性(如果我们有一个构建脚本,在其他平台上选择了不同的构建系统,它将无法很好地工作)。坚持使用 CMake 命令可以保持一致的抽象层次,避免将来与特定的构建系统耦合。

Windows 上的情况略有不同,现在值得讨论。cmake -B buildcmake --build build 仍然会为我们生成构建文件并构建我们的代码,但底层的构建系统会有所不同。在 Windows 上,尤其是如果你按照 第一章 中的步骤,入门,生成的可能是 Visual Studio/MSBuild 项目文件,并且这些文件随后会被构建。

在 Windows 和 macOS/Linux 之间切换时的一个障碍是这两个独立的构建系统(Make 和 Visual Studio)具有稍微不同的行为(这是一种不幸的巧合)。Make 被称为单配置,而 Visual Studio 是多配置。我们尚未涉及配置的概念,但让我们先看看它们之间的可观察差异。

在 macOS 或 Linux 上,运行了两个 CMake 命令(配置和构建)后,我们可以通过运行以下命令启动我们的可执行文件:

./build/minimal-cmake

奖励是标准的 Hello, World! 程序的变体:

Hello, CMake!

如前所述,不幸的是,这在 Windows 上无法正常工作。相反,我们必须指定配置目录:

build\Debug\minimal-cmake.exe

通过这个小的修改,我们将在 Windows 上看到 Hello, CMake! 被打印出来。

我们将在本章后面更详细地讨论配置以及单配置和多配置之间的差异,但现在,我们知道它们的存在及其主要差异。

另一个有用的提示是,一旦你运行了配置命令(cmake -B build),即使修改了 CMakeLists.txt 文件,也不必再次运行它。只需运行 cmake --build build,CMake 会检查是否有任何更改,并自动重新运行配置步骤。这避免了每次更改时反复运行两个命令。

检查我们的第一个 CMakeLists.txt 文件

既然我们已经使用 CMake 构建了我们的项目,让我们看看位于项目根目录下的 CMakeLists.txt 文件中的命令:

cmake_minimum_required(VERSION 3.28)
project(minimal-cmake LANGUAGES C)
add_executable(${PROJECT_NAME})
target_sources(${PROJECT_NAME} PRIVATE main.c)
target_compile_features(${PROJECT_NAME} PRIVATE c_std_17)

前述代码是制作 CMake 项目时可以采用的最低配置。project 还有一些其他可选参数,我们稍后会讲到,我们或许能够在不指定 target_compile_features 的语言版本的情况下进行设置(这样做的弊端是我们就会依赖平台上编译器的默认设置,而这些设置可能并非我们想要的。这也有可能使我们的 CMakeLists.txt 文件在跨平台时变得不太便携,因为不同平台或编译器的默认设置可能不同)。

大写或小写命令

在实际使用中,看到 CMake 命令全大写(例如 ADD_EXECUTABLE 而不是 add_executable)并不罕见。在 CMake 的早期版本中,命令必须使用大写字母,但今天 CMake 命令实际上是大小写不敏感的(aDD_eXecuTAble 技术上可以工作,但不推荐模仿)。现代的做法倾向于使用小写命令,这是本书中贯穿使用的风格。值得简要提到的是,CMake 变量(与命令不同)是区分大小写的,并且通常按照惯例使用大写字母。

让我们逐一分析每一行语句,了解它的作用以及为什么需要它。

设置最低版本

首先让我们来看一下如何设置可以与我们项目一起使用的最低(或最旧)版本的 CMake:

cmake_minimum_required(VERSION 3.28)

每个 CMakeLists.txt 文件必须以前述语句开始,以告诉 CMake 在运行时,执行文件所需的最低 CMake 版本号是什么。版本越高,可用的功能就越多(同时也会有警告,提示可能已经被弃用或从旧版本中删除的内容)。在指定较高版本(拥有所有最新功能)和略旧版本(更多人可能使用的版本)的之间,需要取得平衡。例如,如果某个使用旧版本 CMake 的人尝试生成我们的项目,当他们尝试配置时,会看到以下错误消息:

CMake Error at CMakeLists.txt:1 (cmake_minimum_required):
    CMake 3.28 or higher is required.  You are running version 3.15.5

如果你正在开发一个你自己或一个小团队将要构建的应用程序,指定最新的版本(至少是你已经安装的版本,在我们的例子中是 3.28)是可以的,也是个好主意。另一方面,如果你正在创建一个希望其他项目轻松采用的库,选择一个稍微旧一点的版本可能会更容易使用(如果你能够放弃一些新功能的话)。例如,在我们的例子中,我们可以轻松将所需版本号降至 3.5,而一切仍然能够正常工作(即使我们实际使用的是 3.28)。然而,如果我们将版本号降至 2.8,就会看到这个警告:

Compatibility with CMake < 3.5 will be removed from a future version of CMake.

随着时间的推移,逐渐增加版本号是很重要的,这样可以保持 CMakeLists.txt 文件与 CMake 最新的更改和改进兼容。一个例子是 CMake 3.193.20 之间的变化。在 CMake 3.20 之前,在列出 target_sources 中的文件时,可以省略引用文件的扩展名。所以我们会使用如下代码:

target_sources(${PROJECT_NAME} PRIVATE main)

这与以下代码是相同的:

target_sources(${PROJECT_NAME} PRIVATE main.c)

如果 CMake 找不到完全匹配的文件,它会尝试附加一个潜在的扩展列表,看看是否有适合的扩展。这个行为容易出错,并可能导致潜在的 bug,因此被修复了。如果你尝试使用版本大于或等于 3.20 的 CMake 配置一个项目,而该项目的要求版本是 3.19 或更低版本,你将看到以下警告信息:

CMake Warning (dev) at CMakeLists.txt:4 (target_sources):
  Policy CMP0115 is not set: Source file extensions must be explicit.  Run "cmake --help-policy CMP0115" for policy details.  Use the cmake_policy command to set the policy and suppress this warning.
  File: /path/to/main.c

我们还没有涉及到策略,所以暂时跳过详细信息,但本质上它们是 CMake 维护者为了避免在发布新版本的 CMake 时破坏项目兼容性的一种方式。

如果你将 cmake_minimum_required(VERSION 3.19) 更新为 cmake_minimum_required(VERSION 3.20),但没有为 main 文件添加显式的扩展名,那么尝试配置时将产生一个硬错误:

CMake Error at CMakeLists.txt:4 (target_sources):
  Cannot find source file: main

这有点偏题,但目的是强调为什么 cmake_minimum_required 非常重要,必须包括。通常来说,涉及 CMake 时最好是明确指定,而不是依赖于可能会根据平台或未来版本变化的隐式行为。

为项目命名

接下来让我们看看如何给我们的项目命名:

project(minimal-cmake LANGUAGES C)

project 是所有 CMakeLists.txt 文件必须提供的第二个必需命令。如果你省略它,你会得到一个有用的错误信息:

CMake Warning (dev) in CMakeLists.txt:
No project() command is present.  The top-level CMakeLists.txt file must contain a literal, direct call to the project() command.  Add a line of code such as
    project(ProjectName)
near the top of the file, but after cmake_minimum_required().
CMake is pretending there is a "project(Project)" command on the first line.

project 命令允许你为顶级项目指定一个有意义的名称,该项目可能是一个库和/或应用程序的集合。project 命令提供了许多附加选项,这些选项可能在指定时非常有用。在我们的示例中,我们提供了 LANGUAGES C 来让 CMake 知道项目包含哪种类型的源文件。这是可选的,但通常是良好的实践,因为它可以防止 CMake 做不必要的工作。如果我们没有在此情况下仅指定 C,CMake 将会搜索 C 和 C++ 编译器(CMake 脚本中使用 CXX 来表示 C++,以避免与不同上下文中的 + 运算符产生歧义)。

其他 project 选项包括:

  • VERSION

  • DESCRIPTION

  • HOMEPAGE_URL

这些选项的有用性可能因项目而异。对于小型本地项目,它们可能过于复杂,但如果一个项目开始获得关注并被更广泛使用,那么添加这些选项对于新用户可能是有帮助的。如需了解更多关于 CMake project 命令的信息,请参阅 cmake.org/cmake/help/latest/command/project.html#options

声明应用程序

设置好最低版本要求并命名我们的项目后,我们可以请求 CMake 创建我们的第一个可执行文件:

add_executable(${PROJECT_NAME})

add_executable 很重要,因为这是我们项目中执行特定操作的第一行代码。调用此命令将创建 CMake 所称的 目标

目标通常是一个可执行文件(如这里所示)或一个库(你还可以创建特殊的自定义目标命令)。CMake 提供了命令来直接获取和设置目标的值,而不会相互影响,或影响全局的 CMake 状态。目标是一个非常有用的概念,它使得可以将一组属性和行为封装在一起。可以把目标看作是 CMake 项目中的一个独立单元。它们使得我们能够轻松拥有多个可执行文件或库,并且每个都具有独特的属性,并且可以相互依赖。我们将在本书的其余部分频繁使用目标。

在之前的 add_executable 示例中,我们使用了一个已经为我们创建的现有 CMake 变量。

有两个重要的问题需要解决:

  • 我们是如何知道要使用 PROJECT_NAME 的?

  • 为什么我们需要在 PROJECT_NAME 周围使用 ${}

第一个问题的答案可以通过访问 cmake.org/cmake/help/latest/manual/cmake-variables.7.html 来解决。这个页面是一个有用的资源,列出了所有当前的 CMake 变量。如果我们向下滚动页面,我们会找到 PROJECT_NAME (cmake.org/cmake/help/latest/variable/PROJECT_NAME.html),并看到如下描述:

这是当前目录范围或更高范围内最近调用的 project() 命令所赋予的名称。

在我们的简单示例中,使用这个作为我们正在创建的目标的名称是足够的,因为目标和项目本质上是同一个东西。未来,在创建可能包含多个目标的较大 CMake 项目时,最好为目标名称创建一个单独的变量(例如,${MY_EXECUTABLE}),或者直接使用字面值(例如,my_executable)。我们稍后会讲解如何定义变量。

我们尚未回答的第二个问题是关于稍微奇怪的 ${} 语法。CMake 变量遵循与系统环境变量类似的模式,你可能以前以某种形式遇到过这些变量。为了访问存储在变量中的值,我们需要用 ${} 将其括起来,以有效地取消引用或解包存储的值。举个简单的例子,如果我们在终端中输入 echo PATH,我们将看到打印出的 PATH。然而,如果我们输入 echo ${PATH}(或者在 Windows 上输入 echo %PATH%),我们将看到 PATH 变量的内容(在 macOS 上,这通常是类似 /usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin... 的内容)。CMake 也是一样。做个简单的测试,让我们添加一个调试语句来确认 PROJECT_NAME 的值。我们可以通过在 CMakeLists.txt 文件的底部添加以下命令来实现:

message(STATUS "PROJECT_NAME: " ${PROJECT_NAME})

当我们运行 cmake -B build 时,我们将在控制台中看到 PROJECT_NAME: minimal-cmake 被打印出来。

使用 ${PROJECT_NAME} 作为我们的目标名的一个小优势是,我们保持了 CMakeLists.txt 文件的简洁性,没有引入额外的复杂性。另一个优势是,如果我们决定更改项目的名称,我们只需要在一个地方进行更改(遵循通常的建议,${PROJECT_NAME} 会自动反映新值)。

添加源文件

现在让我们理解如何指定我们要构建的文件:

target_sources(${PROJECT_NAME} PRIVATE main.c)

现在通过 add_executable 定义了一个目标后,我们可以通过该目标的名称(在我们的例子中是 ${PROJECT_NAME},它解包为 minimal-cmake)在其他与目标相关的命令中引用它。这些命令通常以 target_ 为前缀,方便我们识别。与之前的 CMake 命令相比,这些命令的巨大优势在于它们消除了大量潜在问题和关于 CMake 命令作用范围的混淆。在过去,某个 CMakeLists.txt 文件中定义的设置可能会无意中泄露到另一个文件中,往往带来痛苦的后果。通过更加规范地使用目标,我们可以为该目标指定特定的属性和设置,从而避免影响其他目标。

关于 target_sources 命令,这是我们为目标指定要构建的源文件的地方。紧跟在 main.c 之前的参数控制源文件的可见性或作用范围。在大多数情况下,我们希望在这里使用 PRIVATE,以便只有这个目标构建源文件。其他作用范围参数有 PUBLIC(该目标和依赖它的其他目标使用)和 INTERFACE(仅供依赖的目标使用)。我们将在以后回到这些关键字(当我们讨论库时),因为它们出现在所有的 target_ 命令中,并且有多种用途。

设置语言特性

最后,让我们确保明确指定我们正在使用的语言版本:

target_compile_features(${PROJECT_NAME} PRIVATE c_std_17)

我们的 CMakeLists.txt 文件中的最后一条命令是 target_compile_features。这是指定我们希望使用的语言版本的便捷方法,在本例中为 C17。我们也可以更精细地选择特定的语言特性(例如,c_restrict),但选择语言版本更加清晰简洁。你可以在这里查看 C 语言的可用模式和特性:cmake.org/cmake/help/latest/prop_gbl/CMAKE_C_KNOWN_FEATURES.html

我们也可以选择另一种方式,使用 set(CMAKE_C_STANDARD 17)。这会在整个项目中应用此设置。我们可能希望这种行为,但在我们的情况下,我们坚持采用更具目标导向的方法,因此只有 minimal-cmake 目标会受到影响。

就构建小型应用程序而言,这大致涵盖了我们在使用 CMake 时所需的一切。单独来看,这已经非常有用,因为我们现在有了一种完全便携的方式,可以在 Windows、macOS 和 Linux 上运行我们的代码。这使得代码更容易共享和协作。如果其他平台的用户或开发者想查看我们的项目,只要他们安装了 CMake(很可能还需要 Git),他们可以通过几条命令轻松完成。如果你分享的是 Xcode、Visual Studio,甚至是 Make 项目,他们就需要做更多的工作。好消息是,即使用户希望使用 Visual Studio 或 Xcode 来测试或修改代码,他们仍然可以这样做。这将我们引向了使用 CMake 的下一个重要部分:生成器。

CMake 生成器

调用 CMake 部分,我们略过了运行 cmake -B build 时发生了什么。当我们运行 cmake -B build 时,我们要求 CMake 为我们生成构建文件,但到底是什么构建文件呢?CMake 会尽力选择平台的默认值;在 Windows 上是 Visual Studio,而在 macOS 和 Linux 上是 Make。所有潜在生成器的列表可以通过访问 cmake.org/cmake/help/latest/manual/cmake-generators.7.html 或运行 cmake --help 命令找到(默认生成器会用星号标出)。如果你不确定正在使用哪个生成器,可以打开 build/ 文件夹中的 CMakeCache.txt 文件并搜索 CMAKE_GENERATOR。你应该能找到类似下面的行:

INTERNAL, so we shouldn’t depend on this in our scripts, but as a debugging aid it’s sometimes useful to check.
			Specifying a generator
			If we would like more control over the generator CMake uses, we can specify this explicitly by using the `-G` argument, `cmake -B build -G <generator>`, as in this example:

cmake -B build -G Ninja


			Here, we’ve referenced the Ninja build system generator ([`ninja-build.org/`](https://ninja-build.org/)), a build tool designed to run builds as fast as possible. Unfortunately, if we try and run this command on macOS or Linux, we’ll get an error as we currently do not have Ninja installed (fortunately on Windows, Ninja comes bundled with Visual Studio, and if we’re using the Developer Command Prompt or have run `VsDevCmd.bat`, we’ll have it in our path).
			Ninja can be downloaded from GitHub ([`github.com/ninja-build/ninja/releases`](https://github.com/ninja-build/ninja/releases)), and once the executable is on your machine, you can add it to your `PATH` or move it to an appropriate folder such as `/usr/local/bin` or `/opt/bin`.
			Security settings for macOS
			On macOS, you may need to open **System Settings** and navigate to **Privacy and Security** to allow Ninja to run because it is not from an identified developer.
			It may also be easier to acquire Ninja through a package manager, particularly on Linux (e.g., `apt-get` `install ninja-build`).
			Ninja advantages
			Ninja is designed to be fast, so it’s well worth setting it up for use with future chapters when we start building larger third-party dependencies. Ninja will take full advantage of all system cores by default, and this really shows when comparing build times against other generators. Ninja’s multi-config generator support is also useful.
			One thing to mention is even with this change to the generator behind the scenes, we can still use `cmake --build build` to build our project; there is no need to memorize any other build-specific commands. This consistency is invaluable as it reduces the cognitive load when working with different build systems, they’re largely abstracted away from us and we can focus on our project.
			If you have generated some build artifacts using one generator and would like to switch to another, this requires deleting the build folder and starting over (e.g., `rm -rf build` or `cmake -B build –G <new-generator>`). If you aren’t switching generators, a useful argument to be aware of (added in CMake `3.24`) is `--fresh`:

cmake -B build -G --fresh


			Using `--fresh` will remove the existing `CMakeCache.txt` and `CMakeFiles/` directory and restore them to the state they’d be if you were doing the first configure.
			CMake configs
			Now that we know how to specify a generator, we can talk about the one remaining topic in this chapter, configs (a concept inextricably linked to generators themselves). Generators come in two varieties, either single-config or multi-config. We’ve actually already encountered one of each already. Make is a single-config generator, and the default config we built without specifying anything was `Debug`. Visual Studio is a multi-config generator, which is why when we ran our earlier example on Windows, we had to specify the `Debug/` folder inside the `build/` folder instead of only the `build/` folder (`build\Debug\minimal-cmake.exe` versus `build/minimal-cmake`).
			Single-config generators
			With a single-config generator, when we run `cmake -B build`, we can pass an additional argument to set a CMake variable called `CMAKE_BUILD_TYPE`. We do this with `-D` to define a CMake variable and override the default value (one set by CMake or us in our `CMakeLists.txt` file). To be explicit about the config/build type, we’d write the following:

cmake -B build -DCMAKE_BUILD_TYPE=Debug


			Usually, there are at least three build types: `Debug`, `Release`, and `RelWithDebInfo` (there’s also `MinSizeRel` with Visual Studio). These build types essentially control what underlying compiler flags are set for things such as optimization, debugging, and logging through defines. When developing code, we usually want to use the `Debug` configuration to allow us to easily step through our code in a debugger. When we’re ready to share our project with users, we use the `Release` configuration to get maximum performance. `RelWithDebInfo` is a happy medium. Some optimizations may be disabled compared to `Release`, but performance will be similar. Debug symbols are also created to make debugging `Release` builds easier.
			The defaults are more than sufficient for our purposes but, in advanced cases, it is possible to create your own build types (this is easier said than done as you need to know the compiler flags to use across a host of platforms/compilers, but if you ever did need to do this, you can).
			One thing to be aware of when changing `CMAKE_BUILD_TYPE` is the artifacts in your build folder will be completely rebuilt depending on the build type. So, for example, if you have a larger project, and you normally have `-DCMAKE_BUILD_TYPE=Release` set, if you run `cmake -B build -DCMAKE_BUILD_TYPE=Debug` and run `cmake --build build`, the release files will be overwritten, and so switching back again to `Release` will wipe out all the `Debug` build files. For this reason, it is wise to use different folders for the different configurations to make this switching back and forward more efficient. To illustrate, we could have the following:

cmake -B build-debug -G Ninja -DCMAKE_BUILD_TYPE=Debug

cmake -B build-release -G Ninja -DCMAKE_BUILD_TYPE=Release


			To build each config, you’d then use either `cmake --build build-debug` or `cmake --build build-release`. You could also group the different configurations under the build folder (e.g., `build/debug` or `build/release`), but remember each subfolder is completely distinct and nothing is shared between the two when using single-config generators.
			Let’s now explore multi-config generators.
			Multi-config generators
			With a multi-config generator, `CMAKE_BUILD_TYPE` goes away and instead, the config is specified at build time rather than configuration time. It also handles the case described earlier where different build types can overwrite one another.
			With a multi-config generator, you’d configure it in this way:

cmake -B build -G "Visual Studio 17 2022" # Windows

为了简洁起见,年份可以省略。

cmake -B build -G "Visual Studio 17" # Windows

cmake -B build -G Xcode # macOS

cmake -B build -G "Ninja Multi-Config" # Linux


			Then, when building, you pass an additional argument, `--config`, along with the config type:

cmake --build build --config Debug

cmake --build build --config Release


			Multi-config generators will create subdirectories inside the build folder you specified. In the case of Ninja Multi-Config, this will be `Debug`, `Release`, and `RelWithDebInfo` (no `MinSizeRel`). Multi-config generators are a good choice to stick with and, in later chapters, we’ll cover a couple more reasons why to prefer them.
			That covers the most essential operations you’ll perform when working with CMake on a daily basis. There are many more options and tools available to streamline usage and simplify project configuration, but you could survive with what we’ve covered here for some time.
			Project next steps
			Now we’ve been through our first `CMakeLists.txt` file and are more familiar with build types (configs) and generators, it’s time to look at a real program and see how we can start to evolve it with CMake’s help.
			Staying with the book’s sample code, navigate to `ch2/part-2` in your terminal and run the commands we’re now intimately familiar with, `cmake -B build` (feel free to specify a generator of your choosing such as `-G "Ninja Multi-Config"`), followed by `cmake --``build build`.
			After configuring and building, we can run the sample application by typing `./build/Debug/minimal-cmake_game-of-life` on macOS and Linux, or `build\Debug\minimal-cmake_game-of-life.exe` on Windows (for brevity, we’ll use the POSIX path convention from macOS and Linux going forward; this is one reason to recommend using Git Bash from within Terminal on Windows as the experience will be more consistent).
			You should see the following printed (several blank lines omitted here):


@*********************

@@************************

@@**********************



			Press *Enter* on your keyboard and you’ll see the pattern denoted by the `@` symbols update (hitting *Enter* repeatedly will cause the scene to keep updating).
			What you are seeing is an incredibly simple implementation of John Horton Conway’s *Game of Life*. *Game of Life* is an example of cellular automaton. Conway’s *Game of Life* is represented as a grid, with each cell in either an on or off state. A set of rules is processed for each update to decide which cells turn on, which turn off, and which stay the same. The topic is vast; if you would like to learn more about it, please check out the Wikipedia pages about both Conway’s *Game of Life* ([`en.wikipedia.org/wiki/Conway%27s_Game_of_Life`](https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life)) and cellular automaton more generally ([`en.wikipedia.org/wiki/Cellular_automaton`](https://en.wikipedia.org/wiki/Cellular_automaton)).
			For our purposes, we’d just like something interesting to look at so we can start to evolve it over time. The implementation is written in C and the `CMakeLists.txt` file differs from the first one we looked at by only the name (the *Game of Life* implementation lives in `main.c`).
			In the book’s repository (available from [`github.com/PacktPublishing/Minimal-CMake`](https://github.com/PacktPublishing/Minimal-CMake)), every `ch<n>/part-<n>` section in each chapter builds on the last in some small way. To help make sense of these incremental changes, see the following callout about using Visual Studio Code to make visualizing these differences easier.
			Visual Studio Code compare
			A useful feature in Visual Studio Code is the `code .` from your terminal will help with this, so all related files can be easily accessed). It’s then simple to highlight what has changed between versions of our `CMakeLists.txt` files without needing to switch back and forth between them. Focusing on the changes instead of reviewing an entire file, which may be very similar to the previous one, is an efficient strategy.
			Don’t worry too much about the code. It’s not super important how it works; what is important is how CMake can start to help us organize and enhance our application.
			Adding another file
			Before we wrap up, let’s make one small addition to our application. We’d like to improve the performance of our update logic in our current implementation of *Game of Life*. One subtlety of implementing *Game of Life* is we can’t change the board we’re reading from at the same time. If we do, then the cells from the row we’re on will have changed from their earlier state by the time we get to the next row, which will mean the simulation won’t run correctly. In the implementation in `ch2/part2` (a reminder to refer to [`github.com/PacktPublishing/Minimal-CMake`](https://github.com/PacktPublishing/Minimal-CMake) to find this), we simply make a copy of the whole board, read from that in `update_board` (see line 72 in `ch2/part-2/main.c`) and write back to the original board. This is okay, but if most cells don’t change, it’s wasteful. A better approach is to record the cells that change, and then write back to the original board at the end. By doing this, we only need to allocate memory for cells that change instead of the whole board.
			Adding a dynamic array
			Let’s add a simple data structure to make this possible. C unfortunately doesn’t have a built-in dynamic array, which would be particularly useful in this case, so let’s add one.
			Moving to `ch2/part3` from the book’s GitHub repository, there are two new files, `array.h` and `array.c`. To keep them grouped logically together, they’ve been added to a folder called `array`. The interface provided by `array.h` is like that of `std::vector` from C++. It’s a little trickier to use as C doesn’t support generics/templates, but for our purposes, it’ll be a huge help.
			With this file added, we need to ensure CMake knows about it; otherwise, it won’t be built. To do this, we simply add `array/array.c` to the existing `target_sources` command from earlier:

target_sources(${PROJECT_NAME} PRIVATE main.c cmake --build build again (不需要重新配置)。

        忘记添加一个文件

        如果我们没有将 `array.c` 添加到 `CMakeLists.txt` 文件中,而是添加了对 `array` 的使用代码,并尝试编译(`cmake --build build`),那会很有用。编译是可以通过的,但我们会遇到大家都熟悉的问题:链接器错误。以下输出展示了这一点:
ld: Undefined symbols:
  _array_free, referenced from:
      _update_board in main.c.o
  _array_size, referenced from:
      _update_board in main.c.o
      _update_board in main.c.o
      _update_board in main.c.o
  _internal_array_grow, referenced from:
      _update_board in main.c.o
      _update_board in main.c.o
        这是因为链接器找不到列出的函数的实现(例如,`_array_size`)。输出文件,在 macOS/Linux 上是 `array.c.o`,在 Windows 上是 `array.c.obj` 或 `array.obj`,不会被创建(你可以通过进入 `build/CMakeFiles/minimal-cmake_game-of-life.dir/Debug` 来查看这些文件是否存在,如果使用的是 Ninja Multi-Config 生成器,其他生成器会将其放在类似位置)。

        这是使用 CMake 时常见的早期问题(创建了文件但忘记将它们添加到 `CMakeLists.txt` 中)。

        是否使用 GLOB

        到这个时候,值得提到一个常常在 CMake 中出现的话题,那就是是否像前面的例子一样明确列出要构建的文件,还是使用一种 `GLOB`(有效地搜索)每个文件夹层级中的所有源文件的技术。就像软件工程和计算机科学中的一切一样,这里面有权衡。有些情况下,使用 `GLOB` 会更简单快捷。这可能看起来像下面这样:
file(GLOB sources CONFIGURE_DEPENDS *.c)
target_sources(foobar PRIVATE ${sources})
        这可能在你和你的环境中运行得很好,但也有一系列风险。在 `CONFIGURE_DEPENDS`(CMake `3.12` 中新增)出现之前,如果你添加了源文件(例如,从版本控制系统中拉取最新代码)而没有进行配置,运行 `cmake --build build` 时会遇到问题。在这种情况下,CMake 构建会失败。指定 `CONFIGURE_DEPENDS` 可以避免这种情况,但不能保证它与所有生成器兼容,对于更大的项目,可能会引发性能问题。CMake 的维护者仍然建议明确指定要构建的源文件,这是我们在本书中一直遵循的做法。它减少了不小心构建不想要的文件的风险,并且对 `CMakeLists.txt` 文件所做的更改有助于在版本控制中跟踪。前面提到的链接器错误一开始确实让人沮丧,但你很快就会适应,添加新文件也会变得自然而然。

        在添加了新的 `array.c` 文件后,我们可以更改更新函数以使用新的逻辑,并提高代码的性能(`ch2/part-3` 中有一个稍微更激动人心的棋盘配置,值得一看)。

        在 target_sources 中引用接口文件

        最后一个值得提及的点是`array.h`怎么办?由于我们在`main.c`中相对引用了这个文件(使用`#include "array/array.h"`而不是`#include <array/array.h>`),我们不需要在`CMakeLists.txt`文件中明确提到任何包含目录(当我们涉及到库时,这一点会更重要)。如果你使用的是一种生成工具,能够生成一个可以在独立工具中打开的项目或解决方案(例如集成开发环境,如 Visual Studio 或 Xcode),那么你可以像下面这样将`array.h`添加到`target_sources`中:
target_sources(
  ${PROJECT_NAME} PRIVATE main.c array/array.h array/array.c)
        这样,它会出现在项目视图中,这对于维护可能很有用;不过,它并不是构建代码所必需的。由于我们在大多数示例中将使用 Visual Studio Code 和文件夹项目视图,为了简洁起见,我们会省略头文件。指定头文件还有一个好处,那就是如果文件被意外删除,或者无法从源控制中获取,CMake 会在配置步骤中提前失败,而不是在构建时。增加的维护成本可能是值得的,特别是在团队较大的情况下。

        总结

        非常棒,你已经走到了这一步;我们已经覆盖了很多内容!我们从熟悉如何通过终端使用 CMake 开始(`cmake -B build`和`cmake --build build`应该已经深深记在你的脑海中了)。接着,我们通过一个简单的`CMakeLists.txt`文件,检查了最重要的命令以及它们为何需要。然后,我们深入探讨了生成器,研究了单配置生成器和多配置生成器之间的一些差异,以及如何在每种情况下指定构建类型。最后,我们看了我们项目的种子,康威的*生命游戏*实现,并了解了如何在扩展功能时,逐步向现有项目中添加更多文件。

        在下一章中,我们将探讨如何将外部依赖项引入我们的项目。这将使我们能够增强和改善应用程序的功能以及代码的可维护性。这正是 CMake 的强大之处,它帮助我们集成现有的库,而无需从头开始实现一切。

第三章:使用 FetchContent 与外部依赖

现在我们已经启动并运行了 CMake,值得注意的一个非常有用的功能是 FetchContentFetchContent 是 CMake 的一项功能,允许你将外部库(也称为 依赖)引入到你的项目中。只需要几行代码,使用起来快速且方便。它确实依赖于依赖库本身也使用 CMake,但好消息是,使用 C 和 C++ 编写的开源软件中有相当一部分使用 CMake 进行构建。即使该依赖库不使用 CMake,添加 CMake 支持通常也非常简单,并且能使使用该库变得更加轻松。

我们将看到如何在我们的项目中使用 FetchContent,为我们的应用程序引入一些新的有用功能。我们还会讨论一些使用时需要注意的细节。到本章结束时,你将能够自信地使用外部库。

本章我们将覆盖以下主要内容:

  • 为什么选择 FetchContent

  • 使用 FetchContent

  • 描述我们的依赖关系

  • 为依赖设置选项

  • 更新我们的应用程序

技术要求

为了跟上进度,请确保你已满足第一章《入门》中的要求。这些要求包括:

  • 一台运行最新 操作系统 (OS) 的 Windows、Mac 或 Linux 机器

  • 一个可用的 C/C++ 编译器(如果你还没有,建议使用每个平台的系统默认编译器)

本章中的代码示例可以通过以下链接找到:github.com/PacktPublishing/Minimal-CMake

为什么选择 FetchContent

FetchContent 是 CMake 中相对较新的功能,首次出现在 2018 年 3 月的 CMake 3.11 版本中。FetchContent 允许你提供一个源库的路径(通常是某种类型的代码库 URL,尽管 ZIP 文件或本地目录路径的 URL 也被支持),并让 CMake 为你下载(或获取)代码。在最基本的情况下,只需要提供这个路径(我们稍后会介绍一些额外的参数)。

何时使用 FetchContent

FetchContent 的一个非常有价值的特点是,它允许你将任何第三方依赖的源代码从代码库中剥离。当你在没有使用 FetchContent 的情况下与第三方库一起工作时,有几种不同的选择,每种都有不同的权衡。一种解决方案是将第三方库复制/粘贴到你的源代码目录中(理想情况下是在一个专门的文件夹中)。如果该项目使用 CMake,你可以使用一个叫做 add_subdirectory 的功能相对干净地添加该库。另一种方式是将独立的库文件直接添加到你自己的 CMakeLists.txt 文件中,这样做可能会迅速变得不太方便。

拥有源代码的直接访问权限有一些优势,但必须小心避免对其进行任何修改。如果发生修改,将使未来的升级变得异常痛苦。在涉及许可和归属时也需要小心(确保该库的根目录下存在LICENSE文件尤为重要)。

另一种可能的做法是依赖 Git 子模块。它们的优势在于可以将第三方依赖的源代码文件从你的项目中排除(至少是作为跟踪文件),但使用 Git 子模块可能会有些繁琐,并且使得克隆和更新你自己的项目变得更加复杂。

FetchContent 解决了所有这些问题,保持了代码和依赖项之间的良好卫生,避免了引入不必要的复杂性或维护问题。

另一个需要注意的点是,使用 FetchContent 会使依赖项在配置时可用。这意味着你的目标可以在配置时依赖于由依赖项提供的目标(就像该依赖项是本地的一样)。依赖项将在与你的代码同时构建时构建,构建结果将添加到 build 文件夹中的一个名为 _deps 的文件夹内。

什么时候不应该使用 FetchContent

虽然 FetchContent 是一个非常有用的工具,但它并非没有缺点。使用 FetchContent 构建时需要注意的主要权衡是,你在构建自己的代码的同时也在构建依赖项。这通常会增加不必要的工作,并且使得在不重新构建依赖项的情况下重新构建代码变得困难(理想情况下,我们希望只构建一次依赖项,然后忘记它们)。对于小型依赖项来说,这不是大问题,但正如我们稍后将看到的,对于较大的依赖项,使用更好的替代方案会更加合适(我们将在 第六章 中详细讨论,安装依赖项和 ExternalProject_Add)。

使用 FetchContentExternalProject_Add 时需要注意的另一个因素是,所引用的依赖项将来可能会变得不可用(例如,某个仓库可能会被删除,或者远程文件可能会被重命名或移动)。这些是我们需要考虑的风险,采取一些措施,比如为公共仓库创建分支或自托管重要文件,可能是值得考虑的。

最后,如果我们想使用的依赖项目前没有 CMake 支持,我们就无法使用 FetchContent。对于较小的依赖项,添加 CMake 支持可能不会太困难,但对于较大的依赖项来说,这可能是一个挑战。保持 CMake 支持的持续维护也可能成为一个巨大的开销(在这里,CMake 查找模块可以提供帮助,相关内容请参见 第七章为你的库添加安装支持)。

现在我们已经了解了 FetchContent 及其在更大 CMake 环境中的位置,接下来我们可以深入探讨使用它所需的具体命令。

使用 FetchContent

既然我们已经了解了 FetchContent 的作用及其使用原因,接下来我们来看看如何通过 FetchContent 命令将依赖项集成到我们的项目中:

include(FetchContent)
FetchContent_Declare(
  timer_lib
  GIT_REPOSITORY https://github.com/pr0g/timer_lib.git
  GIT_TAG v1.0)
FetchContent_MakeAvailable(timer_lib)
target_link_libraries(${PROJECT_NAME} PRIVATE timer_lib)

我们将要介绍的库是一个跨平台的计时器库,名为 timer_libtimer_lib 将允许我们的 Game of Life 应用程序独立运行,用户无需按 Enter 键来切换到棋盘的下一阶段。

上述代码片段来自书籍 GitHub 仓库中的 ch3/part-1/CMakeListst.txt,并且紧接着我们在 第二章 中回顾的 CMake 命令(见 ch2/part-3/CMakeLists.txt)。接下来,我们将逐一讲解每条命令。

引入其他 CMake 代码

让我们从了解如何使用 CMake 库代码开始:

include(FetchContent)

include 命令用于引入存储在单独文件中的 CMake 功能。FetchContent 是 CMake 提供的模块,可以在你的 CMake 安装文件夹中找到。按照平台的不同,具体如下:

  • Windows: C:\Program Files\CMake\share\cmake-3.28\Modules

  • macOS: /Applications/CMake.app/Contents/share/cmake-3.28/Modules

  • Linux: /opt/cmake-3.28.1-linux-aarch64/share/cmake-3.28/Modules

上述路径与我们在 第一章 入门 中安装 CMake 时的路径相匹配。对于 Windows 和 macOS,路径通常只会根据 CMake 版本有所不同。对于 Linux,路径可能会有所不同,具体取决于 CMake 的安装方式(例如,如果通过 apt 等包管理器安装 CMake,安装位置可能是 /usr/share/cmake-<version>/Modules)。

CMake 知道在 Modules/ 文件夹中搜索这些默认模块(由 CMake 开发者维护)。也可以使用 include 命令来引入我们自己的 CMake 文件。例如,我们可以编写一个简单的 CMake 函数来列出所有 CMake 变量。让我们创建一个新文件 CMakeHelpers.cmake,并将该命令作为函数添加进去:

cd ch3/part-1
code CMakeHelpers.cmake

添加以下代码并保存文件:

function(list_cmake_variables)
  get_cmake_property(variable_names VARIABLES)
  foreach(variable_name ${variable_names})
    message(STATUS "${variable_name}=${${variable_name}}")
  endforeach()
endfunction()

不用担心现在理解该函数的实现。这只是一个示例,用来展示如何提取有用的 CMake 功能以便在我们的 CMakeLists.txt 文件中重用。

ch3/part-1/CMakeLists.txt 中,现在我们可以在 project 命令之后的任何位置写入 include(CMakeHelpers.cmake),然后在脚本的末尾调用 list_cmake_variables()。为了查看所有 CMake 变量在终端中的输出,完成建议的更改后,从 ch3/part-1 目录运行 cmake -B build(如果你不想自己实现这些更改,可以转到 ch3/part-2,那里有一个功能正常的示例)。

该命令的输出非常冗长,默认情况下不建议开启。排序顺序也有些不常见,按区分大小写的顺序排序(大写的Z会出现在小写的a之前),但偶尔启用这种调试功能可以帮助我们更好地理解 CMake 在后台执行的内容:

...
project(example-project)
include(CMakeHelpers.cmake)
...
list_cmake_variables()

你可能注意到我们的include调用与FetchContentinclude调用之间有一个区别,那就是我们必须指定完整的文件名,包括扩展名(include(CMakeHelpers.cmake)而不是include(CMakeHelpers))。这是因为当我们省略.cmake扩展名时,CMake 并不是在查找文件,而是在查找一个模块。模块与我们的示例文件没有区别,唯一的不同是它可以在CMAKE_MODULE_PATH中找到。

为了快速验证这一点,我们可以在调用include之前添加以下代码:

list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR})

前面的命令将包含CMakeLists.txt文件的目录添加到CMAKE_MODULE_PATH中。现在include(CMakeHelpers)可以正常工作了。这不是推荐的做法,而是为了演示没有特殊的语法或命令可以将常规的.cmake文件转换为模块。CMakeHelpers.cmake只需要通过查找CMAKE_MODULE_PATH中的目录来被发现。

要了解更多关于 CMake 的include命令,请参阅cmake.org/cmake/help/latest/command/include.html.

描述我们的依赖

现在,使用FetchContent功能,我们可以指定我们想依赖的库:

FetchContent_Declare(
  timer_lib
  GIT_REPOSITORY https://github.com/pr0g/timer_lib.git
  GIT_TAG v1.0)

FetchContent_Declare允许我们描述如何获取我们的依赖。在这里展示的命令中,我们使用了一小部分选项以简化说明。通常这些选项就足够了,但实际上有更多的选项可供使用。首先,我们需要为依赖命名。需要注意的是,这个名字完全可以是任意的,并不来自库本身。这里我们也可以将依赖命名为CoolTimingLibrary,并在FetchContent_MakeAvailable命令中使用这个名字:

FetchContent_Declare(
  CoolTimingLibrary
  GIT_REPOSITORY https://github.com/pr0g/timer_lib.git
  GIT_TAG v1.0)
FetchContent_MakeAvailable(googletest-distribution, and the targets to depend on are gtest and gtest_main. For our purposes, naming the dependency in the context of our project as GoogleTest is very convenient and helps improve readability.
			The next argument, `GIT_REPOSITORY`, is where to find and download the code. `GIT_REPOSITORY` is just one choice; there are several including `SVN_REPOSITORY` (Subversion), `HG_REPOSITORY` (Mercurial), and `URL` (ZIP file). For open source projects, Git is by far the most popular, but you have alternatives to Git, including but not limited to the preceding list of options.
			FetchContent and ExternalProject_Add
			In this book, we’re explicitly covering `FetchContent` before `ExternalProject_Add` as it’s much easier to get to grips with initially (`ExternalProject_Add` is a useful command we’ll cover in more detail in *Chapter 6*, *Installing Dependencies and ExternalProject_Add*). Something to be aware of is that internally, `FetchContent` is implemented on top of `ExternalProject_Add`, so a lot of the configuration options are the same between the two. If you’re looking for more details about `FetchContent`, start with [`cmake.org/cmake/help/latest/module/FetchContent.html`](https://cmake.org/cmake/help/latest/module/FetchContent.html), but it can also be helpful to consult [`cmake.org/cmake/help/latest/module/ExternalProject.html`](https://cmake.org/cmake/help/latest/module/ExternalProject.html). This covers details such as download, and directory options shared between both `FetchContent` and `ExternalProject_Add`.
			Using libraries from GitHub
			To find the Git repository path referenced in the preceding subsection (where the project is hosted, in this case, GitHub), navigate to the project page ([`github.com/pr0g/timer_lib`](https://github.com/pr0g/timer_lib)), and then click the green **Code** dropdown toward the top right of the page:
			![Figure 3.1: GitHub UI for cloning a repository](https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/min-cmk/img/B21152_03_1.jpg)

			Figure 3.1: GitHub UI for cloning a repository
			Returning to the `FetchContent_Declare` command, after `GIT_REPOSITORY`, we follow up with the `GIT_TAG` argument. `GIT_TAG` is flexible and supports a range of different identifiers. The first and perhaps most obvious is a **Git tag** (the identifier used in the examples presented so far). These are friendly names for Git commits and can signpost versions or releases of a project. To find available Git tags on GitHub, from the project page, click the **Tags** UI option toward the middle of the screen:
			![Figure 3.2: GitHub project page tags link](https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/min-cmk/img/B21152_03_2.jpg)

			Figure 3.2: GitHub project page tags link
			There you’ll see a list of tags (see *Figure 3**.3*). You can usually just note down the most recent one and add that after the `GIT_TAG` argument in your `FetchContent` command. If you need to depend on a particular version of the library, it’s possible to look back through the available tags and select the version you need:
			![Figure 3.3: GitHub tags list view](https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/min-cmk/img/B21152_03_3.jpg)

			Figure 3.3: GitHub tags list view
			If you are concerned that a Git tag may be removed in the future (which can sometimes happen), you can instead use the commit that’s referenced and add a comment after it in your `FetchContent_Declare` command, showing the tag it corresponds to:

GIT_TAG 2d7217 # v1.0


			If a convenient tag is not available, the next best choice is to reference a specific commit hash. Looking at *Figure 3**.3*, we can see that the commit hash listed is `2d72171` (remember, a tag is just a friendly name for a commit). If we want to grab the most recent commit, we can find this from the GitHub project page by clicking the **Commits** link at the center-right of the screen:
			![Figure 3.4: GitHub project page commits link](https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/min-cmk/img/B21152_03_4.jpg)

			Figure 3.4: GitHub project page commits link
			This will list all commits chronologically, with the most recent commits appearing first. Clicking the **Copy** icon (*Figure 3**.5*) will copy the full commit SHA (hash) to the clipboard (don’t worry if you don’t know what this is; it’s just a unique reference to that commit):
			![Figure 3.5: GitHub copy commit SHA UI](https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/min-cmk/img/B21152_03_5.jpg)

			Figure 3.5: GitHub copy commit SHA UI
			We can then paste the content of our clipboard after `GIT_TAG` in our `FetchContent` command:

FetchContent_Declare(

timer_lib

GIT_REPOSITORY https://github.com/pr0g/timer_lib.git

GIT_TAG 2d7217114f1ab10d9b46a2e7544009867b80b59c)

2d72171 也可以正常工作


			Using branch names
			There is one more type of argument that can be passed to `GIT_TAG`, and that is the branch name of your dependency. If `GIT_TAG` is omitted entirely, then CMake will default to the branch behavior, looking for a branch called `master` (most open source projects are moving away from this name and instead opting for `main`, used throughout this book, or `trunk`). It is generally not advisable to use a branch name because you lose the ability to maintain exact snapshots of your project history.
			If your project depends on the `main` branch of `timer_lib`, and `timer_lib` is under active development, then in six months if you want to jump back to an earlier commit in your project and build it, there’s a very good chance your code will fail to compile. This is because it’ll be using the most recent version of `timer_lib`, not the one from six months ago.
			This can be a huge pain. Depending on the rate of change, it can be hard to work out which commit your project would have been using at the time. There may be rare circumstances where setting `GIT_TAG` to a branch name makes sense. For example, when working in a development branch, it might be useful to temporarily set a branch name on `GIT_TAG` to make getting the most up-to-date changes from a dependency quicker (without having to remember to update the commit SHA every few days).
			It’s important to remember to ensure your library has fixed `GIT_TAG` to a commit hash, or possibly a tag, for reliable, reproducible builds when you merge your changes back to your main branch (either by rebasing or squashing in Git parlance). You will thank yourself later when you inevitably have to use `git-bisect` to track down some horrendous bug.
			Using local libraries with FetchContent
			Before we discuss using the dependency we’ve introduced with `FetchContent`, there’s one other useful way to work with it. If you are developing an application and a library at the same time, and they are both closely related, it can be tedious to keep the application in sync when making small incremental changes to the library. You need to commit your changes, push them to the remote repository, and then pull the changes down again in the application project. A better approach is to tell `FetchContent` to look directly at that source folder instead of downloading the dependency and storing it locally (inside the `build/_deps` folder). This can be achieved by setting `SOURCE_DIR`.
			We can write the following:

FetchContent_Declare(

SOURCE_DIR <path/to/dependency>)


			CMake will then use this new path as the source directory, and we can easily make changes there. We will see them reflected in our application immediately when we build it. This path can either be absolute or relative from the build folder of the main application.
			To help illustrate this, let’s look at one concrete example. Let’s take the folder structure of the *Minimal CMake* GitHub repository and create `timer-lib` in the same directory:

├── minimal-cmake

│ └── ch3

│ └── part-1

└── timer-lib


			To reference the local `timer-lib` library, we can write the following in `ch3/part-1/CMakeLists.txt`:

FetchContent_Declare(

timer_lib

SOURCE_DIR ../../../../timer-lib)


			Notice that we used four instances of `../`, as the path is relative to our project’s build folder (`CMAKE_CURRENT_BINARY_DIR`), not its source folder (`CMAKE_CURRENT_SOURCE_DIR`). Essentially, we are targeting `ch3/part-1/build`, not `ch3/part-1`. We could also use `${CMAKE_CURRENT_SOURCE_DIR}/../../../timer-lib` to make the path relative to our project’s source directory if we prefer. If figuring out the relative path is proving difficult, it’s also fine to use an absolute path as a short-term workaround to get things working.
			This is meant as a temporary convenience and isn’t something to push to your main branch, but it can be particularly useful when iterating on functionality that may have been extracted to a separate project. It’s also important to note that `SOURCE_DIR` can be used in combination with a download method (e.g., `GIT_REPOSITORY`) to override the default location where the source code will be downloaded.
			Making the dependency available
			Now that we have described how to retrieve the dependency’s source (using `FetchContent_Declare`), we can instruct CMake to add it to our project and make the dependency ready to use:

FetchContent_MakeAvailable(timer_lib)


			`FetchContent_MakeAvailable` will make the content of our dependency available to the rest of our `CMakeLists.txt` file. The CMake documentation calls this `timer_lib`), but as discussed earlier, we might have brought in other CMake utility scripts we’d like to use.
			Multiple dependencies can be listed in the `FetchContent_MakeAvailable` command (separated by a space), and all `FetchContent_Declare` statements must come before the call to `FetchContent_MakeAvailable`:

FetchContent_Declare(

LibraryA

...)

FetchContent_Declare(

LibraryB

...)

FetchContent_MakeAvailable(LibraryA LibraryB)


			Something to be aware of about `FetchContent_MakeAvailable` is that it is actually an abstraction over several lower-level CMake commands (`FetchContent_GetProperties`, `FetchContent_Populate`, etc.). These allow more fine-grain control over the dependency, but in the majority of cases, they are not required. `FetchContent_MakeAvailable` is usually more than sufficient and much simpler to use. The CMake documentation recommends using `FetchContent_MakeAvailable` unless there is a good reason not to.
			Linking to the dependencies
			With targets now available for our dependency, the last step is to link against them:

target_link_libraries(${PROJECT_NAME} PRIVATE timer_lib)


			When used with a CMake target as shown in the preceding code snippet, `target_link_libraries` is a deceptively powerful command. There is quite a bit going on that CMake is taking care of for us. As the library we’re depending on is using CMake, it has already described what the `include` paths are and where to find the library file itself. This might seem like a small thing, but doing this by hand is a tedious and error-prone process.
			If we were depending on a library we’d built outside of CMake (without using find modules, a topic covered in *Chapter 7*, *Adding Install Support for Your Libraries*), we would have to manually specify the include paths, library path, and library in the following manner:

target_include_directories(

${PROJECT_NAME}

PRIVATE third-party/timer-lib/include)

target_link_directories(

${PROJECT_NAME} PRIVATE

third-party/timer-lib/lib/macos

third-party/timer-lib/lib/win

third-party/timer-lib/lib/linux)

target_link_libraries(${PROJECT_NAME} PRIVATE timer_lib)


			Some details have been omitted in the preceding example, but the sentiment is much the same. If you visit the book’s GitHub repository ([`github.com/PacktPublishing/Minimal-CMake`](https://github.com/PacktPublishing/Minimal-CMake)) and navigate to `ch3/part-3`, you can see a full example (both `x86_64` and `arm64` architectures are supported, to override the architecture, set `MC_ARCH` when configuring).
			This approach is sometimes necessary (especially if a library we’re depending on isn’t using CMake and creating a find module file is too much overhead for what’s needed). Building separately and updating all individual library files can be tiresome and does not scale well if you’re using many dependencies and updating them regularly.
			Another example is also included in `ch3/part-4`, which shows the use of `add_subdirectory` (it is necessary to navigate to `ch3/part-4/third-party` and run `git clone https://github.com/pr0g/timer_lib.git` to download the library before configuring the project from `ch3/part-4`; see `ch3/part-4/README.md` for details). This has the advantage of relying on the CMake target again (so we get all the `include` directories and library paths for free), but it suffers from the problem mentioned at the start of the chapter, where code from other projects can get mixed up in our source tree.
			We’ll stick with the `FetchContent` approach for the rest of this chapter, and with `timer_lib` now added to `target_link_libraries`, we’re ready to start using the dependency in our project.
			Setting options on dependencies
			When bringing in dependencies, there are often situations where we want to customize exactly what gets built. One of the most common examples is whether to build unit tests or not. Usually, libraries will provide an option to build the tests, with the default set to either `on` or `off` (this is something we’ll cover in more detail in *Chapter 4*, *Creating Libraries* *for FetchContent*).
			To understand this in a bit more detail, let’s continue to evolve our sample project, the `Game of Life` implementation introduced in *Chapter 2*, *Hello CMake!*.
			We are going to bring in another library in addition to `timer_lib` called `as-c-math`. This is a linear algebra math library intended for use in 3D applications and games. It also includes a set of 2D operations, which will help to refine our `Game of` `Life` implementation.
			To introduce the new library, let’s use the now-familiar `FetchContent_Declare` command to describe where to find it:

FetchContent_Declare(

as-c-math

GIT_REPOSITORY https://github.com/pr0g/as-c-math.git

GIT_TAG 616fe946956561ef4884fc32c4eec2432fd952c8)


			We can then add it to the `FetchContent_MakeAvailable` command along with `timer_lib`:

FetchContent_MakeAvailable(timer_lib add_library(我们将在第四章为 FetchContent 创建库中进一步了解 add_library),该内容包含我们希望在 target_link_libraries 中链接的目标名称。在这种情况下,它是项目的名称,使用我们在第二章Hello CMake!中讨论的相同技术(使用 ${PROJECT_NAME} CMake 变量)。现在让我们添加这个依赖项,以确保我们正确地链接它:

target_link_libraries(${PROJECT_NAME} PRIVATE timer_lib ch3/part-5, you can see a version of the project with the changes we have listed. Simply run cmake -B build (with your choice of generator; we’ll stick with Ninja Multi-Config) and then cmake --build build:

cmake -B build -G "Ninja Multi-Config"

cmake --build build


			There’s one thing you might spot in the output:

[11/11] 正在链接 C 可执行文件 _deps/as-c-math-build/Debug/as-c-math-test


			It looks like we’re inadvertently building the unit tests for `as-c-math`. If you navigate to `build/_deps/as-c-math-build/Debug` and run `as-c-math-test`, sure enough, you’ll see that the tests run. In our case, this is a waste of resources, as we’re unlikely to be making changes to the library and would hope that the test suite is already passing.
			The good news is that there’s a way to disable this right after our `FetchContent_Declare` command. If we navigate to the `CMakeLists.txt` file for `as-c-math` (which will have been downloaded for us in `build/_deps/as-c-math-src`), at the top of the file we can see this command:

option(AS_MATH_ENABLE_TEST "启用测试" ON)


			This is a CMake variable used to decide whether we should build the test target or not. Scrolling a little further down, we can see the following:

if(AS_MATH_ENABLE_TEST)

...

endif()


			It is generally good practice when creating libraries to segment any additional utilities such as tests, coverage, and documentation from the main build so users can choose to opt in to what they want to use. The good news is that we can set the `AS_MATH_ENABLE_TEST` variable from our project.
			In this case, we know that we don’t want to build the tests, and we also want to hide this property from users of our library as it’s an implementation detail. We can do this by adding a `set` command right after the `FetchContent_Declare` command for `as-c-math`:

FetchContent_Declare(

as-c-math

GIT_REPOSITORY https://github.com/pr0g/as-c-math.git

GIT_TAG 616fe946956561ef4884fc32c4eec2432fd952c8)

set(AS_MATH_ENABLE_TEST OFF CACHE INTERNAL "")

set(AS_MATH_ENABLE_COVERAGE OFF CACHE INTERNAL "")

FetchContent_MakeAvailable(timer_lib as-c-math)


			For a full example of the preceding step, see `ch3/part-6/CMakeLists.txt`.
			In a perfect world, it would be simpler if we could just write `set(AS_MATH_ENABLE_TEST OFF)`, but the extra arguments are important to add as a best practice. The reasons why we must add them relate to the CMake cache.
			The previously mentioned `option(AS_MATH_ENABLE_TEST "Enable testing" ON)` command is essentially syntactic sugar for the following:

set(AS_MATH_ENABLE_TEST ON CACHE BOOL "启用测试").


			What this does is add the variable to the CMake cache (stored in `build/CMakeCache.txt`). This keeps track of a bunch of settings and variables so CMake doesn’t have to recalculate them on every run. It is also used to allow variables to be edited by users using the CMake GUI or `ccmake` (`ccmake` is a command-line tool for manually editing cache variables; it is only available on macOS and Linux).
			When we override a variable, we first pass the name (`AS_MATH_ENABLE_TEST`), then the value (`OFF`), and then we pass `CACHE` to indicate that this value should be updated in the cache. To clarify, if we leave things as they are in `ch3/part5` and look inside `CMakeCache.txt`, we’ll see the following:

//启用覆盖率

AS_MATH_ENABLE_COVERAGE:BOOL=OFF

//启用测试

AS_MATH_ENABLE_TEST:BOOL=ON


			These are listed under the `EXTERNAL cache entries` section. If we now add `set(AS_MATH_ENABLE_TEST OFF)` to our `CMakeLists.txt` file, tests will be disabled, but the cache entry will be left over with the earlier value. This could cause problems depending on the scope of where `AS_MATH_ENABLE_TEST` is defined in our `CMakeLists.txt` file.
			Another thing to note is if you do a fresh configure with just `set(AS_MATH_ENABLE_TEST OFF)` added to your `CMakeLists.txt` file, then the value suppresses the variable from ever ending up in the cache. This inconsistency can lead to esoteric problems between new and old builds and so is best avoided. It also means that the value can never be overridden from the command line (if you did want to briefly enable tests, passing `cmake -B build -D AS_MATH_ENABLE_TEST=ON` would have no effect).
			The final two arguments (`BOOL` and `"Enable testing"`) are required by CMake cache variables. CMake will complain if you don’t provide these:

set 给定无效的参数用于 CACHE 模式:缺少类型和文档字符串


			As mentioned, the type shows the kind of value to be stored (`BOOL` in our case for `option`), and the `ccmake`.
			When using the full form of `set` mentioned above, when querying `build/CMakeCache.txt`, we can see that the `as-c-math` variables have been updated to look like this:

//启用覆盖率

AS_MATH_ENABLE_COVERAGE:INTERNAL=OFF

//启用测试

AS_MATH_ENABLE_TEST:INTERNAL=OFF


			These will not appear in the CMake GUI or `ccmake` but can still be overridden with `-DAS_MATH_ENABLE_TEST=ON` from the command line if needed.
			In short, when enabling or disabling features for dependencies exposed in their `CMakeLists.txt` file, prefer `set(<variable> <value> CACHE <type> <docstring>)` to the shorter alternative. Make sure to also set these values between the `FetchContent_Declare` and `FetchContent_MakeAvailable` commands for a given dependency. Do all this and you shouldn’t run into any issues.
			Updating our application
			With `as-c-math` added to our `CMakeLists.txt` file (see `ch3/part-7/CMakeLists.txt`), we can now include the library in our project. We simply add `#include <as-ops.h>` at the top of `main.c` (notice the use of angle brackets (`<>`) to indicate that this is an external dependency. Quotation marks (`""`) also work, but using `<>` has the advantage of advertising to the reader that this file is outside the main project).
			If we review `main.c`, we can see it’s changed quite significantly. Instead of thinking of the board as a table with rows and columns, the logic has been updated to treat it as a grid with `x` and `y` coordinates. This is to make things a little more idiomatic when it comes to integration with the math library, but the transition can be a little jarring as the ordering of elements has changed. We traditionally write rows, then columns (`r, c`), which is the vertical position first, then horizontal. With `x` and `y` coordinates, we traditionally write `x` then `y` (`x, y`), with `x` being the horizontal position and `y` being vertical. Right now, the top left of the board is still `(0, 0)`, with `y` growing downward, but this might change in the future. These implementation details are outside the focus of this book but are included for completeness. Don’t worry too much about the changes; the good news is that when running `ch3/part7`, things look identical to how they did before.
			As we can see, what often happens with projects is that when new code is added, we decide how best to structure it after the fact. This is where CMake helps us break things up and provide reusable components.
			Summary
			Fantastic work making it to this point; there was a lot to take in. In this chapter, we covered what `FetchContent` is and why you might want to use it. We touched on how to extract useful functionality in your `CMakeLists.txt` file and then walked through the `FetchContent_Declare` and `FetchContent_MakeAvailable` commands in detail. We saw where to find commits and tags for projects on GitHub, and then how to use `FetchContent` to bring in a simple dependency to enhance our app. We then looked at how to link to our dependency (along with a few alternative approaches) to ensure we could use the code in our project. Finally, we covered how to override settings exposed by dependencies in our `CMakeLists.txt` file and discussed a small update to our *Game of* *Life* application.
			This is another significant achievement under your belt. Being able to effectively understand and use `FetchContent` is an incredibly valuable skill. It unlocks a wealth of software (open source or otherwise) for easy integration with your application.
			Now that we have the knowledge to consume libraries, the next step is learning how to create our own. In the next chapter, we’ll look at breaking out the core of our `Game of Life` application into a library that we can consume in our application and understand exactly what’s needed to make a CMake project consumable by `FetchContent`.


第四章:为 FetchContent 创建库

第三章《使用 FetchContent 处理外部依赖》中,我们详细了解了如何作为应用程序开发者使用 FetchContent。这是非常有用的,如果你不打算创建自己的库,那么这些知识会对你大有帮助。然而,如果你对创建库以在多个项目间共享(或者更好的是,与更广泛的开源社区共享)充满兴趣,那么本章将适合你。

在本章中,我们将介绍用于创建库的 CMake 命令,并通过 FetchContent 使其易于访问。你将在这里学到的技能不仅对你的库有帮助,还可以应用到其他不使用 CMake 的项目中。根据库的大小和复杂性,通常只需几个命令就能为库添加 FetchContent 支持。

在本章中,我们将讨论以下主要主题:

  • 使库兼容 FetchContent

  • 将生命游戏移到库中

  • 将生命游戏做成共享库

  • 最终的跨平台补充

  • 接口库

技术要求

为了跟上进度,请确保你已经满足第一章《入门》的要求。包括以下内容:

  • 一台运行最新 操作系统OS)的 Windows、Mac 或 Linux 机器

  • 一个可工作的 C/C++ 编译器(如果你还没有的话,建议使用系统默认的编译器,适用于每个平台)

本章中的代码示例可以在 https://github.com/PacktPublishing/Minimal-CMake 找到。

使库兼容 FetchContent

回到我们正在进行的项目,让我们从识别一块可以重用的代码开始:array。我们将把这个功能提取到一个独立的库中,以便从主应用程序中使用,并且将来可能在其他项目中重用(或与其他开发者共享,供他们尝试)。

项目结构

在我们查看 CMakeLists.txt 文件之前,先对项目结构做一些小的调整,以确保我们的库遵循常见的惯例。这些调整并非严格必要(我们在第三章《使用 FetchContent 处理外部依赖》中包含的库(timer_libas-c-math)并未遵循这些指南),但了解这些惯例是有用的,并且它们将帮助我们在项目不断发展时保持整洁和有序。

从我们在第二章《你好,CMake!》和第三章《使用 FetchContent 处理外部依赖》中看到的 array/ 文件夹开始,结构如下:

.
├── CMakeLists.txt
├── array
│   ├── array.c
│   └── array.h
├── build
│   └── ...
└── main.c

为了支持重用,我们将把array.harray.c移到我们“生命游戏”应用程序之外的新文件夹中(如果你在跟随教程,请在minimal-cmake仓库之外创建一个名为minimal-cmake-array的新文件夹,并将array.harray.c复制到接下来展示的位置)。

为了使一切保持自包含在Minimal CMake书籍仓库中(github.com/PacktPublishing/Minimal-CMake),我们暂时将内容移至ch4/part-1/lib/array(可以将其视为顶级 CMake 项目的同义词)。

结构如下:

.
├── CMakeLists.txt
├── build
│   └── ...
├── include
│   └── minimal-cmake
│      └── array.h
└── src
   └── array.c

请注意引入了两个新目录,includesrc。这些名称在开源生态系统中已经被广泛采用(为什么includesourceincsrc没有更常见,可能是历史上的偶然结果)。根据惯例,include文件夹用于公共头文件(那些需要被客户端包含的头文件);任何仅在库内部使用的头文件(私有头文件)应保存在src文件夹中,与源文件本身一起。

另一种可能性是将array.harray.c保留在根目录中,如下所示:

├── CMakeLists.txt
├── build
    └── ...
└── array.h
└── array.c

这种方式对于小型库来说无疑是可行的,但也有一些缺点。如果我们想添加更多的源文件,它们可能会使根目录变得杂乱,并增加导航的难度。将实现细节保存在src文件夹下,可以给库的用户一个清晰的信号,让他们将注意力集中在其他地方。

创建一个名为项目名称的include文件夹及子目录的一个优势是,可以使消费应用程序或库中的#include指令更加清晰。

以下方式会更加有帮助:

#include <minimal-cmake/array.h>

将前面的代码与以下代码进行对比,后者更难理解:

#include <array.h>

使用第一种方法,可以明确知道依赖项的来源。这还减少了与其他库发生命名冲突的可能性(这种方法属于代码卫生的范畴)。

另一种选择是为库文件添加前缀。例如,我们本可以选择将array.h重命名为mc-array.h,或minimal-cmake-array.h,并省略子文件夹。为文件、函数和类型名称(例如,mc_array_push)添加项目标识符作为前缀,也是避免与其他库命名冲突的好做法。对于 C++,命名空间是首选的机制,但在 C 语言中,我们必须依赖显式的函数和类型前缀。这也是我们在数组实现中将采用的方法。

在这里展示的示例中,src 文件夹没有任何子文件夹。这是随意的,具体如何安排由库的作者决定。对于一个较小的库来说,src 下没有层级的扁平结构可能是可以的。而对于较大的库,我们可能会决定将某些文件分组以便更好地组织。由于 src 文件夹下的所有内容都可以视为库的私有部分,因此 src 下的结构不应影响库的使用者,所以它可以是你喜欢的任何结构。

关于我们的 C 实现有一个简短的说明,我们可能希望将这个库与未来的 C++ 应用程序一起使用。为了适应这种需求,我们需要使用 extern "C" 来包装或注解所有函数,确保当我们用 C++ 编译这个库时,名称修饰(C++ 中支持函数重载的过程)不会启动(在 C 中,你不能重载函数,符号名称保持不变)。我们还需要在编译为普通 C 代码时忽略 extern "C"。为了实现这一点,我们可以使用 __cplusplus 宏来检查我们是否在编译 C++ 代码(__cplusplus 只有在使用 C++ 时才会定义)。将这一切结合起来,我们得到了如下代码:

#ifdef __cplusplus
extern "C" {
#endif // __cplusplus
// our implementation
#ifdef __cplusplus
}
#endif // __cplusplus

最后,采用之前讨论的文件夹结构将使得安装时的工作变得更轻松。实际上,对于较小的库来说,这可能被认为是一种过度工程,特别是如果你从不打算安装这些库的话,但我们还是为了完整性考虑介绍了这一部分,因为这是我们后面需要的内容。

CMakeLists.txt 文件

在设定好文件夹结构后,我们可以查看新 array 库的 CMakeLists.txt 文件。此处包含了完整的 CMakeLists.txt 文件。我们将像之前的章节那样,逐行分析:

cmake_minimum_required(VERSION 3.28)
project(mc-array LANGUAGES C)
add_library(${PROJECT_NAME})
target_sources(${PROJECT_NAME} PRIVATE src/array.c)
target_include_directories(
  ${PROJECT_NAME} PUBLIC $<BUILD_LOCAL_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>)
target_compile_features(${PROJECT_NAME} PRIVATE c_std_17)

让我们略过前两行,它们与之前相同:

cmake_minimum_required(VERSION 3.28)
project(mc-array LANGUAGES C)

这里是强制要求的 cmake_minimum_required 命令,后面紧跟着同样重要的 project 命令。唯一的区别是,我们为我们的库命名为与其功能相匹配的名称(一个数组接口),并且我们还包含了我们在项目中打算使用的前缀(在这种情况下是 mc,代表Minimal CMake)。这可能有些过头,CMake 也提供了其他方法让你通过使用 ALIAS 来为库加上命名空间。我们将在后面的章节回到这个话题,但目前我们所做的已经足够了。

创建库

接下来,我们将介绍一条很久没见过的新命令:

add_library(${PROJECT_NAME})

由于我们创建的是一个库,而不是一个应用程序,我们必须使用add_library命令而不是add_executable。默认情况下,CMake 会为我们创建一个静态库(对于静态库,内容将会被打包进我们的可执行文件并在编译时链接)。为了覆盖这个行为,在配置 CMake 项目时(运行cmake -B build),可以传递-DBUILD_SHARED_LIBS=ON来切换到构建共享库。为了确保在所有平台(Windows、macOS 和 Linux)上都能正常工作,我们需要做一些额外的工作,所以我们暂时不做处理。为了提供不同于默认的设置,可以在我们的CMakeLists.txt文件中添加一个选项,如下所示:

option(BUILD_SHARED_LIBS "Build shared libraries" OFF)

更多关于BUILD_SHARED_LIBS选项的信息,请参见cmake.org/cmake/help/latest/variable/BUILD_SHARED_LIBS.html

为了硬编码静态或共享库,可以通过在库名后传递STATICSHARED来提供库类型给add_library。以下是一个示例:

add_library(${PROJECT_NAME} STATIC can be a good approach. If you’re creating a library that will be built and installed separately from the main application (something we’ll cover in *Chapter 7*, *Adding Install Support for Your Libraries*), giving a user the flexibility to decide to use either static or shared is a nice feature. Unfortunately, BUILD_SHARED_LIBS doesn’t play nicely when composing multiple libraries using FetchContent. Luckily for us, there is a workaround that builds on the topics we’ve covered here. We’ll cover this a little later in the chapter.
			Next up, we have `target_sources`, which has been updated to reference the new location of `array.c`:

target_sources(${PROJECT_NAME} PRIVATE PRIVATE,这里作为array.c是实现细节,我们不希望(也不需要)它重新编译。唯一的区别是我们在新的位置引用它。

        剩下的新命令(我们在 *第三章* ,*使用 FetchContent 与外部依赖项* 中简要提到过,在查看依赖项链接时)是`target_include_directories`:
target_include_directories(
  ${PROJECT_NAME} PUBLIC 
  $<BUILD_LOCAL_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>)
        这个命令告诉依赖项包含文件相对于的位置。我们直接在目标上设置它,并且希望这个属性对客户端或者库的用户可见,这就是为什么我们指定`PUBLIC`而不是`PRIVATE`的原因。

        生成器表达式

        看看之前提到的`target_include_directories`命令,它的第三行可能一开始看起来有点陌生。你看到的是 CMake 提供的一项功能,称为**生成器表达式**。如果我们暂时移除生成器表达式,命令看起来是这样的:
target_include_directories(
  ${PROJECT_NAME} PUBLIC
  ${CMAKE_CURRENT_SOURCE_DIR}/include)
        让我们回顾一下之前检查过的文件结构:
.
├── include
    └── minimal-cmake
        └── array.h
        这样可以确保应用程序通过`#include <minimal-cmake/array.h>`来包含`array.h`。这非常棒,因为这意味着客户端不需要自己设置`include`目录;他们只需链接到目标,并自动继承这个属性。

        在你的项目的`README`文件中包含一个示例,要么是一个小应用程序,要么是一个代码片段,展示如何包含依赖项以及包含路径是什么,这是个不错的主意。用户虽然可以自己搞定,但你提供的信息越多,就越能让使用这个库变得简单,也能降低他们在使用过程中卡住的几率。

        让我们回到之前看到的生成器表达式:
$<BUILD_LOCAL_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
        在最简单的形式下,结构为`$<condition:value>`。如果`condition`被设置(即存在),则提供`value`;否则,表达式的结果为空。生成器表达式有点像 C 或 C++中的三元操作符(`<condition> ? <true> : <false>`)。它本质上是一种简洁、声明式的方式,用来在`CMakeLists.txt`脚本中编写条件,而不需要依赖更冗长的`if`/`else`分支,这种分支采用的是更命令式的编程风格。

        使用生成器表达式时需要找到一个平衡点;它们可以方便并简化`CMakeLists.txt`文件,但如果过度使用,可能会让代码更难理解。要明智地使用它们,如果你认为使用显式的`if`/`else`语句更清晰,就应当选择这种方式。通过使用多个 CMake 变量将复杂的生成器表达式拆解开来也可以是一种有价值的方式,而不是试图将所有内容都写成一个单一的表达式。

        命令`cmake -B build`中,CMake 首先执行配置步骤,然后执行生成步骤。这时,生成器表达式会被求值,项目文件会被创建。如下所示,这是`cmake`命令的输出:
-- Configuring done (8.7s)
-- Generating done (0.0s)
        使用生成器表达式可能会很困难,能够调试表达式的结果是非常有用的。不幸的是,普通的 CMake `message`语句无法与生成器表达式一起输出日志到控制台,因为它们的求值时间不同(配置时间与生成时间不同)。为了解决这个问题,可以通过以下方法将表达式的结果写入文件:
file(GENERATE OUTPUT <filename> CONTENT "$<...>")
        运行`cmake -B build`时,这将把生成器表达式(`"$<...>"`)的结果写入指定的文件名(如果提供了相对路径,它将位于`build/`文件夹内)。然后可以检查文件的内容,确认结果是否符合预期。

        想要了解更多关于生成器表达式及其支持的多种变体,可以访问[`cmake.org/cmake/help/latest/manual/cmake-generator-expressions.7.html`](https://cmake.org/cmake/help/latest/manual/cmake-generator-expressions.7.html)。

        包含接口

        我们已经讨论了为什么指定`target_include_directories`很重要以及什么是生成器表达式,但没有解释为什么特别需要`BUILD_LOCAL_INTERFACE`。原因在于,这使得我们能够根据是否在构建库或在安装后使用它来使用不同的包含路径。安装对库来说很重要,这是我们将在*第七章*《*为你的库添加安装支持*》中详细讲解的内容,但现在,只需知道有这种替代方案即可。在库的`CMakeLists.txt`文件中,通常会看到类似这样的内容:
target_include_directories(
  ${PROJECT_NAME} PUBLIC 
  $<BUILD_LOCAL_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
  $<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>)
        根据上下文,目标将在以下情况下设置不同的包含路径:如果它依赖于并在同一构建树中构建(如`FetchContent`或`add_subdirectory`),或者安装到另一个位置并从那里依赖(称为导入目标)。安装库的包含文件通常与库本身的不同(开发者可能希望将包含层次结构扁平化,使库接口更易于使用)。通常在创建库时指定`BUILD_LOCAL_INTERFACE`是一个不错的主意。如果以后决定添加安装支持,可以再添加`INSTALL_INTERFACE`。通过明确这一点,您可以避免将来需要匹配构建和安装接口。

        BUILD_LOCAL_INTERFACE 与 BUILD_INTERFACE

        你可能会遇到`BUILD_INTERFACE`,除此之外还有`BUILD_LOCAL_INTERFACE`。`BUILD_LOCAL_INTERFACE`是一个较新的生成表达式(在 CMake `3.26`版本中添加),它仅在同一构建系统中的另一个目标使用时才会展开其内容,而`BUILD_INTERFACE`会在同一构建系统中的另一个目标使用时展开其内容,并且当属性通过`export`命令导出时也会展开。由于我们不打算从构建树中导出目标,因此我们选择了这两个命令中限制性更强的那个。

        最后,我们将编译特性设置为标准版本,以确保在不同编译器之间获得一致的行为:
target_compile_features(${PROJECT_NAME} PRIVATE c_std_17)
        这就是我们通过`FetchContent`将我们的库提供给其他用户所需的一切。

        使用我们的库

        现在,我们可以更新应用程序的现有`CMakeLists.txt`文件,将新的数组库引入:
...
FetchContent_Declare(
  minimal-cmake-array
  GIT_REPOSITORY https://github.com/PacktPublishing/Minimal-CMake.git
  GIT_TAG 2b5ca4e58a967b27674a62f22ece4f846bc0aa78
  SOURCE_SUBDIR ch4/part-1/lib/array) # look just in array folder
FetchContent_MakeAvailable(timer_lib as-c-math minimal-cmake-array)
target_link_libraries(
  ${PROJECT_NAME} PRIVATE timer_lib as-c-math CMakeLists.txt file to a new app folder, with the array library moving to a new lib folder. The folder structure now looks like this:

├── app

│   ├── CMakeLists.txt

│   └── main.c

└── lib

└── array

├── CMakeLists.txt

├── include

└── src


			This means we need to run our CMake configure and build commands (`cmake -B build` and `cmake --build build`) from `part-<n>/app`, instead of `part-<n>` (you could also use the `-S` option and pass the source folder explicitly, as discussed in *Chapter 2*, *Hello, CMake!* if preferred).
			A complete example is presented in `ch4/part-1/app` to show how everything fits together. A small detail to note is the use of `SOURCE_SUBDIR` in the `FetchContent_Declare` command. This lets us specify a subdirectory in the repository as the root to use for `FetchContent`. As we’ve extracted our `array` type to a library in the *Minimal CMake* repository, we can treat that folder as the root of the CMake project (for completeness, the full repository will be downloaded, but only the files specified under `SOURCE_SUBDIR` will be used in the build).
			We can also use `SOURCE_DIR` and a relative path, which can be useful when we’re working on the library and application together. This would look like the following:

FetchContent_Declare(

minimal-cmake-array

SOURCE_DIR ../../lib/array)


			This means any changes to `array` will immediately be reflected in the main application. Just remember to pick a commit for the library when you’re committing your changes to make it easier to go back to earlier points in your project history for reproducible builds.
			Moving Game of Life to a library
			We started by extracting the `array` type from our application as it was a simpler piece of functionality to start with. At this point, we’d like to pull out the core *Game of Life* logic to a separate library. We’re going to make it possible to build it as either a static or shared library, in preparation for potentially integrating it with other languages in the future. This will require us to provide an interface and move the functionality to separate files.
			To prepare for our *Game of Life* code being used as a shared library, we’ll keep the concrete implementation of the *Game of Life* board hidden and expose functionality through a series of functions. The interface looks as follows:

// 前向声明板

typedef struct mc_gol_board_t mc_gol_board_t;

// 生命周期

mc_gol_board_t* mc_gol_create_board(int32_t width, int32_t height);

void mc_gol_destroy_board(mc_gol_board_t* board);

// 处理

void mc_gol_update_board(mc_gol_board_t* board);

// 查询

int32_t mc_gol_board_width(const mc_gol_board_t* board);

int32_t mc_gol_board_height(const mc_gol_board_t* board);

bool mc_gol_board_cell(

const mc_gol_board_t* board, int32_t x, int32_t y);

// 变异

void mc_gol_set_board_cell(

mc_gol_board_t* board, int32_t x, int32_t y, bool alive);


			This is a C-style interface where we forward declare the Game of Life board type (`mc_gol_board_t`) and provide create and destroy functions to manage the lifetime. By hiding concrete types, we make it easier to integrate our library with other languages in the future and avoid potential **application binary interface** (**ABI**) incompatibilities across different compilers (such as layout, padding, or alignment). Function interfaces also help with encapsulation and backward compatibility.
			With our interface defined, we can follow the same approach that we did with `array` and create a static library encapsulating our Game of Life implementation. If you review `ch4/part-2/lib/gol`, you’ll see the updated structure. We’ve also been able to move `as-c-math` and `mc-array` so that they’re private dependencies of the new *Game of Life* library (`mc-gol`) and remove them from the main app’s `CMakeLists.txt` file. To disambiguate the application and library, we’ll also rename our app to `minimal-cmake_game-of-life_console`.
			With this in place, we can focus on the changes necessary to make this a shared library.
			Making Game of Life a shared library
			We will start by working through the changes between `ch4/part-2` and `ch4/part-3` to see what updates are needed to make `mc_gol` a shared library. The focus will be `ch4/part-3/lib/gol/CMakeLists.txt`, but we’ll also need to update `ch4/part-3/lib/gol/include/minimal-cmake-gol/gol.h` and `ch4/part-3/app/CMakeLists.txt`.
			Visual Studio Code – Compare Active File With...
			This is a quick reminder to use the Visual Studio Code feature known as `ch4/part-2/lib/gol/CMakeLists.txt` and `ch4/part-3/lib/gol/CMakeLists.txt`). The `diff` view makes the changes clear without needing to switch back and forth.
			The first difference is the addition of a new `option` command for `mc-gol`:

option(MC_GOL_SHARED "启用共享库(动态链接)" OFF)


			The CMake `option` command allows the library user to compile `mc-gol` as either `STATIC` or `SHARED` (it defaults to `OFF` to match the CMake default of static libraries). The `option` name is also prefixed with `MC_GOL` to help with readability and reduce the chance of name collisions in other projects.
			We’ve refrained from using `BUILD_SHARED_LIBS` in this case because using this would apply to all libraries we’re building (including `mc-array` and `as-c-math`). We would like those libraries to be compiled statically as normal and only allow `mc-gol` to be explicitly compiled as a shared library.
			If we were only building our library and linking to external dependencies that had already been built, `BUILD_SHARED_LIBS` would work well, but this isn’t what we want when composing libraries with `FetchContent`.
			To support only building `mc-gol` as `SHARED`, we need a little more logic before the `add_library` command:

set(MC_GOL_LIB_TYPE STATIC)

if(MC_GOL_SHARED)

set(MC_GOL_LIB_TYPE SHARED)

endif()


			Here, we introduce a new CMake variable called `MC_GOL_LIB_TYPE`, which we default to `STATIC`. Only if the `MC_GOL_SHARED` option is turned on do we set it to `SHARED`. We then pass this CMake variable to the `add_library` command to decide the library type:

add_library(${PROJECT_NAME} ${MC_GOL_LIB_TYPE})


			We’ll skip over the change to `target_include_directories` for now as it’s a side effect of what we’ll talk about next.
			Here, we’re focusing on making our library cross-platform. To ensure our shared library works consistently across macOS, Windows, and Linux, we need to take some extra steps to support this. With the preceding change, if we try to build and run our project on Windows with `MC_GOL_SHARED` set to `ON` (`cmake -B build -DMC_GOL_SHARED=ON`), our application will fail to link. This is because Windows requires symbols from a shared library (in our case, functions) to be explicitly exported; otherwise, they are hidden, and they’re only available internally to the library. This contrasts with macOS and Linux, where all symbols are usually exported by default.
			To work around this, we must explicitly annotate the functions we want to make available to other applications with special compiler directives. These are different across Windows and macOS/Linux (Visual Studio versus GCC/Clang). Fortunately, CMake provides an incredibly useful feature called `generate_export_header` that provides a cross-platform solution for us. To use it, add the following to your `CMakeLists.txt` file:

include(GenerateExportHeader)

generate_export_header(${PROJECT_NAME} BASE_NAME mc_gol)


			First, we bring in the `GenerateExportHeader` module, which provides the `generate_export_header` command, and then we call it while providing the project name and a base name for the library (`mc_gol`). This will create a file called `mc_gol_export.h` in the `mc-gol` build folder.
			This briefly brings us back to the change to `target_include_directories` we skipped over earlier. To ensure our header (`gol.h`) can include `mc_gol_export.h`, we need to ensure it is added to the target’s include path. To achieve this, we’ll add `${CMAKE_CURRENT_BINARY_DIR}` to `target_include_directories`.
			This can be done in one of two ways. First, we can pass two generator expressions like so:

target_include_directories(

${PROJECT_NAME}

PUBLIC \(<BUILD_LOCAL_INTERFACE:\){CMAKE_CURRENT_SOURCE_DIR}/include/>

\(<BUILD_LOCAL_INTERFACE:\){CMAKE_CURRENT_BINARY_DIR}/>)


			Alternatively, we can wrap the generator expression in quotes and pass the second directory as a list (separated by semicolons):

target_include_directories(

${PROJECT_NAME}

PUBLIC "\(<BUILD_LOCAL_INTERFACE:\){CMAKE_CURRENT_SOURCE_DIR}/include/build/mc_gol_export.h,我们会看到几个宏已为我们生成。对我们而言,最重要的一个是 MC_GOL_EXPORT。按照我们当前在 macOS 或 Linux 上的设置,它目前不会展开任何内容(因为默认所有符号都是可见/公共的),但在 Windows 上,当构建共享库时,我们会看到已经生成了以下内容:

#    ifdef mc_gol_EXPORTS
        /* We are building this library */
#      define MC_GOL_EXPORT __declspec(dllexport)
#    else
        /* We are using this library */
#      define MC_GOL_EXPORT __declspec(dllimport)
#    endif
        编译指令 `__declspec(dllexport)` 和 `__declspec(dllimport)` 是微软特有的。当构建共享库时,`__declspec(dllexport)` 用于使符号可供库外部使用,而在使用库时,必须存在 `__declspec(dllimport)` 来显示哪些符号正在被导入。利用 CMake 为我们生成这些宏非常方便;它保证无论我们为哪个平台构建,或者启用了哪些编译器设置,都会做出正确的处理。

        如果我们决定再次将 `mc-gol` 构建为静态库,那么 `MC_GOL_EXPORT` 将不会展开。构建静态版本库时,我们可以设置一个额外的 `#define`,在这种情况下是 `MC_GOL_STATIC_DEFINE`。我们可以这样定义:
target_compile_definitions(
  ${PROJECT_NAME}
  PUBLIC $<$<NOT:$<BOOL:${MC_GOL_SHARED}>>:MC_GOL_STATIC_DEFINE, but only if we’re not building a shared library. This will guarantee that MC_GOL_EXPORT won’t be expanded when building as a static library (see ch4/part-5/lib/CMakeLists.txt for an example). This can be useful if you’re reusing a generated version of mc_gol_export.h that has MC_GOL_EXPORT set to something you don’t want. In our case, it’s not strictly necessary but it can be a good failsafe to keep in place.
			To learn more about `GenerateExportHeader`, you can read the full documentation, which is available at [`cmake.org/cmake/help/latest/module/GenerateExportHeader.html`](https://cmake.org/cmake/help/latest/module/GenerateExportHeader.html).
			With `mc_gol_export.h` created, and our `target_include_directories` command updated, all that remains is to annotate our symbols (in the case of `gol.h`, our functions) with `MC_GOL_EXPORT`. Here’s an example:

MC_GOL_EXPORT mc_gol_board_t* mc_gol_create_board(

int32_t width, int32_t height);


			On Windows, when `mc_gol` is built, the macro is substituted with `__declspec(dllexport)`, and when it’s later used as a dependency from our application, `MC_GOL_EXPORT` is substituted with `__declspec(dllimport)`.
			Making things work on Windows
			We’re nearly there! The last change we need to make is to our application’s `CMakeLists.txt` file (`ch4/part-3/app/CMakeLists.txt`) to ensure things work correctly on Windows.
			Let’s configure and build our project with `MC_GOL_SHARED` set to `ON`, like so:

cmake -B build -DMC_GOL_SHARED=ON

cmake --build build


			Assuming Visual Studio is picked as the default generator (it being a multi-config generator, our executable will end up in the `Debug/` folder unless a different config is provided), we can try to run our application with the following command:

./build/Debug/minimal-cmake_game-of-life_console.exe


			The unwelcome news is this will fail on startup with the following error:

C:/Path/to/minimal-cmake/ch4/part-3/app/build/Debug/minimal-cmake_game-of-life_console.exe: 加载共享库时出错:?: 无法打开共享对象文件:没有这样的文件或目录


			The reason for this is that our application cannot find `mc-gol.dll` to load. This has happened because, on Windows, an application will search for a shared library (called a `PATH` environment variable. We haven’t told our executable where to search for `mc-gol.dll` or moved the DLL next to our executable, so it can’t find it.
			To get things working, we could update the `PATH` variable from the terminal:

set PATH=C:\Path\to\minimal-cmake\ch4\part-3\app\build_deps\minimal-cmake-gol-build\Debug;%PATH%


			This, however, is a tedious manual step and deals with absolute paths (not exactly portable). A much better idea is just to copy or move the DLL to the same folder as the executable.
			There are two ways to do this in our example. The first is to update `RUNTIME_OUTPUT_DIRECTORY` of `mc_gol` to that of our current executable. In our application’s `CMakeLists.txt` file, we can add this line:

if(WIN32)

set_target_properties(

mc-gol 属性 RUNTIME_OUTPUT_DIRECTORY

${CMAKE_CURRENT_BINARY_DIR})

endif()


			As we’re building `mc-gol` ourselves, we can set properties on it as if we’d added the library locally. The preceding command will ensure `mc-gol.dll` will be written directly to `build\Debug`, instead of `build\_deps\minimal-cmake-gol-build\Debug`. This command also handles single and multi-config generators correctly (if we were to switch to the Ninja single-config generator, `mc-gol.dll` would end up in the `build\` folder).
			As a brief aside, it’s worth mentioning that `RUNTIME_OUTPUT_DIRECTORY` refers to `.dll` files on Windows (as well as executable files), but on macOS and Linux, it is `LIBRARY_OUTPUT_DIRECTORY`, which refers to `.dylib` (macOS) and `.so` (Linux) shared library files. This can be a little counterintuitive and will be important a little later when we return to `ch4/part4`.
			The second way to copy `mc-gol.dll` to the same directory as our executable is to use a CMake custom command. Here is the one we’ll use:

if(WIN32)

add_custom_command(

TARGET ${PROJECT_NAME}

POST_BUILD

COMMAND

${CMAKE_COMMAND} -E copy_if_different $<TARGET_FILE:mc-gol>

\(<TARGET_FILE_DIR:\){PROJECT_NAME}>

VERBATIM)

endif()


			This sets up a custom command to run immediately after the build completes (`POST_BUILD`). The target the command is bound to is our application, and the command copies the target file (`$<TARGET_FILE:mc-gol>`) to the directory of our application’s target binary file (`$<TARGET_FILE_DIR:${PROJECT_NAME}>`). In this case, when `mc-gol.dll` is built, it is written to `build\_deps\minimal-cmake-gol-build\Debug\mc-gol.dll` first, after which it is copied to `build\Debug` once our application (`minimal-cmake_game-of-life_console`) has finished building.
			One advantage of this approach over using the `set_target_properties(... RUNTIME_OUTPUT_DIRECTORY` method is that this works for libraries outside the current build (for example, installed libraries found using `find_package`, something we’ll cover in *Chapter 6*, *Installing Dependencies and ExternalProject_Add*). This consistency is one reason to prefer this approach; however, it depends on the type of application you’re building. If you know the library will always be included in the main build using `FetchContent` or `add_subdirectory`, then sticking with setting `RUNTIME_OUTPUT_DIRECTORY` is a fine choice.
			Making things relocatable on macOS and Linux
			We spent a bit of time dealing with DLL loading issues on Windows, but both macOS and Linux also need some attention to work reliably across different locations. The reason we had to copy `mc-gol.dll` to the application folder on Windows was that our application wouldn’t start without it there. The good news is that on macOS and Linux, we don’t need to do that because when we build the project, our application will record the location of the shared library and know where to load it from.
			This works great until we decide to move our library to another location. Suppose we want to zip up the contents of our project and share it with a friend, or just check it runs on another machine. If we try this as-is, chances are you’ll see the following error:

dyld[10168]: 未加载库:@rpath/libmc-gol.dylib

原因:尝试了:'/path/to/minimal-cmake/ch4/part-3/app/build/_deps/minimal-cmake-gol-build/libmc-gol.dylib'(没有这个文件)


			This is because the absolute path of where the library was found when it was built is baked into our application. This means we can move our application (`minimal-cmake_game-of-life_console`), but if we move `mc-gol.dylib` (macOS) or `mc-gol.so` (Linux), things will break. Fortunately, there is a straightforward way to solve this.
			What we’re going to rely on is changing the `RPATH` (runtime search path) variable of our executable to include `@loader_path` (on macOS) and `$ORIGIN` (on Linux). This is effectively a way to refer to the application wherever it is on the filesystem. What this means is that just like on Windows, our application will search for the shared library in the folder it’s running from, so we simply need to copy the shared library (`.dylib`/`.so`) to the application folder. We only need to do this when we want to distribute the application, and we can either use `set_target_properties(... LIBRARY_OUTPUT_DIRECTORY)` or rely on the same method we used to copy the Windows `.dll` file to the same folder.
			To change the `RPATH` variable, we can use the following CMake commands:

set_target_properties(

${PROJECT_NAME} 属性 BUILD_RPATH @loader_path) # 仅限 macOS

set_target_properties(

${PROJECT_NAME} 属性 BUILD_RPATH $ORIGIN) # 仅限 Linux

set_target_properties(

${PROJECT_NAME}

PROPERTIES

BUILD_RPATH

"\(<\)<PLATFORM_ID:Linux>:\(ORIGIN>\)<$<PLATFORM_ID:Darwin>:@loader_path>")分别为 macOS 和 Linux 设置 set_target_properties,然后使用生成器表达式来设置正确的 RPATH 值,以便根据平台进行调整(在此情况下不会在 Windows 上设置任何内容)。

        要检查 `RPATH` 的值,可以在 macOS 上使用 `otool` 工具或在 Linux 上使用 `readelf` 工具(这两个工具分别显示其平台的对象文件)。在 macOS 上使用 `otool -l minimal-cmake_game-of-life_console` 命令,以及在 Linux 上使用 `readelf -d minimal-cmake_game-of-life_console` 命令,将显示列出的值。

        以下是在 macOS 上使用 `otool` 的输出片段:
Load command 16
          cmd LC_RPATH
      cmdsize 32
         readelf on Linux:

0x..001 (NEEDED)  共享库:[ld-linux-aarch64.so.1]

0x..01d @loader_path 和 $ORIGIN 出现如预期。

        要了解有关 `CMake` 中 `RPATH` 处理的更多信息,请访问 [`gitlab.kitware.com/cmake/community/-/wikis/doc/cmake/RPATH-handling`](https://gitlab.kitware.com/cmake/community/-/wikis/doc/cmake/RPATH-handling)。在配置共享库时,有许多不同的方法,我们只是初步探讨了一个可能的解决方案。这是一个可以继续探索的领域,具体取决于您将要创建的应用程序类型。在本书后面讨论安装库和打包项目时,我们一定会重新讨论这些主题。

        最终跨平台增强

        在结束之前,让我们来介绍一些小更新,以确保我们的库在不同平台上更为一致。我们可以使用现在熟悉的 `set_target_properties` 命令,仅对我们的库应用这些设置。

        前两个相关属性是 `C_VISIBILITY_PRESET` 和 `VISIBILITY_INLINES_HIDDEN`。我们将 `C_VISIBILITY_PRESET` 设置为 `hidden`,将 `VISIBILITY_INLINES_HIDDEN` 设置为 `ON`。这可以确保在 Windows 上的 Visual Studio 编译器(MSVC)和 macOS/Linux 上的 Clang/GCC 编译器之间,默认情况下,除非使用 `MC_GOL_EXPORT` 显式注释符号,否则它们将保持隐藏。这有助于防止不同平台之间的不兼容性。

        启用这些设置后,如果我们在 macOS 或 Linux 上像往常一样运行 `cmake -B build` 来重新生成我们的导出头文件,我们将看到以下内容:
#    ifdef mc_gol_EXPORTS
        /* We are building this library */
#      define MC_GOL_EXPORT
__attribute__((visibility("default")))
#    else
        /* We are using this library */
#      define MC_GOL_EXPORT
__attribute__((visibility("default")))
#    endif
        这比看到以下内容要好:
#    ifdef mc_gol_EXPORTS
        /* We are building this library */
#      define MC_GOL_EXPORT
#    else
        /* We are using this library */
#      define MC_GOL_EXPORT
#    endif
        启用这些设置后,如果我们尝试在 macOS 或 Linux 上使用尚未明确导出的符号(类型或函数),我们将会得到链接错误,就像在 Windows 上一样。如果我们正在开发跨平台库,建议尽可能保持行为在各个平台上的一致性。不自动导出所有符号默认有很好的理由,可以减少导出符号表的大小和整体二进制大小。

        接下来的两个属性是 `C_STANDARD_REQUIRED` 和 `C_EXTENSIONS`。我们将 `C_STANDARD_REQUIRED` 设置为 `ON`,将 `C_EXTENSIONS` 设置为 `OFF`。

        将 `C_STANDARD_REQUIRED` 设置为 `ON` 确保我们能够获取到在 `target_compile_features` 中使用 `c_std_17` 指定的最小 C 语言版本。也可以通过 `set_target_properties` 和 `C_STANDARD 17` 来设置语言版本,尽管可以说,`target_compile_features` 更加清晰,这也是为什么本书中更倾向于使用它的原因。

        将 `C_EXTENSIONS` 设置为 `OFF` 确保我们不会不小心使用不同编译器厂商添加的、不符合 C 标准(或如果我们使用了 `CXX_EXTENSIONS` 则是 C++ 标准)的语言特性。同样,这是为了帮助强制执行跨平台代码,使其不依赖于仅在某个编译器或平台上可用的特性。如果你打算只为一个平台或编译器进行构建,这一点不那么重要,但养成这个习惯是个好做法。特别是如果有一天你决定将代码移植到另一个平台,避免依赖特定编译器的特性将让这个过程变得更加容易。

        最终的表达式如下所示:
set_target_properties(
  ${PROJECT_NAME}
  PROPERTIES C_VISIBILITY_PRESET hidden
             VISIBILITY_INLINES_HIDDEN ON
             C_STANDARD_REQUIRED ON
             C_EXTENSIONS OFF)
        为了更保险起见,如果我们不是以共享库的方式构建 `mc-gol`,我们还会添加 `MC_GOL_STATIC_DEFINE`(尽管在这种情况下,这并不是严格必要的,但这是一个很好的、低成本的防御性措施,可以避免将来可能出现的链接时问题,这取决于 `mc_gol_export.h` 的状态)。

        若想查看所有内容,可以访问 [`github.com/PacktPublishing/Minimal-CMake`](https://github.com/PacktPublishing/Minimal-CMake) 并查看 `ch4/part-5/lib/gol/CMakeLists.txt`。

        这就完成了我们对 *生命游戏* 库的所有修改!在进入下一章之前,我们还有一个重要的主题尚未讨论。

        接口库

        除了静态库和共享库外,还有另一种常见的库类型,通常被称为 `.h` 文件)。它不会在编译或链接时预先处理,`.h` 文件只是被包含进去,然后与主应用程序的源代码一起编译。

        仅头文件库因为其易于集成而非常受欢迎(你只需将 `.h` 文件包含到项目中,通常一切就能正常工作)。缺点是,每当你更改代码时,你必须重新编译该库,这会带来额外的开销,这种开销根据库的复杂度可能会很大。仅头文件库在 C++ 中尤其常见,尤其是模板库,因为它们的实现必须出现在头文件中。

        幸运的是,CMake 提供了一种直接的方法来创建仅头文件库,这些库可以像其他库一样使用。这里展示了一个完整的仅头文件 `CMakeLists.txt` 文件:
cmake_minimum_required(VERSION 3.28)
project(mc-utils LANGUAGES C)
add_library(${PROJECT_NAME} INTERFACE)
target_include_directories(
  ${PROJECT_NAME}
  INTERFACE $<BUILD_LOCAL_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include/>)
target_compile_features(${PROJECT_NAME} INTERFACE c_std_17)
        该文件应该与我们之前看到的 `CMakeLists.txt` 文件非常相似。主要的不同点是添加了 `INTERFACE` 关键字,取代了 `add_library` 命令中的 `STATIC` 或 `SHARED`,以及特定的 `target_...` 命令中的 `PUBLIC` 或 `PRIVATE`。

        `INTERFACE`关键字告知 CMake 这个目标没有源文件需要构建,也不会生成任何工件(库文件)。它所做的只是提供使用它的要求(在我们的例子中,我们指定了包含文件的位置,并要求使用 `c_std_17` 或更高版本)。`INTERFACE` 关键字还允许我们通过 `target_sources` 命令为依赖的目标指定一组源文件进行编译(我们将在*第九章*中看到此用途,*为项目编写测试*)。

        上面的代码是一个人为的示例,我们提取了一个不特定于*生命游戏*的单一有用工具函数,未来可能会使用(并且可能会添加)。这个函数是 `try_wrap`,它本质上是一个更强大的取模函数,能在处理负数时更好地进行环绕运算。

        现在,我们可以像下面这样在`mc-gol`中使用这个库:
FetchContent_Declare(
  minimal-cmake-utils
  GIT_REPOSITORY <path/to/git-repo>
  GIT_TAG <commit-hash>)
FetchContent_MakeAvailable(minimal-cmake-utils)
target_link_libraries(<main-app> PRIVATE mc-utils)
        我们技术上并没有链接到这个库,但我们必须将目标添加为 `target_link_libraries` 的依赖项,以便为我们的目标应用程序填充包含搜索路径。然后,我们只需要在 `gol.c` 中添加 `#include <minimal-cmake/utils.h>` 以访问该函数。

        由于这仍然是一个 C 语言的仅头文件库,我们需要用 `static` 来注解我们的函数实现,以避免链接错误。这将导致在每个翻译单元(`.c` 文件)中生成函数的副本,这并不理想,但在这个简单的例子中是可行的。C++ 对仅头文件库的支持要好得多。在这种情况下,应该首选 `inline` 关键字(`inline` 在 C 语言中也受支持,但它在 C 中的含义与 C++ 中有所不同,使用起来也稍微复杂一些)。

        以这种方式使用仅包含头文件的库提供了在*第三章*中讨论的所有优势,*使用 FetchContent 处理外部依赖项*,包括将代码和依赖项分开,并使设置包含路径变得更加简单。

        你可以在 `ch4/part6/lib/utils/CMakeLists.txt` 和 `ch4/part6/app/CMakeLists.txt` 中找到完整的示例。

        摘要

        如果你已经走到这一步,给自己一个值得的鼓励——你已经走了很长一段路!在本章中,我们讨论了如何使库与`FetchContent`兼容。这包括回顾项目的物理结构、如何创建库,以及如何使用生成器表达式来控制包含接口。接着,我们查看了如何使用我们的新库。在此基础上,我们将我们的*生命游戏*逻辑提取到一个具有新接口的独立库中。我们深入探讨了如何将其制作成共享库,以及在 Windows、macOS 和 Linux 之间需要考虑的许多问题,还探讨了 CMake 如何帮助我们(通过导出头文件、在 Windows 上为 DLL 复制创建自定义命令,以及如何定制目标属性以帮助在 macOS 和 Linux 上创建可移动的库)。最后,我们通过做一些小的改进来帮助避免跨平台问题,并查看了接口(或仅头文件)库以及如何使用 CMake 创建它们。

        如果你还没有,请花一些时间通过访问[`github.com/PacktPublishing/Minimal-CMake`](https://github.com/PacktPublishing/Minimal-CMake)来熟悉本章讨论的示例,并尝试配置和构建这些项目(请参见`ch4`中的逐步示例)。实际的示例对于构建对这些概念的理解和熟悉非常有帮助。希望其中一些示例应该很容易提取并用于你的项目。了解如何创建库是一个重要的里程碑,并且为编写别人可以轻松使用的代码提供了令人兴奋的机会。

        现在你已经对创建库有了扎实的理解,是时候看看如何利用一些有用的 CMake 功能,使日常开发更快、更简单和更可靠了。我们将在下一章中做具体介绍。




第二部分:扩展

在通过 CMake 帮助成功启动并运行我们的应用程序,创建了第一个库之后,我们接下来将着重于简化 CMake 的使用(这一主题我们将在全书中多次回顾)。这将使日常使用变得更简单,并改善任何新开发人员希望设置你项目的入职体验。回到我们的应用程序,我们将介绍一个新命令,可以更好地处理更大的依赖项,将其与主构建解耦。接着我们将反过来展示如何使你自己的库以这种方式可用,这样你和其他人都能依赖于你构建的有用功能。第二部分将通过展示如何简化构建,减少所需的手动步骤,并确保我们能够通过单一的 CMake 命令从多个依赖项中生成可执行文件来结束。

本部分包含以下章节:

  • 第五章简化 CMake 配置

  • 第六章安装依赖项和 ExternalProject_Add

  • 第七章为你的库添加安装支持

  • 第八章使用超级构建简化入职过程

第五章:精简 CMake 配置

在本章中,我们将从项目中退一步,解决一些使用 CMake 时的日常痛点。我们将重点讨论如何消除使用 CMake 时的一些粗糙细节,以使日常开发更加轻松,并讨论一些工具和技术来减少手动操作。这些方法还将帮助不熟悉你的项目的用户更快上手,而无需知道所有正确的配置选项。

在本章中,我们将讨论以下主要主题:

  • 回顾我们如何使用 CMake

  • 使用脚本避免重复命令

  • 转向 CMake 预设

  • 进一步使用 CMake 预设

  • 返回 CMake 图形界面

技术要求

为了跟上进度,请确保你已经满足第一章《入门》的要求。包括以下内容:

  • 一台运行最新操作 系统OS)的 Windows、Mac 或 Linux 机器

  • 一个可用的 C/C++编译器(如果你还没有,建议使用每个平台的系统默认编译器)

本章中的代码示例可以通过以下链接找到:github.com/PacktPublishing/Minimal-CMake

回顾我们如何使用 CMake

在本书的第一部分,我们故意专注于直接从终端运行所有 CMake 命令。这是熟悉 CMake 并理解其工作原理的一个很好的方法,但随着你对 CMake 的熟悉,反复输入这些命令会变得让人厌烦。如果你的项目开始添加几个不同的配置选项,尤其是如果你有一个演示或项目希望分享,期待不熟悉的用户输入冗长且容易出错的命令是不可行的。

第一个看起来可能是一个有前景的想法是直接在你的CMakeLists.txt文件中设置变量,并提示用户在那里更改值。这样做的主要问题是它会变成维护噩梦,并且使得同时支持不同的构建配置变得极其困难。你能从CMakeLists.txt文件中提取的设置越多越好,这样可以为自己和其他人将来使用时提供更多的自定义点。

如果我们最好将设置保存在CMakeLists.txt文件之外,那么我们需要用户通过熟悉的-D<variable>=<value>格式在命令行上传递它们。这种灵活性非常好,但如果用户每次配置时都必须提供多个变量,可能会变得混乱且容易出错。

例如,如果我们拿我们的生命游戏项目来举例,我们已经有了相当多的选项可以在命令行传递,其中一些是我们自己设置的,有些是 CMake 提供的。一个正常的命令可能如下所示:

cmake -B build -G "Ninja Multi-Config" -DMC_GOL_SHARED=ON

如果我们决定使用 Ninja 单配置生成器并显式设置构建类型,它看起来会是这样的:

cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DMC_GOL_SHARED=ON

这已经开始看起来像是大量的输入,而且从这里开始只会变得更糟。是的,你只需要输入一次这些内容来启动并运行,但对于新加入团队/项目的人来说,这可能是痛苦的,甚至对于经验丰富的开发者,在新工作空间或平台上检查代码时也会感到乏味。那么,有什么替代方案呢?

使用脚本避免重复的命令

一开始一个完全有效的选择是,在你选择的平台上引入简单的 shell 或批处理脚本,以封装常用的 CMake 命令。例如,在 macOS 上,我们可以创建一个名为 configure-default.sh 的脚本,它作为用户初始使用的有主张的默认配置,并且符合我们的日常使用。在 macOS/Linux 上,这可能看起来像下面这样:

#!/bin/bash
cmake -B build -G "Ninja Multi-Config" -DMC_GOL_SHARED=ON

要创建并使这个文件可执行,我们可以从终端运行以下命令:

touch configure-default.sh
# modify file
chmod +x configure-default.sh

在 Windows 上,我们可以依赖用户使用 Git Bash(这样他们就可以执行 .sh 脚本),或者创建相应的 .bat 文件:

@echo off
cmake -B build -G "Ninja Multi-Config" -DMC_GOL_SHARED=ON

为了提供更多灵活性,提供几个脚本并根据它们的设置命名也会很有帮助;例如,生成器的类型(例如,configure-ninja.shconfigure-vs-2022.batconfigure-xcode.sh 等)或我们构建的库的类型,无论是静态库还是共享库(例如,configure-shared-ninja.shconfigure-static-vs-2022.bat 等)。

除了加速日常开发外,创建这些脚本的另一个优点是可以作为一种文档形式,帮助用户了解如何配置和调整你的应用程序或库,而不必一开始就去翻找CMakeLists.txt文件。这再次平滑了学习曲线,并允许新开发者从终端自行迭代这些命令。

Git 中的一个有用功能是能够在你的仓库中创建自定义的 .gitignore 规则。这些可以添加到 .git/info/exclude 文件中,因此值得建议用户复制现有的配置脚本,将其重命名为 configure-<username>.sh/bat,然后将其添加到 .git/info/exclude 文件中。

直到现在,我们只关注了 CMake 配置阶段,因为第一次配置命令通常有最多的选项。将我们的配置脚本与构建命令结合使用也很有帮助,这样用户就可以一次性配置并构建应用程序。一个 configure-build.sh/bat 文件可能看起来像这样:

#!/bin/bash
cmake -B build -G "Ninja Multi-Config" -DMC_GOL_SHARED=ON
cmake --build build --config Debug
cmake --build build --config RelWithDebInfo

更好的做法是将配置逻辑分开,然后从 configure-build 脚本中调用它。这可以通过在 macOS/Linux 上执行以下操作来实现:

#!/bin/bash
./configure-default.sh
cmake --build build --config Debug
cmake --build build --config RelWithDebInfo

在 Windows 上,可以通过以下方式实现:

@echo off
CALL configure-default.bat
cmake --build build --config Debug
cmake --build build --config RelWithDebInfo

要尝试这些脚本,请参见书籍附带仓库中的ch5/part-1/app

如果你使用的是单配置生成器,为每种构建类型指定自己的子文件夹可能会很方便(尽管实际上,多配置生成器提供的功能非常优秀,能便捷地为你处理这些复杂性)。

如果你愿意,也可以包含一个调用来运行应用程序,尽管这取决于你正在构建的应用程序类型,并且如果在 READMECMakeLists.txt 文件中提供了关于输出文件所在位置的清晰指示,则不应有必要。工作目录(你从中运行应用程序的目录)在这里可能很重要,所以如果加载其他资源时,请记住这一点(我们将在 第十章**,打包项目 以供共享 中介绍如何处理这个问题)。

拥有这些脚本对你以及任何希望查看或贡献你项目的用户或维护者来说可能是有帮助的,但维护起来可能会变得很麻烦。如果你正在构建一个跨平台项目,支持单独的 .bat.sh 脚本也会让人感到沮丧。另一个缺点是,这些脚本需要从它们所在的终端运行。试图从操作系统文件浏览器中运行它们可能不起作用,因为工作目录通常会被设置为用户的主目录(在 macOS/Linux 上是~/,在 Windows 上是 C:\Users\<username>)。

脚本图形用户界面支持

如果你下定决心,可以将工作目录设置为文件所在的位置。在 macOS 和 Linux 上,可以通过在 .sh 文件开头添加 cd "$(dirname "$0")" 来实现($0 展开为文件名,dirname 给出包含它的文件夹),在 Windows 上,可以在 .bat 文件的开头添加 cd /d "%~dp0"%~dp0 是一个批处理变量,展开为文件的驱动器和路径)。你需要记住根据 CMake 安装位置的不同,在某些情况下更新路径(例如,如果 CMake 没有安装在 Linux 的默认系统位置),你还可能希望在 macOS 上将 .sh 文件重命名为 .command,以便可以轻松地从 Finder 中运行。由于额外的复杂性,接下来的部分我们将仅从终端运行。

幸运的是,CMake 有一个相对较新的功能,它在很大程度上(尽管不是完全)消除了对 .bat.sh 脚本的需求,这个功能叫做 CMake 预设(从 CMake 3.19 版本开始提供),我们将在下一节中介绍。

转向 CMake 预设

.sh.bat 文件可以与 CMake 紧密集成,并可以与其他工具如 Visual Studio Code、Visual Studio 和 CLion(一个跨平台的 C/C++ 开发环境)一起使用。

要开始使用 CMake 预设,我们需要在项目根目录下创建一个名为 CMakePresets.json 的文件。CMake 预设仅是一个以 {} 作为根的文件。CMakePresets.json 文件有多个部分,涵盖 CMake 构建的各个阶段(配置、构建、测试、打包等)。一开始,我们将专注于配置和构建部分,但随着项目的不断发展,我们将在后续章节中再次回到 CMakePresets.json 文件。

编写 CMake 预设文件

编写 CMake 预设文件有时会因其使用 JSON 而变得具有挑战性。为了简化工作,强烈建议使用内置 JSON 语法支持的文本编辑器(Visual Studio Code 是一个显著的例子)。这样,如果你缺少引号或闭括号,编辑器会立即给出反馈,用红色或黄色下划线标出问题。运行 cmake --preset <preset> 时,如果 CMakePreset.json 文件无效,将输出 JSON Parse Error 错误,并附上列和行号,但通过视觉编辑器的反馈,你在输入时就能知道存在问题。

让我们回顾一下一个最小化的 CMakePresets.json 文件:

{
  "version": 8,
  "configurePresets" : [
    {
      "name": "default",
      "generator": "Ninja Multi-Config",
      "binaryDir": "${sourceDir}/build",
      "cacheVariables": {
        "MC_GOL_SHARED": "ON"
      }
    }
  ]
}

在打开的 JSON 对象大括号后,我们必须首先提供一个数字,表示模式的版本(截至目前,8 是最新版本,并且适用于 CMake 3.28 及以上版本)。如果你查阅 CMake 关于预设的文档(参见 cmake.org/cmake/help/latest/manual/cmake-presets.7.html),功能通常与特定的模式版本相关联。

下一个键是 configurePresets,它映射到不同配置的值数组(这就像我们可能有一个或多个 .bat.sh 脚本,提供不同的配置选项)。目前我们只提供了一个,但将来添加更多非常简单。该对象的第一个键是 name 字段;这是唯一必需的字段,其他键是可选的(为了简洁起见,我们省略了更多字段)。

遍历一组选项,我们可以看到每个选项如何对应于我们原本在命令行中使用的内容:

"generator": "Ninja Multi-Config", # -G "Ninja Multi..."
"binaryDir": "${sourceDir}/build", # -B build
"cacheVariables": {
  "MC_GOL_SHARED": "ON"            # -D MC_GOL_SHARED=ON
}

添加此预设后,我们可以从根目录运行 cmake --list-presets 来查看可用预设的列表:

Available configure presets:
  "default"

如果我们希望对用户更加友好,可以像这样提供一个 displayName 字段:

"name": "default",
"displayName": "Default Configuration",
... # as before

运行 cmake --list-presets 将显示以下内容:

Available configure presets:
  "default" - Default Configuration

也可以提供描述(使用 description 字段);不过,这不会在命令行或 CMake GUI 中显示。描述可能会在其他工具中显示;例如,当选择 CMake: Configure 时,Visual Studio Code 选择显示它,在 命令面板 中显示:

图 5.1:Visual Studio Code CMake 预设描述

图 5.1:Visual Studio Code CMake 预设描述

它的存在为使用该预设的用户提供了文档,因此根据上下文可能值得包括此信息。添加配置预设后,只需从根目录运行 cmake --preset default,即可让 CMake 使用提供的设置配置项目。该命令将输出已提供的 CMake 变量及其对应的值,随后是常见的配置输出:

Preset CMake variables:
  MC_GOL_SHARED="ON"
-- The C compiler identification is AppleClang 15...
-- Detecting C compiler ABI info
-- ...

若要查看此功能的实际示例,请查看书籍随附的代码库中的ch5/part-2/app/CMakePresets.json

我们已经介绍了添加单个 CMake 预设的方法,这对于将默认结构化配置选项添加到项目中非常有用,但 CMake 预设的功能远不止于此。

深入了解 CMake 预设

如果我们希望更进一步,为用户提供更多灵活性,CMakePresets.json 还提供了一些其他字段,值得了解。第一个字段是 inherits,它允许一个预设继承另一个预设的值。某些键/值对不会被继承(包括 namedisplayNamedescriptioninherits 本身),但几乎所有其他内容都会被继承。下一个字段是 hidden;它允许定义一个预设,但阻止其在运行 cmake --list-presets 时显示给最终用户。这对于定义基本或通用类型非常方便,这些类型可以继承更多具体类型,然后只需提供少量自定义字段。

作为示例,假设我们的 生命游戏 项目,我们可以像下面这样定义一个 CMake 预设文件:

{
  "version": 8,
  "configurePresets": [
    {
      "name": "base",
      "hidden": true
      "binaryDir": "${sourceDir}/build/${presetName}",
      "generator": "Ninja Multi-Config"
    },
    {
       "name": "shared",
       "inherits": "base",
       "cacheVariables": {
         "MC_GOL_SHARED": "ON"
        }
    },
    {
      "name": "static",
      "inherits": "base",
      "cacheVariables": {
        "MC_GOL_SHARED": "OFF"
      }
    }
  ]
}

它以一个名为 base 的配置预设开始,其中 hidden 字段被设置为 true。在那里,我们根据任何后续预设名称定义了一个二进制目录:

"binaryDir": "${sourceDir}/build/${presetName}"

${sourceDir}${presetName} 被称为 ,它们根据项目上下文展开为有意义的值(例如,${sourceDir} 会展开为项目根目录)。在这种情况下,我们还提供了我们首选的生成器:“generator:"`Ninja Multi-Config"。

通常,不建议在基础预设中提供特定的生成器(特别是因为并非所有客户端都安装了 Ninja);相反,更简单的做法是依赖于特定平台的默认生成器,并将特定的生成器覆盖项作为后续选项提供。在我们的案例中,我们选择始终使用 Ninja 多配置生成器,以保持在 macOS、Windows 和 Linux 上的一致性。

随后的两个配置使用 inherits,基本上将 binaryDirgenerator 的值复制到它们自己中,而无需重复代码行。我们为每个配置提供了一个唯一的名称(sharedstatic),并分别指定了 MC_GOL_SHARED CMake 选项为 ONOFF。然后,用户可以通过 cmake --preset staticcmake --preset shared 来配置 生命游戏 控制台应用程序,以使用库的静态或共享版本。

有帮助的是,宏会在所使用的预设的上下文中解析,这意味着在前面的示例中,当 base 被继承到 staticshared 时,${presetName} 变量会被分别替换为 staticshared。这意味着我们最终会得到两个构建文件夹,<project-root>/build/shared<project-root>/build/static,它们不会互相覆盖。

如果我们运行 cmake --list-presets,我们会看到以下内容:

Available configure presets:
  "shared"
  "static"

如果我们接着运行 cmake --preset sharedcmake --preset static,我们会看到以下文件夹结构:

.
└── build
    ├── shared
    └── static

关于前述代码的完整示例,请参见书籍随附仓库中的 ch5/part-3/app/CMakePresets.json

CMake 预设覆盖

CMake 预设的一个非常方便的特性是它们与 CMake 命令行参数很好地组合。假设在前面的示例中,我们想使用 shared CMake 预设,但更愿意使用与 Ninja 多配置不同的生成器。为此,我们只需在命令行中传递一个不同的生成器,CMake 会覆盖 CMake 预设中的值:

cmake --preset shared -G Xcode

上面的代码会使用预设中的所有值,除了生成器,在此案例中,它会优先选择 Xcode。我们也可以覆盖多个值,因此一个可能更好的选择可能是以下内容:

cmake --preset shared -G Xcode -B build/xcode-shared

新的 xcode-shared 构建文件夹在将来如果我们决定恢复使用 Ninja 多配置生成器时,就不会与已定义的 build/staticbuild/shared 文件夹发生冲突(如果使用构建预设,需要显式地传递此文件夹,下一节会介绍这一点,所以从长远来看,将 Xcode 作为配置预设是个不错的选择)。还值得简要提到,Xcode 也是一个多配置生成器,因此在配置时不需要为我们指定构建类型。

拥有快速尝试不同选项的灵活性是非常棒的,但我们又回到了在终端中输入长命令的老问题。幸运的是,CMake 预设中有一个特别有用的功能,可以帮助我们,那就是 CMake 用户预设。

CMakeUserPresets.json 文件(与共享的 CMakePresets.json 文件相对)。CMakePresets.json 会隐式包含在 CMakeUserPresets.json 中,因此可以从那里继承现有的预设,就像我们之前所做的那样。

要添加一个自定义预设,使用我们选择的生成器(在此案例中为 Xcode),我们只需将以下内容添加到 CMakeUserPresets.json 中:

{
  "version": 8,
  "configurePresets": [
    {
      "name": "xcode-static",
      "inherits": "static",
      "generator": "Xcode"
    }
  ]
}

然后我们可以运行 cmake --preset xcode-static 来配置我们的项目,使用 CMake,并且由于我们在 base 预设中指定了 binaryDir,我们的构建文件会自动创建在 build/xcode-static 中。

需要注意的是,虽然 CMakePresets.json 旨在供多个开发者共享并提交到源代码管理中,但 CMakeUserPresets.json 并不是。它纯粹用于本地开发,应当添加到你的 .gitignore 文件或等效文件中,以避免将其从你的机器中上传(在 Minimal CMake 仓库中,CMakeUserPresets.json 已经被添加到 .gitignore 文件中)。

CMake 预设中另一个有用的功能是 condition 字段。它用于决定一个预设是否应该启用。在前面的例子中,我们指定了 Xcode,该生成器仅在 macOS 上有效,因此我们可以更新我们的预设,包含以下几行:

...
"generator": "Xcode",
"condition": {
  "type": "equals",
  "lhs": "${hostSystemName}",
  "rhs": "Darwin"
}

Darwin 是 CMake 用来识别 macOS 的方式。有关 CMake 如何确定其运行的操作系统的更多信息,请参见 cmake.org/cmake/help/latest/variable/CMAKE_HOST_SYSTEM_NAME.html

上述代码确保当我们运行 cmake --list-presets 时,在 macOS 以外的平台上不会看到 xcode-static。如果我们尝试运行 cmake --preset xcode-static,我们会得到以下错误信息:

CMake Error: Could not use disabled preset "xcode-static"

condition 检查在常规的 CMakePresets.json 文件中最为有用,以确保开发人员在运行 cmake --list-presets 时,根据所使用的平台不会看到不必要的选项。

示例已包含在本书的仓库中。可以通过导航到 ch5/part3/app 并查看 CMakeUserPresets.json.example 来找到它。要尝试该预设,只需将文件重命名,去掉 .example 后缀即可。

其他类型的 CMake 预设

到目前为止,我们关于 CMake 预设所涵盖的内容主要集中在 CMake 配置(使用 configurePresets)上。配置预设通常是最常用的,我们只是触及了可用设置的表面。在我们继续之前,看看其他类型的预设是有用的。这些预设包括构建、测试、打包和工作流预设。现在,我们只介绍构建和工作流预设,但随着我们将测试和打包引入到应用程序中,我们将继续回到预设。

buildPresets 字段。它们可以通过调用 cmake --build --list-presets 来显示,并且在某些工具中也可见(例如,在 Visual Studio Code 的 CMake Tools 插件中,我们将在第十一章支持工具和下一步中介绍)。构建预设不像配置预设那样对日常开发有如此大的影响,但它们也有自己的用途。在我们简化的示例中,我们展示了 buildPresets 可能如何配置:

"buildPresets": [
    {
      "name": "shared",
      "configurePreset": "shared"
    },
    {
      "name": "static",
      "configurePreset": "static"
    }
]

构建预设之间可以共享的内容通常较少,因此我们暂时省略了一个隐藏的基础构建预设。每个构建预设必须映射到一个configurePreset;因此,我们将每个构建预设映射到一个配置预设,该配置预设对应我们应用程序的版本,使用的是静态或共享版本的生命游戏库。我们还可以添加另一个字段,称为configuration,它相当于从命令行调用 CMake 时传递的--config。这看起来像如下所示:

{
  "name": "static-debug",
  "configurePreset": "static",
  "configuration": "Debug
}

这样做的问题是,我们需要shared-debugshared-releasestatic-debugstatic-release等等。这可能是必要的,当我们开始实现如持续集成CI)构建脚本时,它也会派上用场,但现在来看可能有些过头(值得一提的是,如何避免构建预设的组合爆炸是 CMake 维护者 Kitware 正在研究的一个开放问题)。

要调用一个构建预设,我们运行cmake --build --preset <build-preset-name>(首先运行cmake --preset <configure-preset-name>),如以下示例所示:

cmake --preset shared
cmake --build --preset shared

提个小提醒,在此上下文中,也可以通过--config来指定配置,而无需在CMakePresets.json文件中包含所有配置变体,这对于本地开发非常有用。以下是一个示例:

cmake --build --preset shared --config Release

我们现在提到的最后一个有用的预设是工作流预设。工作流预设允许你将多个预设串联在一起,依次运行,允许你用一个命令潜在地配置、构建、测试和打包。配置预设必须先执行,然后可以运行后续的任何预设(目前,我们只有一个构建预设,但将来可能希望扩展这一点)。

工作流预设采取以下形式:

"workflowPresets": [
  {
    "name": "static",
    "steps": [
      {
        "type": "configure",
        "name": "static"
      },
      {
        "type": "build",
        "name": "static"
      }
    ]
  }
]

它们可以通过cmake --workflow --preset <workflow-preset-name>来调用。在我们的情况下,我们运行以下命令:

cmake --workflow --preset static

然后我们将看到以下输出:

Executing workflow step 1 of 2: configure preset "static"
...
Executing workflow step 2 of 2: build preset "static"
...

不幸的是,我们无法在--workflow命令中提供--config覆盖。这意味着需要构建预设变体来指定配置(在多配置生成器的情况下),以便工作流能够构建所有不同的配置。

最后,要显示所有预设,我们可以从命令行运行cmake --list-presets all,一次显示所有类型的预设。预设名称只需要在同一预设类型内唯一,因此我们可以为配置、构建和工作流预设使用相同的名称:

Available configure presets:
  "shared"
  "static"
Available build presets:
  "shared"
  "static"
Available workflow presets:
  "static"
  "shared"

要查看构建和工作流预设的示例,可以花点时间访问附带的Minimal CMake库中的ch5/part-4/app/CMakePresets.json

CMake 预设是保持 CMakeLists.txt 文件简洁、不含配置细节的绝佳机制。它们需要小心处理,因为随着设置的组合爆炸,预设的数量可能会呈指数增长。从最常见的预设开始是一个不错的起点;它们可以在未来扩展,以处理更复杂的配置,帮助跨团队协作和项目维护。它们与 CMake 工具也能够很好地集成。在 第十一章支持工具与后续步骤中,我们将讨论 CMake 预设如何使在 Visual Studio Code 中的构建与调试变得轻松。

在这一节中,我们了解了如何创建配置 CMake 预设以避免重复,如何将 CMake 预设与命令行重写结合使用,以及构建和工作流预设的作用。CMake 预设还能做更多事情,稍后我们将在测试和打包部分回顾它们的用法。接下来,我们将重新熟悉 CMake GUI。

回归 CMake GUI

本书中,我们几乎专注于从命令行/终端使用 CMake。这是熟悉 CMake 工作原理和理解最常用命令的最佳方式。它通常是最快完成任务的方式,我们将继续使用它,但有时候,从新视角审视项目也是值得的。

这就是 CMake GUI 的作用所在。CMake GUI 提供的功能有些有限(你不能直接从 GUI 构建项目),但获取所有相关 CMake 变量的图形化视图通常非常有帮助。

打开 CMake GUI 最可靠的方法是从项目根目录运行cmake-gui .。这样可以确保工具继承你从终端配置的相同环境变量。这在 Windows 上尤其重要,因为我们使用的是Visual Studio 命令提示符,而在 macOS 上,从Finder打开时,环境变量与从终端打开时不同。如果不这样做,CMake GUI 可能无法找到 CMake、C/C++ 编译器或我们想要使用的生成器(例如,在 Windows 上使用 Ninja)。

Windows 和 macOS 上的 CMake 安装程序会添加一个快捷方式/图标用于打开 CMake GUI,但不幸的是,通过这种方式打开并不总是能成功。如果你想在 Linux 桌面上打开 CMake GUI,你可以导航到/opt/cmake-3.28.1-linux-<arch>/bin/并双击cmake-gui,或者为 CMake GUI 添加一个桌面图标(如果你是按照第一章的方式安装 CMake,入门部分)。使用cmake-gui .命令启动 CMake GUI 是最可靠的跨平台方法,可以在正确的地方打开 CMake GUI。文档中说明了可以传递-S-B来指定源目录和构建目录;然而,根据个人经验,这在所有平台上并不总是有效。一旦项目配置完成,仅运行cmake-gui(不带.)将会打开你上次离开的地方。如果没有提供起始目录,你可以从工具内部选择源目录和构建目录,尽管这个过程可能有点繁琐。也可以结合使用 CMake 预设和 CMake GUI,直接在命令行提供预设,或者在工具内选择一个预设。

第一次打开 CMake GUI 时,你会看到类似于以下的界面:

图 5.2:CMake GUI

图 5.2:CMake GUI

顶部区域显示源目录、预设(如果选择了)和构建目录。中间区域显示配置后的所有 CMake 变量,底部区域则显示你通常在终端运行 CMake 时看到的输出。

CMake GUI 被设置为更好地与可以在某种CMakeLists.txt文件中打开的项目文件一起工作,且这些工具可以处理配置(例如微软的 Visual Studio Code 或 JetBrains 的 CLion)。ch5/part-5/app中有一个更新版的CMakePreset.json文件,展示了 Xcode、Visual Studio 和 Ninja Multi-Config 的配置。

一旦源目录和构建文件夹设置完成,点击配置将显示所有新的(或更改的)CMake 变量。这些变量会以红色显示,刚开始可能会让人感到有些困惑,但这并不是错误。再次按下配置按钮检查是否有任何 CMake 变量发生变化;所有红色高亮应随之消失:

图 5.3:初始配置后的 CMake GUI

图 5.3:初始配置后的 CMake GUI

CMake GUI 明确区分了配置生成步骤,而这两个步骤通常在命令行运行时是一起完成的。配置完成后,点击生成按钮来创建项目文件。在之前的示例中,我们使用了 Visual Studio 生成器,因此点击打开项目将会打开 Visual Studio 解决方案。此时可以直接使用 Visual Studio 构建项目。

切换 MC_GOL_SHARED 到一个未分组的部分,但将来,所有以 MC_ 开头的条目将会被分组在一起。

查看我们在 第三章 中看到的list_cmake_variables函数,使用 FetchContent 和外部依赖,虽然它是一个不错的起点,并且通常足够用。

最后一个有用的功能是添加条目按钮。点击它会提供一个表单,用于将新的 CMake 变量添加到 CMake 缓存中。这个界面比从命令行添加变量要友好一些。记得在添加新变量后重新运行配置(它会以红色出现在工具的中央部分,作为提醒)。还有一个相应的移除条目按钮,它会将 CMake 变量从缓存中移除。

了解 CMake 图形界面是很有帮助的,但在本书的剩余部分,我们将主要依赖命令行来完成工作。如果你更喜欢图形界面,也可以使用它,大部分内容在 CMake 图形界面和命令行之间是可以互换的。

小结

恭喜你完成了这一部分的内容。在这一章中,我们学习了如何使用简单的脚本消除反复输入相同 CMake 命令的单调感,以及这如何让新用户在查看你的项目时更加轻松。接着,我们探讨了如何使用 CMake 预设进一步优化项目配置,并确保保持我们的CMakeLists.txt文件的整洁。我们还了解了如何使用 CMake 预设来创建配置、构建和工作流命令。最后,我们深入了解了 CMake 图形界面,以便更好地理解它的工作原理以及我们可以用它做什么。

在下一章中,我们将切换主题,回到我们的生命游戏项目。我们将放弃控制台应用程序,转向一个真正的跨平台窗口体验。为此,我们将学习如何向项目中添加更大的依赖,并理解安装一个库的真正含义。

第六章:安装依赖项和ExternalProject_Add

在本章中,我们将深入探讨FetchContent来下载其他库,它们最终还是会进入相同的构建文件夹。安装稍微不同。安装时,我们会在构建时将库与应用程序完全分离。然后,我们会采取第二步,将其安装到一个应用程序能够找到的位置。安装可能听起来神秘,但本质上只是将一组文件从一个位置复制到另一个位置(尽管需要遵循既定的约定)。

一旦我们熟悉了手动构建和安装库,我们将探讨如何利用ExternalProject_Add显著减少安装时所需的手动步骤。这将使我们能够更清洁地将外部库与我们不断发展的应用程序集成。幸运的是,学习的新命令不多,当你完成了一次过程后,它可以轻松地转移到其他项目中。

在本章中,我们将涵盖以下主要主题:

  • 什么是安装

  • 安装一个库

  • 使用已安装的库

  • 使用ExternalProject_Add简化安装

  • 使用ExternalProject_Add处理多个库

技术要求

为了跟随本书内容,请确保你已满足第一章《入门》一节中列出的要求。这些要求包括以下内容:

  • 一台运行最新操作 系统OS)的 Windows、Mac 或 Linux 机器

  • 一个可用的 C/C++编译器(如果你没有,建议使用系统默认的编译器)

本章中的代码示例可以通过以下链接找到:github.com/PacktPublishing/Minimal-CMake

什么是安装?

安装,本质上就是将文件从一个地方复制到另一个地方。一旦这些文件被复制到特定位置,应用程序(或其他库)在构建时就可以在那里查找它们。

安装在实践中有几个优点。第一个优点是,你可以构建一次库,只将必要的文件安装到已知位置,然后让多个应用程序使用它。这可以节省大量时间和资源,避免不必要地重复构建相同的代码。另一个优点是,只有所需的文件才会被复制到安装位置。当我们正常构建时,构建文件夹会充满许多中间文件,这些文件应用程序可能不需要(取决于我们的库)。而当我们安装时,我们只会指定必要的文件(通常是构建后的库文件,如.lib/.a.dll/.dylib/.so、头文件和 CMake 配置文件)。我们还可以通过只安装我们希望公开的头文件,并以比内部构建结构更简单的布局,来更精确地控制库的接口。

默认情况下,当我们安装一个库时,文件会被复制到预定的系统位置。在 macOS 和 Linux 上,这通常是/usr/local/lib(用于库文件)、/usr/local/include(用于头文件)、/usr/local/bin(用于可执行文件)和/usr/local/share(用于任何类型的文档或许可证文件)。在 Windows 上,这通常是C:/Program Files (x86)/<library-name>,库名称下会有libincludesharebin子文件夹。当我们进入安装库的阶段时,我们将更详细地回顾文件夹结构,并查看哪些文件被包含。

要找到已安装的库,CMake 需要知道在哪里查找。将库安装到前面提到的默认位置之一的好处是,CMake 已经知道在哪里搜索,因此在配置依赖该库的项目时,我们不需要提供其他信息。此方法的一个缺点是它会改变我们运行的全局主机环境,这可能并非始终是你想要的。这涉及到缺乏隔离性,我们将在稍后的安装库部分中展示如何解决这个问题。另一个需要注意的问题是,安装到系统位置通常需要提升的权限,而库的构建者可能没有这些权限。例如,在 Windows 上将库安装到C:\Program Files\需要管理员权限。

接下来,我们将查看下载和安装库所需的步骤。

安装库

在使用已安装的库之前,我们先使用 CMake 安装一个库。我们将选择一个库,用于我们生命游戏应用程序,继续改进其功能;我们将使用的库叫做Simple Directmedia LayerSDL)。SDL 是一个跨平台的窗口库,支持输入、图形、音频等多种功能。SDL 2 是最新的稳定版本,尽管在撰写本文时,SDL 3 已提供预发布版本供试用。SDL 2 以 zlib 许可证发布,允许在任何类型的软件中自由使用。要了解更多关于 SDL 的信息,请访问www.libsdl.org/

SDL 是一个开源项目,方便地托管在 GitHub 上;可以通过访问github.com/libsdl-org/SDL来访问。从 SDL 的 GitHub 主页开始,通过点击ch6/part-1/third-party来复制.git URL(作为提醒,Minimal CMake的配套示例可以通过访问github.com/PacktPublishing/Minimal-CMake找到)。

运行以下命令将仓库克隆到third-party/sdl(如果你更愿意的话,可以使用clone.sh/.bat脚本,还有一些其他便捷脚本包含了配置和构建库所需的命令):

git clone https://github.com/libsdl-org/SDL.git sdl

我们创建了新的third-party文件夹,作为applib的同级目录,用来存放外部依赖项。这是为了将代码在当前章节中逻辑上分组,但在实际项目中,如果更方便的话,它可以被移动到顶层文件夹。为了避免 SDL 仓库被嵌套在Minimal CMake仓库中产生问题,third-party文件夹的.gitignore文件中已加入了sdl。我们本可以使用 Git 子模块并运行git submodule initgit submodule update,但这里的目的是展示手动安装库的每一步。如果在自己的项目中简化配置,您可以自由使用 Git 子模块,但在使用之前,请务必阅读后面的章节,使用 ExternalProject_Add 简化安装,以查看 CMake 提供的另一种替代方案。

克隆完 SDL 仓库后,在构建之前,我们需要确保使用正确的 SDL 版本。SDL 的默认分支(main)现在是 SDL 3,但由于此版本仍处于预发布阶段且在积极开发中,我们将使用 SDL 2 进行我们的生命游戏项目。在写这篇文章时,最新版本是2.30.2

将目录切换到sdl文件夹并检查最新的稳定版本(如果使用了clone.sh/.bat脚本,您已经在正确的分支上):

cd sdl
git checkout release-2.30.2

要找到最新的版本,您可以从 SDL GitHub 仓库中点击release-2.XX.X行(可以使用release-2.30.2,示例就是基于这个版本进行测试的)。选择正确的版本后,回到third-party目录(cd ..)。

仅克隆我们需要的内容

为了避免克隆整个仓库并执行额外的检查特定标签的步骤,可以改用git clone https://github.com/libsdl-org/SDL.git --branch release-2.30.2 --depth 1 sdl命令。这样只会克隆我们需要的分支并执行浅克隆,省略除最新 Git 提交以外的所有内容。这样可以将仓库大小从大约 187MB 减少到 91MB,节省了大约 50%。clone.sh/bat脚本使用这种方法,可以替代前面的手动步骤。

为了将构建文件夹放在 SDL 源代码树之外,让我们从third-party文件夹运行 CMake,并将源代码和构建目录的位置传递给 CMake(分别为sdlbuild-sdl):

cmake -S sdl -B build-sdl -G "Ninja Multi-Config"

执行此命令将为 SDL 配置并生成构建文件,就像我们在前几章的示例中所做的那样。CMake 将输出大量来自 SDL 的诊断信息,显示它为哪个架构构建,能够找到哪些编译器功能以及可以访问哪些标准库函数。这些信息对于了解 SDL 将使用哪些功能以及在出现问题时帮助诊断非常有用。

运行命令后,您应该会在输出的末尾看到以下内容:

...
-- Configuring done (20.7s)
-- Generating done (0.1s)
-- Build files have been written to: path/to/minimal-cmake/ch6/part-1/third-party/build-sdl

在执行构建之前,我们漏掉了一个重要的参数。当我们最初配置时,讨论了 CMake 如果没有指定覆盖位置,默认会安装到一个位置。这有时是你想要的,但一个很大的缺点是这样做会导致项目的构建不再是自包含的。你在项目的外部进行写操作,可能会对系统上的其他应用程序造成无意的更改。

解决此问题的一种方法是为项目选择某种容器化或虚拟化方案(例如,为Minimal CMake创建一个虚拟机,所有必需的依赖项可以安装在默认系统位置)。这样可以保持隔离,但需要更多的时间和精力来设置。幸运的是,还有一种替代方案。

配置 SDL 时,我们可以传递另一个命令行参数,称为CMAKE_INSTALL_PREFIX

cmake -S sdl -B build-sdl -G "Ninja Multi-Config" install in the same directory we’re running CMake from (this will have install appear alongside sdl and build-sdl):

└── 第三方

├── build-sdl

├── install

└── sdl


			The main advantage of this approach is that we keep everything self-contained within our project. Nothing we do within the confines of *Minimal CMake* will affect the system overall in any way. For example, we won’t inadvertently install a library that overrides a system version that is already on our machine. One downside is that we may lose out a little on the ability to reuse this library when building other applications, but by installing SDL in this way, we’ve divided it from our main application, and we will not need to rebuild it if we decide to destroy and recreate our application’s build folder. It is a separate entity, distinct from *Minimal CMake*, and this is one of the advantages of installing SDL 2, instead of including it in our build as we did with other libraries and `FetchContent` in earlier chapters.
			One other thing to note is we created a generic folder called `install`, not a folder called `install-sdl`. This is because it’s fine and often preferred to install multiple libraries in the same location. This makes depending on more than one library a lot simpler when telling CMake where to find the libraries. To learn more about `CMAKE_INSTALL_PREFIX`, see [`cmake.org/cmake/help/latest/variable/CMAKE_INSTALL_PREFIX.html`](https://cmake.org/cmake/help/latest/variable/CMAKE_INSTALL_PREFIX.html).
			Ensure that you run the preceding CMake configure command including `-DCMAKE_INSTALL_PREFIX=install` (it will be a lot quicker the second time). It’s also worthwhile checking that the CMake `CMAKE_INSTALL_PREFIX` cache variable is set to the value you expect by using the CMake GUI, or opening `build-sdl/CMakeCache.txt` in a text editor and searching for `CMAKE_INSTALL_PREFIX`.
			We are now able to build SDL 2 and install it into our `install` folder. There are two ways to perform this. The first is to provide the install target to CMake when building to have it build and then immediately install the library:

cmake --build build-sdl --target install 在构建命令之后,我们表示我们想要构建安装目标,该目标依赖于库的构建。因此,库必须首先构建,然后才能安装。其依赖图如下:

install -- depends --> SDL2
        记住,由于我们使用的是多配置生成器,默认情况下,这将构建并安装`Debug`配置。要构建并安装库的`Release`版本,我们需要显式指定`Release`配置:
cmake --build build-sdl --target install --config Release
        也可以使用单独的 CMake `install`命令安装库:
cmake --install build-sdl
        要使此命令正常工作,首先需要构建库,因为在仅配置后运行该命令会生成以下错误:
CMake Error at build-sdl/cmake_install.cmake:50 (file):
  file INSTALL cannot find
  "/path/to/minimal-cmake/ch6/part-1/third-party/build-sdl/Release/libSDL2-2.0.0.dylib":
  No such file or directory.
        注意,默认情况下,`--install`命令会查找`Release`配置,而不是`Debug`配置。为了让安装命令按预期执行,首先构建库的`Release`版本,然后运行安装命令:
cmake --build build-sdl --config Release
cmake --install build-sdl
        如果你构建了`Debug`或`RelWithDebInfo`版本的库,也可以将`--config`传递给`--install`命令来安装这些版本:
cmake --install build-sdl Release version of the library to get the best possible performance. It usually isn’t necessary to install the Debug version unless you need to debug a difficult-to-diagnose issue with how your application is interacting with the library. On Windows, link errors can occur due to conflicting symbols caused by a mismatch between runtime libraries used by the Debug and Release version of a library and application. A simple fix is to ensure that both the library and application are built with the same configuration.
			The CMake `--install` command also provides a `--prefix` option to set or override the install directory. This can be useful if you forgot to provide `CMAKE_INSTALL_PREFIX` or want to install the library to a different location without reconfiguring:

cmake --install build-sdl CMAKE_DEBUG_POSTFIX 变量。通常将调试版本和发布版本的库安装到同一文件夹中会很方便(就像我们在安装目录中所做的那样)。如果我们先构建了库的调试版本并安装它,然后构建了发布版本并安装它,调试库文件将被覆盖。为了避免这种情况,CMake 可以将后缀添加到库的调试版本(通常约定使用小写字母 d)。这意味着在安装文件夹中,我们会看到如下内容(在 Windows 上,.dll 文件会在 bin 文件夹中,其他所有文件都会在 lib 文件夹中):

# macOS
libSDL2-2.0.dylib, libSDL2-2.0d.dylib, libSDL2.a, libSDL2d.a
# Windows
SDL2.dll, SDL2d.dll, SDL2.lib, SDL2d.lib
# Linux
libSDL2-2.0.so, libSDL2-2.0Debug or Release version depending on the configuration we’re building.
			To summarize, to efficiently clone, build, and install SDL 2, run the following commands from the `ch6/part-1/third-party` folder:

git clone https://github.com/libsdl-org/SDL.git --branch release-2.30.2 --single-branch sdl --depth 1

cmake -S sdl -B build-sdl -G "Ninja Multi-Config" -DCMAKE_INSTALL_PREFIX=install

cmake --build build-sdl --config Release

cmake --install build-sdl --config Release


			Several helper scripts have been added to `ch6/part-1/third-party` to automate this process. You can run `everything.sh`/`bat` to perform the preceding steps (`Debug` configs are also built and installed).
			With SDL `2` installed to a known folder, we can now review our `CMakeLists.txt` file in `ch6/part-1/app` to see the changes needed to use the new dependency. We’ll walk through what these changes are in the next section.
			Using an installed library
			With our library installed, what remains is to integrate it into our existing application so we can start using the functionality provided by SDL. Let’s begin by looking at the changes made to our `CMakeLists.txt` file for our application.
			We won’t share the entire file here as much of it is the same as before, but we’ll call out the significant changes as we go. To see a complete example, review `ch6/part-1/app/CMakeLists.txt` from the book’s accompanying repository.
			The first, and perhaps most important addition, is as follows:

find_package(SDL2 CONFIG REQUIRED)


			The `find_package` command is an incredibly useful tool to bring external dependencies into our project. In the example shown above, the first argument is the name of the dependency to find (in our case, this is `SDL2`). The name of the dependency is defined in one of two ways, and that is linked to the second parameter we’ve specified, `CONFIG`.
			CMake search modes
			The `find_package` command can run in one of two search modes: **Config** mode or **Module** mode. We’ll look at each in turn, starting with Config mode, which we’ll be using most often.
			Config mode
			It’s easiest to think of Config mode as the native way for CMake to search for packages. Config mode is the mode to use when the dependency has itself been built and installed using CMake. As part of the install process, a file with the `<package-name>-config.cmake` or `<PackageName>Config.cmake` name will have been created by CMake, and this is what CMake will search for. This file includes all the relevant information needed to use the library (the location of built artifacts, include paths, etc.).
			If you want to have a look at the file generated for SDL 2, it can be found by going to `ch6/part-1/third-party/install/lib/cmake/SDL2/SDL2Config.cmake` after building and installing SDL 2\. Don’t worry too much about the contents of the file just yet; SDL 2 is quite a complex dependency, so there’s a lot going on. All that’s important for us is understanding that this file exists and why it’s needed. When we install our own library, we’ll walk through things in more detail.
			Module mode
			The second search mode `find_package` can run in is called Module mode. Instead of searching for a config file, CMake looks for a file called `Find<PackageName>.cmake`. CMake will search for these files in several default locations (listed in `CMAKE_MODULE_PATH`). It’s also possible to add more locations to `CMAKE_MODULE_PATH` if our `Find<PackageName>.cmake` file is found somewhere else.
			`Find<PackageName>.cmake` files are hand-crafted files and something not usually generated by CMake. If we are in an ecosystem that uses libraries built by CMake, and we are building libraries or executables using CMake, we largely don’t need to think about Module mode.
			The one big advantage to Module mode, however, is being able to integrate libraries that are not built using CMake with our project. The `Find<PackageName>.cmake` file is a bridge between CMake and other build systems. Writing a find module file is usually easier than porting a dependency to CMake (especially if it’s a dependency you have little control over), but for what we’ll be doing, we can mostly avoid them. We’ll show a simplified example of such a script in *Chapter 7*, *Adding Install Support for Your Libraries*, but to make them fully portable requires a lot of effort. Using CMake to generate config files for us tends to be a lot simpler and eliminates the need for us to maintain a `CMakeLists.txt` file and `Find<PackageName>.cmake` at the same time, removing the risk of these two files getting out of sync.
			Returning to find_package
			If we briefly return to the `find_package` command we added to our `CMakeLists.txt` file, we can cover the remaining arguments:

find_package(SDL2 CONFIG,我们在这里告诉 find_package 命令只查找配置文件,如果找不到依赖项,它不会回退到模块模式。这有助于确保我们能找到我们所需的确切依赖项。第三个参数 REQUIRED 告诉 CMake 如果找不到 SDL2,应该停止处理 CMakeLists.txt 文件。这主要有助于确保我们得到更清晰的错误信息,避免在无效状态下继续配置。

        要开始使用 `SDL2`,我们唯一需要做的改动是将其添加到 `target_link_libraries` 命令中。现在的命令看起来像这样:
target_link_libraries(
  ${PROJECT_NAME} PRIVATE
  timer_lib mc-gol SDL2::). This is a find_package command does all this for us behind the scenes; we just need to remember to link against SDL2::SDL2, not SDL2 (we also need SDL2::SDL2main as we’re creating an executable, and not only linking SDL2 to another library). This convention is useful to be able to see which libraries are external (imported), or not, in the target_link_libraries command.
			There’s a final change we need to make to ensure things work correctly on Windows. As `SDL2`, by default, is built as a shared library, we need to copy the `SDL2` DLL file (`SDL2.dll` or `SDL2d.dll`) to the same directory as our application. We can do this in the exact same way as we did with `mc_gol.dll` by using the function that follows:

将 SDL2.dll 复制到与可执行文件相同的文件夹中

add_custom_command(

TARGET ${PROJECT_NAME}

POST_BUILD

COMMAND

${CMAKE_COMMAND} -E copy_if_different

$<TARGET_FILE:SDL2::SDL2>

\(<TARGET_FILE_DIR:\){PROJECT_NAME}>

VERBATIM)


			That covers all changes our `CMakeLists.txt` file needs to start using SDL `2`. For a complete example, see `ch6/part-1/app/CMakeLists.txt` in the accompanying repository.
			Informing CMake where to find our library
			We now have everything we need to use SDL `2` from our application, but things won't work if we run our familiar CMake command as follows:

cmake -B build


			When we do, we’ll see the following error printed:

CMake 错误位于 CMakeLists.txt 文件的第 4 行(find_package):

无法找到由 "SDL2" 提供的任何以下名称的包配置文件:

SDL2Config.cmake

sdl2-config.cmake

将 "SDL2" 的安装前缀添加到 CMAKE_PREFIX_PATH,或者将 "SDL2_DIR" 设置为包含上述文件之一的目录。如果 "SDL2" 提供了一个单独的开发包或 SDK,请确保已安装。


			This is a good thing because we know that CMake can’t find the library we installed at the start of the chapter. If running `cmake -B build` succeeds, then it means CMake has found SDL 2 from another location that we may not be aware of. One example of this happening on macOS was CMake finding SDL 2 in `/opt/homebrew/lib/cmake/SDL2`, which had been installed as a dependency of `ffmpeg` (a cross-platform tool to record, convert, and stream both audio and video).
			A sound piece of advice from the software testing community is to *see it fail*, as we then know precisely whether our next change was the thing to fix the problem or not. Otherwise, there’s no guarantee that it wasn’t something else. One way to verify that SDL 2 is found from the location we installed it in is to pass several additional arguments to `find_package`:

find_package(

SDL2 CONFIG REQUIRED

NO_CMAKE_ENVIRONMENT_PATH

NO_CMAKE_SYSTEM_PACKAGE_REGISTRY

NO_SYSTEM_ENVIRONMENT_PATH

NO_CMAKE_PACKAGE_REGISTRY

SDL2_DIR),它会显示依赖项所在的文件夹(对于我们来说,应该是 /path/to/minimal-cmake/ch6/part-1/third-party/install/lib/cmake/SDL2)。可以通过打开 ch6/part-1/app/build/CMakeCache.txt 文件并搜索 SDL2_DIR(或更一般地,<LIBRARY_NAME>_DIR),在 CMake GUI 中检查,或运行 cmake -L 快速列出所有 CMake 缓存变量(也可以使用 ccmake 从终端查看和编辑缓存变量,尽管如 第三章 所述,使用 FetchContent 管理外部依赖,这只适用于 macOS 和 Linux)。

        提供库的位置

        当我们配置应用程序时,需要告诉 CMake 在哪里找到我们安装的库。我们可以通过在配置步骤中使用命令行设置 `CMAKE_PREFIX_PATH` 来实现:
cmake -B build -DCMAKE_PREFIX_PATH=../third-party/install
        CMake 现在能够找到我们在之前步骤中安装的库。

        在早期版本的 CMake 中,需要提供 `CMAKE_PREFIX_PATH` 的绝对路径。可以通过在 macOS 和 Linux 上使用 `$(pwd)`,或在 Windows 上使用 `%cd%` 来解决这个问题:
# macOS/Linux
-DCMAKE_PREFIX_PATH=$(pwd)/../third-party/install
# Windows
-DCMAKE_PREFIX_PATH=3.28 and above (just keep this in mind if you encounter any issues finding libraries with CMAKE_PREFIX_PATH using earlier versions of CMake).
			With that, we just need to build (`cmake --build build`), and we can now launch our latest incarnation of *Game of Life*. The application has been renamed to `minimal-cmake_game-of-life_window`, as we’ve now moved away from displaying our *Game of Life* simulation in the console/terminal to a full windowed application with the help of SDL. The full list of commands to see things running for yourself are as follows:

cd ch6/part-1/third-party

./everything.sh # (在 Windows 上是 everything.bat)

cd ../app

cmake -B build -DCMAKE_PREFIX_PATH=../third-party/install

cmake --build build


			`everything.sh/bat` more or less unwraps to the following:

git clone https://github.com/libsdl-org/SDL.git --branch release-2.30.2 --depth 1 sdl

cmake -S sdl -B build-sdl -G "Ninja Multi-Config" -DCMAKE_INSTALL_PREFIX=install

cmake --build build-sdl --config Release

cmake --install build-sdl --config Release


			After running `minimal-cmake_game-of-life_window`, you should be rewarded with something resembling the following:
			![Figure 6.1: The windowed output of Game of Life with SDL 2](https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/min-cmk/img/B21152_06_1.jpg)

			Figure 6.1: The windowed output of Game of Life with SDL 2
			If any problems are encountered when running the preceding steps, the first thing to check is the internet connection. As part of the configure step, CMake needs to download SDL 2, and if there’s no internet, this step will fail. This might not be immediately obvious but do keep it in mind when running these examples (if you are working on Linux, ensure you also have the dependency `libgles2-mesa-dev` installed on your system. This is mentioned in *Chapter 1**, Getting Started*, but is easy to miss. To install it, run `sudo apt-get install` `libgles2-mesa-dev` from your terminal).
			The changes needed to display our *Game of Life* implementation are confined to `main.c`. They aren’t terribly important in the context of CMake, but for those who are interested, we first do some initial setup (`SDL_CreateWindow` and `SDL_CreateRenderer`). We then initialize our board as before and start our main loop, where we poll the SDL event loop, then perform our update logic. We’re using `SDL_RenderClear` to clear the display, `SDL_RenderFillRect` to draw the cells on our board, and `SDL_RenderPresent` to update the display. When a quit event is intercepted (the window corner cross is pressed or *Ctrl* + *C* is entered from the terminal), the application quits and cleans up the resources it created at the start.
			CMakePreset improvements
			We would be remiss not to mention how we can simplify the setup by utilizing CMake presets in our `app` folder. In our `CMakePresets.json` file, we can add `CMAKE_PREFIX_PATH` to our `cacheVariables` entry like so:

...

"cacheVariables": {

"CMAKE_PREFIX_PATH": "${sourceDir}/../third-party/install"

}


			This removes the need for us to pass `-DCMAKE_PREFIX_PATH` at the command line. We can now, just as before, run a command such as `cmake --preset shared-ninja` to generate a Ninja Multi-Config project using the shared version of our *Game of* *Life* implementation.
			Using ExternalProject_Add to streamline installation
			So far, we’ve used several of CMake’s lower-level commands to download, build, and install our new SDL 2 dependency. It’s important to understand this manual process to gain a deep appreciation of one of CMake’s most useful features, `ExternalProject_Add`.
			To the uninitiated, using `ExternalProject_Add` can be quite confusing. One of the fundamental things to understand is that `ExternalProject_Add` must run as a separate step before you try to build a project that uses the dependencies it makes available. With everything we’ve just discussed when it comes to installing manually, this should now make more sense. `ExternalProject_Add` is essentially syntactic sugar to streamline the process we outlined. There are some clever ways to more tightly integrate it into our main build (see the discussion of super builds in *Chapter 8*, *Using Super Builds to Simplify Onboarding*), but for now, we’ll continue to keep it separate.
			The `ch6/part-2/third-party` folder shows an initial transition from using our configure, build, and install shell scripts, to relying entirely on CMake and `ExternalProject_Add`. The `third-party` folder has a new `CMakeLists.txt` file that looks like the following:

cmake_minimum_required(VERSION 3.28)

project(third-party)

include(ExternalProject)

ExternalProject_Add(

SDL2

GIT_REPOSITORY https://github.com/libsdl-org/SDL.git

GIT_TAG release-2.30.2

GIT_SHALLOW TRUE

CMAKE_ARGS -DCMAKE_INSTALL_PREFIX=<INSTALL_DIR>)


			To start, we have the obligatory `cmake_minimum_required` and `project` commands, followed by an `include` command to bring in the `ExternalProject` module (this makes `ExternalProject_Add` and related functions available to us).
			Next comes the `ExternalProject_Add` command itself. The first argument is the name of the project; in our case, this is `SDL2`. We then specify the location to find the source code using `GIT_REPOSITORY` (just as we did earlier with `FetchContent`), and a specific `GIT_TAG`, which maps to the version of the code we’d like to build (this is the same value we passed to the `--branch` argument in our `git clone` command earlier). We also specify `GIT_SHALLOW TRUE` to limit the number of files that need to be downloaded.
			The last argument, `CMAKE_ARGS`, allows us to set the values to pass to CMake as if we were running it from the command line. In this case, we pass the same argument as we did when running `cmake -B build` ourselves: `-DCMAKE_INSTALL_PREFIX`. For now, instead of using the `install` folder we used earlier, we set the install folder to `<INSTALL_DIR>`. This is a default value provided by CMake in the context of `ExternalProject_Add`. There are several such values provided, and it’s useful to know what their default values map to:

TMP_DIR      = /tmp

STAMP_DIR    = /src/-stamp

DOWNLOAD_DIR = /src

SOURCE_DIR   = /src/

BINARY_DIR   = /src/-build

INSTALL_DIR  =

LOG_DIR      = <STAMP_DIR>


			All paths listed are relative to the build folder. Here, `<prefix>` becomes `<Project>-prefix`, which is `SDL-prefix` in our case, so we see the following:

.

└── build

└── SDL2-prefix

├── bin

├── include

│    └── SDL2

├── lib

│    ├── cmake

│    └── pkgconfig

├── share

│    ├── aclocal

│    └── licenses

├── src

│    ├── SDL2

│    ├── SDL2-build

│    └── SDL2-stamp

└── tmp


			For more information about the `ExternalProject_Add` folder structure, see [`cmake.org/cmake/help/latest/module/ExternalProject.html#directory-options`](https://cmake.org/cmake/help/latest/module/ExternalProject.html#directory-options).
			All that’s needed to download, build, and install the dependency is to run `cmake -B build` and `cmake --build build`. To have our application find the installed library, we just need to update our `CMakePresets.json` file in `ch6/part-2/app` to have it point to the new location using `CMAKE_PREFIX_PATH`:

"cacheVariables": {

"CMAKE_PREFIX_PATH": "${sourceDir}/../third-party/build/SDL2-prefix"

}


			By leveraging `ExternalProject_Add`, we can reduce the maintenance overhead of our earlier approach and more tightly integrate our overall build with CMake. It’s worth reiterating, however, that we’re doing the exact same thing we were before with the manual install commands, just slightly more concisely.
			Improving our use of ExternalProject_Add
			Using `ExternalProject_Add` is a huge improvement over the more manual process we outlined at the start of the chapter, but there are a few further improvements we can still make.
			The first thing we’re lacking is a way to set the build type of the `ExternalProject_Add` project. Unfortunately, things behave slightly differently when using single or multi-config generators. In the case of single-config generators, the build type (set with `-DCMAKE_BUILD_TYPE=<Config>` at configure time) is not passed through to the `ExternalProject_Add` command. When installing the library, we see the following displayed in the CMake output:

-- 安装配置:""


			This shows that the build config has not been explicitly set, so there’s a little more work we need to do to properly support this.
			In the case of multi-config generators, as the build type is provided at build time (with `cmake --build build --config <Config>`), we can build and install the configuration of our choosing (we also get a build folder per config too, which is another reason to prefer multi-config generators where possible).
			In `ch6/part-3/third-party/CMakeLists.txt`, we’ve added the following block:

get_property(isMultiConfig GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)

if(NOT isMultiConfig)

if(NOT CMAKE_BUILD_TYPE)

如果没有提供构建类型,则默认为 Debug(匹配 CMake 默认行为)

set(CMAKE_BUILD_TYPE

Debug

CACHE STRING "" FORCE)

endif()

为不同的构建类型分配自己的文件夹,适用于单配置生成器

set(build_type_dir ${CMAKE_BUILD_TYPE})

将构建类型参数传递给 ExternalProject_Add 命令

set(build_type_arg -DCMAKE_BUILD_TYPE=$)

endif()


			We first check whether we’re using a multi-config generator; this is performed by querying the `GENERATOR_IS_MULTI_CONFIG` property. If we are, there’s nothing more to do, but if not, we first set `CMAKE_BUILD_TYPE` to `Debug` if it hasn’t already been provided, and we create two new CMake variables, `build_type_dir` and `build_type_arg` (we introduce these two new variables so they evaluate to nothing when using a multi-config generator). These map to the build type directory and the build type argument that are passed to CMake. We’re effectively reimplementing a version of multi-config generators, so if you can use a multi-config generator and avoid this, it’s likely easier, but as we want this to be used by as wide an audience as possible, accommodating single-config generators gracefully is a friendly thing to do.
			With this defined, we must make some minor adjustments to our `ExternalProject_Add` command to take advantage of these new variables:

ExternalProject_Add(

...

BINARY_DIR \({CMAKE_CURRENT_BINARY_DIR}/SDL2-prefix/src/SDL2-build/\){build_type_dir}

INSTALL_DIR ${CMAKE_CURRENT_SOURCE_DIR}/install

CMAKE_ARGSBINARY_DIR 指向与当前配置相对应的文件夹(我们会得到与多配置生成器相同的布局),并在 CMAKE_ARGS 中传递构建类型(例如,CMAKE_BUILD_TYPE=Debug)以供配置时使用。

        如果我们通过运行以下命令使用单配置生成器进行测试:
cmake -B build -G Ninja
cmake --build build
        我们将在安装输出中看到这一点,而不是像之前那样的空字符串:
-- Install configuration: "Debug"
        我们可以显式地指定不同的配置,例如以下内容:
cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
        然后我们将在安装输出中看到以下内容:
-- Install configuration: part-1) to have installed files go to a shared install folder (see ch6/part-3/third-party/CMakeLists.txt for a full example).
			Handling multiple libraries with ExternalProject_Add
			For the final example in this chapter, we’re going to look at bringing in another larger dependency. The dependency in question is a graphics library called `bgfx` ([`github.com/bkaradzic/bgfx`](https://github.com/bkaradzic/bgfx)). `bgfx` is a cross-platform rendering library that works across Windows, macOS, and Linux, as well as many other platforms including iOS, Android, and consoles. It’s a large project that can take some time to build, which makes it a perfect candidate for installing and using with `ExternalProject_Add`.
			As we’re still writing our application in C, we need to use the C interface provided by `bgfx`, and to do so, we must use it as a shared library. One issue with this is that we need to build the shaders we’re going to load using a tool built as part of `bgfx` called `shaderc`, and this needs `bgfx` to be compiled statically (at least on Windows). To account for this, we have two references to `bgfx`: `bgfx` and `bgfxt` (we’ve added the `t` postfix for tools). We compile one version of the library statically and ensure that the tools get built and installed to our `install/bin` directory. Then we build a shared version of the library for use by our application. The setup in `ch6/part-4/third-party/CMakeLists.txt` isn’t perfect – we wind up having to clone the library twice – but it’s a reasonably simple solution, and we only need to do it once and can then forget about it. Building `bgfx` might take a little while to complete, as it’s by far the largest dependency we’ve used so far. Using Ninja Multi-Config as the generator should help, and if you are using a different generator, it’s possible to pass `--parallel <jobs>` after the `cmake --build build` command to ensure that you’re making the most of your available cores.
			Shaders
			Shaders are special programs that run on a device’s **Graphics Processing Unit** (**GPU**). They perform a wide variety of operations and can become exceedingly complex, achieving all manner of interesting graphical effects. Our application provides basic shader implementations. The first is a vertex shader, responsible for moving/positioning our geometry (often called transforming). The second is a pixel (or fragment) shader, which decides what color the pixels on our screen should be. To learn more about shaders and graphics programming in general, visit [`learnopengl.com/`](https://learnopengl.com/) for a great introduction. It’s OpenGL-specific, but many of the concepts can be applied to any graphics API.
			The updated `CMakeLists.txt` file now has two additional `ExternalProject_Add` calls:

ExternalProject_Add(

bgfxt

GIT_REPOSITORY https://github.com/bkaradzic/bgfx.cmake.git

GIT_TAG v1.127.8710-464

GIT_SHALLOW TRUE

BINARY_DIR \({CMAKE_CURRENT_BINARY_DIR}/bgfxt-build/\){build_type_dir}

INSTALL_DIR ${CMAKE_CURRENT_SOURCE_DIR}/install

CMAKE_ARGS ${build_type_arg} -DCMAKE_INSTALL_PREFIX=<INSTALL_DIR>

CMAKE_CACHE_ARGS -DCMAKE_DEBUG_POSTFIX:STRING=d)

ExternalProject_Add(

bgfx

GIT_REPOSITORY https://github.com/bkaradzic/bgfx.cmake.git

GIT_TAG v1.127.8710-464

GIT_SHALLOW TRUE

DEPENDS bgfxt

BINARY_DIR \({CMAKE_CURRENT_BINARY_DIR}/bgfx-build/\){build_type_dir}

INSTALL_DIR ${CMAKE_CURRENT_SOURCE_DIR}/install

CMAKE_ARGS ${build_type_arg} -DCMAKE_INSTALL_PREFIX=<INSTALL_DIR>

-DBGFX_LIBRARY_TYPE=SHARED -DBGFX_BUILD_TOOLS=OFF

-DBGFX_BUILD_EXAMPLES=OFF

CMAKE_CACHE_ARGS -DCMAKE_DEBUG_POSTFIX:STRING=d)


			Most of the arguments are the same as with the `SDL2` `ExternalProject_Add` command (`GIT_REPOSITORY`, `GIT_TAG`, `GIT_SHALLOW`, etc.), just with `bgfx`-specific values. The `BINARY_DIR` path has been condensed slightly for no other reason than to not exceed the 250-character path limit set by CMake on Windows (this isn’t usually a problem, but with this example already being somewhat nested in the file structure, it can occasionally cause problems). We install `bgfx` to the same `third-party/install` directory as `SDL2`, which means that we don’t need to append another folder path to `CMAKE_PREFIX_PATH` in our `CMakePresets.json` file inside our `part-4/app` folder.
			One extra argument we pass is `-DCMAKE_DEBUG_POSTFIX:STRING=d`, which ensures that debug versions of the library get a `d` appended to them (e.g., `bgfx` for `Release` becomes `bgfxd` in `Debug`). This is handy to allow both versions of the library to be installed to the same location without stomping on one another (SDL 2 already does this by default, but we need to pass this setting to `bgfx`). We use `CMAKE_CACHE_ARGS` for this to specify cache variables. Behind the scenes, this argument is converted to a CMake `set` call with `FORCE` provided to guarantee that no entry in the cache can override this value.
			The next call is remarkably similar to the earlier `bgfxt` command. We just pass several extra arguments when configuring the `bgfx` library to build it as a shared library (and disable building any of the examples and tools):

-DBGFX_LIBRARY_TYPE=SHARED -DBGFX_BUILD_TOOLS=OFF

-DBGFX_BUILD_EXAMPLES=OFF


			The last addition worth mentioning is our use of `DEPENDS`. This allows `ExternalProject_Add` commands to define an ordering, so we know `bgfxt` will be built before `bgfx` starts building. In our case, this guarantees that we install the shared library after the static one. This can be useful when you are building multiple libraries at once that may depend on one another. For example, let’s say that we were building `libcurl` ([`curl.se/libcurl`](https://curl.se/libcurl)), an excellent networking library, which itself depends on `OpenSSL` ([`www.openssl.org`](https://www.openssl.org)), which is used for secure network communication. We must ensure that `OpenSSL` is built first, so `libcurl` would need `DEPENDS OpenSSL` added to its `ExternalProject_Add` command.
			Running the bgfx example
			After building `bgfx` as our new external dependency, one step remains before we can run the application. `bgfx` requires us to provide shaders for our application to transform our geometry and color the pixels to display. We need to compile the shaders before running our application, and to perform this step, we need to run the `compile-shader-<os>.sh/bat` file. This internally invokes `shaderc` (one of the tools built when we compiled `bgfx` statically) and passes parameters specific to the platform (DirectX 11 on Windows, Metal on macOS, and OpenGL on Linux). Compiling the shader source files in `ch6/part-4/app/shader` will produce binary files we’ll load in the main application, located in the `shader/build` subdirectory. It’s not necessary to understand these files in detail – just think of them as resources our application needs to be aware of. Dealing with these files and how to handle loading them will be something we’ll revisit when installing, and later packaging, our application.
			Once the shaders are built, we can then build and run our application. It’s fine to compile the application before the shaders, we just need to make sure to build the shaders before attempting to run the application. One thing to note is that for now, we need to run the application from the source folder (the location of `main.c`), as our compiled binary files are loaded relative to that folder. This means ensuring that we start the application from `ch6/part-4/app`, such as in the example below:

./build/shared-ninja/Debug/minimal-cmake_game-of-life_window


			If we don’t do this (for example, by changing the directory to the `/build/shared-ninja/Debug` folder from the preceding example), then the shader files we’re loading (`fs_vertcol.bin` and `vs_vertcol.bin`) won’t be found. As mentioned earlier, we’ll cover ways to address this when we discuss installing and packaging in *Chapter 10**, Packaging the Project* *for Sharing*.
			It’s possible to set the working directory for our application when using other tools such as Visual Studio Code, Xcode, Visual Studio, and CLion. To do this in Visual Studio Code, open the `ch6/part-4/app` as its own project by running `code .` from that folder. Then open `launch.json` and set `"program"` to the location of the executable, and `"cwd"` to `"${workspaceFolder}"` (see *Chapter 11*, *Supporting Tools and Next Steps*, for a more detailed walkthrough of how to set this up if required).
			Running the application from `ch6/part-4/app` will again display *Game of Life*, but this time drawing with `bgfx` instead of `SDL_Renderer`.
			![Figure 6.2: The windowed output of Game of Life with bgfx](https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/min-cmk/img/B21152_06_2.jpg)

			Figure 6.2: The windowed output of Game of Life with bgfx
			The `ch6/part-4/app/main.c` file has changed quite a bit from `part-3` (another reminder to use the excellent **Compare Active File With...** in Visual Studio Code to view the differences). The changes aren’t important in the context of CMake but might be interesting for those wanting to learn more about (very simple) graphics programming.
			Summary
			A round of applause for reaching this milestone. Installing dependencies is a key skill to master when using CMake, which is why we learned how to do this now. It’s essential to understand what is happening behind the scenes when we install a library and then try to use it from our main application. We outlined several ways to install libraries and discussed various pros and cons of each approach. We then looked at how to integrate those installed libraries into our application. Afterward, we introduced `ExternalProject_Add` and saw how it can simplify installing larger external dependencies in a structured way. Finally, we walked through a slightly more complex example, making use of multiple dependencies and build stages.
			In the next chapter, we’ll be changing sides and delving into how we can provide installation support for our own libraries. The CMake install commands are notoriously difficult to understand and can appear quite obscure on first inspection. We’ll walk through each command in detail and look at installing our *Game of Life* library, as well as a helper rendering library. We’ll also be introduced to our first find module file.






第七章:为你的库添加安装支持

第六章,《安装依赖项和 ExternalProject_Add》中,我们讨论了如何安装现有库并在项目中使用它们。了解如何使用已安装的库非常有用,特别是当与ExternalProject_Add结合使用时。现在,我们将转变思路,看看如何为我们自己的库添加安装支持。这是一个庞大的话题,提供了许多不同的选项。我们无法涵盖所有内容,但我们将着重介绍 CMake 的install命令,它的作用以及如何使用它,还会介绍一些可用的配置选项。

在本章中你将学到的技能,如果你以后选择使自己的库可安装并通过ExternalProject_Add或将你的应用打包,都会非常有用。通过这样做,你将使其他开发者更容易使用你的库,并可能避免他们多次构建它。

本章我们将涵盖以下主要内容:

  • 为库添加安装支持

  • 处理嵌套依赖项

  • 何时以及如何使用COMPONENTS

  • 支持不同版本的库

  • 编写查找模块文件

技术要求

为了继续学习,请确保你已经满足第一章,《入门》中列出的要求。包括以下内容:

  • 一台运行最新操作系统(OS)的 Windows、Mac 或 Linux 机器

  • 一个有效的 C/C++编译器(如果你还没有,建议使用系统默认的编译器)

本章中的代码示例可以通过以下链接找到:github.com/PacktPublishing/Minimal-CMake

为库添加安装支持

为库添加安装支持时,一切都围绕着install命令。install命令是我们告诉 CMake 要安装什么内容,以及文件的相对布局。好消息是,我们现有的CMakeLists.txt文件几乎不需要做什么更改,并且在大多数情况下,也不需要添加太多内容。不过,第一次看到我们添加的内容时可能会有些困惑,我们将在这里尽量解释清楚。

与前几章一样,我们将通过一个具体的示例,展示如何为我们现有的最简单库之一mc-array添加安装支持。这个静态库提供了在 C 语言中支持可调整大小的数组(非常类似于 C++中的std::vector)。我们在整个《生命游戏》应用中都使用了它,它是一个特别有用的工具。

我们将从查看ch7/part-1/lib/array/CMakeLists.txt开始。第一个变化是,我们已经明确通过提供STATIC参数将这个库提交为静态库,来进行使用:

add_library(${PROJECT_NAME} mc-gol in *Chapter 4*, *Creating Libraries for FetchContent*), and we won’t be providing install support for a shared library either (we’ll cover the differences later when looking at adding install support to our mc-gol library). We could have added STATIC at the outset, but making these gradual improvements over time is never a bad thing.
			Next, we include a CMake module we haven’t come across before called `GNUInstallDirs`.

include(GNUInstallDirs)


			The `GNUInstallDirs` module provides variables for standard installation directories. Even though the name refers to `GNUInstallDirs` will give us a good standard directory structure on whatever platform we’re using. To learn more about `GNUInstallDirs`, please refer to [`cmake.org/cmake/help/latest/module/GNUInstallDirs.html`](https://cmake.org/cmake/help/latest/module/GNUInstallDirs.html).
			The next minor change is updating the `target_include_directories` command to handle providing the location of `.h` files for both the regular build (`BUILD_LOCAL_INTERFACE`) and when using the installed version of the library (`INSTALL_INTERFACE`):

target_include_directories(

${PROJECT_NAME}

PUBLIC

\(<BUILD_LOCAL_INTERFACE:\){CMAKE_CURRENT_SOURCE_DIR}/include>

FetchContent),第一个生成器表达式 BUILD_LOCAL_INTERFACE 将评估为 true,因此 \({CMAKE_CURRENT_SOURCE_DIR}/include 将被使用。当我们的库被安装时,包含文件的相对位置可能与正常构建时不同,因此我们可以提供相对于安装目录稍微不同的位置。`\){CMAKE_INSTALL_INCLUDEDIR}是由 GNUInstallDirs 提供的,我们将在稍后的install` 命令中使用它,将 .h 文件安装(复制)到安装文件夹中。也可以使用相对路径来为 $<INSTALL_INTERFACE> 提供路径,这将从安装前缀的根目录进行评估:

$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR} variable here makes the relationship between the target_include_directories command and the later install command clearer.
			For good measure, we add the `DEBUG_POSTFIX` property to our target to ensure both the `Debug` and `Release` versions of the library can be installed in the same directory without overwriting one another:

set_target_properties(${PROJECT_NAME} PROPERTIES mc-array,当我们分别构建 Debug 和 Release 时,我们将在 macOS 和 Linux 上得到 libmc-arrayd.a 和 libmc-array.a,在 Windows 上得到 mc-array d.lib 和 mc-array.lib。

        有了这些,我们就可以开始添加 `install` 命令本身了。

        CMake 安装命令

        CMake 的 `install` 命令非常灵活,但在第一次遇到时可能会觉得难以理解。需要注意的一点是,`install` 的功能会根据传递给它的参数而大不相同。对于熟悉 C++ 的人来说,可以将 `install` 的实现看作是某种形式的 `install`,其功能依赖于上下文(它们的顺序或位置是有意义的),这也可能让事情变得相当混乱。考虑到这一点,让我们看一个具体的例子。

        传递给 `install` 的第一个参数决定了将执行哪种类型的 `install` 命令;这些被称为 `TARGETS`、`EXPORT` 和 `DIRECTORY`。该规则本质上定义了 `install` 命令所关注的内容。

        让我们回顾一下 `CMakeLists.txt` 文件中的第一个安装命令:
install(
  TARGETS ${PROJECT_NAME}
  EXPORT ${PROJECT_NAME}-config
  ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR})
        该命令的作用是将我们库的构建产物安装到指定位置,在 macOS/Linux 上是 `libmc-array.a`,在 Windows 上是 `mc-array.lib`,并将其安装到我们使用 `CMAKE_INSTALL_PREFIX` 选定的 `lib` 文件夹中(如果未提供该路径,则为默认的系统位置)。

        第一个参数 `TARGETS` 指的是我们要安装的目标。在我们的案例中,这就是我们之前在 `CMakeLists.txt` 文件中使用 `add_library` 命令创建的库。由于这是一个小型库,我们重用了项目名称作为目标,但如果我们选择了其他名称,我们将引用那个名称。下面是一个展示这个的例子:
add_library(dynamic-array STATIC)
...
install(
  TARGETS dynamic-array
  ...
        请参见 `ch7/part-2/lib/array/CMakeLists.txt`,了解如何使用不同的库名称(目标),而不是重用项目名称。我们将在本章其余部分采用这种方法,帮助区分项目/包与各个目标/库。

        我们传递给第一个`install`命令的第二个参数是`EXPORT ${PROJECT_NAME}-config`。导出意味着将目标提供给其他项目(通常通过`find_package`)。通过添加这行代码,我们通知 CMake,我们希望导出这个目标,并将其与一个以我们项目命名的配置文件(`${PROJECT_NAME}-config`,在我们这里扩展为`mc-array-config`)关联。此步骤尚未创建导出文件,但允许我们在后续的`install`命令中引用该导出,以生成`<project-name>-config.cmake`文件(我们也可以选择`${PROJECT_NAME}Config`,让 CMake 为我们生成`<ProjectName>Config.cmake`文件)。

        最后的参数是`ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}`。这告诉 CMake 我们希望安装静态库的`.a`/`.lib`文件的位置。这并不是严格必要的,因为 CMake 会选择一个默认的系统位置,但显式指定并使用`GNUInstallDirs`提供的有用变量`CMAKE_INSTALL_LIBDIR`会使我们的`CMakeLists.txt`文件更加自文档化。`${CMAKE_INSTALL_LIBDIR}`很可能会扩展为`lib`,但使用这种方式提供了一个自定义点,如果用户希望覆盖库安装目录,可以根据需要进行调整。

        只需将前面的`install`命令添加到我们的`CMakeLists.txt`文件中,如果我们从`ch7/part-1/lib/array`目录运行以下命令:
cmake -B build -G "Ninja Multi-Config" -DCMAKE_INSTALL_PREFIX=install
cmake --build build --target install
        我们会看到以下输出:
[2/3] Install the project...
-- Install configuration: "Debug"
-- Installing: .../array/install/lib/libmc-arrayd.a
        (为提供上下文,`ch7/part-1/lib/array/CMakeLists.txt`已经包含了一个完整的安装示例,但可以随意注释掉后面的`install`命令,看看每个命令安装了什么,没安装什么。)

        这是一个好的开始,但 CMake 仍然缺少足够的信息来找到并使用我们的库。现在库文件已经安装,导出目标也已经创建,我们可以查看下一个`install`命令:
install(
  EXPORT ${PROJECT_NAME}-config
  DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME}
  NAMESPACE minimal-cmake::)
        这个`install`命令涉及到`EXPORT`规则。第一个参数`EXPORT ${PROJECT_NAME}-config`将这个命令与先前介绍的`${PROJECT_NAME}-config`导出链接在一起。此命令创建并安装导出文件,确保它最终被放入我们的`install`文件夹中(即`mc-array-config.cmake`文件)。我们需要提供此文件应安装的位置。如果我们没有提供,将看到以下错误信息:
install EXPORT given no DESTINATION!
        这是通过`DESTINATION`参数实现的。我们再次依赖于`GNUInstallDirs`变量`CMAKE_INSTALL_LIBDIR`,然后指定一个名为`cmake`的文件夹,接着是我们的项目名称。CMake 知道要搜索这个位置,如果需要,也可以调整`${PROJECT_NAME}`和`cmake`的顺序(如果安装到默认系统目录,这一点更为重要,它决定了 CMake 配置文件的组织方式):
DESTINATION ${CMAKE_INSTALL_LIBDIR}/EXPORT install command is NAMESPACE. This adds a prefix to the target to be exported to reduce the chance of naming collisions with other libraries. It’s also a standard convention to disambiguate imported targets from regular targets within a CMake project (something we touched on in *Chapter 6*, *Installing Dependencies* *and ExternalProject_Add*).

NAMESPACE minimal-cmake::


			If we now repeat the earlier commands (or just run `cmake --build build --target install` again), we’ll see the newly created config files be created and installed:

[2/3] 正在安装项目...

-- 安装配置:“调试”

-- 正在安装:.../array/install/lib/libmc-arrayd.a

-- 安装:.../array/install/lib/cmake/mc-array/mc-array-config.cmake

-- 安装:.../array/install/lib/cmake/mc-array/mc-array-config-debug.cmake


			Note that we get two config files, one common one, and one specific to the configuration we built (this will be `Debug` by default). If we pass `--config Release` when building, a corresponding `mc-array-config-release.cmake` file is created:

cmake --build build --target install --config Release


			The preceding command will output:

...

-- 安装:.../array/install/lib/cmake/mc-array/mc-mc-array-config.cmake 和 mc-array-config-debug.cmake/mc-array-config-release.cmake 文件,以查看底层发生了什么。有许多生成的代码我们可以安全地忽略,但需要注意的关键行如下:

add_library(minimal-cmake::mc-array STATIC IMPORTED)
set_target_properties(minimal-cmake::mc-array PROPERTIES
  INTERFACE_INCLUDE_DIRECTORIES "${_IMPORT_PREFIX}/include")
        CMake 正在为我们创建一个导入的目标(因为 `mc-array` 已经构建完成),然后设置该目标的属性以显示在哪里可以找到它的 `include` 文件。然后它会遍历不同的构建配置文件,并在每个文件中根据配置为导入的目标设置更多属性:
set_property(TARGET minimal-cmake::mc-array APPEND PROPERTY IMPORTED_CONFIGURATIONS DEBUG)
set_target_properties(minimal-cmake::mc-array PROPERTIES
  IMPORTED_LINK_INTERFACE_LANGUAGES_DEBUG "C"
  IMPORTED_LOCATION_DEBUG "${_IMPORT_PREFIX}/lib/libmc-arrayd.a")
        这通知 CMake 库的位置,以及构建类型(或配置)。

        深入研究这些文件并不是经常需要的,但大致了解它们在做什么,对于理解如何调试无法找到的文件问题非常有帮助。也值得注意的是,这里并没有什么魔法;CMake 只是自动生成了我们本该手动编写的许多命令(而且很可能做得比我们自己写得更好)。

        最终(幸运的是最简单的)`install` 命令涉及到 `DIRECTORY` 规则:
install(DIRECTORY ${CMAKE_CURRENT_LIST_DIR}/include/minimal-cmake/
  DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/minimal-cmake)
        在这个安装步骤中,我们只是将一个目录的内容复制到另一个目录,在这种情况下是 `include` 目录。我们使用 `CMAKE_CURRENT_LIST_DIR` 获取当前 `CMakeLists.txt` 文件的路径,然后从那里引用源目录中的 `include` 文件。对于 `DESTINATION`,我们使用 `CMAKE_INSTALL_INCLUDEDIR`(这在之前的 `target_include_directories` 调用中使用过,它与 `INSTALL_INTERFACE` 生成表达式一起使用)。这将把我们的 `include` 文件放在 `include/minimal-cmake` 下(根据约定,`CMAKE_INSTALL_INCLUDEDIR` 会扩展为 `include`)。

        如果我们再运行一次 `cmake --build build --target install`,我们会看到我们的 `include` 文件会被复制到预期的安装位置:
...
-- Installing: .../array/install/include/minimal-cmake
-- Installing: .../array/install/include/minimal-cmake/array.h
        文件集

        CMake `3.23` 引入了一个名为 `target_sources` 命令的新特性。指定一个文件集可以使头文件自动作为 `TARGET` 安装命令的一部分进行安装。我们在本书中坚持传统方法,但请查看 [`cmake.org/cmake/help/latest/command/target_sources.html#file-sets`](https://cmake.org/cmake/help/latest/command/target_sources.html#file-sets) 了解更多关于指定文件集的信息。

        有了 `.h` 文件,我们现在拥有了安装我们的库并在另一个项目中使用它所需的一切。

        `install` 文件夹结构现在看起来如下:
.
├── include
│   └── minimal-cmake
│       └── array.h
└── lib
     ├── cmake
     │   └── mc-array
     │       ├── mc-array-config-debug.cmake
     │       ├── mc-array-config-release.cmake
     │       └── mc-array-config.cmake
     └── libmc-arrayd.a
        我们已经涵盖了很多内容,所以如果一切并不完全明了也不用担心。安装、导出和导入目标的概念随着你使用得越多会变得越清晰。一个积极的方面是,添加安装支持仅仅花了大约 10 行代码,在大局上看,这并不算太多。在`ch7/part-2/lib/array`中,除了将`mc-array`目标的名称更改为`dynamic-array`,我们还添加了一个`CMakePreset.json`文件来设置`install`目录(`"installDir"`)。只需运行`cmake --preset default`,然后运行`cmake --build build --target install`。

        现在让我们看看如何在我们的*生命游戏*应用中使用我们的数组库。

        使用我们新安装的库

        好消息是,使用我们刚刚安装的库变得简单多了。如果我们回顾一下`ch7/part-2/app/CMakeLists.txt`,与早期的`ch6/part-4/app/CMakeLists.txt`相比,有两个添加和一个删除。

        第一个添加如下:
find_package(mc-array CONFIG REQUIRED)
        包的名称(`mc-array`)是我们分配给导出的名称。这与目标名称不同。它相当于我们分配给配置文件(`mc-array-config.cmake`)的名称。

        用于将`minimal-cmake-array`引入构建的`FetchContent`调用已经被移除,因为它不再需要,唯一的其他变化是更新`target_link_libraries`命令,使其引用新目标并添加我们之前添加的命名空间前缀:
target_link_libraries(
  ${PROJECT_NAME} PRIVATE app folder’s CMakeLists.txt file. The last change needed is to update the CMAKE_PREFIX_PATH variable stored in the CMakePresets.json file to include the install path of where mc-array was installed. We could have installed the library to the same install folder as our previously installed dependencies, but installing it to a separate location and providing multiple paths can sometimes be useful. To provide more than one install path, separate each with a semicolon:

"CMAKE_PREFIX_PATH":

"${sourceDir}/../third-party/installshared-ninja",然后构建我们选择的配置,例如:

cmake --preset shared-ninja
cmake --build build/shared-ninja --config Debug
        我们现在已经成功地从当前应用中使用了已安装的库。可以像往常一样运行应用,通过在应用目录下运行`./build/shared-ninja/Debug/minimal-cmake_game-of-life_window`来启动应用。由于每个`part-<n>`都是完全独立的,所以每个示例仍然需要像之前一样安装第三方依赖,并通过运行`compile-shader-<platform>.sh/bat`编译着色器。每章的`README.md`文件都描述了构建和运行每个示例所需的命令;请参考它们以回顾所有必要的步骤。

        现在我们已经安装了一个简单的静态库,接下来我们将探讨一个稍微复杂一点的案例,处理安装一个有自己依赖关系的库。

        处理嵌套依赖

        当我们说到嵌套依赖时,我们指的是我们想要依赖的库的依赖项(你可以把这些看作是间接依赖,也叫做**传递性依赖**)。例如,如果一个应用依赖于库 A,而库 A 又依赖于库 B,那么就应用而言,库 B 嵌套在库 A 中。无论这个依赖是私有的(对应用隐藏)还是公共的(对应用可见),都影响我们如何处理它。

        在我们刚才看到的示例(`mc-array`)中,幸运的是,当提供安装支持时,我们不需要担心任何依赖项。如果有依赖项,事情会变得稍微复杂一些,但一旦我们理解了需要做什么以及为什么做,支持起来并不复杂。

        为了更好地理解这一点,我们将注意力转向我们的*生命游戏*库,`mc-gol`,它位于`ch7/part-2/lib/gol`。最好为这个库以及`mc-array`添加安装支持,所以,如果我们复制刚才演示的三个 CMake `install`命令,并将它们添加到`ch7/part-2/lib/gol/CMakeLists.txt`的底部,同时在顶部添加`include(GNUInstallDirs)`并更新`target_include_directories`与`INSTALL_INTERFACE`,我们可以看看会发生什么(请参阅`part-3`获取确切的更改)。

        如果我们从`ch7/part-2/lib/gol`配置、构建并安装共享版本的库(通过设置`MC_GOL_SHARED=ON`),一切都会按预期工作,但如果我们尝试构建库的静态版本,我们会看到 CMake 报告三处错误:
CMake Error: install(EXPORT "mc-gol-config" ...) includes target "mc-gol" which requires target "as-c-math" that is not in any export set.
CMake Error: install(EXPORT "mc-gol-config" ...) includes target "mc-gol" which requires target "mc-array" that is not in any export set.
CMake Error: install(EXPORT "mc-gol-config" ...) includes target "mc-gol" which requires target "mc-utils" that is not in any export set.
        这些错误的原因是,我们在`mc-gol`中使用的依赖项是通过`FetchContent`引入的,也需要被导出,因为它们被视为构建和安装的静态库的一部分。即使这些依赖项在`target_link_libraries`中被标记为`PRIVATE`,情况依然如此。处理这个问题有两种不同的方法。我们将作为`mc-gol`的一部分来看第一个方法,并作为我们将在*公共嵌套* *依赖项*部分中提取的新库来看第二个方法。

        私有嵌套依赖项

        在`mc-gol`的情况下,我们显式地将所有依赖项设置为私有(它们仅在`gol.c`中使用),并且我们不希望客户端或库的用户知道它们。幸运的是,有一个方便的生成器表达式,我们可以使用它来确保这些目标不会被导出,并且只在构建时可见。我们之前在`target_include_directories`的上下文中遇到过它,那就是`BUILD_LOCAL_INTERFACE`:
target_link_libraries(
  ${PROJECT_NAME} PRIVATE
  $<BUILD_LOCAL_INTERFACE will ensure they are not treated as part of the export set and no further work is needed. This is the approach taken in ch7/part-3/lib/gol/CMakeLists.txt. The library is updated to search for mc-array in the installed location instead of using FetchContent and updates the target name to be game-of-life. The target_link_libraries command looks as follows:

target_link_libraries(

game-of-life PRIVATE

$<BUILD_LOCAL_INTERFACE:minimal-cmake::dynamic-array

as-c-math 和 mc-utils)在 BUILD_LOCAL_INTERFACE 内,但将所有内容放入其中可以确保 minimal-cmake::dynamic-array 不会被添加到生成的 mc-gol-config.cmake 文件的 INTERFACE_LINK_LIBRARIES 中。

        唯一需要的其他更改是确保我们安装共享库文件(在`mc-array`的情况下我们不需要这个,因为它只能构建为静态库)。现在,`install` `TARGET` 命令如下所示:
install(
  TARGETS game-of-life
  EXPORT ${PROJECT_NAME}-config
  ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
  LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
  LIBRARY and RUNTIME along with the corresponding install locations will ensure the .dylib/.so files on macOS/Linux, and .dll files on Windows, are copied to the correct location when building and installing the library as shared.
			Public nested dependencies
			When our nested dependencies are public, and are part of the interface of our library, there’s a little more work we need to do. To demonstrate this, we’re going to create a new library called `mc-draw`, found in `ch7/part-4/lib/draw`, that will provide some useful debug drawing functions using `bgfx` for reuse in other projects. It will also give us an example of how to export a nested dependency.
			The overall shape of the `mc-draw` `CMakeLists.txt` file is remarkably like that of `mc-gol`. Both can be built as either static or shared libraries, and both now separate the project name from the target name (in the case of `mc-draw`, the target/library name is `draw`, with the `minimal-cmake` namespace).
			The first significant difference is a subtle change to the `target_link_libraries` command:

target_link_libraries(

draw

PUBLIC as-c-math

PRIVATE $<BUILD_LOCAL_INTERFACE:bgfx::bgfx minimal-cmake::dynamic-array>)


			We’re keeping `minimal-cmake::dynamic-array` and `bgfx` as private dependencies (we could make `bgfx` public too, following the same approach as we’ll describe here, but as the windowed *Game of Life* application we’re building already depends on it, we don’t need to at this time). The main change is making `as-c-math` public. This makes the transitive dependency explicit, so any application using `minimal-cmake::draw` will also get `as-c-math` without also needing to depend on it separately.
			The next change is an addition to the `install` `EXPORT` command:

install(

EXPORT ${PROJECT_NAME}-config

DESTINATION \({CMAKE_INSTALL_LIBDIR}/cmake/\){PROJECT_NAME}

NAMESPACE minimal-cmake::

使用find_package命令首先定位as-c-math依赖项,然后再尝试定位依赖于as-c-math的 draw 库。

        单独来看,所有前述的变更只是将原本会生成的`mc-draw-config.cmake`文件的名称更改为`mc-draw-targets.cmake`,但这是因为我们将自己制作配置文件,然后从那里引用这个生成的文件。为此,我们在与`CMakeLists.txt`文件相同的目录中创建一个新的文件,命名为`mc-draw-config.cmake.in`。这是一个模板文件,用于生成实际的`mc-draw-config.cmake`文件,其内容如下:
@PACKAGE_INIT@
include(CMakeFindDependencyMacro)
find_dependency(as-c-math)
include(${CMAKE_CURRENT_LIST_DIR}/mc-draw-targets.cmake)
        第一行在使用`configure_package_config_file`时是必需的(这是我们稍后会介绍的命令),然后我们引入一个 CMake 模块(`CMakeFindDependencyMacro`),以便我们能在`as-c-math`上调用`find_dependency`。`find_dependency`命令是`find_package`的包装器,专门设计用于在包配置文件中使用(有关`find_dependency`的更多信息,请参见[`cmake.org/cmake/help/latest/module/CMakeFindDependencyMacro.html`](https://cmake.org/cmake/help/latest/module/CMakeFindDependencyMacro.html))。

        即使我们使用`FetchContent`将`as-c-math`引入,并作为主构建的一部分进行构建,它也需要安装支持,以便我们能够将其作为导出集的一部分。这是为了让调用`find_package(mc-draw)`时首先能找到`as-c-math`,正如之前提到的(要查看如何添加此支持,请参考[`github.com/pr0g/as-c-math`](https://github.com/pr0g/as-c-math)并查看`bfdd853`提交)。最后一行包含了我们之前`install`的`EXPORT`命令生成的文件(`mc-draw-targets.cmake`)。

        剩下的就是手动使用此模板生成新的`mc-draw-config.cmake`文件。为此,我们使用前面提到的`configure_package_config_file`命令。我们需要先包含`CMakePackageConfigHelpers` CMake 模块,然后调用代码如下:
configure_package_config_file(
  ${PROJECT_NAME}-config.cmake.in ${PROJECT_NAME}-config.cmake
  INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME})
install(FILES 
  "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}-config.cmake"
  INSTALL_DESTINATION. The following install command just copies the config file to the right location as part of the install process (notice how the values passed to INSTALL_DESTINATION and DESTINATION match).
			`configure_package_config_file` is used to ensure the config file is relocatable (it avoids the config file using hardcoded paths). For more information about `CMakePackageConfigHelpers`, please see [`cmake.org/cmake/help/latest/module/CMakePackageConfigHelpers.html`](https://cmake.org/cmake/help/latest/module/CMakePackageConfigHelpers.html).
			With these changes made, we can now install our library and update our `CMakeLists.txt` application file. Looking at `ch7/part-4/app/CMakeLists.txt`, we can see we now have the obligatory `find_package` call to find the installed library:

find_package(mc-draw CONFIG REQUIRED)


			This is followed by an update to `target_link_libraries` to link against the new target (`minimal-cmake::draw`). The only other addition is to remember to copy the `.dll` file to the app build folder using the familiar `add_custom_command` and `copy_if_different` operation so our Windows builds work as expected.
			When trying to configure, if the `as-c-math` dependency cannot be found (this can be replicated by commenting out `find_dependency` in the `mc-draw-config.cmake.in` file before installing), then CMake will output an error resembling the following:

找到的包配置文件:

../minimal-cmake/ch7/part-4/lib/draw/install/lib/cmake/mc-draw/mc-draw-config.cmake

但它将 mc-draw_FOUND 设置为 FALSE,因此包“mc-draw”被认为是未找到。包给出的原因是:

以下导入的目标被引用,但缺失:在问题解决之前显示为 NOT_FOUND。

        一切应该已经正常工作,所以从`ch7/part-4/app`开始,如果我们配置并构建(使用 CMake 预设将使这变得更简单),我们可以启动我们更新后的应用程序:
cmake --preset multi-ninja
cmake --build build/multi-ninja
./build/multi-ninja/Debug/minimal-cmake_game-of-life_window
        记住,我们还需要构建并安装`part-4`中的其他必需库,包括位于`third-party`文件夹中的`SDL2`和`bgfx`,以及位于`lib`文件夹中的`mc-array`、`mc-gol`和`mc-draw`。每个`lib`文件夹中都有`CMakePreset.json`文件来正确配置安装和前缀路径;只需运行`cmake --preset list`显示可用的预设,然后运行`cmake --preset <preset-name>`进行配置。要构建和安装,请为每个库运行`cmake --build build/<build-folder> --target install`。

        关于第三方依赖的提醒,只需在`third-party`文件夹中运行`cmake -B build -G <generator>`和`cmake --build build`,让`ExternalProject_Add`处理所有与`SDL2`和`bgfx`的相关操作。最后要记住的是,如果你还没有编译着色器,可以在构建并安装`bgfx`后,从`app`文件夹运行对应的批处理/脚本来编译。

        奖励将是一个更新版的*生命游戏*应用程序,借助我们新的`mc-draw`库,界面颜色和网格线将更加愉悦。

        ![图 7.1:改进版生命游戏应用程序](https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/min-cmk/img/B21152_07_1.jpg)

        图 7.1:改进版生命游戏应用程序

        更新后的*生命游戏*应用程序现在也可以互动了。点击网格外的地方将暂停和恢复模拟,而点击一个网格单元将切换该单元的开关状态。通过对网格进行少量修改,生成的各种奇特图案很有趣。

        接下来,我们将看看如何将库拆分为独立的部分,这些部分被称为**组件**。

        何时以及如何添加组件

        随着库的增长,可能会有一个时刻,当将整体库拆分为几个更小的部分时变得有意义。在这一点上,你可能会开始把库看作是一个包,由一个或多个不同的库组成。将包拆分为独立的组件,对于用户来说非常有帮助,因为他们只需要链接所需的功能。这有助于减小应用程序的二进制文件大小,并通过减少与外部依赖项的链接时间来提高构建速度。对于库本身,拆分代码也有利于解耦和打破依赖。

        为了演示这个,我们将更新我们的`mc-draw`库,使其包含三个独立的组件:`vertex`、`line`和`quad`。然后,我们可以在`find_package`调用中明确请求所需的组件,如下所示:
find_package(mc-draw CONFIG REQUIRED target_link_libraries:

target_link_libraries(

...

minimal-cmake::vertex

minimal-cmake::line

minimal-cmake::quad

...


			To understand how to achieve this, let’s review `ch7/part-5/lib/draw/CMakeLists.txt`. The key difference is instead of creating a single library with `add_library` called `draw`, we instead create three libraries called `vertex`, `line`, and `quad` in a nearly identical way (it’s completely possible to create utility functions to remove some of the boilerplate, but for simplicity, the examples omit this to avoid any potentially confusing abstractions).
			Each library (or component) has its own `<component>-config.cmake` file, just as we had with the top-level package. This is the mechanism by which we create the components we refer to in the `find_package` command. Fortunately, we don’t have to create each of these ourselves, but we do need to update our existing `mc-draw-config.cmake.in` file.
			Before we do so, the first target we need to talk about is `vertex`. This is depended on by both `quad` and `line` (it appears in their `target_link_libraries` command). It is now responsible for exporting the `as-c-math` dependency, so, the approach we took before of creating a `*-targets.cmake` file goes to `vertex` (we now have a `vertex-config.cmake.in` file with the earlier logic, referring internally to `vertex-targets.cmake` instead of `mc-draw-targets.cmake`). The top-level package config template file, `mc-draw-config.cmake.in`, has been updated to the following:

@PACKAGE_INIT@

可以搜索的有效组件

set(_draw_supported_components vertex line quad)

遍历组件,尝试查找

foreach(component \({\){CMAKE_FIND_PACKAGE_NAME}_FIND_COMPONENTS})

如果我们找不到组件,设置绘图库为 no

找不到后,通知用户缺少的组件

if (NOT ${component} IN_LIST _draw_supported_components)

set(mc-draw_FOUND False)

set(mc-draw_NOT_FOUND_MESSAGE "不支持的组件:${component}")

else()

include(\({CMAKE_CURRENT_LIST_DIR}/\){component}-config.cmake)

endif()

endforeach()


			After the required `@PACKAGE_INIT@` string, we provide a variable holding all the available components provided by the package. What follows is then a check to ensure the components requested from `find_package` are in our list of supported components. Say a user tries to request a component that does not exist, for example:

find_package(mc-draw CONFIG REQUIRED COMPONENTS circle)


			Then, they will see the following error message:

找到的包配置文件:

../minimal-cmake/ch7/part-5/lib/draw/install/lib/cmake/mc-draw/mc-draw-config.cmake

但它将 mc-draw_FOUND 设置为 FALSE,因此包“mc-draw”被认为未找到。包给出的原因是:

不支持的组件:circle


			This kind of error message is helpful to let a user know whether they’ve mistyped a component or are trying to use one that does not exist.
			One other important detail to note is we do not need to have a component map directly to a single target as we’ve done so previously, with an individual component for `vertex`, `quad`, and `line`. In a larger package, we may choose to create a `geometry` component that contains all the geometric libraries in our application (e.g., `vertex`, `quad`, `line`, `circle`, etc.). For a simple example showing this technique, see [`github.com/pr0g/cmake-examples/tree/main/examples/more/components`](https://github.com/pr0g/cmake-examples/tree/main/examples/more/components), which shows grouping multiple libraries under a single component (two libraries, `hello` and `hey`, are made part of the `greetings` component, with `goodbye` made part of the `farewells` component).
			COMPONENT versus COMPONENTS
			There is unfortunately another keyword in CMake called `COMPONENT` that also happens to be part of the `install` command. It bears no relation to the `COMPONENTS` keyword that’s part of `find_package`. It is used to split install artifacts based on how the library/package is to be used (`Runtime` and `Development` are commonly suggested components to separate runtime and development functionality, for example). We haven’t covered the `COMPONENT` keyword in the context of the `install` command, but to learn more about how to use it, see the CMake install documentation ([`cmake.org/cmake/help/latest/command/install.html`](https://cmake.org/cmake/help/latest/command/install.html)) and CMake `install` command documentation ([`cmake.org/cmake/help/latest/manual/cmake.1.html#install-a-project`](https://cmake.org/cmake/help/latest/manual/cmake.1.html#install-a-project)) (`cmake --install <build> --``component <comp>`).
			Whether you decide to use components or not will very much depend on the type and size of the library you’re building. One example of a library that relies heavily on components is the `s3`, `ec2`, etc.).
			Supporting different versions of a library
			Back in *Chapter 2*, *Hello, CMake!*, when we introduced the `project` command, we touched on the `VERSION` parameter, but haven’t yet had the opportunity to see how to apply it, and why it’s useful. In this section, we’ll show how to add a version to our `mc-draw` library and how to request the correct version from our `find_package` command.
			Versioning for libraries is important for us to know what functionality and interface the library we’re currently using provides. Versioning is used to manage change, and, most importantly, handle API updates that may cause breaking changes. The software industry has largely adopted `<Major>.<Minor>.<Patch>` format (e.g., `1.45.23`) where numbers further to the left represent a more notable change. For a full introduction, see [`semver.org/`](https://semver.org/). Luckily, CMake supports this format, so it’s easy to integrate into our project.
			If we look at `ch7/part-6/lib/draw/CMakeLists.txt`, we can see the changes needed to add version support by reviewing the differences between it and the corresponding file in `part-5`. The first change is adding `VERSION` to our project command:

project(

mc-draw

LANGUAGES C

我们可以通过与 CMakeLists.txt 文件相同的方式引用项目名称,只是这次不是使用 ${PROJECT_NAME},而是使用 ${PROJECT_VERSION}。

        然后,大多数剩余的修改只是将 `${PROJECT_NAME}` 替换为 `${PROJECT_NAME}-${PROJECT_VERSION}`,或者在 `ARCHIVE`、`LIBRARY` 和 `RUNTIME` 目标的情况下附加它。这样做的原因是为了使得在同一位置安装多个版本的相同库成为可能。

        我们需要的最后一个添加是 `write_basic_package_version_file` CMake 命令,用于为我们生成一个 `*-config-version` 文件。它看起来如下:
write_basic_package_version_file( 
  "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}/${PROJECT_NAME}-config-version.cmake"
  VERSION ${PROJECT_VERSION}
  COMPATIBILITY SameMajorVersion)
        我们将其命名为 `mc-draw-config-version` 并将 `${PROJECT_VERSION}` 传递给 `VERSION` 参数。`COMPATIBILITY` 字段用于决定库在接受版本请求时的严格程度。选项有 `AnyNewerVersion`、`SameMajorVersion`、`SameMinorVersion` 和 `ExactVersion`。我们选择了 `SameMajorVersion`,这意味着只要版本的第一个数字匹配,库就会被找到。

        我们应用程序文件夹中的 `CMakeLists.txt` 文件中的 `find_package` 调用稍微更新为以下内容:
find_package(mc-draw 3.5.4 (even if the major versions match), things will fail (the version requested must always be the same or lower than the installed library). By using SameMajorVersion, if we request 2.0, things will fail; the following is an example showing the expected output:

CMake 错误位于 CMakeLists.txt 的第 16 行 (find_package):

找不到与请求的版本 "2.0" 兼容的 "mc-draw" 包的配置文件。

以下配置文件被考虑过,但未被接受:

../minimal-cmake/ch7/part-6/lib/draw/install/lib/cmake/mc-draw-3.5.4/mc-draw-config.cmake,AnyNewerVersion,当我们安装库时,要求 2.0 不会产生错误,因为已安装的版本 3.5.4 会大于请求的 2.0 版本。决定使用哪种方案可能会很困难,这将取决于你愿意支持的向后兼容性。有关不同兼容模式的更多信息,请参见 cmake.org/cmake/help/latest/module/CMakePackageConfigHelpers.html#generating-a-package-version-file

        编写查找模块文件

        在我们结束关于安装的讨论之前,还有一个话题是有用的。到目前为止,我们只讨论了使用配置模式查找依赖项,但也有一种模式,我们在 *第六章* 中简单提到过,叫做 **模块模式**。模块模式在与没有使用 CMake 本地构建的库集成时很有用(因此无法为我们自动生成配置文件)。

        在 `ch7/part-7/cmake` 目录中,添加了一个新的文件 `Findmc-gol.cmake`,它充当了我们将在 `ch7/part-7/lib/gol/install` 中安装的 `mc-gol` 库的查找模块文件。该查找模块文件从技术上讲是多余的,因为我们可以像以前一样使用生成的 `mc-gol-config.cmake` 文件进行配置模式,但假设我们使用一个单独的工具构建了这个库,并知道构建产物(库文件)和头文件的位置。

        要将 `mc-gol` 库引入我们的构建中使用 `find_package`,我们首先需要查看 `Findmc-gol.cmake` 查找模块文件。第一行使用 `find_path` CMake 命令来填充 `mc-gol_INCLUDE_DIR` 变量:
find_path(
  mc-gol_INCLUDE_DIR minimal-cmake-gol PATHS ${mc-gol_PATH}/include)
        `mc-gol_PATH` 变量是我们在配置主应用程序时提供的,用来指定相对于包含文件和库文件的路径(这在 `ch7/part-7/app` 中的 `CMakePresets.json` 文件中设置)。本质上,发生的情况是一种模式匹配,我们传递给 `PATHS` 的值与 `include` 文件所在的路径匹配。

        下一行执行几乎相同的操作,不过这次不是填充 `include` 目录变量,而是使用 `find_library` 填充 `mc-gol_LIBRARY`,保存库文件的路径:
find_library(
  mc-gol_LIBRARY
  NAMES game-of-life game-of-lifed
  PATHS ${mc-gol_PATH}/lib)
        `NAMES` 参数要求提供我们库的精确名称(这里可以提供多个名称)。我们还必须包含带有 `d` 后缀的库的调试版本名称,因为我们使用 `CMAKE_DEBUG_POSTFIX` 来区分库的 `Debug` 和 `Release` 版本。如果我们不这么做,`find_library` 将找不到库的调试版本。还值得一提的是,`find_library` 并不递归查找,所以我们必须提供库文件存储的精确文件夹位置。

        接下来是一个非常有用的 CMake 提供的工具,名为 `find_package_handle_standard_args`,用于在找不到前面提到的两个变量(`mc-gol_INCLUDE_DIR` 和 `mc-gol_LIBRARY`)时进行适当的消息处理。它还处理与 `find_package` 调用相关的其他细节,尽管目前我们不需要关注这些细节。如果你想了解更多关于该命令幕后做了什么,可以访问 [`cmake.org/cmake/help/latest/module/FindPackageHandleStandardArgs.html`](https://cmake.org/cmake/help/latest/module/FindPackageHandleStandardArgs.html) 获取更多信息。

        最后,如果库文件被找到,我们会调用 `add_library` 并将 `minimal-cmake::game-of-life` 作为一个导入的目标,同时使用 `set_target_properties` 将我们填充的变量与 `minimal-cmake::game-of-life` 目标关联起来:
if(mc-gol_FOUND AND NOT TARGET minimal-cmake::game-of-life)
  add_library(minimal-cmake::game-of-life UNKNOWN IMPORTED)
  set_target_properties(
    minimal-cmake::game-of-life
    PROPERTIES
      IMPORTED_LOCATION "${mc-gol_LIBRARY}"
      INTERFACE_INCLUDE_DIRECTORIES "${mc-gol_INCLUDE_DIR}")
endif()
        前面提到的两个命令非常类似于 CMake 在 `ch7/part-7/lib/gol/install/lib/cmake/mc-gol` 文件夹中的 `mc-gol-config-debug/release.cmake` 和 `mc-gol-config.cmake` 文件为我们生成的命令。为了简化,我们没有做太多工作来处理不同的配置(调试版与发布版)或库类型(静态与共享),但如果需要,这一切都是可以实现的。

        倒数第二步是让 CMake 知道在哪里找到我们新的 `Findmc-gol.cmake` 文件,并填写 `mc-gol_PATH` 变量。我们在 `ch7/part-7/app/CMakePresets.json` 文件中完成这两项工作,通过更新 `CMAKE_MODULE_PATH` 以包含我们新找模块文件的位置,并设置 `mc-gol_PATH` 为我们库文件所在的位置:
  ...
  "CMAKE_MODULE_PATH": "${sourceDir}/../cmake",
  "mc-gol_PATH": "${sourceDir}/../lib/gol/install"
}
        最后的修改是在 `ch7/part-7/app` 中的 `CMakeLists.txt` 文件,我们必须明确指定 `MODULE`,而不是 `CONFIG`,用于 `mc-gol`:
find_package(mc-gol cmake --preset multi-config will display:

...

-- 找到 mc-gol: /Users/tomhultonharrop/dev/minimal-cmake/ch7/part-

7/lib/gol/install/lib/libgame-of-lifed.dylib

...


			It’s likely there won’t be a need to write find module files often, but they can be incredibly helpful in a pinch. It’s much preferable to have CMake do the arduous work of generating and installing config files for us, but if that isn’t a possibility, find modules provide a useful workaround.
			Summary
			Three cheers for making it to the end of this chapter. Installing is not an easy concept to master, and this is without a doubt the trickiest part of CMake we’ve covered so far. In this chapter, we covered adding simple install support to a static library and then differentiating our package name from the installed library. We then looked at adding install support to a library with private dependencies and how to handle public dependencies as well. Next, we covered splitting up a library or package into components and then looked at how to provide robust versioning support. We closed by reviewing how to create a find module file to integrate libraries built outside of the CMake ecosystem. We also continued to improve and refine our *Game of* *Life* application.
			In the next chapter, we’re going to return to streamlining our setup yet again. Right now, we’re back to having to perform a lot of manual installs, which can become tedious, so we’ll look at improving this with the help of `ExternalProject_Add`. To improve things further, we’ll turn to what are often referred to as **super builds**, to neatly combine building our external dependencies and main project in one step. Finally, we’ll look at installing our application and will get things ready for packaging.










第八章:使用超级构建简化入门

在本章中,我们将回到简化和精简项目设置的工作中。在开发过程中,添加功能和应对随之而来的复杂性之间总有一种自然的推拉关系。在第七章《为你的库添加安装支持》中,我们花了大量时间切换目录并运行 CMake 命令。为了构建我们的应用程序,我们需要穿越至少五个文件夹(third-partyarraydrawgolapp),并在途中运行大量 CMake 命令。这是学习 CMake 的一种极好的方式,但当你想要完成工作时,这并不有趣。它还可能阻碍不熟悉的用户访问或贡献你的项目。

现在是时候解决这个问题了。你将在本章中学到的技能将有助于减少启动和运行项目所需的手动步骤。本章将向你展示如何去除平台特定的脚本,并自动化更多的构建过程。

在本章中,我们将介绍以下主要内容:

  • 使用ExternalProject_Add与你自己的库

  • 配置超级构建

  • 使用 CMake 自动化脚本

  • 在嵌套文件中设置选项

  • 安装应用程序

技术要求

要跟随本教程,请确保已满足第一章《入门》的要求。这些要求包括以下内容:

  • 一台运行最新操作系统(OS)的 Windows、Mac 或 Linux 机器

  • 一个可用的 C/C++ 编译器(如果你还没有,建议使用每个平台的系统默认编译器)

本章中的代码示例可以通过以下链接找到:github.com/PacktPublishing/Minimal-CMake

使用 ExternalProject_Add 与你自己的库

在上一章中,我们主要依靠手动 CMake 构建和安装命令(以及一些 CMake 预设的帮助)来增加我们对 CMake 的熟悉度,并以稍低的抽象层次工作,以理解 CMake 在后台所做的事情。现在我们对这些概念已经更加熟悉,是时候去除在每个单独的库文件夹中导航并运行以下熟悉的 CMake 命令的乏味工作了:

cmake --preset <preset-name>
cmake --build <build-folder> --target install

我们可以开始更新项目,利用 CMake 提供的更多有用功能。首先,我们将更新现有的第三方 CMakeLists.txt 文件,不仅引入 SDL 2 和 bgfx,还包括我们创建并依赖的库。这将消除我们手动安装这些库的需要,并允许我们运行一对 CMake 命令(配置和构建/安装)来获取我们所需的所有依赖项,以支持我们的生命游戏应用程序。

让我们首先查看ch8/part-1/third-party/CMakeLists.txt。该文件与之前大致相同,只是在现有的ExternalProject_Add命令下,我们添加了对位于ch8/part-1/lib中的库的引用。

这是mc-array库的示例:

ExternalProject_Add(
  mc-array
  SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../lib/array
  BINARY_DIR ${CMAKE_CURRENT_BINARY_DIR}/mc-array-build/${build_type_dir}
  INSTALL_DIR ${CMAKE_CURRENT_SOURCE_DIR}/install
  CMAKE_ARGS ${build_type_arg} -DCMAKE_INSTALL_PREFIX=<INSTALL_DIR>
  CMAKE_CACHE_ARGS -DCMAKE_DEBUG_POSTFIX:STRING=d)

该命令看起来与我们在第六章中讲解的非常相似,安装依赖项和 ExternalProject_Add。唯一的真正不同之处是对SOURCE_DIR的引用。由于本书仓库的布局稍显不传统,我们可以直接引用源文件夹,因为库的源代码存储在同一仓库中:

SOURCE_DIR URL or GIT_REPOSITORY link. If we did, for some reason, want to refer to an older version of one of the libraries at a specific moment in the Git history of our project, we could use this approach:

ExternalProject_Add(

mc-array

GIT_REPOSITORY https://github.com/PacktPublishing/Minimal-CMake.git

GIT_TAG 18535c9d140e828895c57dbb39b97a3307f846ab

SOURCE_SUBDIR ch8/part-1/lib/array

...


			We did the same thing in *Chapter 3*, *Using FetchContent with External Dependencies*, when using `FetchContent`. The preceding command will clone the entire repository into the build folder of our third-party `CMakeList.txt` file, and then treat the `ch8/part-1/lib/array` directory as the root of the repository (at least as far as `ExternalProject_Add` is concerned). It’s not often needed but can be useful if a repository holds more than one CMake project.
			While we’re making changes to `third-party/CMakeLists.txt`, we’ll also make one small improvement to how we handle our bgfx dependency. When bgfx was first introduced in *Chapter 6*, *Installing Dependencies and ExternalProject_Add* (see `ch6/part-4`), we wound up needing to clone the repository twice, once for the static version of the library (needed to build the tools), and again for the shared version of the library that our application linked against. The good news is there’s a handy technique we can apply to download the library once. The following is an extract of the changes with the differences highlighted:

ExternalProject_Add(

bgfxt

GIT_REPOSITORY https://github.com/bkaradzic/bgfx.cmake.git

GIT_TAG v1.127.8710-464

...

ExternalProject_Get_Property(bgfxt SOURCE_DIR)

ExternalProject_Add(

bgfx

URL "file://${SOURCE_DIR}"

DEPENDS bgfxt

...


			The first `ExternalProject_Add` call is the same as before; we let it know the repository and specific `GIT_TAG` to download. However, in the second version, instead of repeating those lines, we first call `ExternalProject_Get_Property`, passing the `bgfxt` target and the `SOURCE_DIR` property. `SOURCE_DIR` will be populated with the location of the `bgfxt` source code once it’s downloaded, and in the second command, we point the `bgfx` target to reference that source code location. As the code is identical and it’s just how we’re building it that’s different, this saves a bit of time and network bandwidth downloading the same files all over again.
			You might have noticed that we’re using `URL "file://${SOURCE_DIR}"` as opposed to `SOURCE_DIR ${SOURCE_DIR}`. This is because if we tried to use `SOURCE_DIR`, the `ExternalProject_Add` command would fail at configure time because `SOURCE_DIR` is expected to already be present when we configure. As this isn’t the case with our dependency (the source for `bgfxt` will only be downloaded and made available at build time), we can use the `URL` option with a local file path shown by `file://`, which will cause the file path to instead be resolved at build time.
			With the addition of the three new `ExternalProject_Add` calls referencing our libraries, when building our project, we only need to visit two directories, as opposed to the earlier five. We can navigate to `ch8/part-1/third-party` and run the following CMake commands:

cmake -B build -G "Ninja Multi-Config"

cmake --build build --config Release


			This will download, build, and install all our dependencies at once. We then only need to navigate to `ch8/part-1/app` and run the following:

cmake --preset multi-ninja

cmake --build build/multi-ninja --config Release


			This will build and link our application. We’ve also tidied up our `CMakePresets.json` file to have `CMAKE_PREFIX_PATH` only refer to `${sourceDir}/../third-party/install`, as opposed to the numerous install folders we had for each of our internal dependencies in the last chapter.
			Finally, to launch the application, we first need to compile our shaders and then launch the application from our application root directory (`ch8/part-1/app`):

./compile-shader-.sh/bat

./build/multi-ninja/Release/minimal-cmake_game-of-life_window


			This is a substantial improvement from before, but we can do better. The main build is still split across two stages (dependencies and application), and we still have the annoying issue of needing to remember to compile the shaders (an easy step to overlook). We want to achieve the holy grail of one command to bootstrap everything. Let’s start by seeing how we can solve the first problem by reducing our build steps from two to one.
			Configuring a super build
			For those unfamiliar with the term **super build**, it is a pattern to bundle external dependencies and the local build together in one step. Behind the scenes, things are still being built separately, but from the user’s perspective, everything happens at once.
			Super builds are great for getting up and running with a project quickly. They essentially automate all the configuration and dependency management for you. One other useful quality about them is they’re opt-in. If you want to build the dependencies yourself and install them in a custom location, letting the application explicitly know where to find them, that’s still possible, and the super build won’t get in the way.
			Super builds provide the best of both worlds. They are a singular way to simply build a project, and they can also be easily disabled, allowing you to configure your dependencies as you see fit.
			Integrating super build support
			We’re going to walk through the changes necessary to add super build support by reviewing `ch8/part-2`. All changes are confined to the `app` subfolder.
			To begin with, to have our app feel a bit more like a real CMake project (where our CMake project is the root folder), we’re going to move the `third-party` folder inside `app`. The structure now looks like this:

.

├── app

│ └── third-party

└── lib


			This is compared to how it was before:

.

├── app

├── third-party

└── lib


			This is more representative of a real CMake project and will make enabling and disabling super builds a bit easier.
			We’ll start by looking at the changes in `ch8/part-2/app/CMakeLists.txt`. The first and only changes are right at the top of the file:

option(SUPERBUILD "执行超级构建(或不执行)" OFF)

if(SUPERBUILD)

add_subdirectory(third-party)

return()

endif()


			The first change is simply the addition of a new CMake option to enable or disable super builds. It’s defaulted to `OFF` for now, but it’s easy to change, and we, of course, have some new CMake presets we’ll cover later in this section, which provide a few different permutations people might want to use.
			Next comes the new functionality that only runs if super builds are enabled. It’s worth emphasizing that everything in our application’s `CMakeLists.txt` file stays exactly the same as before. We can easily revert to the earlier way of building if we wish to by simply setting `SUPERBUILD` to `OFF`. Even using both at the same time is easy and convenient (we’ll get into how to do this a bit later).
			Inside the super build condition, we call `add_subdirectory` on the `third-party` folder (this was one of the reasons we moved it inside the `app` folder to make composing our `CMakeLists.txt` scripts a little easier). In this instance, we could have used `include` instead of `add_subdirectory`. However, the advantage of `add_subdirectory`, in this case, is that when CMake is processing the file, it will process it where it currently lives in the folder structure. This means that `${CMAKE_CURRENT_SOURCE_DIR}` will refer to `path/to/ch8/part-2/app/third-party`, not `path/to/ch8/part-2/app`.
			If we’d instead used `include` and referred to the `CMakeLists.txt` file explicitly (`include(third-party/CMakeLists.txt)`), this would have the effect of copying the contents of the file directly to where the `include` call is (we’re effectively substituting the `include` call with the contents of the file). The issue with this is that `${CMAKE_CURRENT_SOURCE_DIR}`, which is used inside `ch8/part-2/app/third-party/CMakeLists.txt`, will refer to `ch8/part-2/app` instead of `ch8/part-2/app/third-party`. This means that the behavior of our third-party `ExternalProject_Add` commands will differ when called as part of a super build instead of when being invoked directly from the `third-party` folder. In this instance, the relative paths we’re using to refer to our internal library files will not resolve correctly and the location of the third-party install folder will be in `app` instead of `app/third-party`. We want to avoid this, so `add_subdirectory` is a better choice in this instance.
			Let’s now follow the execution flow CMake will take and look at the changes made to our third-party `CMakeLists.txt` file found in `ch8/part-2/app/third-party`. The convenient thing is that we’ve kept the ability to build the third-party dependencies separately if we want to. Things will work just as they did before when we’re not using a super build.
			The first change is to configure where the third-party build artifacts should go depending on whether we’re using a super build or not. If we’re using a super build, we want to use a separate build folder to store all the third-party dependency files. The reason for this is to keep the same separation we had before when building our third-party dependencies separately. If we don’t do this, our third-party build artifacts will be added to the same build folder as our main application. This means that if we want to remove our application build folder and rebuild, we need to build all our third-party dependencies again.
			One way to achieve this is with the following check:

if(SUPERBUILD AND NOT PROJECT_IS_TOP_LEVEL)

set(PREFIX_DIR ${CMAKE_CURRENT_SOURCE_DIR}/build)

else()

set(PREFIX_DIR ${CMAKE_CURRENT_BINARY_DIR})

endif()


			When using a super build, the build files will be added to `third-party/build` (`CMAKE_CURRENT_SOURCE_DIR` will refer to the folder that the current `CMakeLists.txt` file is being processed in). The one downside to this approach is that we’ve hard-coded where the third-party build folder is, and it cannot be modified by users (they could still build the third-party libraries separately and specify `CMAKE_CURRENT_BINARY_DIR` using `-B`, but not as part of a super build).
			To make things more flexible, we can provide a CMake cache variable with a reasonable default that users can override:

set(THIRD_PARTY_BINARY_DIR

"${CMAKE_SOURCE_DIR}/build-third-party"

CACHE STRING "第三方构建文件夹")

if(NOT IS_ABSOLUTE ${THIRD_PARTY_BINARY_DIR})

set(THIRD_PARTY_BINARY_DIR

"\({CMAKE_SOURCE_DIR}/\){THIRD_PARTY_BINARY_DIR}")

endif()

set(PREFIX_DIR ${THIRD_PARTY_BINARY_DIR})


			Here, we introduce `THIRD_PARTY_BINARY_DIR`, which we default to `app/build-third-party` (we could have stuck with `third-party/build`, but this way, our app and third-party build folders will stay closer together to make clean-up easier). We also ensure to handle if a user provides a relative path by using `if(NOT IS_ABSOLUTE ${THIRD_PARTY_BINARY_DIR})`. In this case we append the path provided to `CMAKE_SOURCE_DIR` (which, in our case, will be the `app` folder). This check also treats paths beginning with `~/` on macOS and Linux as absolute paths. It’s a little more code, but the increased flexibility can be incredibly useful for our users.
			The extra check of `AND NOT PROJECT_IS_TOP_LEVEL` is to guard against someone accidentally setting `SUPERBUILD` to `ON` when building the third-party dependencies separately as their own project. `SUPERBUILD` will have no effect if this is the case. `PROJECT_IS_TOP_LEVEL` can be used to check whether the preceding call to `project` was from the top-level `CMakeList.txt` file or not (for more information, please see [`cmake.org/cmake/help/latest/variable/PROJECT_IS_TOP_LEVEL.html`](https://cmake.org/cmake/help/latest/variable/PROJECT_IS_TOP_LEVEL.html)). 
			By introducing the `PREFIX_DIR` variable, we can later pass this to `PREFIX` in the `ExternalProject_Add` command, along with the dependency name, to ensure that the build files wind up in `app/build-third-party/<dep>` instead of `app/build`. When building normally, `CMAKE_CURRENT_BINARY_DIR` will resolve to whatever the user sets as their build folder as part of the third-party CMake configure command.
			We use `PREFIX_DIR` in the `ExternalProject_Add` command like so:

ExternalProject_Add(

...

PREFIX ${PREFIX_DIR}/

BINARY_DIR \({PREFIX_DIR}/<name>/build/\){build_type_dir}

INSTALL_DIR ${CMAKE_CURRENT_SOURCE_DIR}/install

...)


			The `PREFIX` argument to `ExternalProject_Add` sets the root directory for the dependency. We use the new `PREFIX_DIR` variable, along with the external project name. This ensures that all dependencies we’re building are isolated from one another, avoiding any risk of file naming collisions when downloading and building them. It also makes it easier to rebuild a specific dependency by deleting one of the subfolders and then running the CMake configure and build commands again. Lastly, we will also update `BINARY_DIR` to refer to `PREFIX_DIR` instead of `CMAKE_CURRENT_BINARY_DIR` to handle whether we’re building things as a super build or not.
			Each dependency has the same changes applied; there’s only one more change at the end of the file, also wrapped inside a super build check. The change is yet another call to `ExternalProject_Add`, only this time with a bit of a twist: we’re calling it with the source directory of our top-level CMake project:

if(SUPERBUILD AND NOT PROJECT_IS_TOP_LEVEL)

ExternalProject_Add(

${CMAKE_PROJECT_NAME}_superbuild

DEPENDS SDL2 bgfx mc-gol mc-draw

SOURCE_DIR ${CMAKE_SOURCE_DIR}

BINARY_DIR ${CMAKE_BINARY_DIR}

CMAKE_ARGS -DCMAKE_PREFIX_PATH=${CMAKE_PREFIX_PATH}

-DSUPERBUILD=OFF ${build_type_arg}

INSTALL_COMMAND "")

endif()


			We start by naming the external project the same as the CMake project currently being processed, with `_superbuild` appended to differentiate them (`CMAKE_PROJECT_NAME` refers to the top-level CMake project). We then ensure that the project depends on all our third-party dependencies using the `DEPENDS` argument so it will only be built when they all are ready. `SOURCE_DIR` is where we set `ExternalProject_Add` to look for our root `CMakeLists.txt` file (`CMAKE_SOURCE_DIR` refers to the top level of the current CMake source tree, which is usually synonymous with the folder containing our root `CMakeLists.txt` file). We also let it know where to find the third-party dependencies with `CMAKE_PREFIX_PATH`.
			We next pass through the `SUPERBUILD` option, only this time, we’re explicitly setting it to `OFF`. The key insight to this approach is realizing that we’re calling our root `CMakeLists.txt` script recursively. The second time through, as `SUPERBUILD` is `OFF`, we’ll process the file as normal after waiting for all our dependencies to become available (this is what `DEPENDS` guarantees). Finally, we need to disable the `INSTALL_COMMAND` option, as our application doesn’t currently provide an install target (we haven’t added any install functionality), so we just set it to an empty string.
			Looping back to the top-level `CMakeLists.txt` file in `ch8/part-2/app`, after the call to `add_subdirectory`, we simply `return` and finish processing (remember, we will have processed this file in its entirety in the `ExternalProject_Add` command with `SUPERBUILD` set to `OFF`).
			Those are all the changes we need to support super builds. To make enabling them a bit easier, we’ve also added a new CMake configure preset called `multi-ninja-super`. It uses a new hidden configure preset called `super`, as shown here, with `SUPERBUILD` set to `ON`:

"name": "super",

"hidden": true,

"cacheVariables": {

"SUPERBUILD": "ON"

}


			The new preset we added then inherits `super` as well as `multi-ninja`:

"name": "multi-ninja-super",

"inherits": ["multi-ninja", "super"]


			To take advantage of this, from `ch8/part-2/app`, run the following commands:

cmake --preset multi-ninja-super

cmake --build build/multi-ninja-super


			Taking it one step further, we can also add a build preset that uses the new configure preset, and finally a workflow preset that uses them both together:

构建预设

"name": "multi-ninja-super",

"configurePreset": "multi-ninja-super"

工作流预设

"name": "multi-ninja-super",

"steps": [

{

"type": "configure",

"name": "multi-ninja-super"

},

{

"type": "build",

"name": "multi-ninja-super"

}

]


			This allows us to configure and build everything in one command:

cmake --workflow --preset multi-ninja-super


			Ninja and super builds on Windows
			It is possible, if you are using super builds on Windows, that after the first build, you may hit the `ninja: error: failed recompaction: Permission denied` error. This appears to be a Windows-specific issue with Ninja related to file paths. Running the same CMake command again will resolve the error. However, if the issue persists, it may be worth experimenting with other generators on Windows such as Visual Studio.
			One last reminder is that, by default, this will build in a debug configuration (`Debug`), which we might not want, so adding `"configuration": "Release"` to our build preset is likely a good idea (see `ch8/part-2/app/CMakePreset.json` for a full example).
			We can then run our app as usual from the project root directory (`ch8/part-2/app`) with the following command (not forgetting to first build the shaders):

./compile-shaders-.sh/bat

./build/multi-ninja-super/Release/minimal-cmake_game-of-life_window


			Earlier in the section, we briefly touched on the fact that super builds and regular builds can coexist seamlessly. For example, we can configure our existing `multi-ninja` preset after configuring and building the super build, as we know all third-party dependencies are now downloaded and installed (`multi-ninja` still has `CMAKE_PREFIX_PATH` set to `app/third-party/install`). This can be quite useful as configuring `multi-ninja-super` again and then building the application will trigger a check of all dependencies. This is usually quite fast, but if the dependencies are stable and aren’t changing often, creating a separate non-super build folder for active development will avoid this. You essentially have two build folders underneath `build`:

build/multi-ninja-super # 超级构建

build/multi-ninja # 常规构建


			We get these two build folders thanks to our use of CMake presets and the `binaryDir` property, which lets us use the current preset name (`${sourceDir}/build/${presetName}`). *Chapter 5*, *Streamlining CMake Configuration*, contains more information about this topic for reference.
			One final gotcha to mention is that when using a multi-config generator, changing the config passed to the build command will only trigger the application to rebuild in the new configuration, not all the dependencies. To ensure that all dependencies are rebuilt, it is necessary to configure before running the build command (`cmake --``build <build-folder>`).
			For example, this might look as follows:

默认构建为 Debug

cmake --preset multi-ninja-super

在 Debug 模式下构建所有内容

cmake --build build/multi-ninja-super

仅在 Release 模式下构建应用程序

cmake --build build/multi-ninja-super --config Release

必须先重新运行配置

cmake --preset multi-ninja-super

现在以 Release 模式构建所有内容

cmake --build build/multi-ninja-super --config Release


			Using a single config generator behaves the same. It’s just something to be aware of, as normally, changing the `--config` option passed to the CMake build command with multi-config generators will rebuild everything in the new configuration (this is, unfortunately, one downside of using the super build pattern, but it’s a fairly minor one given the benefits it brings).
			The last thing to be aware of with super builds is clean-up. Before, we only needed to delete the build folder to remove all build artifacts, but now, because we’ve split things, there are two folders (three, if you count the install folder) to delete. For completeness, to get back to a clean state, remember to delete the following folders (default locations listed here):

app/build

app/build-third-party

app/third-party/install


			With super builds, we can now run a single CMake command and have our project downloaded and built in one step. This is a substantial improvement on what we had before, but there’s one last issue to address: automating the shader compilation step. We’ll look at how to achieve this in the next section.
			Automating scripts with CMake
			We’ve removed a lot of manual steps that we were dealing with at the start of the chapter, but one remains. This is the requirement to build the shaders needed by bgfx to transform and color our geometry. Up until now, we’ve been relying on running custom `.bat`/`.sh` scripts from the `app` folder before running our *Game of Life* application, but there’s a better possibility. In this section, we’ll show how to make this process part of the build itself, and use CMake to achieve a cross-platform solution without the need for OS-specific scripts.
			To start with, we’re going to do away with our existing `.bat`/`.sh` scripts and replace them with `.cmake` files. We’ll pick macOS as the first platform to update; the file will be called `compile-shader-macos.cmake`, and will live under a new `cmake` folder in the `app` directory (equivalent files for Windows and Linux will differ in the exact same way as the existing scripts).
			We’re eventually going to invoke these scripts from our top-level `CMakeLists.txt` file. However, before we do, it’s useful to introduce a CMake operation we haven’t covered so far, and that is the ability to run a CMake script from the command line using `cmake –P` (see [`cmake.org/cmake/help/latest/manual/cmake.1.html#run-a-script`](https://cmake.org/cmake/help/latest/manual/cmake.1.html#run-a-script) for more details). As a quick example, we can create a file called `hello-world.cmake` and add a simple `message` command to output `Hello, world!`:

hello-world.cmake

message(STATUS "Hello, World!")


			If we invoke it from the command line by running `cmake -P hello-world.cmake`, we’ll see the following output:

-- Hello, World!


			(If we include `hello-world.cmake` in a `CMakeLists.txt` file, it will run at configure time and `Hello, World!` will be printed then).
			The CMake functionality to invoke a script also supports first providing CMake variables from the command line using the familiar `-D` argument introduced in *Chapter 2*, *Hello, CMake!* (importantly appearing before `-P`):

cmake -DA_USEFUL_SETTING=ON -P cmake-script.cmake


			We’ll use this in our shader script example a little later to help control the output when invoking the command.
			CMake provides a wealth of useful modules and functions to support file and path manipulation. We’re going to take advantage of them as we craft a CMake script to build our shaders. It’s important to ensure that we have a consistent working directory when invoking `compile-shader-<platform>.cmake`. There are some subtle differences when running from a top-level CMake project and invoking the script using `-P` directly. For example, if we decided to use `CMAKE_SOURCE_DIR` when specifying our paths, this would work correctly when running from the top-level `CMakeLists.txt` file, and when invoking `compile-shader-<platform>.cmake` from the app folder (e.g., `cmake -P cmake/compile-shader-macos.cmake`), but would fail if a user tried to run it from the nested `cmake` folder itself. This is because `CMAKE_SOURCE_DIR` will default to the folder holding the top-level `CMakeLists.txt` file when part of a CMake configure step, and to the folder CMake was invoked from when running `cmake -P path/to/cmake-script.cmake` (this is the same problem we had with the `.``sh`/`.bat` scripts).
			To account for these differences, we’re going to use a CMake path-related function to ensure that our script’s working directory is always set to the `app` folder. The function we’re going to use is called `cmake_path`. Added in CMake `3.20`, `cmake_path` provides utilities to manipulate paths, decoupled from the filesystem itself (to learn more about `cmake_path`, see [`cmake.org/cmake/help/latest/command/cmake_path.html`](https://cmake.org/cmake/help/latest/command/cmake_path.html)). In our case, we’d like to find the directory containing our `compile-shader-<platform>.cmake` file. This can be performed with the following command:

cmake_path(

GET CMAKE_SCRIPT_MODE_FILE PARENT_PATH

COMPILE_SHADER_DIR)


			In the preceding command, we can see the following arguments:

				*   The first argument, `GET`, describes the type of operation we’d like to perform.
				*   The next argument, `CMAKE_SCRIPT_MODE_FILE` ([`cmake.org/cmake/help/latest/variable/CMAKE_SCRIPT_MODE_FILE.html`](https://cmake.org/cmake/help/latest/variable/CMAKE_SCRIPT_MODE_FILE.html)), holds the full path to the current script being processed. It’s important to note that this variable is only set when using `cmake -P` to execute the script. It will not be populated when using `include`. A check for this variable can be included at the top of the script and a warning issued if a user incorrectly tries to include it (see `ch8/part-3/app/cmake/compile-shader-<platform>.cmake` for an example).
				*   The following argument, `PARENT_PATH`, is the component to retrieve from the preceding path. In this case, we are requesting the parent path of the current script file (essentially, the directory it is in). To see what other components are available, please see [`cmake.org/cmake/help/latest/command/cmake_path.html#decomposition`](https://cmake.org/cmake/help/latest/command/cmake_path.html#decomposition).
				*   The final argument, `COMPILE_SHADER_DIR`, is the variable to populate the result with.

			Now we have this directory, we just need to go one level up to reach the `app` folder. We can achieve this using the same command, only substituting the first argument with the variable we populated in the preceding command.

cmake_path(

GET COMPILE_SHADER_DIR PARENT_PATH

COMPILE_SHADER_WORKING_DIR)


			We now have a consistent and portable way to automatically retrieve the `app` folder. We can use the `COMPILE_SHADER_WORKING_DIR` variable in the following CMake script commands.
			CMake provides another useful utility called `file` that can be used for a wide array of file and path manipulations (as opposed to `cmake_path`, this command does interact with the filesystem). In our simple case, we just need to create a new folder (the `build` folder in the `app/shader` directory), which can be achieved with the following `file` command:

file(

MAKE_DIRECTORY

${COMPILE_SHADER_WORKING_DIR}/shader/build)


			The first argument is the operation to perform, and the second is where to do it. This ensures that we now have an output directory to hold our compiled shader files. To learn more about the `file` command, see [`cmake.org/cmake/help/latest/command/file.html`](https://cmake.org/cmake/help/latest/command/file.html).
			We’re next going to make use of a CMake command called `execute_process`, which allows us to run child processes from within a CMake script. In this case, we’re going to replicate the contents of our `compile_shader_macos.sh` file inside the `execute_process` command. The following is an example of what this looks like:

execute_process(

COMMAND

third-party/install/bin/shaderc

-f shader/vs_vertcol.sc

-o shader/build/vs_vertcol.bin

--platform osx --type vertex

-i ./ -p metal --verbose

WORKING_DIRECTORY ${COMPILE_SHADER_WORKING_DIR})


			We first call `execute_process`, and then pass the `COMMAND` argument. What follows are the same instructions we would pass at the command line, which were previously invoked from our `.sh` script. We then pass one more argument, `WORKING_DIRECTORY`, to specify where the listed commands should be run relative to (this is populated by the variable we created earlier referring to the `app` directory, regardless of whether the script is being run using `cmake -P` or whether it is being invoked from a `CMakeLists.txt` file). We can now build our shaders using `cmake -P path/to/app/cmake/compile-shader-macos.cmake` from any folder of our choosing (to understand what else `execute_process` can do, see [`cmake.org/cmake/help/latest/command/execute_process.html`](https://cmake.org/cmake/help/latest/command/execute_process.html)).
			Before we look at invoking our new scripts from `CMakeLists.txt` as part of the main build, there’s a small improvement we can make to our new `compile-shader-macos.cmake` file. Up until now, we’ve been passing the `--verbose` flag to the *bgfx* `shaderc` program to show the full output of compiling our shaders. This can sometimes be useful, but it’s unlikely that we want to see this as part of the main build every time we either configure or build using CMake. Even with the `--verbose` argument removed, the output is still generated when invoking `shaderc`, which, in the default case, we might want to hide.
			To work around this, let’s introduce a new CMake variable called `USE_VERBOSE_SHADER_OUTPUT` to our `compile-shader-<platform>.cmake` scripts. This will default to `OFF` and will control two internal CMake variables. The first is `VERBOSE_SHADER_OUTPUT`, which will substitute the direct reference to `--verbose`:

option(

USE_VERBOSE_SHADER_OUTPUT

"显示着色器编译输出" OFF)

如果(USE_VERBOSE_SHADER_OUTPUT)

set(VERBOSE_SHADER_OUTPUT --verbose)

endif()

execute_process(

COMMAND

...

${VERBOSE_SHADER_OUTPUT}

WORKING_DIRECTORY ${COMPILE_SHADER_WORKING_DIR})


			When we invoke `cmake -P cmake/compile-shader-<platform>.cmake`, we now won’t, by default, see the full output from `shaderc`, but we can easily enable it again by setting `VERBOSE_SHADER_OUTPUT` to `ON`:

cmake --verbose,shaderc 仍然会将一些信息输出到终端,这可能会干扰正常的 CMake 构建输出。为了隐藏这些信息,我们可以引入另一个 CMake 变量叫做 QUIET_SHADER_OUTPUT,然后将其设置为 ERROR_QUIET(或在 Linux 上设置为 OUTPUT_QUIET)以抑制execute_process命令的所有输出(OUTPUT_QUIET 和 ERROR_QUIET 分别对应标准输出和标准错误输出,例如 C 语言中的fprintfstdoutstderr,以及 C++中的std::coutstd::cerr)。

        我们的最终代码如下:
if(USE_VERBOSE_SHADER_OUTPUT)
  set(VERBOSE_SHADER_OUTPUT --verbose)
else()
  set(QUIET_SHADER_OUTPUT ERROR_QUIET OUTPUT_QUIET)
endif()
execute_process(
  COMMAND
  ...
  ${VERBOSE_SHADER_OUTPUT}
  ${QUIET_SHADER_OUTPUT}
  WORKING_DIRECTORY ${COMPILE_SHADER_WORKING_DIR})
        这意味着我们目前无法拥有非详细输出;要么全开,要么全关,但这通常足以满足我们调用这些脚本的需求。所有`compile-shader-<platform>.cmake`文件中的变化几乎是相同的,现在我们已经准备好查看如何从我们应用的`CMakeLists.txt`文件中调用这些脚本。

        从 CMakeLists.txt 调用 CMake 脚本

        我们的脚本现在可以从`CMakeLists.txt`文件中调用。首先,我们需要根据构建的平台引用正确的文件。我们可以通过简单的条件检查来实现:
if(WIN32)
  set(COMPILE_SHADER_SCRIPT
      ${CMAKE_SOURCE_DIR}/cmake/compile-shader-windows.cmake)
elseif(LINUX)
  set(COMPILE_SHADER_SCRIPT
      ${CMAKE_SOURCE_DIR}/cmake/compile-shader-linux.cmake)
elseif(APPLE)
  set(COMPILE_SHADER_SCRIPT
      ${CMAKE_SOURCE_DIR}/cmake/compile-shader-macos.cmake)
endif()
        现在我们可以引用`COMPILE_SHADER_SCRIPT`来获取适合我们平台的文件。接下来有两种不同的方式可以自动调用我们的脚本。一个方法是使用`include`将脚本直接引入到我们的`CMakeLists.txt`文件中:
include(${COMPILE_SHADER_SCRIPT})
        不幸的是,现有的`compile-shader-<platform>.cmake`文件不能直接与此方法一起使用。我们需要更新如何填充`COMPILE_SHADER_WORKING_DIR`。我们可以通过以下检查来实现:
if(CMAKE_SCRIPT_MODE_FILE AND NOT CMAKE_PARENT_LIST_FILE)
  # existing approach
else()
  set(COMPILE_SHADER_WORKING_DIR ${CMAKE_SOURCE_DIR})
endif()
        当 CMake 脚本作为`CMakeLists.txt`文件的一部分被调用时,`CMAKE_SCRIPT_MODE_FILE`不会被设置(也叫填充),而`CMAKE_PARENT_LIST_FILE`是包含它的 CMake 文件的完整路径。通过使用这两个检查,我们可以确保只有在文件以脚本模式运行且未被其他文件包含时,才会执行第一个分支。如果我们知道该文件是从`CMakeLists.txt`文件中调用的,我们可以简单地将`COMPILE_SHADER_WORKING_DIR`设置为`CMAKE_SOURCE_DIR`(它将是包含根`CMakeLists.txt`文件的文件夹),这样一切就会按预期工作。

        使用这种方法时,每次配置时都会构建着色器。还有一种替代方法可以代替使用`include`,那就是使用我们之前遇到过的 CMake 命令`add_custom_command`。通过`add_custom_command`,我们可以指定一个目标和命令执行的时机(在下面的示例中,我们使用`POST_BUILD`在应用程序构建完成后调用该命令)。完整的命令如下:
add_custom_command(
  TARGET ${PROJECT_NAME}
  POST_BUILD
  COMMAND ${CMAKE_COMMAND} -P ${COMPILE_SHADER_SCRIPT}
  WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
  VERBATIM)
        这个命令对我们的用户非常方便,并确保在应用程序运行之前先编译着色器。缺点是,目前该命令可能比严格必要时运行得更频繁,如果命令变得更复杂并开始需要更长时间运行,将来可能会成为一个问题。

        还有一个`add_custom_command`的替代版本,它不是接受一个目标(`TARGET`),而是接受一个输出文件(`OUTPUT`)。通过`DEPENDS`参数可以列出依赖项,只有在输出文件需要更新时,命令才会执行。这种方法非常高效,但遗憾的是,设置起来稍微复杂一些,而由于前面的命令执行较快,当前使用的是较简单的版本(要了解更多关于`add_custom_command`的信息,请查看[`cmake.org/cmake/help/latest/command/add_custom_command.html`](https://cmake.org/cmake/help/latest/command/add_custom_command.html)))。

        添加新命令后,我们已经拥有了使用一个命令构建整个应用程序和附带资源(着色器)所需的一切。从`ch8/part-3/app`目录下,运行以下命令:
cmake --workflow --preset multi-ninja-super
        一旦构建完成,剩下的就是运行应用程序本身:
./build/multi-ninja-super/Release/minimal-cmake_game-of-life_window
        当然,我们也可以像之前一样使用 CMake 的`--preset`和`--build`参数分别进行配置和构建。不过,利用`--workflow`在这里特别方便。

        审查`ch8/part-3/app/CMakeLists.txt`和`ch8/part-3/app/cmake/compile-shader-<platform>.cmake`文件,查看上下文中的所有内容。你可能会注意到一个小变化,那就是在每个`compile-shader-<platform>.cmake`文件的顶部,加入了一个简化的检查,以确保它们必须在脚本模式下运行:
if(NOT CMAKE_SCRIPT_MODE_FILE)
  message(
    WARNING
      "This script cannot be included, it must be executed using `cmake -P`")
  return()
endif()
        提醒一下,`ch8/part-2/app`和`ch8/part-3/app`。

        在嵌套文件中设置选项

        我们为简化和精简应用程序构建所采取的步骤已经带来了巨大的变化,并将在未来节省时间和精力。然而,我们在这个过程中不幸失去了一样东西,那就是调整依赖项构建方式的能力。之前,当我们使用`FetchContent`并直接构建依赖项时,我们可以传递各种构建选项来设置是否将特定库构建为静态库或共享库。在*第七章*中,*为库添加安装支持*,当我们考虑单独构建库并手动安装时,我们也可以决定如何构建它们。不幸的是,通过使用`ExternalProject_Add`,我们失去了一些灵活性,因为没有额外的支撑框架,无法直接将选项传递给`ExternalProject_Add`命令。

        幸运的是,失去的灵活性并不难恢复。所需的仅仅是创建我们自己的 CMake 选项,然后将它们作为`CMAKE_ARGS`参数的一部分转发给内部的`ExternalProject_Add`命令。

        例如,如果我们查看`ch8/part-4/app/third-party/CMakeLists.txt`,我们可以看到在文件顶部,我们添加了两个新选项:
option(
  MC_GOL_SHARED
  "Enable shared library for Game of Life" OFF)
option(
  MC_DRAW_SHARED "Enable shared library for Draw" OFF)
        我们使用了与库中实际存在的名称相同的名称,以保持一致性,但如果我们选择将变量与我们正在构建的应用程序分组,仍然可以自由调整命名。然后,我们将这些新值传递给`ExternalProject_Add`,其形式如下:
ExternalProject_Add(
  mc-draw
  ...
  CMAKE_ARGS ... -DMC_DRAW_SHARED=${MC_DRAW_SHARED}
  ...)
        这使我们即使在使用`ExternalProject_Add`引入依赖项时,也能决定是否将其构建为静态库或共享库。我们不希望更改的库暴露的其他选项可以硬编码。

        我们还需要对`compile-shader-<platform>.cmake`脚本做同样的事情。由于我们从`CMakeLists.txt`文件调用脚本的方式,`USE_VERBOSE_SHADER_OUTPUT`设置不会自动检测到(如果我们使用`include`,它会被拾取并添加到主项目的`CMakeCache.txt`文件中)。为了解决这个问题,我们只需将该设置添加到`CMakeLists.txt`文件中,然后将其传递给脚本的调用:
option(
  USE_VERBOSE_SHADER_OUTPUT 
  "Show output from shader compilation" OFF)
...
add_custom_command(
  TARGET ${PROJECT_NAME}
  POST_BUILD
  COMMAND
    ${CMAKE_COMMAND} -D USE_VERBOSE_SHADER_OUTPUT=${USE_VERBOSE_SHADER_OUTPUT}
    -P ${COMPILE_SHADER_SCRIPT}
  WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
  VERBATIM)
        请参阅`ch8/part-4/app/CMakeLists.txt`以获取完整示例。

        这种变量传递的需求主要出现在`ExternalProject_Add`中。当使用超级构建时,我们需要记住遵循相同的方法,将选项传递到嵌套的应用程序中。这就是为什么有时使用普通的非超级构建项目会很有用(请参见`ch8/part-4/app/CMakePresets.json`中的`multi-ninja`和`multi-ninja-super` CMake 预设作为示例)。配置应用程序的选项数量通常较少,您可以将剩余的、不需要更改的选项直接设置在`ExternalProject_Add`调用中,但有时提供一种更改这些选项的方法会很有用。

        安装应用程序

        本章的最后,我们将看一个最终的添加内容,那就是如何为我们的应用程序添加安装支持。这有助于为打包做好准备,并确保我们的应用程序具有可移植性。

        我们要做的第一个更改是将一个`CMAKE_INSTALL_PREFIX`变量添加到我们应用程序的`CMakePresets.json`文件中,以确保我们的应用程序安装在相对于项目的路径中:
"CMAKE_INSTALL_PREFIX": "${sourceDir}/install"
        接下来的一些更改将专门针对`ch8/part-5/app/CMakeLists.txt`。首先,我们需要像为库一样包含`GNUInstallDirs`,以访问标准的 CMake 安装位置(在这个例子中,我们只关心`CMAKE_INSTALL_BINDIR`)。

        我们想要实现的高层目标是拥有一个可重定位的文件夹,包含我们的应用程序可执行文件、需要由应用程序加载的共享库以及运行时所需的资源(我们编译的着色器文件)。我们可以通过以下 CMake 安装命令来实现这一目标。

        第一个步骤很简单,它将应用程序的可执行文件复制到`install`文件夹:
install(
  TARGETS ${PROJECT_NAME}
  RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
        我们提供目标以复制,并使用`RUNTIME`类型来指代可执行文件,同时指定复制目标路径(这通常是`bin`,并且会相对于我们在`CMakePresets.json`文件中提供的`CMAKE_INSTALL_PREFIX`变量)。

        接下来,我们需要复制应用程序启动时需要加载的共享库文件。由于我们正在开发一个跨平台应用程序,为了简化起见,我们将所有共享库文件(Windows 上的`.dll`、macOS 上的`.dylib`和 Linux 上的`.so`)复制到与应用程序相同的文件夹。这与我们之前在 Windows 上做的非常相似,但现在我们将在所有平台上做同样的事,以保持一致性。复制这些文件的简化安装命令如下所示:
install(
  FILES
    $<TARGET_FILE:SDL2::SDL2>
    ...
        由于我们的*生命游戏*和*绘图*库可以编译为静态库或共享(动态)库,我们需要检查目标是否是正确的类型,然后再复制它,否则我们将不必要地复制`.lib`/`.a`静态库文件。

        我们可以通过这里显示的生成器表达式来实现:
$<$<STREQUAL:$<TARGET_PROPERTY:minimal-cmake::line,TYPE is equal to SHARED_LIBRARY, then substitute the path to the shared library, otherwise, do nothing (the expression will evaluate to an empty string).
			Sticking with the dynamic libraries, there’s another minor change we need to make. If you recall *Chapter 4*, *Creating Libraries for FetchContent*, we discussed the topic of making libraries relocatable on macOS and Linux by changing the `RPATH` variable of the executable. We achieved this by using `set_target_properties` to update the `BUILD_RPATH` property of the executable. To ensure things work correctly for both build and install targets, we need to update this command slightly. The changes are shown here:

set_target_properties(

${PROJECT_NAME}

PROPERTIES

INSTALL_RPATH

"\(<\)<PLATFORM_ID:Linux>:\(ORIGIN>\)<$<PLATFORM_ID:Darwin>:@loader_path>"

对于 BUILD_RPATH 属性,我们将 INSTALL_RPATH 属性更新为在 Linux 上解析为 $ORIGIN,在 macOS 上解析为 @loader_path(这样做是为了使可执行文件在与自身相同的文件夹中查找共享库)。由于我们希望正常构建的目标和已安装的目标表现一致,因此我们还将 BUILD_WITH_INSTALL_RPATH 设置为 TRUE,这大致相当于将相同的生成器表达式传递给 BUILD_RPATH,就像之前一样(我们在这里的意思是 BUILD_RPATH 应该与 INSTALL_RPATH 相同)。

        现在,通过将共享库分别复制到构建和安装文件夹中,我们实现了构建目标和安装目标之间的相同行为。构建完成后,我们可以安全地移除已安装的依赖项(即在 `app/third-party/install` 中的已安装库文件),并继续运行应用程序(然而,这样会破坏我们重新编译和构建的能力,因为在没有先通过生成新的超级构建或配置并从 `third-party` 文件夹构建的情况下,无法恢复第三方依赖项)。

        `RPATH` 处理是一个复杂的话题,这里提供的解决方案只是处理共享库安装的一种方法。要了解更多内容,请参考 CMake 属性文档页面上的与 `RPATH` 相关的变量([`cmake.org/cmake/help/latest/manual/cmake-properties.7.html`](https://cmake.org/cmake/help/latest/manual/cmake-properties.7.html))以及 CMake 社区 Wiki 上关于 `RPATH` 处理的部分([`gitlab.kitware.com/cmake/community/-/wikis/doc/cmake/RPATH-handling`](https://gitlab.kitware.com/cmake/community/-/wikis/doc/cmake/RPATH-handling))。

        为了确保完全的跨平台兼容性,还需要进行最后一个添加,才能正确处理在 Linux 上为我们的应用程序安装 SDL 2 依赖项。当 SDL 2 在 Linux 上构建时,它会提供作为共享库的一部分的多个文件。这些文件与 `libSDL2-2.0.so.0.3000.2` 相关,但这不是动态链接器查找的文件。`libSDL2-2.0.so.0` 文件是指向 `libSDL2-2.0.so.0.3000.2` 的符号链接,它是动态链接器在查找 SDL 2 库时使用的文件。确保我们安装这两个文件非常重要;否则,应用程序将在运行时找不到共享库。

        为了支持这一点,我们只需要在 `add_custom_command` 和 `install` 调用中再添加一条规则,那就是除了 `TARGET_FILE` 之外,还需要安装 `TARGET_NAME_SOFILE`,如下所示:
$<TARGET_FILE:SDL2::SDL2>
$<$<PLATFORM_ID:Linux>:$<TARGET_SONAME_FILE:SDL2::SDL2>>
        我们还添加了一个条件生成器表达式,只有在 Linux 平台上才会评估此表达式,因为在其他平台上不需要。

        最后,我们需要安装的最后一组文件是我们编译的着色器。我们将它们安装到与从 `app` 文件夹启动时相同的相对位置。实现这一目标的 CMake `install` 命令如下所示:
install(DIRECTORY ${CMAKE_SOURCE_DIR}/shader/build
        DESTINATION ${CMAKE_INSTALL_BINDIR}/shader)
        我们将`shader`下的`build`目录复制到我们安装可执行文件和共享库文件的相同文件夹中。这样,当我们从`ch8/part-5/app/install/bin`运行应用程序时,它在相对位置上看起来与之前从`ch8/part-5/app`运行时相同。

        经过最后的更改,我们拥有了安装应用程序所需的一切。我们现在只需要运行以下命令,从`ch8/part-5/app`文件夹构建并安装我们的应用程序:
cmake --build build/multi-ninja-super
        当从超级构建目录(`multi-ninja-super`)构建时,由于我们使用`ExternalProject_Add`来包装我们的项目(在`app/third-party/CMakeLists.txt`的末尾为`${CMAKE_PROJECT_NAME}_superbuild`),安装操作会在运行`cmake --build build/multi-ninja-super`时自动发生(就像我们直接从`third-party`文件夹构建第三方依赖一样)。在配置后首次构建时,无需传递`--target install`(尝试传递该参数实际上会导致错误,因为找不到安装目标)。之后的构建或从普通构建文件夹(例如`build/multi-ninja`)构建时,将需要`--target install`参数,因为安装目标将可用。最后,再次运行配置命令(例如`cmake --preset multi-ninja-super`)将重置此行为,以便用于后续的构建。

        如果我们之前使用`--workflow`预设构建了我们的应用程序,我们也可以改用 CMake 的`--install`命令:
cmake --install build/multi-ninja-super
        要启动应用程序,安装完成后,切换到`ch8/part-5/app/install/bin`目录并运行`./minimal-cmake_game-of-life_window`。可以自由地探索`ch8/part-5/app`的内容,以查看所有上下文,并通过运行我们到目前为止介绍的不同 CMake 命令进行实验(从`cmake --preset list`开始,然后运行`cmake --preset <preset>`是一个不错的起点)。还可以尝试将`app/install/bin`文件夹复制或移动到新的位置(或与之匹配的操作系统和架构的计算机上),以验证应用程序是否仍然能够启动并成功运行。

        总结

        是时候再休息一下,让我们所涵盖的内容稍微消化一下了。我们触及了一些高级 CMake 特性,如果你感到有些头晕也不用担心。你练习和实验这些概念的次数越多,理解会越来越清晰。

        在本章中,我们从手动安装自己的库转向利用`ExternalProject_Add`来自动化安装过程。这大大减少了设置项目时的繁琐步骤,并且是一种适用于未来项目的有用策略。接着,我们查看了为项目设置超级构建的过程,这提供了一种使用单个命令构建所有内容的方式,同时不失去我们所期望的灵活性。这种技术进一步简化了项目配置,是为用户创建应用程序时提供的极好的默认设置。

        之后,我们了解了 CMake 如何替代跨平台脚本,并自动化外部过程,如着色器编译,将其纳入核心构建,而不是事后考虑的事情。这可以节省很多在创建和支持定制的每个平台脚本时的开销。接下来,我们花了一些时间理解如何暴露嵌套依赖中的自定义点,以继续让用户控制他们如何构建库。没有这一点,用户可能不得不编辑 `CMakeLists.txt` 文件,进而带来另一个维护难题。最后,我们演示了如何安装应用程序,使其共享变得轻松。这使我们更接近一个完全可分发的应用程序,并摆脱了依赖项目布局来运行代码的束缚。

        在下一章,我们将介绍一个与 CMake 一起捆绑的配套工具——CTest。CTest 是一个非常有用的工具,帮助简化执行各种测试。我们将学习如何将测试添加到我们的库和应用程序中,并了解如何使用另一个 CMake 工具——CDash 来共享测试结果。


第三部分:总结

现在我们已经有了一个完全功能的应用程序,我们希望确保它能够继续正常运行,这时测试就显得尤为重要。我们将展示如何使用附带的 CMake 工具——CTest,为你的库和应用程序提供多种测试支持。在创建应用程序后,最关键的要求是能够将其分享给他人(并确保它不仅仅能在你的机器上运行)。这时打包就派上用场了;我们将展示如何使用另一个 CMake 相关工具——CPack,为 Windows、macOS 和 Linux 制作可分发的包。最后,我们将看看今天有哪些工具可以与 CMake 无缝集成,使得使用 CMake 变得更加容易。到书的最后,你将学到很多内容,但总还有更多可以探索的地方;因此,我们将花一些时间来看看还有哪些资源可以帮助你继续你的 CMake 之旅。

本部分包括以下章节:

  • 第九章为项目编写测试

  • 第十章为项目打包以便共享

  • 第十一章支持工具和下一步

第九章:为项目编写测试

在本章中,我们将讨论 CMake 如何帮助我们处理软件开发中的一个极其重要的方面:测试。测试在任何广泛使用或长期存在的项目中都是至关重要的,它有助于建立对功能的信心,并在添加和改进新特性时帮助避免回归。在一个正常的项目中,强烈建议从一开始就考虑测试;之后引入测试会是一项挑战。幸运的是,借助我们通过将功能拆分为独立库的项目结构,测试变得更加简单。

CMake 提供了一个名为 CTest 的附加应用程序,旨在将多种类型的测试整合到一个平台下。我们将看到如何将测试添加到我们的库中以及应用程序中,并了解如何利用 CTest 使从 CMake 运行它们变得更简单。

在本章中,我们将涵盖以下主要主题:

  • 理解 CTest

  • 向库中添加单元测试

  • 向应用程序添加端到端测试

  • 添加其他类型的测试

  • 使用 CDash 与 CTest

技术要求

为了跟随本章内容,请确保你已满足 第一章《入门》的要求。这些要求包括以下内容:

  • 一台运行最新 操作系统OS)的 Windows、Mac 或 Linux 计算机

  • 一个可工作的 C/C++ 编译器(如果你还没有,建议使用每个平台的系统默认编译器)

本章中的代码示例可以通过以下链接找到:github.com/PacktPublishing/Minimal-CMake

理解 CTest

在我们开始查看如何将 CTest 添加到现有的 CMakeLists.txt 文件并使用 ctest 命令行应用程序之前,理解 CTest 是什么,以及,或许更重要的是,理解它不是什麽,十分重要。

CMakeLists.txt 文件有两个组成部分,用于描述和添加测试,另一个是 ctest 命令行界面CLI),用于在编译测试后运行它们。CTest 本身并不是一个特定语言的测试库。完全可以在一个由 CMake 创建的项目中添加测试,而根本不使用 CTest(例如,通过创建一个依赖于著名测试库的单独测试可执行文件,如 Google Test (github.com/google/googletest) 或 Catch2 (github.com/catchorg/Catch2))。CTest 并不是这些库的替代品,后者在编写单元测试和集成测试方面提供了极好的支持。

测试类型

在本章中,我们将提到三种不同类型的测试:单元测试、集成测试和端到端测试。简而言之,单元测试通常测试一个独立的类型或组件,而不会引入任何依赖(例如,测试在特定数学类型(如向量或矩阵)上的操作就算作单元测试)。集成测试则模糊不清;它们通常位于一个范围内,涉及多个类型/类/组件的交互,以确保它们按预期执行(例如,在一个游戏中,集成测试可能会检查玩家角色和相机组件的交互)。在这一阶段,这也是引入桩(stubs)和/或模拟(mocks)的地方(这是一种避免创建昂贵或不可靠依赖(如数据库或远程 API)的方法),事情可能会变得更加复杂(由于在 CMake 上下文中单元测试和集成测试非常相似,本章将专注于单元测试)。最后,端到端测试模拟最终用户与应用程序的交互。这些测试通常是最复杂且最脆弱的,但仍然具有价值,保持少量的端到端测试可以确保整个应用程序按预期执行,而无需手动检查。这三种测试类型通常在测试金字塔中表示(单元测试在底部,集成测试居中,端到端测试在顶部)。一般建议是,金字塔越高,这种类型的测试就越少(单元测试很多,端到端测试很少),这主要由时间、可靠性和成本等指标驱动。

CTest 提供的是一个统一的接口,用于一起运行多种测试并以一致的方式报告失败。这在处理多种语言和风格的不同类型测试时非常有价值。例如,一个应用程序可能有一组使用 C 或 C++ 编写的单元测试和集成测试,这些测试被编译为一个独立的测试可执行文件,还有端到端测试,它启动并运行应用程序让它自我测试(通常通过脚本语言,如 Python,或内置的测试运行器),以及用来验证生成文件的临时 shell 脚本。通过 CTest,所有这些测试方法都可以结合起来并通过单一命令执行,输出结果只显示是否通过或失败。

CTest 是一个极其灵活的工具,支持多种不同类型的测试(甚至可以在测试阶段编译代码)。我们不会涵盖它的所有功能,但我们会尽力覆盖一些最有用的操作,并为您将来在自己的项目中使用 CTest 提供一个起点。

向库添加单元测试

现在我们了解了 CTest 提供的功能,让我们来看一个具体的例子,展示如何向现有的两个库添加单元测试,我们从mc-array开始。首先要说明的是,我们可以选择几种不同的方式来构建项目以支持测试。一个选择是创建一个与根目录CMakeLists.txt文件解耦的子目录:

.
├── CMakeLists.txt
├── ...
└── tests
    ├── CMakeLists.txt
    └── tests.cpp

使用这种设置,用户需要进入子文件夹并运行标准的 CMake 配置和构建命令。测试项目将会链接到顶层应用程序,可能依赖于使用SOURCE_DIR的相对路径的FetchContent

另一种选择是保持前述布局,但在启用测试选项时使用add_subdirectory来添加tests子文件夹。嵌套的CMakeLists.txt文件可以链接到库,因为在调用add_subdirectory时,库会在作用域内。如果库足够小,也可以完全省略tests文件夹,将测试可执行文件直接放在根级别的CMakeLists.txt文件中。

ch9/part-1/lib/array/CMakeLists.txt中,我们选择了将内容保持在一行,而在ch9/part-1/lib/gol/CMakeLists.txt中,我们使用了add_subdirectory。这只是为了给出两种版本的示例;内容几乎是相同的。唯一值得注意的区别是在引用项目中的测试文件时,在嵌套文件夹示例中指定了CMAKE_SOURCE_DIR。这是为了确保文件路径相对于根CMakeLists.txt文件,而不是tests子文件夹。此外,在调用ctest时,两个版本之间还需要一个细微的区别,我们将在本节后面讨论。

CMakeLists.txt 的 CTest 更改

ch9/part-1/lib/array/CMakeLists.txt开始,让我们一步步了解如何添加 CTest 支持。

第一个更改是添加一个名为MC_ARRAY_BUILD_TESTING的新选项,用于启用或禁用构建测试:

option(MC_ARRAY_BUILD_TESTING "Enable testing" OFF)

请注意,我们使用MC_ARRAY前缀来减少与其他项目发生冲突的可能性。我们还将其默认为OFF(CMake 常量表示假;我们也可以使用0NOFALSE,但在此上下文中OFF最为清晰。有关更多信息,请参见cmake.org/cmake/help/latest/command/if.html#constant)。我们这样做是为了成为一个负责任的公民,防止下游用户在忘记禁用MC_ARRAY_BUILD_TESTING时,不小心构建测试。

CMakeLists.txt文件的底部,我们检查MC_ARRAY_BUILD_TESTING选项是否已定义,只有在其定义时,我们才会引入 CTest 模块:

include(CTest)

当我们包含此模块时,CMake 会创建一个新的 BUILD_TESTING 选项。不幸的是,这个选项默认设置为 ON,从用户的角度来看并不理想。如果我们决定在 CMakeLists.txt 文件的顶部包含 CTest 模块,我们可以在测试代码周围使用 if (BUILD_TESTING) 检查;然而,在 FetchContent 的上下文中包含此项目时,这就是一个全有或全无的设置。例如,我们的 生命游戏 库依赖于 mc-array,如果我们使用 FetchContent 包含了 mc-array,并且 mc-arraymc-gol 都使用 BUILD_TESTING,那么我们只能运行所有的测试或不运行任何测试。我们可能只希望在更改 生命游戏 库时运行它的测试,因此每个项目的选项让我们能够更好地控制哪些项目构建它们的测试。

include(CTest) 之后,我们使用 FetchContent 引入一个名为 dynamic-array-test 的测试库,并添加我们有的、能验证其功能的新测试文件:

add_executable(dynamic-array-test)
target_sources(
  dynamic-array-test PRIVATE src/array.test.c)

请注意,我们已将新测试文件添加到与 array.c 相同的物理位置:

.
├── CMakeLists.txt
└── src
    ├── array.c
    └── .test being inserted between the file name and extension. For unit tests, this is a common approach and has the big advantage of making the test code easy to find. It’s well understood that tests provide an incredibly valuable form of documentation, containing lots of examples of how to use a particular type. By keeping both implementation and test code together, it makes maintaining and understanding the code easier. Another benefit of tests as documentation is that tests are more likely to be kept up to date as code changes because not doing so will result in the tests failing or not compiling (that’s as long as you’re building and running them regularly, of course).
			The previously outlined approach is recommended for unit tests when there’s a one-to-one mapping between the tests and the type but is less applicable for higher-level integration tests. In those cases, maintaining a dedicated testing folder is usually advisable (either at the project root or split across directories grouped by functionality). Whatever you decide, the most important thing is having any tests at all, wherever they may be.
			We then link our new test application against both `unity` and our `dynamic-array` library and set the target compile features we care about:

target_link_libraries(

dynamic-array-test PRIVATE dynamic-array unity)

target_compile_features(

dynamic-array-test PRIVATE c_std_17)


			The last and most relevant command for this section is `add_test`:

add_test(

名称 "动态数组单元测试"

COMMAND dynamic-array-test)


			This registers our new `dynamic-array-test` executable with CTest so it can invoke it and report the outcome (this essentially means we can use `ctest` to run it). The first argument, `NAME`, allows us to provide a name for the test; this is what will be displayed in the output when running `ctest`. The next argument, `COMMAND`, is the test to run. In our case, this is an executable target, so we pass the target name of our test executable directly, but as we’ll see later, this can be one of many different commands.
			Very briefly, one command we haven’t included is `enable_testing()`. You may spot this in other examples, but it is technically redundant as `enable_testing()` is called automatically by the `include(CTest)` command (there are some cases where it is required however, for example when splitting tests across different files and using `add_subdirectory`, see *Chapter 11**, Supporting Tools and Next Steps* for an example). To see the complete example, please refer to `ch9/part-1/lib/array/CMakeLists.txt`. It’s encouraged to use the Visual Studio Code `ch8/part-5/lib/array/CMakeLists.txt` to more easily see the differences.
			Running the tests
			We now have everything we need to build and run our tests. Navigate to `ch9/part-1/lib/array` and run the following commands:

cmake --preset test

cmake --build build/test

ctest --test-dir build/test-C Debug


			The first two commands we’ve seen many times before in one form or another; these will configure and build our application and tests (we’ve updated `CMakePresets.json` to include a new `"test"` preset with `MC_ARRAY_BUILD_TESTING` set to `ON`).
			With those out of the way, let’s briefly walk through the `ctest` command. The first argument is `--test-dir`, which we use to specify the build directory containing the tests (this saves having to `cd` into the `build` folder and run `ctest`). The next argument, `-C` (short for `--build-config`), allows us to specify the configuration to test. This is needed because we’re using the `"Ninja Multi-Config"` generator; if we’d used a single config generator, the `build` folder would already have a build type defined through `CMAKE_BUILD_TYPE` and the `-C` argument could be omitted.
			Running the preceding command produces the following output:

内部 ctest 更改目录: .../ch9/part-1/lib/array/build/test

测试项目 ../ch9/part-1/lib/array/build/test

开始 1: 动态数组单元测试

1/1 测试 #1: 动态数组单元测试 ... 已通过 0.17 秒

100% 测试通过,1 个测试中没有失败

总测试时间(实际) = 0.17 秒


			It’s worth briefly mentioning if we omitted `include(CTest)` and `add_test(...)` in our `CMakeLists.txt` file, we’d lose the ability to use `ctest`, but we’d still be able to run our compiled test executable, like so:

./build/test/Debug/dynamic-array-test


			For simple use cases, this might be sufficient, but the more consistent `ctest` interface makes test commands portable across different platforms. `ctest` begins to really shine when we want to combine running several different kinds of tests into a single command.
			One other useful argument that can be passed to `ctest` is the `--verbose` option. This will display additional output from the tests (in the following case, the output will match running the test executable directly):

ctest --test-dir build -C Debug --output-on-failure 参数可用于使 CTest 仅在测试失败时输出。这可以帮助避免随着测试套件的增长而输出过多的杂乱信息:

ctest --test-dir build -C Debug ctest command has a bewildering number of options, not all of which we can cover here. To learn more about the different arguments and configuration options, please consult https://cmake.org/cmake/help/latest/manual/ctest.1.html for more information.
			We’ve covered how to add unit tests to some of our existing libraries and have seen how to invoke them using CTest. Next, we’re moving to the other end of the spectrum and will see an example of adding end-to-end tests for our *Game of* *Life* application.
			Adding end-to-end tests to an application
			Creating end-to-end tests for an application can be a challenge, and usually relies on an external tool or scripting language to send commands to the application to drive it. To support this, in our *Game of Life* application, we’re going to add one last library that will not only enhance our application but also make it testable end to end.
			The library in question is called **Dear ImGui** ([`github.com/ocornut/imgui`](https://github.com/ocornut/imgui)), an open source (MIT licensed) immediate mode **graphical user interface** (**GUI**), originally designed for use in games, but now used across a wide variety of applications.
			Immediate versus retained UI
			There are two main styles of UI libraries, often referred to as retained mode and immediate mode. A **retained mode** UI tends to require its widgets to be created and managed explicitly. A popular example of this is the Qt (pronounced *cute*) UI library. **Immediate mode** libraries do not require widgets to be created; instead, simply calling a function will display a UI element. There are pros and cons to each approach. Retained mode tends to be favored for UI-heavy applications, while immediate mode is preferred for graphical overlays for games or developer tools (though there are exceptions to both). We’ve opted for Dear ImGui due to its ease of use and simple integration with SDL 2.
			Integrating a UI library
			Before we look at how we go about creating end-to-end tests for our application, we’re first going to add Dear ImGui to our project. The initial integration is shown in `ch9/part-2`. Dear ImGui, like `bgfx`, does not natively support CMake, however, because Dear ImGui is a relatively small library, it’s easy to add a CMake wrapper around it.
			The repository we’ll use is [`github.com/pr0g/imgui.cmake`](https://github.com/pr0g/imgui.cmake), which takes a very similar approach to the `bgfx` CMake repository we saw in *Chapter 6*, *Installing Dependencies and ExternalProject_Add*. The main Dear ImGui repository is embedded as a Git submodule, and a `CMakeLists.txt` file is added at the root of the repository to aggregate the source files and produce a library using CMake (this makes integrating with `FetchContent` or `ExternalProject_Add` possible).
			We add Dear ImGui as a new third-party dependency in `ch9/part-2/third-party/CMakeLists.txt` and update our super build project and main `CMakeLists.txt` file accordingly to link against the new dependency.
			One other important change we’re going to make is to finally switch our application from using C to C++. This is to prepare it for being able to integrate the Dear ImGui Test Engine. Dear ImGui is written in C++, but C bindings do exist for it (see [`github.com/cimgui/cimgui`](https://github.com/cimgui/cimgui) for an example, which also comes with CMake support). They do not yet unfortunately exist for the testing library, so upgrading to C++ is a necessary step. The changes are minimal though, and as we’ve chosen to use C++ 20, we get to take advantage of designated initializers, which we’d been using in C (essentially a convenient way to initialize structs) with only a minor change in syntax.
			There are a few small additions we need before we can integrate Dear ImGui (see `ch9/part-2/app/imgui`). The first is a render backend (as we’re using `bgfx`, we need it to implement a handful of functions required by Dear ImGui), and the second is a platform backend (in this case, we use the SDL 2 platform backend provided by the Dear ImGui repository available from [`github.com/ocornut/imgui/tree/master/backends`](https://github.com/ocornut/imgui/tree/master/backends)).
			With these changes added, we can now add our Dear ImGui code. We’re going to add a few simple options to make interacting with our *Game of Life* application a bit easier. The changes include a simulation time control to adjust the amount of time between each update, the ability to pause and resume the simulation, to step the simulation a frame at a time when it’s paused, to clear the board, and to return the board to its original state. The results are shown in *Figure 9**.1*.
			![Figure 9.1: Game of Life with Dear ImGui controls](https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/min-cmk/img/B21152_09_1.jpg)

			Figure 9.1: Game of Life with Dear ImGui controls
			As a quick reminder, to configure and build the example shown previously, navigate to `ch9/part-2/app` and use the following command:

cmake --workflow --preset multi-ninja-super


			The application can then be launched by running the executable produced (again from the same directory):

./build/multi-ninja-super/Release/minimal-cmake_game-of-life_window


			Dear ImGui is incredibly powerful and comes with an enormous amount of functionality; our simple example is only scratching the surface. To see what else you can do with Dear ImGui, try adding a call to `ImGui::ShowDemoWindow()` right after `ImGui::NewFrame()` to see more of what it’s capable of.
			Integrating end-to-end tests using Dear ImGui
			With Dear ImGui integrated, we can now look at bringing in the Dear ImGui Test Engine (available from [`github.com/ocornut/imgui_test_engine`](https://github.com/ocornut/imgui_test_engine)). The Dear ImGui Test Engine has a slightly more restrictive license and requires obtaining a paid license in certain cases (see the `LICENSE.txt` file for more details). However, for derivative software released under an open source license (such as this book’s accompanying source code), it is free to use.
			Turning our attention to `ch9/part-3/app`, we’re first going to upgrade our third-party dependency from `imgui.cmake` to `imgui-test-engine.cmake` (see [`github.com/pr0g/imgui-test-engine.cmake`](https://github.com/pr0g/imgui-test-engine.cmake) for reference; it follows the same pattern as the previous `imgui.cmake` library). The `imgui-test-engine.cmake` library publicly depends on `imgui.cmake` (`imgui.cmake` is a transitive dependency), so we can make this small change to our `CMakeLists.txt` files, and things will continue working as they did before.
			The Dear ImGui Test Engine requires us to make changes to our source code to integrate it, and we only want this code to be compiled and executed when in test mode. To facilitate this, we can use a CMake `option` to determine whether we’re building a testable version of our application or a regular one. At the top of `ch9/part-3/app/CMakeLists.txt`, we have the following line:

option(MC_GOL_APP_BUILD_TESTING "启用测试" OFF)


			This is defaulted to `OFF`, but if a user passes `-DMC_GOL_APP_BUILD_TESTING=ON` when configuring (or adds or updates a CMake preset with this setting), tests will be enabled. The test target itself is wrapped in `if (MC_GOL_APP_BUILD_TESTING)` just as we did when adding tests to our libraries earlier in the chapter.
			Because we’re testing an application, and not a library, we can’t add a new test target and link against our application as linking against executables isn’t allowed. We must recompile the application again, only with our testing code turned on. To avoid a lot of repeated code in our `app/CMakeLists.txt` file, we’ve introduced a new `INTERFACE` target called `${PROJECT_NAME}-common`. An `INTERFACE` target allows you to specify usage requirements including source files, compile definitions, libraries, and more. The target won’t be built itself but can be used by other targets (in our case, our normal application and test application), simply by calling `target_link_libraries` with the new `${``PROJECT_NAME}-common` target.
			A snippet from `ch9/part-3/app/CMakeLists.txt` using this approach is shown here:

add_library(${PROJECT_NAME}-common INTERFACE)

target_sources(

${PROJECT_NAME}-common

INTERFACE

main.cpp imgui/sdl2/imgui_impl_sdl2.cpp

imgui/bgfx/imgui_impl_bgfx.cpp)

...

add_executable(${PROJECT_NAME})

target_link_libraries(

\({PROJECT_NAME} PRIVATE 项目名称变量带有通用后缀,并将其标记为 INTERFACE。然后像之前一样添加源文件和库,只是我们不再直接将它们添加到可执行文件中,而是使用 INTERFACE 库。在通过 add_executable 创建可执行文件后,我们只需要链接 `\){PROJECT_NAME}-common,即可引入它所定义的所有使用要求。好消息是,我们随后可以对 ${PROJECT_NAME}-test` 可执行目标做同样的事情,而无需进一步重复。

        目标属性仅适用于设置它们的目标,因此如果我们将它们设置在`${PROJECT_NAME}-common`上,它们不会传递到我们的主应用程序(`${PROJECT_NAME}`)或测试目标(`${PROJECT_NAME}-test`)。为了避免这两个目标之间的重复,一个解决方法是创建一个名为`set_common_target_properties`的 CMake 函数,它接受一个目标作为参数。我们可以将共享代码移到这个函数内,并为主应用程序和测试代码调用这个新函数。以下是这段代码的一个片段(完整示例见`ch9/part-3/app/CMakeLists.txt`):
function(set_common_target_properties TARGET_NAME)
  set_target_properties(
    ${TARGET_NAME}
    …
endfunction()
set_common_target_properties(CMakeLists.txt file, when defining the new test target, we set the MC_GOL_APP_BUILD_TESTING compile definition (this matches the CMake option for consistency but needn’t be the same):

target_compile_definitions(

${PROJECT_NAME}-test PRIVATE main.cpp 文件,我们可以在其中包装我们的测试初始化代码,并用#ifdef进行条件编译:

#ifdef MC_GOL_APP_BUILD_TESTING
  // register tests
  RegisterGolTests(engine, board);
  // queue tests
  ImGuiTestEngine_QueueTests(
    engine, ImGuiTestGroup_Tests, "gol-tests",
    ImGuiTestRunFlags_RunFromGui);
#endif
        我们在`main.cpp`文件的顶部前向声明了`RegisterGolTests`函数,并在一个单独的文件`gol-tests.cpp`中提供实现,我们仅在测试目标中包含这个文件:
target_sources(
  MC_GOL_APP_BUILD_TESTING again to wrap a call to ImGuiTestEngine_IsTestQueueEmpty(engine) to check when all tests have finished running. When this happens, we ensure the total number of tests run is equal to the total number of successful tests, and then terminate the application, returning either 0 for success or 1 for failure.
			The tests themselves, residing in `gol-tests.cpp`, allow us to script interactions with Dear ImGui, and because Dear ImGui interfaces with SDL 2, it can simulate mouse movements and clicks our application can respond to. To achieve this, a small change is needed to our input handling in `main.cpp`; we need to switch to using Dear ImGui instead of SDL 2 directly.
			For example, the check to see if the left mouse button has been clicked goes from the following:

if (current_event.type == SDL_MOUSEBUTTONDOWN) {

SDL_MouseButtonEvent* mouse_button =

(SDL_MouseButtonEvent*)&current_event;

if (mouse_button->button == SDL_BUTTON_LEFT) {

...


			To instead, look like this:

if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {

...


			Inside `RegisterGolTests`, we then can write a full end-to-end test to move the mouse, issue a click, and check the state of the *Game of* *Life* board:

t = IM_REGISTER_TEST(e, "gol-tests", "点击棋盘");

t->UserData = board;

t->TestFunc = [](ImGuiTestContext* ctx) {

const auto* board = (mc_gol_board_t*)ctx->Test->UserData;

ctx->SetRef("生命游戏");

ctx->MouseMoveToPos(ImVec2(200, 200));

ctx->MouseClick(ImGuiMouseButton_Left);

ctx->MouseMoveToPos(ImVec2(400, 200));

ctx->MouseClick(ImGuiMouseButton_Left);

IM_CHECK_EQ(mc_gol_board_cell(board, 6, 6), true);

IM_CHECK_EQ(mc_gol_board_cell(board, 19, 6), true);

};


			It’s not necessary to understand every line, but the important detail is we can now test our application as if we were a user, which can be incredibly useful for thorny kinds of test cases we’d like to cover.
			One other quick thing to mention is the Dear ImGui Test Engine also provides an interactive UI option to selectively run tests and view their output. To enable this, and stop tests from being automatically queued, pass `-D MC_GOL_APP_INTERACTIVE_TESTING=ON` when configuring the project. A CMake preset with this setting enabled has also been added called `multi-ninja-test-interactive` (see `ch9/part-3/app/main.cpp` for the full implementation).
			Integrating end-to-end tests with CTest
			Returning to our application’s `CMakeLists.txt` file, we can now see how we can integrate the preceding test application with CTest. All that’s needed (other than the obligatory call to `include(CTest)`), is the familiar `add_test` command:

add_test(

NAME "生命游戏端到端测试"

COMMAND ${PROJECT_NAME}-test

我们在本章之前看到的add_test命令用于注册我们的库测试,这一次,我们传递了一个额外的参数WORKING_DIRECTORY,并将其设置为CMAKE_SOURCE_DIR,以确保我们的应用程序使用 CMake 根目录,从而确保着色器文件可以在预期的相对位置访问。

        另一种选择是将编译后的着色器文件从`app/shader/build`复制到与编译后的测试应用程序相同的文件夹中,然后将`WORKING_DIRECTORY`设置为`${CMAKE_BINARY_DIR}/$<CONFIG>`(这在单配置生成器和多配置生成器中都能正确工作,因为在单配置生成器中,`$<CONFIG>`会解析为空字符串)。

        在一切编译和注册正确之后,剩下的就是运行测试应用程序。这可以通过从`ch9/part-3/app`文件夹执行以下命令来实现:
cmake --preset multi-ninja-super-test
cmake --build build/multi-ninja-super-test
ctest --test-dir build/multi-ninja-super-test -C Debug
        在我们结束这一部分之前,值得注意的是,我们可以通过在`CMakePreset.json`文件中添加对测试预设的支持进一步改进这一点。我们可以添加一个名为`"testPresets"`的键,并使用如下所示的 JSON 对象:
{
  "name": "multi-ninja-super-test",
  "configurePreset": "multi-ninja-super-test",
  "configuration": "Debug"
}
        然后,我们只需要在配置和构建完成后运行`ctest --preset multi-ninja-super-test`来启动我们的测试(这样可以存储许多我们原本需要在命令行中传递给`ctest`的配置选项)。有关`testPresets`提供的不同选项的更多信息,请参阅[`cmake.org/cmake/help/latest/manual/cmake-presets.7.html#test-preset`](https://cmake.org/cmake/help/latest/manual/cmake-presets.7.html#test-preset)。

        最后一步是为之前的所有代码包含一个 CMake 工作流预设,这样我们就可以通过以下命令配置、构建和测试所有内容:
cmake --workflow --preset multi-ninja-super-test
        这涵盖了在使用 CMake 和 CTest 帮助下创建可测试版本的应用程序时需要了解的主要内容。接下来,我们将讨论如何将更多类型的测试直接添加到我们的应用程序中,并将它们与 CTest 集成。

        添加其他类型的测试

        测试是一个非常广泛的话题,通常应用程序需要多种类型的测试来有效地覆盖其行为和功能。CTest 的一个优点是它可以与这些多样化的测试类型集成,并允许它们一起管理和运行。在本节中,我们将讨论 CTest 支持的另外两种类型的测试。

        内部测试

        我们将讨论的第一个示例仍然严格来说是单元测试,但我们将它添加到应用程序的上下文中,而不是通过提取功能到单独的库来进行。这在短期内很有用,特别是当某些功能无法或不应该被提取时。我们选择的示例是视口投影函数,它将从世界空间映射到屏幕空间,然后再返回。以前,这些函数是添加到我们的`main.c`(现在是`main.cpp`)文件中的,无法在其他文件中使用。我们可以将这两个函数提取到新的文件对中,命名为`screen.h`和`screen.cpp`,并在`main.cpp`中包含`screen.h`。

        这种重构使我们能够添加测试,以验证函数的行为,并帮助捕捉回归问题,以防将来我们决定重构或优化内部实现。为了添加测试,我们可以遵循与本章开始时看到的库示例相同的方法,新增一个名为`screen.test.cpp`的文件来保存我们的测试。我们将使用著名的 C++测试库 Catch2 来进行测试。我们选择使用 Catch2 而不是本章开始时介绍的 Unity 测试库的原因是,Catch2 是专为 C++构建的,并且拥有许多有用的功能(如函数重载和不需要手动调用测试,也叫自动测试注册,等等)。我们可以通过`FetchContent`或`ExternalProject_Add`将其作为依赖项添加。由于 Catch2 构建需要一些时间,我们选择了第二种方法。我们在`ch9/part-4/app/third-party`中的更新后的第三方`CMakeLists.txt`文件现在包含以下内容:
if(MC_GOL_APP_BUILD_TESTING)
  ExternalProject_Add(
    Catch2
    GIT_REPOSITORY https://github.com/catchorg/Catch2.git
    GIT_TAG v3.6.0
    ...
endif()
if(SUPERBUILD AND NOT PROJECT_IS_TOP_LEVEL)
  if(MC_GOL_APP_BUILD_TESTING)
    set(TEST_DEPENDENCIES Catch2)
  endif()
  ExternalProject_Add(
    ${CMAKE_PROJECT_NAME}_superbuild
    DEPENDS
      SDL2 bgfx imgui-test-engine.cmake
      mc-gol mc-draw ${TEST_DEPENDENCIES}
    ...
endif()
        首先,我们只有在构建应用程序的测试时才会包含 Catch2。然后我们引入了一个变量`TEST_DEPENDENCIES`,如果未设置`MC_GOL_APP_BUILD_TESTING`,它将评估为空字符串,如果设置了,则为`Catch2`。然后我们确保将这个变量传递给`ExternalProject_Add`调用中的`DEPENDS`参数,用于我们的超级构建。

        如果你查看`ch9/part-4/app/third-party/CMakeLists.txt`,在文件的顶部,我们还添加了`MC_GOL_APP_BUILD_TESTING` CMake 选项,它出现在`ch9/part-4/app/CMakeLists.txt`中。严格来说,这个设置是多余的,但它确保在单独构建第三方依赖项或作为超级构建时的一致性。

        现在 Catch2 作为第三方依赖项可用后,我们可以返回到应用程序的`CMakeLists.txt`文件,并检查需要在那里进行的更改。在`if(MC_GOL_APP_BUILD_TESTING)`块内,在我们的端到端测试可执行文件配置之后,我们添加了测试重构后的`screen.cpp`代码所需的命令。首先,我们使用`find_package`命令来引入我们在前面部分中添加的 Catch2 库:
find_package(Catch2 REQUIRED CONFIG)
        然后我们需要设置一个新的可执行文件来编译我们的测试。不幸的是,像这样为应用程序添加测试比我们在本章开始时看到的库案例要复杂一些。正如前面提到的,不能将可执行文件链接到测试中,因此我们不能添加新的测试可执行文件并与应用程序链接进行测试。相反,我们需要指定我们想要测试的文件,并与主应用程序使用的任何库链接,这些库可能在编译时需要。

        以下是`ch9/part-4/app/CMakeLists.txt`中的一个提取,展示了如何进行操作:
add_executable(${PROJECT_NAME}-unit-test)
target_sources(
  ${PROJECT_NAME}-unit-test PRIVATE
    src/viewport/screen.cpp
    src/viewport/screen.test.cpp)
target_link_libraries(
  ${PROJECT_NAME}-unit-test
    Catch2::Catch2WithMain as-c-math)
target_compile_features(
  ${PROJECT_NAME}-unit-test PRIVATE cxx_std_20)
        我们首先创建一个新的可执行文件 `${PROJECT_NAME}-unit-test`(它会扩展为 `minimal-cmake_game-of-life_window-unit-test`)。接下来,我们添加构建和运行测试所需编译的文件(`screen.cpp` 和 `screen.test.cpp`)。我们必须链接 Catch2(`Catch2WithMain` 有助于避免为测试创建自定义的 `main()` 入口点;有关更多信息,请参见 [`github.com/catchorg/Catch2/blob/devel/docs/cmake-integration.md#cmake-targets`](https://github.com/catchorg/Catch2/blob/devel/docs/cmake-integration.md#cmake-targets))和 `as-c-math`,这是 `screen.h/cpp` 接口和实现所依赖的。最后,我们确保明确设置语言版本(在此情况下为 C++ `20`),以确保在不同编译器和平台之间使用一致的语言版本。

        最后的步骤就是使用这里显示的 `add_test` 命令将测试可执行文件注册到 CTest:
add_test(
  NAME "game of life unit tests"
  COMMAND ${PROJECT_NAME}-unit-test)
        默认情况下,如果 CTest 检测到命令返回 `0`,它会报告成功;对于任何非零值,它会报告失败。这是一个普遍遵守的约定,不仅是 CTest,Catch2 和之前提到的 C 测试库 Unity 也都处理这一点。

        为了确认这一点,可以通过一个简单的控制台命令检查可执行文件在程序退出时的返回值。在运行应用程序后,在 Windows 上使用此命令(如果使用 PowerShell 或命令提示符):
echo %ERRORLEVEL%
        如果使用 macOS 或 Linux(或 Windows 上的 GitBash 或等效工具),请使用此命令:
echo $?
        在 Catch2 中,返回的数字是失败测试的数量。为了验证这一点,我们可以在 `screen.test.cpp` 文件中更改一个或两个期望结果值,重新编译测试可执行文件,运行它,然后运行前面的某个命令。如果两个测试失败,我们将看到以下输出:
> .../minimal-cmake_game-of-life_window-unit-test.exe
> echo $?
2
        如果由于某种原因,默认行为不足以满足需求,CTest 提供了一个 `PASS_REGULAR_EXPRESSION` 和 `FAIL_REGULAR_EXPRESSION` 属性,可以在测试中设置,以检查来自 `stdout` 或 `stderr` 的特定模式。例如,要验证 Catch2 运行的所有测试都成功,我们可以使用以下正则表达式检查:
set_tests_properties(
  "game of life unit tests"
  PROPERTIES "All tests passed" when no failures occur, which CTest checks for; it will report success if it detects it. This is a somewhat contrived example but can be useful in different situations where an exit code may not be available. See https://cmake.org/cmake/help/latest/prop_test/PASS_REGULAR_EXPRESSION.html for more information.
			CMake script tests
			The last kind of test we’ll cover is using CMake itself to run a CMake script file (like the CMake scripts we created to compile our shaders). We’re going to add a simple test to verify that the shaders required for the application to run have been compiled successfully. To achieve this, we create a new CMake script called `shaders-compiled.cmake` and add it to our `tests` directory. All it does is check for the existence of our shader files; a snippet is shown here:

if(NOT EXISTS

${CMAKE_SOURCE_DIR}/shader/build/vs_vertcol.bin)

message(FATAL_ERROR "vs_vertcol.bin 丢失")

endif()


			It’s not a particularly granular test, but if it fails, it is a useful early warning that the overall build is not functioning correctly and gives us a useful indicator of where to look.
			We can run this file directly by running `cmake -P tests/shaders-compiled.cmake` from `ch9/part-4/app`. When the `message(FATAL_ERROR ...` command is met, CMake will cease processing and return a non-zero error code (we can again verify this using `echo $?` or equivalent).
			To run this test as part of our top-level project, we can add the following to the testing section of our `CMakeLists.txt` file in `ch9/part-4/app`:

add_test(

NAME "着色器编译"

COMMAND ${CMAKE_COMMAND} -P tests/shaders-compiled.cmake

WORKING_DIRECTORY ${CMAKE_SOURCE_DIR})


			This runs the file just as we did from the command line, and by providing a variable for `WORKING_DIRECTORY`, we ensure the test runs from `CMAKE_SOURCE_DIR` rather than the `build` folder so the relative paths to our shader files resolve correctly.
			One final tip is that now that we have multiple tests to run, when working on a particular test, it can be useful to skip the others. In this situation, the `--tests-regex` (`-R` for short) command-line option can be used to select only the tests we want to run. For example, to only run the unit tests for our application, we could use the following command:

ctest --test-dir build/multi-ninja-super-test -R "生命游戏单元测试" -C Debug


			CMake also offers the ability to associate tests with specific labels. A pattern matching one or more of the labels can be passed to `ctest` to then have it only run the tests with that particular label. This can be achieved using `set_tests_properties`:

set_tests_properties(

"生命游戏端到端测试"

PROPERTIES --label-regex (-L) 和与 ctest 匹配的模式:

ctest --test-dir build/multi-ninja-super-test --label-exclude (-LE) to do the opposite, and not run any tests that match the label (in the preceding example, using -LE slow would run all tests that are not labeled slow).
			There are many more command-line arguments available for `ctest`, which are worth reviewing. They can be found by visiting [`cmake.org/cmake/help/latest/manual/ctest.1.html`](https://cmake.org/cmake/help/latest/manual/ctest.1.html).
			Using CDash with CTest
			One last topic to cover in the context of testing is integrating with another CMake tool called CDash. **CDash** is a web-based software testing server that can be used to present the results of running CTest. CDash displays a dashboard showing which tests are passing and which are failing and can also be used to display the current code coverage, as well as any build warnings or errors.
			The good news is adding CDash support to our project requires minimal effort. We’ll briefly walk through the changes required and look at adding code coverage support on macOS and Linux to be displayed from CDash.
			Creating a CDash project
			The first step we need to take is to create an account and a new project with CDash. While it’s possible to self-host a CDash server, using the CDash service provided by Kitware is a quick and easy way to get set up. This can be achieved by visiting [`my.cdash.org/`](https://my.cdash.org/), creating an account, and then navigating to [`my.cdash.org/user`](https://my.cdash.org/user) and scrolling down to the **Administrator** section. Here, there is then a **Start a new** **project** option.
			When creating a project, there are several options to provide, including the project name, description, whether the project is private, protected, or public, and whether submissions should be authenticated or not. For *Minimal CMake*, we have created a new public project, which can be found by visiting [`my.cdash.org/index.php?project=minimal-cmake`](https://my.cdash.org/index.php?project=minimal-cmake).
			Once your project has been created, the next step is to connect your local project to CDash. To do this, we add a new file to the root of our CMake project (in our case, this is `ch9/part-5/app`) called `CTestConfig.cmake`. Its contents are as follows:

set(CTEST_PROJECT_NAME minimal-cmake)

set(

CTEST_SUBMIT_URL

https://my.cdash.org/submit.php?project=minimal-cmake)


			There are many more options you can set, but for our purposes, we’re simply specifying the project name, and where the build artifacts should be uploaded to. For more complex cases, it’s possible to specify nightly build times, the maximum number of warnings or errors to be detected, and memory checks. For a full list of variables, please see [`cmake.org/cmake/help/latest/manual/cmake-variables.7.html#variables-for-ctest`](https://cmake.org/cmake/help/latest/manual/cmake-variables.7.html#variables-for-ctest).
			Uploading test results
			With the CDash project created and `CTestConfig.cmake` added to our project, we can run the following CTest command to run our tests and upload the results to CDash:

ctest --test-dir -C Debug -D 选项在此上下文中与我们之前使用的方式略有不同(用于设置 CMake 缓存变量);在这里,-D 指的是 CDash Web 仪表板(--dashboard),并告知 CTest 充当 CDash 客户端。这基本上意味着在运行完测试后,结果将上传到我们在 CTestConfig.cmake 文件中设置的 CDash 项目。

        在这里,`Experimental`指的是模式,`Experimental`是供个人开发者测试本地更改的模式。还有多个其他模式(`Nightly`,`Continuous`)可以独立配置,并在不同的上下文中使用。

        通过此更改,我们可以查看 CDash Web 界面,了解哪些测试已运行以及它们是否成功或失败。

        ![图 9.2:CDash 测试结果](https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/min-cmk/img/B21152_09_2.jpg)

        图 9.2:CDash 测试结果

        增强的可视性可以让开发团队清楚地知道哪些测试通过或失败,这对于及时发现问题和早期检测回归非常有帮助。

        添加代码覆盖率

        CDash 提供的另一个有用功能是一个干净的界面,用于报告在运行测试时执行的代码行。不幸的是,这仅在**GNU 编译器集合**(**GCC**)和 Clang 编译器中受支持,因此默认情况下在 Windows 上无法使用(尽管在 Windows 环境中设置 Clang 并不困难,如果你有决心的话)。

        为了支持捕获代码覆盖率信息,我们需要在`CMakeLists.txt`文件中做一些小的修改。完整示例请参见`ch9/part-5/app/CMakeLists.txt`,但关键的代码行如下所示:
target_compile_options(${TARGET_NAME} PRIVATE --coverage)
target_link_options(${TARGET_NAME} PRIVATE --coverage to both the compile and link options for our test targets. Internally, CMake is using a tool called gcov to generate coverage information. gcov itself is outside the scope of this book. It can be used without CMake or CTest, but fortunately for us, CTest does a nice job of providing a simple interface that wraps gcov and we can treat it as an implementation detail for now.
			One last change is to limit the amount of coverage information that’s reported (to essentially ignore files we don’t care about). This can be achieved by adding a new file called `CTestCustom.cmake.in` that contains the `CTEST_CUSTOM_COVERAGE_EXCLUDE` CTest variable, which allows us to pass coverage paths to ignore to CTest:

set(CTEST_CUSTOM_COVERAGE_EXCLUDE

${CTEST_CUSTOM_COVERAGE_EXCLUDE}

"src/imgui/sdl2" "third-party/install/include")


			CTest will look for this file in the CMake `build` folder (`CMAKE_BINARY_DIR`), so we need to copy the template file to the `build` folder when we run the CMake configure step. To do this, we use `configure_file`, added at the bottom of our testing block in our application’s `CMakeList.txt` file:

configure_file(

CTestCustom.cmake.in

${CMAKE_BINARY_DIR}/CTestCustom.cmake COPYONLY,表示不应进行任何变量替换。现在,当我们运行之前看到的 ctest 命令时,覆盖率信息也会被上传,并与测试结果一起提交。可以查看文件的整体测试覆盖率百分比,并逐行查看在运行测试时执行了哪些代码:

        ![图 9.3:CDash 覆盖率结果](https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/min-cmk/img/B21152_09_3.jpg)

        图 9.3:CDash 覆盖率结果

        这只是对 CDash 的一个非常简短的介绍,仅仅触及了它的表面。除了使用默认的`ctest`功能外,还可以完全脚本化`ctest`的执行(请参阅[`cmake.org/cmake/help/latest/manual/cmake-commands.7.html#ctest-commands`](https://cmake.org/cmake/help/latest/manual/cmake-commands.7.html#ctest-commands)以查看`ctest`命令的完整列表)。还可以设置定期的夜间构建和各种类型的报告,以及启用几种形式的静态分析(源代码错误检测)。如果你决定选择其他工具或不需要可视化功能,也完全可以不使用 CDash;CTests 可以独立使用。

        摘要

        这标志着我们第一次涉足测试的结束。虽然我们没有涵盖很多内容,但希望这已经让你对 CTests 的功能有所了解,并且理解它如何将多种不同的测试方法结合起来。

        在本章中,我们介绍了 CTest,以理解它是什么以及它如何帮助我们管理跨库和应用程序的各种测试。测试至关重要,理解 CTest 在测试生态系统中的定位非常重要。我们展示了如何在我们的基础库中添加单元测试时使用 CTest,如何在应用程序内结构化单元测试,以及如何创建一个独立的可测试可执行文件来运行完整的端到端测试。我们还展示了如何编写 CMake 脚本来测试项目的其他部分。所有这些都通过 CTest 进行协调和连接。这些技能将帮助你构建成功且可靠的软件项目。

        接着,我们简要浏览了 CDash,了解它提供了哪些功能以及它如何与 CTest 集成。我们查看了测试结果和代码覆盖率报告,了解了像 CDash 这样的工具如何帮助软件团队更有效地协作。

        在下一章,我们将把注意力转向 CMake 的另一个配套工具——CPack。我们将使用它来打包我们的应用程序,使其准备好进行分发,并探讨一些与平台特定差异处理相关的挑战。







第十章:为共享打包项目

在本章中,我们将讨论 Minimal CMake 的最后一个主要主题——打包。这是将我们构建的软件转化为可以共享的格式的过程。当然,您也可以在没有打包步骤的情况下共享软件,但这样做通常是一个手动过程,容易出错,也不符合平台的预期规范——例如,Windows 的图形安装程序、macOS 的磁盘映像(.dmg)或 Linux(Ubuntu)Debian 包(.deb)。

我们将展示如何使用 CPack 来为 macOS、Windows 和 Linux 打包我们的应用程序。我们将介绍需要对 CMakeLists.txt 文件进行的更改以及创建软件包所需的命令。好消息是,我们已经完成了大部分的繁重工作,许多更改通常是为了处理平台特定的差异。

在本章中,我们将涵盖以下主要主题:

  • 理解 CPack

  • 相对于可执行文件加载资源

  • 集成 CPack

  • 构建 macOS 包

  • 添加 CPack 预设

  • CPack 的其他用途

技术要求

为了跟随本书的进度,请确保你满足 第一章《入门》中概述的要求。这些要求包括:

  • 一台安装有最新 操作 系统OS)的 Windows、Mac 或 Linux 机器

  • 一个可用的 C/C++ 编译器(如果你还没有,建议使用系统默认的编译器)

本章中的代码示例可以通过以下链接找到:github.com/PacktPublishing/Minimal-CMake

理解 CPack

通过终端使用 cpack。理解 CPack 最好的方式是把它看作是 CMake 安装命令的封装工具。在 第八章,《使用超级构建简化入门》中,我们已经完成了为我们的应用程序创建安装命令的过程,也就是说,我们已经做了打包应用程序所需的工作。CPack 的作用是处理与安装软件相关的特定平台约定,它能很好地抽象化这些工作,使你无需过多担心。

打包的优势在于,能够避免让用户自己构建我们的软件。我们之前讨论过的与从安装目录(app/install/bin 文件夹)运行应用程序、将 DLL 复制到 Windows 上正确的文件夹以及库搜索路径(我们在 Linux/macOS 上执行的 RPATH 处理)相关的主题,已经为此做好了准备,使打包步骤变得更加简单。

CPack 提供了多个包生成器,这些生成器的指定方式类似于我们通过 -G 命令行选项传递给 CMake 的构建系统生成器。其中一些是特定于平台的(例如,macOS 上的 BundleDragNDrop),其他一些则需要额外的软件安装(例如,Windows 上的 Nullsoft Scriptable Install SystemNSIS));我们将至少介绍每个平台上的一个生成器,并展示每种情况下所需的 CPack 命令。

相对于可执行文件加载资源

在我们开始向 CMakeLists.txt 文件中添加任何 CPack 命令之前,还有一个我们之前忽略的最终话题需要讨论,那就是如何确保我们加载的资源文件可以相对于可执行文件被找到。

Minimal CMake 中,我们几乎一直从终端启动应用程序,但我们必须非常小心从哪里启动应用程序。从 app 文件夹中启动应用程序是可行的(例如,./build/multi-ninja/Release/minimal-cmake_game-of-life_window),将目录切换到 install/bin 文件夹并从那里启动应用程序也可以正常工作(这是因为我们确保将着色器复制到 install 文件夹中的正确相对位置)。问题是,如果我们尝试从其他文件夹(例如,主目录)启动应用程序,着色器将无法加载。

你将看到如下错误信息:

Shaders not found. Have you built them using compile-shader-<platform>.sh/bat script?

很可能你已经构建了着色器(特别是在我们在 第八章 中添加了自定义命令来自动为我们完成此操作之后,使用超级构建简化入门);问题是,当从主目录运行时,我们的应用程序会在 ~/shaders/build 中寻找着色器,而不是在 path/to/app/install/bin/shaders/build 中。与 Windows 相比,macOS 和 Linux 上的情况要更复杂一些。在 Windows 上通过图形界面启动应用程序时,工作目录默认会设置为包含可执行文件的文件夹,但在 macOS 和 Linux 上,工作目录会是用户的主目录(~/$HOME)。

为了解决这个问题,我们需要更新我们的应用程序,使其相对于可执行文件加载资源文件,而不是当前工作目录。为了实现这一点,我们需要查询应用程序在运行时的当前目录。根据使用的平台不同,这有多种方法(例如,macOS 上的 _NSGetExecutablePath,Linux 上的 readlink,Windows 上的 GetModuleFileName,以及其他一些替代方法)。幸运的是,既然我们使用的是 SDL 2,我们可以使用一个名为 SDL_GetBasePath 的工具函数(更多信息请参见 wiki.libsdl.org/SDL2/SDL_GetBasePath),它可以为我们处理所有这些跨平台的情况(它还处理了 macOS 特定的包的差异)。

我们将对CMakeLists.txt文件和main.cpp文件做几个小改动,以支持这一点。从ch10/part-1/app中的CMakeLists.txt文件开始,我们将把着色器从其原始位置复制到我们的build文件夹,以确保从那里启动可执行文件能够按预期工作。为了使事情更加清晰,我们将删除shader/build文件夹,并将编译后的bin文件安装到一个名为shader的新文件夹中,位于应用程序旁边(稍后我们会相应更新main.cpp文件)。

之后,文件夹结构将如下所示:

├── minimal-cmake_game-of-life_window
└── shader
      ├── fs_vertcol.bin
      └── vs_vertcol.bin

为了实现这一点,我们需要更新我们正在使用的add_custom_command调用,以便将我们的共享库文件(.dylib/.so/.dll)复制到构建文件夹,并包括一个步骤来复制着色器文件。其形式如下(省略现有的copy命令):

add_custom_command(
  TARGET ${PROJECT_NAME}
  POST_BUILD
  ...
  COMMAND
    ${CMAKE_COMMAND} -E copy_directory
      ${CMAKE_SOURCE_DIR}/shader/build
    $<TARGET_FILE_DIR:${PROJECT_NAME}>/shader
  VERBATIM)

我们将使用copy_directory命令,将shader/build文件夹的内容复制到目标位置的子文件夹shader中。需要注意的是,复制着色器的自定义命令并没有明确依赖于之前的编译着色器的自定义命令。这可能意味着,如果编译着色器失败,这个命令仍然会执行,但没有效果(或失败)。如第八章中讨论的,使用超级构建简化入门,我们可以使用add_custom_commandOUTPUT变体,并使用DEPENDS参数确保第二个add_custom_command仅在第一个成功后运行。由于TARGET版本更简单,我们将继续使用它来演示后续的例子,但在某些情况下,OUTPUT版本会非常有用(有关更多细节,请参见cmake.org/cmake/help/latest/command/add_custom_command.html)。为了保持与安装布局一致,我们还需要稍微修改我们之前创建的安装命令,将我们的着色器复制到安装树中。我们不再复制整个shader/build文件夹,而是将内容复制到一个新的名为shader的文件夹中。我们可以使用以下命令实现:

install(
  DIRECTORY ${CMAKE_SOURCE_DIR}/shader/build/
  DESTINATION ${CMAKE_INSTALL_BINDIR}/shader
  FILES_MATCHING
  bin files, we will use the install DIRECTORY option, FILES_MATCHING, with the PATTERN match kind (this uses a glob matching pattern; for more complex cases, it’s possible to use REGEX and provide a regular expression instead). Note that we also added a trailing forward slash at the end of build on the first line to ensure that we only copy the folder’s contents, not the folder itself.
			These are the only changes we need to make to our `CMakeLists.txt` file; let’s now turn our attention to `main.cpp`. The change we need to make to this is small and self-contained, as shown in the following snippet:

char* base_path = SDL_GetBasePath();

std::string vs_shader_full_path;

vs_shader_full_path.append(base_path);

vs_shader_full_path.append("shader/vs_vertcol.bin");

...

std::vector vs_shader =

read_file(vs_shader_full_path.c_str());

...

SDL_free(base_path);


			We first call `SDL_GetBasePath()` to get the path to where our executable is running, and then we use the `std::string` C++ type for convenience to build a path to the file we want to load (there is undoubtedly a faster and more efficient method, using C++17’s filesystem library and/or `std::string_view`, but this is intended as a quick, simple example). Once we have the full path to our file, we can pass it to our existing `read_file` function, and when we’ve finished using it, we need to clean up `base_path` using `SDL_free`, as we’re responsible for managing its lifetime.
			With these changes applied, it’s now possible to launch our application from Finder on macOS, GNOME on Linux (the default Ubuntu desktop), and through File Explorer on Windows. With the relative loading of assets out of the way, we now have everything we need to start adding CPack support to our application.
			Integrating CPack
			Integrating CPack is deceptively simple; the majority of the work comes from the `install` commands we’ve already covered. The process involves setting several CPack-related variables (beginning with the `CPACK_` prefix) to project-specific values, and then adding `include(CPack)` at the very end of our `CMakeLists.txt` file. In fact, with the current state of our `CMakeLists.txt` file from `ch10/part-1/app`, it’s possible to just add `include(CPack)` at the end, and then running `cpack` will do something useful.
			One quick reminder is that CPack will default to installing a `Release` build, so ensure that you’ve built the `Release` configuration of the application; otherwise, invoking `cpack` will produce an error resembling the following:

文件 INSTALL 无法找到

"path/to/build/multi-ninja-super/Release/minimal-cmake_game-of-life_window":

没有这样的文件或目录。


			To invoke `cpack` directly, you need to tell it where to find a newly generated file called `CPackConfig.cmake`. This file gets created after running a CMake configure step when the `include(CPack)` command is added to our `CMakeLists.txt` file. `CPackConfig.cmake` will appear at the root of the build folder; it’s also usually sensible to provide a directory for the packaged files to be added to (similarly to how we provide a build folder for CMake to store our build files).
			The following is an example of invoking CPack after we’ve configured and built our project (e.g., by running `cmake --workflow --``preset multi-ninja-super`):

cpack --config build/multi-ninja-super/--config 提供 CPackConfig.cmake 文件的路径,然后使用 -B 创建一个名为 package 的新文件夹来存储打包后的文件。调用 CPack 将根据平台产生不同的结果,类似于 CMake,因为每个平台都会有自己的默认生成器(在这种情况下,是包生成器而不是构建系统生成器)。我们可以像在 CMake 中一样使用 -G 来指定使用哪种生成器。

        CPack 的真正复杂性(在核心 `install` 逻辑完成后)来自于配置你关心的具体生成器(运行 `cpack --help` 将列出你所在平台上所有可用的生成器)。为了限定各种生成器的范围,我们将为每个平台选择一个最适合该平台上应用程序安装方式的生成器。一个完整的工作示例已在 `ch10/part-2/app/CMakeLists.txt` 中给出,并且包含一个新的 `packaging` 文件夹,里面有特定于包的资产和文件。我们将首先介绍常见的 `CPACK_` 变量设置,然后依次讲解每个平台。

        CPack 常见属性

        除了 `include(CPack)` 命令外,还有许多 CPack 变量可以设置,用来配置打包项目的各种设置。有些变量是所有 CPack 生成器共享的(完整的列表可以在 [`cmake.org/cmake/help/latest/module/CPack.html#variables-common-to-all-cpack-generators`](https://cmake.org/cmake/help/latest/module/CPack.html#variables-common-to-all-cpack-generators) 查阅),还有些变量是特定于某个生成器的——例如,macOS 上的 CPack Bundle 生成器以 `CPACK_BUNDLE_` 开头(它的完整变量列表可以在 [`cmake.org/cmake/help/latest/cpack_gen/bundle.html`](https://cmake.org/cmake/help/latest/cpack_gen/bundle.html) 查阅)。并非所有通用的 CPack 变量都适用于每个生成器,但它们会适用于多个生成器(例如,`CPACK_PACKAGE_EXECUTABLES` 就被 NSIS、WiX 和 Inno Setup 生成器使用)。

        我们将从最基本的常见变量开始(还有许多其他变量被省略;你可以随意尝试这些变量并将它们添加到你未来的项目中)。我们将首先指定的是 `CPACK_PACKAGE_NAME`:
set(CPACK_PACKAGE_NAME "minimal-cmake_game-of-life")
        这是非常重要的,并且几乎所有 CPack 生成器都会使用。如果省略此项,目标名称将被用作包名称。在我们的案例中,我们将其设置为 `"minimal-cmake_game-of-life"`(这里要特别注意,名称中不能有空格,因为如果有空格,某些平台/生成器在安装时可能会失败)。

        我们将使用的下一个常见变量(仅用于 Windows NSIS 安装程序)是 `CPACK_PACKAGE_EXECUTABLES`:
set(
  CPACK_PACKAGE_EXECUTABLES
  PROJECT_NAME) and the second is a friendly name for the application once the package has been installed by a user (this makes sure things such as the Start Menu icon on Windows has this name).
			That’s it for the common variables; we could specify other properties about the project, such as the version, description, and vendor, but we’ll skip those for now. Next, we’re going to look at our Windows NSIS installer and what other variables are needed.
			The CPack Windows NSIS package
			To create something that resembles a traditional Windows installer for our application, we’re going to use the NSIS package. If you don’t already have this installed, you can download it from [`nsis.sourceforge.io/Download`](https://nsis.sourceforge.io/Download) (the examples in this book were tested with NSIS `3.10`). Once this is installed, specifying the NSIS generator in CPack should work (if you don’t have it installed, you’ll get an error that CPack can’t find NSIS).
			The NSIS installer should more or less work out of the box with our current setup; all we need to do is run the following command (the exact build folder shown here, `build/multi-ninja`, may differ in your case):

cpack --config build/multi-ninja/CPackConfig.cmake -G package。运行安装程序将引导我们完成一系列步骤,然后将安装文件复制到 C:\Program Files\minimal-cmake_game-of-life 0.1.1。开始菜单快捷方式也会添加到 C:\ProgramData\Microsoft\Windows\Start Menu\Programs\minimal-cmake_game-of-life 0.1.1。

        我们设置的两个初始`CPACK_NSIS_`变量是为了给安装程序的欢迎屏幕一个友好的标题,并确保在高 DPI 显示器上显示清晰:
set(CPACK_NSIS_PACKAGE_NAME "Minimal CMake - Game of Life")
set(CPACK_NSIS_MANIFEST_DPI_AWARE true)
        有一个重要的东西我们还缺少,以使我们的应用程序看起来更专业,那就是图标(不幸的是,接下来的章节我们大部分时间都要花在这个上,因为每个平台的图标处理方式不同)。

        我们首先需要创建一个符合 Windows 预期格式的图标。一个非常棒的工具是`.ico`文件,位于`ch10/part-2/app/packaging/windows`,名为`mc_icon.ico`,但了解如何为自己的图标执行此操作未来会很有帮助。

        一旦图标文件可用,我们需要添加一个特定于 NSIS CPack 的变量来引用它。这个 CPack 选项是`CPACK_NSIS_MUI_ICON`:
set(
  CPACK_NSIS_MUI_ICON
  "${CMAKE_SOURCE_DIR}/packaging/windows/mc_icon.ico")
        这将把我们创建的图标与 NSIS 安装程序关联起来,因此在通过安装程序时,我们将在窗口的左上角和 Windows 任务栏看到该图标。但是,这不会为安装后的应用程序创建图标。为此,我们需要暂时跳出 CPack,并在我们的`CMakeLists.txt`文件中做一个小更新。我们还必须添加一个 Windows 所需的额外文件。

        我们需要添加的文件叫做资源定义脚本,以`.rc`扩展名结尾(要了解更多关于 Windows 资源文件的信息,请访问[`learn.microsoft.com/en-us/windows/win32/menurc/about-resource-files`](https://learn.microsoft.com/en-us/windows/win32/menurc/about-resource-files))。

        该文件包含以下内容:
IDI_ICON1 ICON "mc_icon.ico"
        这将链接资源定义脚本与我们生成的图标文件。然后,需要将`icon.rc`文件编译到我们的可执行文件中,这可以通过将`icon.rc`添加到`target_sources`来实现。
target_sources(
  ${PROJECT_NAME} PRIVATE packaging/windows/icon.rc)
        通过这个更改,我们将看到应用程序的开始菜单和桌面快捷方式使用的图标。有更多的 CPack NSIS 选项可以进一步自定义安装程序体验,我们暂时跳过这些;完整列表请访问[`cmake.org/cmake/help/latest/cpack_gen/nsis.html`](https://cmake.org/cmake/help/latest/cpack_gen/nsis.html)。

        另一个需要注意的小细节,虽然这略微超出了 CPack 的范围,但属于让我们的应用程序准备好发布的范畴,那就是隐藏启动应用程序时出现的控制台窗口。这一点可能之前不太显眼,因为我们大部分时间都是从终端启动应用程序。你可能已经注意到,当从 Windows GUI 启动时,会在后台出现一个控制台窗口,显示我们添加的调试控制台输出。这个窗口在开发过程中可能很有用,但对于 `Release` 版本来说,最好将其隐藏。可以通过将 `WIN32_EXECUTABLE` 属性传递给 `set_target_properties` 来实现,并确保只有在 CMake 配置设置为 `Release` 时才进行设置。

        这可以通过将以下内容添加到我们的`CMakeLists.txt`文件中完成:
set_target_properties(
  ${PROJECT_NAME} PROPERTIES WIN32_EXECUTABLE
  if(WIN32) block to ensure that we only set these values on Windows (see ch10/part-2/app/CMakeLists.txt for the complete example). Here’s another quick reminder that using Visual Studio Code’s part-<n>/ folders for each chapter).
			The last (and optional) change we can make to our `CMakeLists.txt` file is to add a desktop shortcut for our application. This can be achieved by using `CPACK_NSIS_EXTRA_INSTALL_COMMANDS` and `CPACK_NSIS_EXTRA_UNINSTALL_COMMANDS`. The following snippet shows how this is done:

set(

CPACK_NSIS_EXTRA_INSTALL_COMMANDS

"CreateShortCut '\(DESKTOP\\\\Minimal CMake - Game of Life.lnk' '\)INSTDIR\\bin\\${PROJECT_NAME}.exe'")


			We use the `CreateShortCut` command to create a shortcut on the user’s desktop, named `Minimal CMake - Game of Life.lnk`, and link it to the name of the executable in its install location.
			The abundance of backslashes unfortunately isn’t a typo; they are needed to escape the backslash character at multiple stages. To represent a literal backslash in CMake, it must be escaped by using a backslash character (`\`), and NSIS also expects the path it uses to be separated by backslashes, and they too need to be escaped. The processing of the path reduces the backslashes from 4 to 2, and finally to 1 (by the time NSIS sees the path). This is a bit ugly and confusing, but unfortunately, there isn’t much we can do about it.
			With that final change, we’re done crafting our Windows installer. There’s more we can add, but this should give you a solid base to work from when building your own installers in the future.
			![Figure 10.1: The Windows NSIS installer](https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/min-cmk/img/B21152_10_1.jpg)

			Figure 10.1: The Windows NSIS installer
			To package our application on Windows using the NSIS installer, use the following command from `ch10/part-2/app` (remembering to have first configured and built a `Release` configuration of the application):

cpack --config build/multi-ninja/CPackConfig.cmake -G NSIS64 -B package


			This will create an executable installer, and running it will install *Minimal CMake – Game of Life* in your `Program Files` directory, like any other application you might install (a quick note that administrator privileges are required to install to `Program Files`). An uninstaller is also generated, which makes it easy to remove the application and associated shortcuts in the future.
			The CPack macOS bundle package
			We are now going to look at the commands needed to package an application bundle on macOS. In this section, we’re going to show how to use the `Bundle` generator. There is another approach we’ll touch on later in the chapter that shows how to build a macOS bundle directly (this works a little differently from how we’ve configured things so far and differs significantly from other platforms, so using it will depend on your exact situation and preference).
			The good news is that the `CMakeLists.txt` changes are confined to a single block, as shown here:

set(CPACK_BUNDLE_NAME "Minimal CMake - Game of Life")

set(CPACK_BUNDLE_PLIST

"${CMAKE_SOURCE_DIR}/packaging/macos/info.plist")

set(CPACK_BUNDLE_ICON

"${CMAKE_SOURCE_DIR}/packaging/macos/gol.icns")

set(CPACK_BUNDLE_STARTUP_COMMAND

"${CMAKE_SOURCE_DIR}/packaging/macos/bundle-run.sh")


			We’ll walk through each line to understand what it’s doing and why it’s needed:

				*   The first line (`CPACK_BUNDLE_NAME`) simply sets the name of the application bundle. This is the name that will appear inside the bundle when it’s opened and dragged to the application folder.
				*   The second line (`CPACK_BUNDLE_PLIST`) refers to an information property list (`info.plist` for short) that is used to store metadata about the application. This is the mechanism used by macOS and iOS to store configuration information for applications (to learn more about information property lists, go to [`developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Introduction/Introduction.html`](https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Introduction/Introduction.html)). There’s a lot of properties that can be added to this file, but for our purposes, we only need one for the time being, and that’s the `CFBundleIconFile` property. This will refer to the icon file in the bundle, which will share the name of the bundle (this is different from the name of the icon file (`gol.icns`) before packaging; the `.icns` file is renamed to match the value of `CPACK_BUNDLE_NAME` inside the bundle). The `info.plist` file will be added to the `Contents` folder of the bundle.
				*   The third variable (`CPACK_BUNDLE_ICON`) refers to the icon file to use for the bundle. The file we’re using here is `gol.icns`, which was generated by running the `generate-icons.sh` script in the packaging folder. It internally uses `sips` ([`ss64.com/mac/sips.html`](https://ss64.com/mac/sips.html)) on macOS to generate icons of increasing size (all power of 2 dimensions) from a source image (for things to work, ensure that the source image you use is 1,024 x 1,024 pixels in size), and then it uses `iconutil` ([`www.unix.com/man-page/osx/1/iconutil/`](https://www.unix.com/man-page/osx/1/iconutil/)) to create the `.icns` file for CPack (and our `info.plist` file) to refer to. With these changes, we’ll get an icon for our bundle and application after it’s installed.
				*   The last variable (`CPACK_BUNDLE_STARTUP_COMMAND`) holds a path to a small helper startup script to ensure that we can launch our application from the bundle. This file will be copied to `Contents/MacOS` inside the bundle.

			The content of the file it refers to (`bundle-run.sh`) is as follows:

cd "$(dirname "$0")"

../Resources/bin/minimal-cmake_game-of-life_window


			The first line changes the directory to the location of the script that’s currently running (so we’ll end up in the bundle’s `Contents/MacOS` folder), and the second line launches our executable (all resources will then load relative to it). It’s a little unconventional but works well when dealing with a cross-platform `CMakeLists.txt` file.
			To package our application on macOS using the bundle generator, navigate to `ch10/part2/app`, and then run the following command (again, remember to build the application in the `Release` configuration first):

cpack --config build/multi-ninja/CPackConfig.cmake -B package -G .dmg 文件),该文件可以打开,之后将打包的应用程序拖到“应用程序”文件夹中进行安装(只需将应用程序从“应用程序”文件夹移动到 macOS Bin 中即可卸载它)。

        ![图 10.2:macOS 磁盘映像](https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/min-cmk/img/B21152_10_2.jpg)

        图 10.2:macOS 磁盘映像

        还有其他方法可以自定义已挂载磁盘映像的外观,例如创建自定义的 `.DS_Store` 文件,并使用 `CPACK_DMG_DS_STORE` 变量来引用它。请参阅 `ch10/part-4/app/CMakeLists.txt` 和 `ch10/part-4/app/packaging/macos/custom_DS_Store` 了解示例。

        CPack Linux Debian 包

        现在我们将回顾 `ch10/part-2/app` 中的 `CMakeLists.txt` 文件的更改,以了解支持 Linux 上 Debian(`.deb`)安装程序所做的添加。

        好消息是这些更改是有限的。第一个更改类似于我们在 Windows 上所做的操作,目的是确保安装后应用程序会显示图标。这一次,我们需要添加两个额外的`install`命令,分别用于复制 Linux `.desktop` 文件(负责应用程序出现在 Linux GUI 中)和相关的 `.png` 图像到安装位置:
install(
  FILES ${CMAKE_SOURCE_DIR}/packaging/linux/mc-gol.desktop
  DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/applications)
install(
  FILES packaging/linux/mc-gol-logo.png
  DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/icons)
endif()
        第一个命令将我们的`.desktop`文件复制到数据根目录,相对于我们选择的安装文件夹(默认情况下,这是`<install-folder>/share`)。根据惯例,`.desktop`文件通常会位于`share`下的`applications`文件夹中,因此我们将`applications`附加到`CMAKE_INSTALL_DATAROOTDIR`。图标本身会在`share/icons`中查找,因此我们需要确保将其也复制到该位置。

        在这个例子中,我们使用了 Debian 包生成器;当我们安装包时,文件将被复制到平台标准位置(`/usr/share/icons`、`/usr/share/applications`、`/usr/bin`等)。这样做的好处是,我们不需要在`.desktop`文件中硬编码绝对路径,因为可执行文件和图标可以在预期的位置找到。

        为了完整性,`.desktop`文件的内容如下:
[Desktop Entry]
Name=Minimal CMake - Game of Life
Comment=Interactive Game of Life simulation
Exec=minimal-cmake_game-of-life_window
Icon=mc-gol-logo
Terminal=false
Type=Application
Categories=Development
        由于我们正在创建一个窗口化应用程序,我们将`Terminal`设置为`false`,并添加一些额外的元数据,帮助描述我们构建的应用程序类型(有关`.desktop`文件的更多信息,请参见[`wiki.archlinux.org/title/Desktop_entries`](https://wiki.archlinux.org/title/Desktop_entries)以获取有用的概述)。请注意,在指定图标时,我们不包括扩展名;我们只需要提供名称。

        为了支持 Linux 上的 Debian 包,我们唯一需要做的代码更改是提供包的维护者名称。可以使用以下命令来实现:
set(CPACK_DEBIAN_PACKAGE_MAINTAINER "<maintainer-email>")
        这是一个有用的方法,用户可以在遇到问题或有反馈时联系包的所有者/维护者。此字段是必须提供的;否则,CPack 会返回错误并且不会生成 Debian 包。

        应用这些更改到我们的`CMakeLists.txt`文件后,我们现在可以运行 CPack 并提供 DEB 包生成器。在构建项目的发布配置(例如,`cmake --build build/multi-ninja --config Release`)之后,只需从`ch10/part-2/app`目录运行以下命令:
cpack --config build/multi-ninja/CPackConfig.cmake -G .deb file in the package folder. To install the package to the system, use the following command:

sudo dpkg -i package/minimal-cmake_game-of-life-0.1.1-Linux.deb


			This will install the package and make it available in your path; it can be launched by typing `minimal-cmake_game-of-life_window` from the terminal (try changing the directory to your `$HOME`*/*`~` folder and launching it), or by navigating to `Minimal CMake – Game` `of Life`.
			![Figure 10.3: The installed icon on Linux (Ubuntu)](https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/min-cmk/img/B21152_10_3.jpg)

			Figure 10.3: The installed icon on Linux (Ubuntu)
			The last important detail to cover is if we wish to uninstall an application from our system, we can do this by running the inverse of the `dpkg -i` command, which takes the following form:

sudo dpkg -P minimal-cmake_game-of-life


			This will remove the executable, libraries, icons, and `.desktop` file from the system and restore it to the state it was before the package was installed.
			That covers creating three separate package generators on three separate platforms. There are, of course, many more, and the packages we did create can continue to be improved and refined, but this should hopefully give a taste of how to use CPack and some of the details to be aware of.
			In the next section, we’re going to spend a bit of time on a macOS-specific topic, relating to CMake’s built-in support for application bundles.
			Building a macOS bundle
			When looking at macOS in *The CPack macOS bundle package* section earlier, we used the CPack generator `Bundle` type to package our application’s build artifacts in a macOS bundle. There is, however, an interesting alternative that is worth briefly mentioning.
			CMake provides an option to directly build an application as a macOS bundle. The setting is called `MACOSX_BUNDLE`, and it can be passed directly to `add_executable` or set separately with `set_target_properties`, as shown here:

set_target_properties(

${PROJECT_NAME} PROPERTIES Minimal CMake - Game of Life.app 位于 build//文件夹中,而不是我们迄今为止看到的可执行文件和松散的文件集合,包括库和资源。事实上,.app文件只是一个包含所有这些文件的文件夹;唯一的区别是它以一个稍微整洁的包的形式呈现。从 Finder 中,如果右键点击.app文件并点击 CMakeLists.txt 文件(查看 ch10/part-3/app/CMakeLists.txt 以查看完整示例)。

        可执行文件最终会被放入一个名为 `MacOS` 的文件夹中,我们的共享库(`.dylib` 文件)会被添加到名为 `Frameworks` 的文件夹中。最后,我们的着色器(以及 `.icns` 文件)会被添加到一个名为 `Resources` 的文件夹中。这个布局是 macOS 应用程序的标准布局,CMake 使得支持它相对容易。值得一提的改动是对 `set_target_properties` 的 `INSTALL_RPATH` 命令进行了小的更新,将 `Frameworks` 添加到搜索路径中:
$<$<PLATFORM_ID:Darwin>:@loader_path;.dylib files relative to its location, without the files needing to be in the same folder. We also used an add_custom_target command to create the Frameworks folder for us before we tried to copy any files there:

add_custom_target(

create_frameworks_directory ALL

COMMAND

${CMAKE_COMMAND} -E make_directory

\(<TARGET_FILE_DIR:\){PROJECT_NAME}>/../Frameworks

COMMENT "创建 Frameworks 目录")

add_dependencies(

${PROJECT_NAME} create_frameworks_directory)


			We use `add_dependencies` to ensure that this happens before our main executable is built (`${PROJECT_NAME}` will depend on the `create_frameworks_directory` target, ensuring that `create_frameworks_directory` happens first).
			We can forgo a lot of our install commands in the case of a macOS bundle because it handles copying all the files to the correct location internally; for the install step, we just need to install `BUNDLE`:

install(TARGETS ${PROJECT_NAME} if(APPLE) block. 我们使用 CMake 的 set_source_files_properties 命令来设置各种文件的包位置(这包括我们的着色器文件,vs_vertcol.bin 和 fs_vertcol.bin 以及新的 gol.icns 文件)。这些文件被复制到 Resources/shader 文件夹和包内 Contents 文件夹中的 Resources 文件夹(.app 文件)。

        以下代码片段展示了如何实现这一点:
set_source_files_properties(
  shader/build/vs_vertcol.bin shader/build/fs_vertcol.bin
  PROPERTIES MACOSX_PACKAGE_LOCATION "Resources/shader")
set_source_files_properties(
  packaging/macos/gol.icns
  PROPERTIES MACOSX_PACKAGE_LOCATION target_sources command:

target_sources(

${PROJECT_NAME} PRIVATE shader/build/vs_vertcol.bin

设置 MACOSX_BUNDLE 为 TRUE 后,CMake 将使用位于 /Applications/CMake.app/Contents/share/cmake-3.28/Modules(CMAKE_MODULE_PATH)中的一个名为 MacOSXBundleInfo.plist.in 的 Info.plist 模板文件,而不是我们自己创建 Info.plist 文件。然后可以使用 set_target_properties 提供多个 MACOSX_BUNDLE_ 属性来覆盖这些值(完整列表请参见 https://cmake.org/cmake/help/latest/prop_tgt/MACOSX_BUNDLE_INFO_PLIST.html)。

        在我们的例子中,我们设置了一些通常有用的属性;对于我们的用途,`MACOSX_BUNDLE_ICON_FILE` 是最显著的,它确保我们的应用程序具有独特的外观。下面展示了部分命令:
set_target_properties(
  ${PROJECT_NAME}
  PROPERTIES OUTPUT_NAME "Minimal CMake - Game of Life"
    MACOSX_BUNDLE_BUNDLE_NAME
      "Minimal CMake - Game of Life"
    MACOSX_BUNDLE_GUI_IDENTIFIER
      "com.minimal-cmake.game-of-life"
    MACOSX_BUNDLE_ICON_FILE "gol.icns"
  ...)
        也可以通过提供一个自定义的 `Info.plist` 文件并使用 `MACOSX_BUNDLE_INFO_PLIST` 从我们的 `CMakeLists.txt` 文件中引用它,来覆盖模板。

        唯一剩下的变化是从我们的 `CMakeLists.txt` 文件中移除了 `set(CPACK_BUNDLE_...` 调用,因为它们不再需要。通过这些更改,我们可以将应用程序构建为 macOS 包,并且在打包应用程序时,不再使用之前的 `Bundle` 生成器,而是可以使用 `DragNDrop` 生成器。这里展示了一个示例:
cpack --config build/multi-ninja/CPackConfig.cmake \
  -B package -G CPACK_DMG_... variables. As mentioned earlier, when discussing the Bundle generator, these values can be used to further customize the macOS disk image (for example, CPACK_DMG_DS_STORE can be used to refer to a customized .DS_Store file to provide a custom layout; for more information, see https://cmake.org/cmake/help/latest/cpack_gen/dmg.html and ch10/part-4/app as an example). It’s worth highlighting that the previous Bundle generator we used inherits from DragNDrop, which is why the CPACK_DMG_... settings can also be used to customize the .dmg file when using the Bundle generator.
			When building macOS applications that are going to be installed and launched through Finder, using the `MACOSX_BUNDLE` approach is incredibly useful and a sensible option to take. However, it does make maintaining a cross-platform application a little more complicated, as things behave quite differently between platforms. Whether you choose to use it or not will depend on your specific use case. Hopefully, showing how both approaches can be used is useful (`ch10/part-3/app` contains a full example for reference). For the remaining part of this chapter, we’ll switch back to the approach shown in `part-2` to help keep things more consistent across Windows, macOS, and Linux.
			Adding CPack presets
			Before closing out this chapter, it’s worth covering one more useful topic when it comes to CPack, and that’s its support for CMake presets. CMake provides the package presets (`packagePresets`) field in `CMakePresets.json`, and various CPack options can be set there, instead of from the command line or inside our `CMakeLists.txt` file.
			The upshot of this is that we don’t have to write the following on macOS:

cpack --config build/multi-ninja/CPackConfig.cmake -B package -G Bundle


			We can instead write this:

cpack –preset macos


			This will use the preconfigured options we’ve set for macOS; an example listing is shown here:

"packagePresets": [

{

"name": "base",

"configurePreset": "multi-ninja-super",

"packageDirectory": "${sourceDir}/package",

"hidden": true

},

{

"name": "macos",

"condition": {

"type": "equals",

"lhs": "${hostSystemName}",

"rhs": "Darwin"

},

"generators": ["Bundle"],

"inherits": ["base"]

},

...


			We start with a base preset that can be shared across multiple package presets (one for each platform), and then we set the package directory to `${sourceDir}/package` so that we can omit `-B package` from the command line. We must also provide `configurePreset` so that CPack knows which build folder to use (this is because the build folder is specified by the configure preset). We then provide our actual preset; the preceding example is for macOS, but presets are added for each platform in `ch10/part-4/app/CMakePresets.json`. By using the `condition` property, we ensure that this preset only appears when running on macOS, and we also provide the explicit generator to use (in this case, `Bundle`).
			The other useful thing about package presets is that they can be included in workflow presets, making it possible to configure, build, test, and package all with one command.
			As CPack requires the `Release` configuration to be compiled (`-DCMAKE_BUILD_TYPE=Release` when configuring with a single-config generator, or `--config Release` when building with a multi-config generator), we skip the testing preset in the packaging workflow, as we explicitly build it in a `Debug` configuration (see the `multi-ninja-super-test` build preset in `ch10/part-4/app/CMakePresets.json`).
			The following is an example of the workflow preset for Linux:

{

"name": "multi-ninja-super-package-linux",

"steps": [

{

"type": "configure",

"name": "multi-ninja-super"

},

{

"type": "build",

"name": "multi-ninja-super"

},

{

"type": "package",

"name": "linux"

}

]

}


			This can be invoked by using the following command (run from `ch10/part-4/app`):

cmake --workflow --preset \

multi-ninja-super-package-linux


			At the time of writing, workflow presets do not currently support the `condition` property we used for other presets. This means that it’s not possible to hide the workflow presets for other platforms, but they will fail to run, as we’ve specified which package preset is allowed on which platform already. It is possible that workflow presets will be updated in the future to inherit `condition` properties from the steps they use; however, there is no timeframe for when this may happen. This topic is an ongoing area of discussion within the CMake community.
			Other uses for CPack
			In addition to the main packaging and installer logic we’ve covered so far in this chapter, there are a couple more uses for CPack that are worth mentioning briefly. The first is the ability to use a standard archive format (such as `.zip`, `.7z`, or `.tar.gz`) to create a snapshot of an application at a certain point in time. It might be useful to do this to share a work-in-progress build with someone before sending them a full installer (running the application from the extracted folder will work and will not affect the wider system). It can also be useful to keep an archive of builds for milestones or releases you can then go back to easily in the future (this is commonly done in the *Tags and Releases* section of projects on GitHub. A good example is a tool such as `ripgrep` ([`github.com/BurntSushi/ripgrep/releases`](https://github.com/BurntSushi/ripgrep/releases)). For a full list of archive formats (and other package generators), run `cpack --help`.
			There is also one more file generated by CPack that we haven’t covered yet, and that’s `CPackSourceConfig.cmake`. By providing this file to the `cpack` `--config` argument, it’s possible to create an archive of the source directory itself, not the built artifacts. We must do a little bit of work to tell CPack which files not to include, which we achieve by setting the `CPACK_SOURCE_IGNORE_FILES` variable before invoking the `include(CPack)` command.
			The following is an example from `ch10/part-5/app/CMakeLists.txt`:

set(CPACK_SOURCE_IGNORE_FILES

"build.*/"

"package.*/"

"install/"

".git/"

".gitignore"

".vscode"

)


			The `CPACK_SOURCE_IGNORE_FILES` variable uses regular expressions to match against the different file and folder paths to discount.
			With this change, we can then run the following command to create a snapshot of the source directory if we wish:

cpack --config build/multi-ninja-super/CPackSourceConfig.cmake -G TGZ -B package-source


			Instead of providing `CPackConfig.cmake` as we did earlier in the chapter, we pass `CPackSourceConfig.cmake`, as well as a package generator (`-G`) to use and a folder (`-B`) to add the archived file to. We could also configure a CMake preset to handle this by using the `configFile` entry to specify the source config file. The following is one way to do this (we’re using `ZIP` instead of `TGZ` as the package generator in this example):

{

"name": "source",

"generators": ["ZIP"],

"packageDirectory": "${sourceDir}/package-source",

"configFile": "CPackSourceConfig.cmake",

"inherits": ["base"]

}


			Review `ch10/part-5/app/CMakePresets.json` to see this in context. It’s then possible to use `cpack --preset source` to create a source package.
			Summary
			You made it! Packaging was the last hurdle on our CMake journey. Making your application shareable with others is a significant achievement, and you now have everything you need to create a cross-platform application that’s easy to build and distribute. This is no small feat, and although there’s of course still plenty more to learn, you’re standing on a solid foundation and have the tools available to build your own applications from scratch.
			In this chapter, we got to know CPack and how it integrates with our existing CMake scripts. We first learned how to handle loading files relative to our executable, an important detail to make sure that running our application from any location works reliably. We then took a tour of CPack, seeing how to provide packaging support for Windows, macOS, and Linux. This is essential for providing a familiar means for users to install your application, matching what they’ve come to expect from existing conventions on their platform. We then took a small detour to discuss building macOS bundles with CMake and the various improvements and trade-offs in doing so. We then looked at how to simplify CPack usage by taking advantage of CMake presets to configure and automate the packaging step, another key factor in keeping our projects clean and maintainable. We concluded by looking at some other uses of CPack, as well as how to package our project’s source and its build artifacts.
			We’ve now covered all the main topics to get you up and running with CMake. In the final chapter, we’re going to cover useful tools available in the CMake ecosystem to make day-to-day development faster and easier. We’ll also touch on where to go next and introduce some topics we weren’t able to cover in detail in this book.







第十一章:支持工具和后续步骤

本章将介绍一些与 CMake 核心生态系统相辅相成的出色工具,帮助使 CMake 开发更加容易、快速和愉快。许多出色的项目扩展和增强了 CMake,了解这些工具可以显著改善你的开发体验。我们还将介绍一些其他流行的集成开发环境IDE),并了解如何让它们与 CMake 兼容。

除此之外,我们还将介绍一些关于开发 C/C++应用程序的推荐实践,以及 CMake 如何帮助实现这些目标,最后还会提供一些关于如何组织 CMake 脚本的建议。最后,我们将展望未来,介绍一些本书未能涵盖的 CMake 话题,并告诉你可以在哪里深入学习这些内容。

本章将覆盖以下主要内容:

  • Visual Studio Code 的 CMake 工具

  • Visual Studio Code 附加功能

  • CMake 与其他 IDE 的配合

  • C/C++构建建议

  • CMake 脚本结构

  • 未来的主题

技术要求

要跟随本书的内容,请确保你已经满足第一章《入门》中列出的要求。包括以下内容:

  • 一台运行最新操作系统OS)的 Windows、Mac 或 Linux 机器

  • 一款可用的 C/C++编译器(如果你尚未安装,建议使用平台的系统默认编译器)

本章中的代码示例可以通过以下链接找到:https://github.com/PacktPublishing/Minimal-CMake。

Visual Studio Code 的 CMake 工具

在本书开头,我们推荐使用Visual Studio Code作为首选编辑器,以确保无论你是在 Windows、macOS 还是 Linux 上开发,都能获得一致的体验。这完全是可选的,但使用 CMake 与 Visual Studio Code 结合起来有很多优点。在本节中,我们将讨论如何最好地使用本书中的示例,并展示如何在 Visual Studio Code 中配置、构建和调试项目。

如果你按照第一章《入门》中的Visual Studio Code 设置部分进行操作,你将已经通过C/C++扩展包安装了CMake Tools

导航 Minimal CMake 源代码

为了能够在Minimal CMake源代码中进行导航,建议从仓库的根目录打开一个 Visual Studio Code 项目。这可以通过克隆仓库,然后从该目录打开 Visual Studio Code 来实现:

cd <your-dev-folder>
git clone https://github.com/PacktPublishing/Minimal-CMake.git mc
code mc

你也可以切换到mc文件夹(cd mc),然后运行code .从该目录打开 Visual Studio Code。

这对于浏览示例以及比较各部分之间的差异和更新非常有用,但遗憾的是,它不适合构建和运行各个示例。为了获得更具代表性的使用 CMake Tools 在 Visual Studio Code 中配置、构建和调试示例的体验,最好为每个包含根级 CMakeLists.txt 文件的目录打开一个新的 Visual Studio Code 实例。例如,在某些后续示例的情况下,你可以在终端中切换到 ch10/part-5/app 目录,然后从该文件夹输入 code . (或者如果你在仓库根目录,可以直接输入 code ch10/part-5/app);这就是你通常与 CMake 项目合作的方式。随附的源代码由多个随着时间演变的嵌套项目组成;每个 ch<n>/part-<n>README.md 文件列出了要打开的根文件夹作为参考。

除了之前描述的工作流外,Visual Studio Code 还支持多根工作区(有关更多信息,请参见 code.visualstudio.com/docs/editor/multi-root-workspaces)。要激活多根工作区,你可以通过命令面板使用 工作区:添加文件夹到工作区… 选项,或者导航到 文件 菜单并选择 添加文件夹到工作区...

图 11.1:添加文件夹到工作区… 选项

图 11.1:添加文件夹到工作区… 选项

然后,你可以选择一个包含 CMakeLists.txt 文件的文件夹,在该文件夹中运行示例(在书中的后续示例中,通常是 app 文件夹)。以下是一个示例,展示了 Visual Studio Code EXPLORER 中的工作区视图:

图 11.2:EXPLORER 中的多工作区视图

图 11.2:EXPLORER 中的多工作区视图

你可以保存工作区并跟踪其中的文件夹。如果选择这样做,一个新文件将会以与工作区同名并带有 .code-workspace 文件扩展名的方式创建。还可以为文件夹添加显示名称,这在多个位置的文件夹具有相同名称时尤其有用(例如,之前提到的底部两个子文件夹,如果没有名称覆盖,它们将被称为 app)。

以下是之前显示的工作区中 .code-workspace 文件的内容:

{
  "folders": [
    {
      "name": "minimal-cmake",
      "path": "."
    },
    {
      "name": "ch8_part-2_app",
      "path": "ch8/part-2/app"
    },
    {
      "name": "ch11_part-3_app",
      "path": "ch11/part-3/app"
    }
  ],
  "settings": {}
}

Visual Studio Code 的 CMake Tools 扩展也支持多根工作区,并使得通过 CMake Tools 扩展侧边栏中的 项目大纲 部分在工作区之间切换成为可能(只需点击齿轮图标以设置活动工作区)。多根工作区对于将多个项目保存在一个单一仓库中非常方便。

配置、构建和调试

在 Visual Studio Code 中,如果你打开了某个文件夹的项目视图,例如:

cd ch2/part-1
code .

按下 F1 会打开命令面板。在命令面板中输入 CMake,会显示所有通过 Visual Studio Code CMake 工具扩展提供的 CMake 命令。在那里,你可以搜索诸如 configurebuild 等命令:

图 11.3:CMake 工具的配置和构建命令

在 Visual Studio Code 的侧边栏中,还有一个非常方便的 CMake 工具面板,提供了一系列选项,允许你在 Visual Studio Code 内部配置、构建、测试、调试和启动应用程序:

图 11.4:Visual Studio Code 的 CMake 工具面板

图 11.4:Visual Studio Code 的 CMake 工具面板

将鼠标悬停在每一行时,右侧会显示一个可以按下的图标,以执行相应的操作。这比在 Visual Studio Code 中为没有使用 CMake 的 C/C++ 应用程序配置 launch.json 文件要简单得多。

使用 macOS 的用户可能会遇到 lldb-mi 的问题。解决方法是在你的工作区或用户的 settings.json 文件中提供 miDebuggerPath。以下是一个示例路径,具体位置可能会有所不同:

"cmake.debugConfig": {
    "miDebuggerPath": "/Users/<username>/.vscode/extensions/ms-vscode.cpptools-1.21.6-darwin-arm64/debugAdapters/lldb-mi/bin/lldb-mi"
}

要打开 settings.json 文件,只需按下 F1 以打开命令面板,然后搜索 settings,并选择 首选项:打开用户设置(JSON)首选项:打开工作区设置(JSON),具体取决于你想在哪设置选项(全系统,或仅此工作区)。

如果因为某些原因,CMake 的 launch.json 文件和手动设置可执行文件位置以及工作目录,操作方法是进入 Visual Studio Code 的 运行与调试 面板,按下 创建一个 launch.json 文件

图 11.5:Visual Studio Code 的运行与调试面板

图 11.5:Visual Studio Code 的运行与调试面板

会打开 launch.json,或者打开命令面板,提供选择调试器的选项。这将根据你所使用的平台有所不同。如果你先使用 CMake 配置和构建,那么会显示 C++ 调试器(如果没有构建文件夹,则只会显示默认选项)。

launch.json 文件最初应如下所示:

{
  // Use IntelliSense to learn about possible attributes.
  // Hover to view descriptions of existing attributes.
  // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
  "version": "0.2.0",
  "configurations": []
}

屏幕右下角有一个名为 添加配置... 的按钮,点击它会显示调试选项列表。根据你的平台选择最合适的选项,例如:

  • macOS{} C/C++:(****lldb) 启动

  • Windows{} C/C++:(****Windows) 启动

  • Linux{} C/C++:(****gdb) 启动

生成的配置然后需要做两个小的修改:"program" 需要设置为可执行文件的位置,"cwd" 应该设置为您希望应用程序运行的工作目录(在大多数情况下,这将与 ${workspaceFolder} 相同)。

这里展示了一个 macOS 配置示例:

{
  "name": "(lldb) Launch",
  "type": "cppdbg",
  "request": "launch",
  "program": "${workspaceFolder}/build/Debug/minimal-cmake_game-of-life",
  "args": [],
  "stopAtEntry": false,
  "cwd": "${workspaceFolder}",
  "environment": [],
  "externalConsole": false,
  "MIMode": "lldb"
}

通过这个更改,按下 F5 或者在 RUN AND DEBUG 侧边栏面板中点击 Start Debugging 播放符号将启动应用程序,并允许您设置断点并逐步调试代码。接下来,我们将看看 CMake 预设如何与 Visual Studio Code 集成。

Visual Studio Code 和 CMake 预设

Visual Studio Code 在配置和构建阶段的处理方式与 CMake 从命令行本地处理方式类似(根据生成器做出最佳猜测,并为诸如 build 文件夹之类的事项选择一些合理的默认值,默认情况下该文件夹设置为 ${workspaceFolder}/build)。

好消息是,Visual Studio Code 在利用 CMake 预设的项目中表现得更好。如果项目根目录下存在 CMakePresets.json 文件,当您在 Visual Studio Code 中通过 CMake Tools 扩展点击配置按钮时,命令面板会提示您首先选择一个预设。它还提供了创建全新预设的选项。如果选择了预设,项目将使用该预设中定义的所有设置进行配置。输出会显示在 OUTPUT 窗口中,通常位于 Visual Studio Code 窗口的底部:

图 11.6: Visual Studio Code CMake – 配置输出

图 11.6: Visual Studio Code CMake – 配置输出

点击 Build 图标(出现在 CMake Tools 扩展中的 Build 标题旁边),然后会为我们构建应用程序,如果点击播放图标(当悬停在 CMake Tools 扩展中的 DebugLaunch 标题上时显示),应用程序将启动。Debug 功能特别有用,因为您可以在 Visual Studio Code 中设置断点,并使用 VARIABLESWATCH 窗口查看程序中变量的状态。

有趣的是,enable_testing() 命令需要添加到我们的 CMakeLists.txt 文件中(紧接着 include(CTest) 后添加即可)。虽然从命令行使用 ctest 可以正常工作,但为了使 Visual Studio Code 中的功能顺利运行,这个命令是必须的。

在打包方面,不幸的是,CMake Tools 不会尊重我们用来隐藏其他平台打包配置的 "condition" 属性。这意味着在 CMake Tools 中查看时,打包预设将会丢失。要恢复它们,只需从 CMakePresets.json 文件中的包预设中删除 "condition" 块。

例如,您需要从 windows 包预设中移除以下内容:

"condition": {
  "type": "equals",
  "lhs": "${hostSystemName}",
  "rhs": "Windows"
},

删除这些内容后,您将看到包预设按预期出现:

图 11.7: CMake Tools 中列出的包预设

图 11.7:CMake Tools 中列出的包预设

点击铅笔图标,你将可以选择一个可用的预设:

图 11.8:从命令面板选择包预设

图 11.8:从命令面板选择包预设

这是一个小小的不便,希望在未来版本的 CMake Tools 中修复。有关我们项目的示例,已做了小调整以使其完全兼容 CMake Tools,请参见ch11/part-1/app

调试 CMakeLists.txt 文件

另一个值得简要提及的优秀功能是支持调试CMakeLists.txt文件。大多数情况下,CMakeLists.txt文件应该足够简单和声明性,不需要调试,但无疑会有一些情况,通过逐步执行代码来查看发生了什么非常有用。打开包含CMakeLists.txt文件的目录中的 Visual Studio Code 后,打开命令面板(F1Cmd + Shift + P 在 macOS 上,Ctrl + Shift + P 在 Windows/Linux 上),搜索CMake Debugger;这将显示CMake:使用 CMake 调试器配置选项。在此之前,如果你添加了一些断点(要添加断点,点击 Visual Studio Code 文本编辑器的左侧边距,位于行号左侧,或位于侧边栏的右侧),执行将在此停止,你可以使用变量观察窗口更好地了解脚本处理时的状态。

图 11.9 显示了你可以期待的一个例子。在其中,我们在一个 APPLE 检查的断点处停止。我们可以看到已经添加的几个观察变量的值,局部部分包含所有相关的缓存变量、局部变量、目录和目标。

图 11.9:Visual Studio Code CMake 调试器

图 11.9:Visual Studio Code CMake 调试器

请注意,调试我们的主CMakeLists.txt文件在超级构建中并不完全有效,因为对ExternalProject_Add命令的调用无法直接处理我们的文件。为了解决这个问题,只需创建另一个常规构建(使用常规 CMake 预设之一,如multi-ninja),然后使用该构建进行调试。

你可以通过访问github.com/microsoft/vscode-cmake-tools并浏览文档来了解更多关于 CMake Tools 的信息。还可以通过访问code.visualstudio.com/docs/cpp/cmake-linux来获取另一个关于如何设置 CMake 和 Visual Studio Code 的视角。

Visual Studio Code 附加功能

本节涵盖了一些与 Visual Studio Code 和 CMake 密切相关的有用工具和功能,这些工具和功能可以简化开发,并且在未来的项目中可能会非常有用。

语法高亮

第一个是一个有用的扩展,它提供了 CMakeLists.txt.cmake 文件的语法高亮。该扩展名为 twxs.cmake。你可以从 marketplace.visualstudio.com/items?itemName=twxs.cmake 下载,或者从 Visual Studio Code 的侧边栏扩展管理器中下载。它不仅提供语法高亮,还提供有用的代码片段和自动补全功能。

生成 compile_commands.json

当使用 Ninja 或 Make 生成器时,可以向 CMake 提供一个缓存变量,叫做 CMAKE_EXPORT_COMPILE_COMMANDS,以启用生成名为 compile_commands.json 的文件。CMAKE_EXPORT_COMPILE_COMMANDS 可以添加到 CMake 预设中,也可以在运行配置步骤时通过命令行传递,例如:

cmake -B build -G Ninja compile_commands.json, is a compilation database, which essentially describes how code is compiled, independent of the build system being used (for more information, see https://clang.llvm.org/docs/JSONCompilationDatabase.html). What’s useful about this file is it’s used by a variety of other tools to perform operations on your code (this covers things such as static analysis, improved editor support, such as navigation and refactoring, and code coverage analysis). Visual Studio Code can also use this file to give improved completions and navigation (e.g., Go to definition).
			To set it, open the Command Palette, type `edit configurations`, and then select `"compileCommands"`, and set the value to something resembling `"${workspaceFolder}/build/multi-ninja/compile_commands.json"`. If the file can be found, the yellow squiggle underneath the path should disappear. In context, it looks like this:

{

"configurations": [

{

...

"compileCommands": "${workspaceFolder}/build/multi-ninja/compile_commands.json"

}

],

"version": 4

}


			When using CMake Tools, this shouldn’t be strictly necessary, as the IntelliSense support from the `"ms-vscode.cmake-tools"` provider should work out of the box. However, knowing how to generate a `compile_commands.json` file is useful, as well as how to set it in Visual Studio Code to get improved suggestions/completions and navigation support.
			Code auto-formatting
			To make formatting your `CMakeLists.txt` files much easier, there is an application to automatically format your CMake files available called `cmake-format` (the repository is hosted on GitHub and can be found here: [`github.com/cheshirekow/cmake_format`](https://github.com/cheshirekow/cmake_format)).
			It comes bundled as part of a Python package called `cmakelang`. To install it, first, ensure you have a recent version of Python installed on your system (to download the latest version of Python, please see [`www.python.org/downloads/`](https://www.python.org/downloads/)). On Linux/Ubuntu, you may wish to use a package manager to do this; it may also be necessary to run `sudo apt install python3-pip` before trying to install `cmake-format`.
			Once Python and Pip (Python’s package manager) are downloaded and available in your path, you can run the following commands:

python3 -m pip install cmakelang

python3 -m pip install pyyaml # cmake-format 所需


			You can then run `cmake-format` from the command line, passing the name of the `CMakeLists.txt` file you wish to format. On Linux (Ubuntu), you may first need to add `~/.local/bin` to your path, as this is the default location Pip installs executables and it might not already be in your path. To achieve this, simply add the following to your `.bashrc` file (found in your `$``HOME` directory):

export PATH="\(HOME/.local/bin:\)PATH"


			When running `cmake-format` from the terminal, pass the `-i` command-line argument to have the file updated in place (if you don’t pass `-i`, the result of the formatting operation will be output to the console). An example command might look like the following:

cmake-format CMakeLists.txt -i


			To take advantage of `cmake-format` inside of Visual Studio Code, you need to install the `cmake-format` Visual Studio Code extension. This can be installed either through Visual Studio Marketplace ([`marketplace.visualstudio.com/items?itemName=cheshirekow.cmake-format`](https://marketplace.visualstudio.com/items?itemName=cheshirekow.cmake-format)) or through the integrated Visual Studio Code extension manager. Once `cmake-format` is installed, running the `cmake-format` will process the open file. Unfortunately, at the time of writing, this does not seem to work reliably on Linux but should work on macOS and Windows.
			There are several configuration options to control how the formatting looks; these are added to a file called `.cmake-format.yaml` that lives at the root of the project. (`cmake-format` will search up the folder structure until it finds a `.cmake-format.yaml` file. In the case of *Minimal CMake*, there’s just one file at the root of the repo used to format all examples.) The contents of the *Minimal CMake* `.cmake-format.yml` file are as follows:

line_width: 80

tab_size: 2

enable_sort: True

dangle_parens: False

dangle_align: 'prefix'

command_case: 'canonical'

keyword_case: 'upper'

line_ending: 'auto'


			Feel free to experiment with different settings to find a style that works for you. To learn more about `cmake-format`, see the documentation available at [`cmake-format.readthedocs.io/en/latest/index.html`](https://cmake-format.readthedocs.io/en/latest/index.html). Regrettably, `cmake-format` is no longer under active maintenance, so it’s possible that if issues are discovered, fixes may not be forthcoming, and it may not be updated to handle newer versions of CMake. Even with that being the case, it’s still an incredibly useful tool, and infinitely superior to formatting things manually. There is also an alternative tool called `gersemi` ([`github.com/BlankSpruce/gersemi`](https://github.com/BlankSpruce/gersemi)), which also formats CMake code and is under active development; it may be worth exploring in the future.
			Diff Folders
			One last tool worth briefly mentioning is an extension called `build`, `build-third-party`, `install`, `package`, and `.vscode` folders to the Diff Folders exclude list (`l13Diff.exclude` in `settings.json`). The diff panel allows you to clearly view changes between multiple files at once.
			![Figure 11.10: Diff panel display in Diff Folders](https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/min-cmk/img/B21152_11_11.jpg)

			Figure 11.10: Diff panel display in Diff Folders
			The options along the top-left panel are useful for customizing the display to show all files, added files, changed files, or deleted files. Diff Folders can be a helpful companion tool when the built-in **Compare Active File With...** Visual Studio Code command is not sufficient.
			CMake with other IDEs
			Throughout this book, we’ve focused exclusively on Visual Studio Code, primarily because it provides a consistent experience across Windows, macOS, and Linux. It is sometimes necessary and useful to use an editor or IDE for a specific platform and knowing how to configure it to play nicely with CMake can be helpful. We’ll briefly cover a few useful settings for Visual Studio, Xcode, and CLion.
			Visual Studio
			If developing on Windows, using the fully-fledged Visual Studio development environment can be especially useful at times. *Visual Studio Community Edition* is completely free and comes with a host of useful features when developing in C++ (see *Chapter 1*, *Getting Started*, for instructions on how to install it).
			When trying to run projects from within Visual Studio (especially examples from earlier parts of this book), things unfortunately might not work as expected. The reason for this is, by default, the working directory Visual Studio uses is the folder of the executable, not the project root that we relied on up until *Chapter 10*, *Packaging the Project for Sharing* (this is because we’d normally launch our executable from the command line).
			To try things out, configure the project using the Visual Studio generator. This can either be done by using the `vs` preset in later chapters, or by specifying the generator directly:

cmake --preset vs # 选项 1

cmake -B build/vs -G "Visual Studio 17" # 选项 2


			Depending on the `ch<n>/part<n>` directory you’re trying this from, you will need to have built the third-party dependencies first, either separately, or as part of a super build. For simplicity, in later examples, we’ll assume you’ve used `cmake --preset multi-ninja-super` to configure and build the project using Ninja, and then can use `cmake --preset vs` to create the Visual Studio generator files. It’s also possible to perform the super build using Visual Studio; there’s just a possibility it might take slightly longer than with Ninja:

cmake -B build/vs -G "Visual Studio 17" -DSUPERBUILD=ON


			To open our project in Visual Studio, after running one of the configure steps mentioned previously, open the `build/vs` folder, and double-click the `.sln` file (for example, `minimal-cmake_game-of-life_window.sln`). When inside Visual Studio, the first thing we need to do is review the **Solution Explorer** window (by default, on the right of the screen) and set the project that’s been created for us as **Startup Project** (this corresponds to our executable target):
			![Figure 11.11: Visual Studio Solution Explorer](https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/min-cmk/img/B21152_11_12.jpg)

			Figure 11.11: Visual Studio Solution Explorer
			This can be achieved by right-clicking the project and selecting the **Set as Startup** **Project** option:
			![Figure 11.12: The Visual Studio Set as Startup Project context menu](https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/min-cmk/img/B21152_11_13.jpg)

			Figure 11.12: The Visual Studio Set as Startup Project context menu
			We’ll then see our project displayed in bold:
			![Figure 11.13: Application set at Startup Project](https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/min-cmk/img/B21152_11_14.jpg)

			Figure 11.13: Application set at Startup Project
			To avoid having to make this change manually every time the solution is generated from scratch, it’s possible to use the `VS_STARTUP_PROJECT` CMake property to refer to the target we want to be the startup project. This can be achieved with the following addition to our `CMakeLists.txt` file:

set_property(

DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}

PROPERTY VS_ 特定项。

        要构建并启动应用程序,我们可以使用屏幕顶部中央的 **本地 Windows 调试器** 选项,或者按 *F5*(如果只构建不运行,使用 *F7*):

        ![图 11.14:Visual Studio 配置和启动选项](https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/min-cmk/img/B21152_11_15.jpg)

        图 11.14:Visual Studio 配置和启动选项

        记得选择与我们为第三方依赖项构建的配置相匹配的配置,如果你使用本书后面的示例。若你在 `Debug` 模式下构建依赖项,然后尝试在 `Release` 模式下构建应用程序(或反之),可能会遇到链接器错误。

        一旦所有构建完成,并且你可以运行可执行文件,在从 *第二章* 到 *第九章* 的所有示例中,你将看到以下错误信息:
Shaders not found. Have you built them using compile-shader-<platform>.sh/bat script?
        这是因为应用程序正在 `build/vs/<config>` 文件夹中寻找资源(着色器)文件,而不是项目的根文件夹,通常我们会从终端在其中运行程序。

        我们在 *第十章*中看到了解决这一问题的一种方式,*为共享项目打包*,但是如果我们还没有达到那个阶段,一个有用的解决方法是提供一个名为 `CMAKE_VS_DEBUGGER_WORKING_DIRECTORY` 的 CMake 缓存变量,用于设置 Visual Studio 中工作目录的位置。这可以通过为整个项目设置 `CMAKE_VS_DEBUGGER_WORKING_DIRECTORY`,或者为特定目标在 `CMakeLists.txt` 文件中添加以下命令来实现:
set_target_properties(
  ${PROJECT_NAME}
  PROPERTIES
    cmake --preset vs), and if it’s open, Visual Studio will show a popup letting us know the solution has been changed and needs to be reloaded.
			Select **Reload All** and then build and run again from within Visual Studio. The application should now run successfully as it will be looking for the shader files in the location we expect. This isn’t something to use when reaching the stage of making your application sharable; the technique outlined in *Chapter 10*, *Packaging the Project for Sharing*, is more appropriate, but this can be a useful tool in the initial stages of development.
			Visual Studio is a great tool and well worth exploring if you’re developing on Windows. The debugging features, profiling tools, and code analysis support are all high quality and provide a lot of utility while developing larger more complex projects. Visual Studio also provides the ability to debug CMake scripts just as Visual Studio Code does (see [`learn.microsoft.com/en-us/cpp/build/configure-cmake-debugging-sessions`](https://learn.microsoft.com/en-us/cpp/build/configure-cmake-debugging-sessions) for more information).
			Xcode
			`Info.plist` file, which were discussed in *Chapter 10*, *Packaging the Project* *for Sharing*.
			To generate a project for Xcode, either use the existing CMake preset we defined or name the generator manually (Xcode will need to be installed before trying this):

cmake --preset xcode # 选项 1

cmake -B build/xcode -G Xcode # 选项 2


			To open the Xcode project, navigate to `build/xcode` (in `.xcodeproj` extension (for example, `minimal-cmake_game-of-life_window.xcodeproj`).
			There, like in Visual Studio, we need to change the working directory to be the root of our project for things to work correctly in some of the earlier examples. Fortunately, this is simple to do, and like how we set `VS_DEBUGGER_WORKING_DIRECTORY` in the case of Visual Studio.
			In our application’s `CMakeLists.txt` file, we need to add the following settings:

set_target_properties(

${PROJECT_NAME} PROPERTIES

XCODE_GENERATE_SCHEME TRUE

XCODE_SCHEME_WORKING_DIRECTORY ${CMAKE_SOURCE_DIR})


			This will update the Xcode scheme for the executable target. This can be viewed by clicking the top bar in Xcode, selecting the name of the target, and then clicking **Edit scheme...**. Clicking the **Options** tab will then display a series of settings, including **Working Directory**:
			![Figure 11.15: Custom working directory in Xcode](https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/min-cmk/img/B21152_11_16.jpg)

			Figure 11.15: Custom working directory in Xcode
			There are a lot more `XCODE_SCHEME_` variables that can also be set to configure a scheme outside of Xcode; for a full list, please see [`cmake.org/cmake/help/latest/prop_tgt/XCODE_GENERATE_SCHEME.html`](https://cmake.org/cmake/help/latest/prop_tgt/XCODE_GENERATE_SCHEME.html). To view other `Info.plist` options that can be configured, click the top-level project in the left-hand sidebar and click the **Build Settings** tab from the top bar. Either scroll down or use the filter to search for the **Info.plist** **Values** section.
			Xcode is necessary when it comes to publishing your app on macOS or iOS. The code signing functionality must be used for this; see [`developer.apple.com/documentation/xcode/distribution`](https://developer.apple.com/documentation/xcode/distribution) for more information on this topic. Xcode also comes bundled with an application called **Instruments**, which includes a suite of tools to perform memory tracking, profiling, and more.
			CLion
			`CMakeLists.txt` file of your project. CLion then stores project-specific settings in a hidden `.idea` folder. Very much like Visual Studio Code, one of the most convenient ways to use CLion with CMake is by using our existing CMake presets. CLion currently only supports CMake presets up to version `6`, so we need to drop our version from `8` to `6` for things to work correctly. With that change applied, it’s possible to load the CMake presets we’ve already defined with all the right settings. CLion doesn’t handle super builds by default so it’s recommended to build a super build configuration separately outside of CLion, and then use a normal preset when working with CLion.
			IDEs can be a huge productivity boost once they’re configured, but they take time to master and can come with a relatively steep learning curve, along with their own quirks and idiosyncrasies. Knowing how to get by without them is useful but don’t be afraid to try them out and see what they have to offer.
			We’re now going to turn our attention to some important topics to be aware of when building our C and C++ code.
			C/C++ build recommendations
			To ensure the code we write is as correct as possible, it’s a wise move to enable as many warnings and checks as we can while working on our project. There are a few ways to achieve this using CMake. The first is ensuring we’re using standard C++ and avoiding any compiler-specific extensions to guarantee our code is cross-platform. This can be achieved with `CXX_STANDARD_REQUIRED` and `CXX_EXTENSIONS`, as shown in the following code (for C, just replace `CXX_` with `C_`):

set_target_properties(

${PROJECT_NAME}

PROPERTIES

CXX_STANDARD_REQUIRED ON

CMakeLists.txt 文件:

set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
        此外,通常建议启用尽可能多的警告,以帮助在编码过程中尽早捕捉错误(这有助于捕获未初始化变量的使用、数组越界访问等问题,以及许多其他问题)。为了启用这些警告,我们需要根据所使用的编译器设置不同的编译器标志。我们可以使用 CMake 生成器表达式来帮助实现这一点,并为所使用的编译器(无论是**Microsoft Visual C++**(**MSVC**)、**GNU 编译器集合**(**GCC**)还是 Clang)设置正确的警告。

        以下代码片段展示了可能的实现方式:
string(
  APPEND
  compile_options
    "$<$<OR:"
    "$<CXX_COMPILER_ID:Clang>,"
    "$<CXX_COMPILER_ID:AppleClang>>:"
      "-Weverything;-Wno-c++98-compat;-Wno-;"
      "-Wno-global-constructors;-Wno-;"
      "-Wno-c++98-compat-pedantic;-Wno->"
    ...
target_compile_options(
  ${PROJECT_NAME} PRIVATE ${compile_options})
        在上面的片段中,我们使用 CMake 变量来保存我们希望看到的各种编译标志,然后通过 `target_compile_options` 命令应用它们。你决定启用或禁用的警告将取决于你希望采用的项目和编码实践。从 `cppbestpractices` GitHub 页面([`github.com/cpp-best-practices/cppbestpractices/blob/master/02-Use_the_Tools_Available.md#compilers`](https://github.com/cpp-best-practices/cppbestpractices/blob/master/02-Use_the_Tools_Available.md#compilers))可以找到一个关于警告及其含义的优秀列表。要查看完整的警告集示例,请参见 `ch11/part-2/app/CMakeLists.txt`。尝试编译项目,看看在 *Minimal CMake* 示例代码中可以检测到多少警告(当然,这些警告的数量是为了演示目的)。

        Unity 构建

        CMake 提供了一种构建设置,称为 `.c` 或 `.cpp` 文件,并将它们连接在一起。这是通过常规的 C/C++ 预处理器 `#include` 指令完成的。CMake 将动态生成这些文件并编译它们,而不是编译现有的 `.c/.cpp` 文件。在我们的 `app` 示例项目中,创建了一个名为 `unity_0_cxx.cxx` 的单一统一 `.cpp` 文件,它包含了项目中的所有 `.cpp` 文件(该文件可以在 `build` 文件夹下的 `CMakeFiles/minimal-cmake_game-of-life_window.dir/Unity` 目录中找到)。

        要启用 Unity 构建,可以在命令行中传递 `-DCMAKE_UNITY_BUILD=ON`(或者你可以创建一个 Unity CMake 预设来做到这一点)。Unity 构建的一个缺点是它违反了 C/C++ 中的一个核心规则,那就是 `.c` 或 `.cpp` 文件可以定义具有内部链接的值,这些定义是私有的。这意味着它们不会与其他文件发生冲突,因为它们是单独编译的(这同样适用于匿名命名空间)。然而,当启用 Unity 构建并且这些源文件被分组在一起时,如果两个变量或函数恰好共享相同的名称,程序将会出错(你最有可能会遇到重复定义符号的错误)。Unity 构建还可能导致相反的问题,即在源文件之间引入隐式依赖。如果 `.cpp` 文件从同一 Unity 文件中的前一个 `.cpp` 文件引入了 `include`,那么它可能会在 Unity 构建中编译成功,但如果缺少该 `include`,它就无法单独编译。

        确保项目在启用和不启用 Unity 构建的情况下都能正常工作可能是一个挑战,除非定期启用这两种构建方式。Unity 构建的另一个缺点是,在某些情况下,它们的使用可能会减慢迭代时间。这是因为对一个 `.cpp` 文件进行更改时,会触发该文件所在的 Unity 文件中所有其他文件的重新编译(因为该 Unity 文件是作为一个整体进行编译的)。构建时间的变化取决于 Unity 文件的分组方式,但对于小的更改,它可能会导致更长的编译时间。在迭代开发时,最好禁用 Unity 构建,并仅在持续集成构建时启用它们,以减少外部资源的使用。

        通过排除可能无法干净编译的文件,确实可以微调 Unity 构建,但这可能是一个繁琐的过程。尝试为一个已经成熟的项目启用 Unity 构建可能会是一个挑战;因此,如果你认为它们会带来好处,最好在开发早期就启用它们。这也非常重要,要衡量和分析节省的时间(如果有的话),以了解它们带来的影响。要了解更多关于 Unity 构建的信息,请参阅 [`cmake.org/cmake/help/latest/prop_tgt/UNITY_BUILD.html`](https://cmake.org/cmake/help/latest/prop_tgt/UNITY_BUILD.html)。

        CMake 脚本结构

        为了尽量保持简单,在*最小化 CMake*中,我们选择将项目中的 `CMakeLists.txt` 文件数量限制在最少的数量,保持大部分内容集中在一个地方(我们每个项目最多有两个 `CMakeLists.txt` 文件,一个用于第三方依赖,一个用于主应用程序)。这有一些优势;集中管理可以让查找内容和理解项目变得更容易,但随着项目的增长,处理一个庞大的单一文件可能会变成维护噩梦(尤其是对于大团队来说)。

        为了改善关注点的分离并使事情更加模块化,可以将`CMakeLists.txt`文件添加到不同的目录中,以处理应用程序的不同部分的构建,然后通过`add_subdirectory`将它们引入主构建中。例如,我们可以将测试和打包逻辑移动到各自的文件夹中,然后从顶层的`CMakeLists.txt`文件中按如下方式包含它们:
if(MC_GOL_APP_BUILD_TESTING)
  enable_testing()
  add_subdirectory(tests)
endif()
add_subdirectory(packaging)
        但是,在进行这些更改时,我们需要注意一些细微之处,特别是在`tests`子文件夹的情况下。通过将逻辑从`app/CMakeLists.txt`移动到`app/tests/CMakeLists.txt`,我们之前使用的任何相对路径将不再有效;因此,我们需要处理这些路径(在我们的情况下,我们需要更新`shaders-compiled.cmake`的路径,并显式地使用`CMAKE_SOURCE_DIR`来包含完整路径)。我们还需要记住从顶层的`CMakeLists.txt`文件中调用`enable_testing()`,否则当使用 CTest 时,子文件夹中的测试将无法被发现。

        由于我们也在`tests`文件夹中创建了一个可执行目标,默认情况下,它将位于`build`文件夹中的`tests`子文件夹中。这将破坏我们的`RPATH`加载;因此,为了保持简单,我们确保它进入与之前相同的输出目录。我们可以通过在`app/tests/CMakeLists.txt`中使用以下命令来实现这一点:
set_target_properties(
  ${PROJECT_NAME}-test PROPERTIES RUNTIME_OUTPUT_DIRECTORY
  ${CMAKE_BINARY_DIR})
        幸运的是,如果我们使用单配置或多配置生成器,`CMAKE_BINARY_DIR`将会正确工作(在多配置生成器的情况下,它将映射到正确的配置文件夹)。要查看完整的上下文,请参阅`ch11/part-3/app`。

        我们可以进一步将安装逻辑移动到一个单独的`CMakeLists.txt`文件中,或者将我们的工具函数提取到新的`.cmake`文件中,并使用`include`将其引入。我们还可以使用在*第九章*中讨论的接口目标技术,*为项目编写测试*,创建一个单独的目标,其中包含所有设置的 C/C++编译警告标志,然后让我们的应用程序和测试链接到该目标。CMake 在脚本结构方面提供了很大的自由度和灵活性,通过时间和经验(并且通过阅读其他`CMakeLists.txt`文件),你将会找到最适合自己的方法。

        未来的主题

        这就是我们 CMake 之旅的终点。本书的目标一直是尽量分享 CMake 的精华部分,而不陷入琐碎的细节(当然也有一些琐碎的部分,但它本可以更糟)。目的是展示可以使用和学习的实用示例。只有亲眼看到像 CMake 这样的工具如何实际运作,才能真正理解它的能力,并开始理解它的工作原理。我们已经覆盖了很多内容,希望你现在可以使用这些工具来开始构建自己的库和应用程序,并将你构建的内容与越来越容易使用的开源软件进行集成。

        话虽如此,仍有很多内容我们没有覆盖,还有许多东西需要学习。如果你有兴趣使用 CMake 来构建其他平台的代码(例如 Android 或 iOS),**工具链文件**是值得研究的内容。它们允许你为与主机平台不同的目标平台构建代码。当构建嵌入式设备、移动平台、不同操作系统或不同架构(例如,ARM 与 x86_64)上的代码时,这非常有用。

        我们没有讨论如何在我们的项目中使用完备的包管理器。值得探索的是开源包管理器 `vcpkg`,它会下载你想使用的库的预构建二进制文件(如果它们适用于你使用的平台/架构的话)(它们也因此使用工具链文件;因此,理解它们的工作原理及其必要性将有所帮助)。

        还有一个有用的工具叫做 `CPM.cmake` ([`github.com/cpm-cmake/CPM.cmake`](https://github.com/cpm-cmake/CPM.cmake)),它是 CMake 的 `FetchContent` 命令的封装器。它提供了一种更简洁的方式来定义依赖项(它们的位置、名称和版本)。例如,使用 Catch2 的代码如下:
include(cmake/CPM.cmake)
CPMAddPackage("gh:catchorg/Catch2@3.6.0")
target_link_libraries(
  ${PROJECT_NAME} ... Catch2::Catch2WithMain)
        还有一个问题是关于持续交付和持续集成,用于在每次变更时自动构建,从而尽早发现问题。深入探讨这一点超出了本书的范围,但如果你想看一个简单的示例,展示如何使用 GitHub Actions 构建、测试和打包代码,可以查看 *Minimal* *CMake* 仓库根目录下的 `.github/workflows/cmake.yml` 文件。

        还有更多资源可以帮助你继续学习 CMake。首先可以查看 CMake 官方文档([`cmake.org/cmake/help/latest/`](https://cmake.org/cmake/help/latest/))。它不是完美的,但它在不断改进,且在查找特定功能或属性的细节时是一个重要的资源。如果你遇到困难并需要寻求帮助,CMake 论坛社区([`discourse.cmake.org/`](https://discourse.cmake.org/))是一个很好的资源,里面有许多 CMake 专家随时准备回答你的问题(通过搜索问题存档也能找到很多有用的信息)。除了 CMake 论坛社区,你还可以访问 C++ Slack 工作区([`cpplang.slack.com/`](https://cpplang.slack.com/))获得更多帮助。那里有一个专门的 CMake 频道,很多友好且乐于助人的人拥有丰富的 CMake 知识,可以为你提供帮助。

        另一个你可能会觉得有用的资源是*《掌握 CMake》*,这本书最初由 Ken Martin 和 Bill Hoffman 编写,现在可以在网上免费阅读,网址是[`cmake.org/cmake/help/book/mastering-cmake/`](https://cmake.org/cmake/help/book/mastering-cmake/)。虽然有些过时,但里面有很多有价值的信息。说到书籍,Craig Scott 编写的*《专业 CMake:实用指南》*([`crascit.com/professional-cmake/`](https://crascit.com/professional-cmake/))是一本非常详细的 CMake 参考书,几乎涵盖了你需要了解的所有内容。

        如果你喜欢本书并希望了解更多关于 CMake 的内容,Packt 出版的几本关于 CMake 的书值得一看,包括*《现代 CMake for C++:探索更好的构建、测试和打包软件的方法》*,*《CMake 最佳实践:用 CMake 升级你的 C++ 构建,达到最大效率和可扩展性》*,以及*《CMake 烹饪书:使用现代 CMake 构建、测试和打包模块化软件》*。

        最后,为了查看更多实际案例,GitHub 上有一些有用的资源库,提供了设置 CMake 项目的建议和经过验证的方法。这些包括来自`cppbestpractices`的`cmake_template`(参见[`github.com/cpp-best-practices/cmake_template`](https://github.com/cpp-best-practices/cmake_template))以及本书作者的[`github.com/pr0g/cmake-examples`](https://github.com/pr0g/cmake-examples)(这就是整个项目的起源)。此外,`awesome-cmake` GitHub 仓库上列出了大量链接和资源,涵盖了库、书籍和文章([`github.com/onqtam/awesome-cmake`](https://github.com/onqtam/awesome-cmake))。

        摘要

        我们(终于)完成了。这标志着本书的结束,我们从 CMake 新手到自信的 CMake 从业者的转变也已经完成。

        在本章中,我们花了一些时间更深入地了解 Visual Studio Code 的 CMake Tools 扩展,并理解它如何让使用 CMake 更加轻松愉快。从调试 CMake 脚本到与 CMake 预设的无缝集成,CMake Tools 在 Visual Studio Code 中处理 CMake 时是一个必不可少的工具。接着,我们介绍了一些其他扩展,以增强语法高亮和自动格式化,改善整体编辑体验。然后,我们将注意力转向其他流行的编辑器,了解如何确保它们从一开始就与我们的项目兼容。之后,我们提出了一些关于如何构建 C/C++ 代码的建议,并分析了需要注意的各种利弊。接下来,我们讨论了如何拆分 `CMakeLists.txt` 文件,以便在项目扩展时保持可管理性。这没有标准答案,但了解一些拆分技巧有助于在项目或团队扩展时保持维护的简易性。最后,我们展望未来,了解 CMake 还能提供哪些功能,并指引您去哪里获取更多的学习资源。

        很荣幸能与您分享这些知识,希望您能从中收获一些有价值的信息。我们的目标一直是让您掌握足够的 CMake 知识,以便完成任务,然后继续构建您的应用程序/库/工具,专注于最重要的事情。CMake 虽然并不完美,但它是 C 和 C++ 生态系统中主要的构建工具,因此熟练掌握它是一项宝贵的技能,并将为您解锁其他框架和库,简化您自己的软件创建过程。

        感谢阅读,祝您构建愉快!


posted @ 2025-10-02 09:35  绝不原创的飞龙  阅读(2)  评论(0)    收藏  举报