min-cmk-merge-0

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. 扫描二维码或访问以下链接

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/min-cmk/img/B21152_QR_Free_PDF.jpg

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(如果你在阅读本书时有更新的版本,随时可以选择那个版本)。

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/min-cmk/img/B21152_01_1.jpg

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

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

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/min-cmk/img/B21152_01_2.jpg

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

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

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

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

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/min-cmk/img/B21152_01_3.jpg

图 1.3:Windows 11 任务栏搜索框

  1. 搜索终端

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/min-cmk/img/B21152_01_4.jpg

图 1.4: Windows 11 应用搜索结果

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

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/min-cmk/img/B21152_01_5.jpg

图 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。你应该能看到以下输出(架构会根据你使用的机器而有所不同):

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/min-cmk/img/B21152_01_6.jpg

图 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),然后按照标准安装说明进行操作。

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/min-cmk/img/B21152_01_7.jpg

图 1.7: CMake Windows 安装程序

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

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/min-cmk/img/B21152_01_8.jpg

图 1.8: CMake 安装程序 PATH 选项

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

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/min-cmk/img/B21152_01_9.jpg

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

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

在 macOS 上安装 CMake

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

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

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

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/min-cmk/img/B21152_01_10.jpg

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

  1. 搜索 App Store

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/min-cmk/img/B21152_01_11.jpg

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

  1. App Store 中搜索 Xcode

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/min-cmk/img/B21152_01_12.jpg

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

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

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/min-cmk/img/B21152_01_13.jpg

图 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),并按照标准安装说明进行操作。

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/min-cmk/img/B21152_01_14.jpg

图 1.14:CMake macOS 安装

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

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

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

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/min-cmk/img/B21152_01_15.jpg

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

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

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/min-cmk/img/B21152_01_16.jpg

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

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

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/min-cmk/img/B21152_01_17.jpg

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

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

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/min-cmk/img/B21152_01_18.jpg

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

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

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/min-cmk/img/B21152_01_19.jpg

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

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

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/min-cmk/img/B21152_01_20.jpg

图 1.20:从终端运行 cmake

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

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

在 Linux(Ubuntu)上安装 CMake

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

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

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

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/min-cmk/img/B21152_01_21.jpg

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

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

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/min-cmk/img/B21152_01_22.jpg

图 1.22:从终端安装 build-essential

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

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/min-cmk/img/B21152_01_23.jpg

图 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. 然后再次点击右上角的 提取

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/min-cmk/img/B21152_01_24.jpg

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

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

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/min-cmk/img/B21152_01_25.jpg

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

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

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

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/min-cmk/img/B21152_01_26.jpg

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

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

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

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/min-cmk/img/B21152_01_27.jpg

图 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++ 扩展包

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/min-cmk/img/B21152_01_28.jpg

图 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:
			<https://github.com/OpenDocCN/freelearn-c-cpp-pt2-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:
			<https://github.com/OpenDocCN/freelearn-c-cpp-pt2-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:
			<https://github.com/OpenDocCN/freelearn-c-cpp-pt2-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:
			<https://github.com/OpenDocCN/freelearn-c-cpp-pt2-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):
			<https://github.com/OpenDocCN/freelearn-c-cpp-pt2-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`;否则,表达式的结果为空生成器表达式有点像 CC++中的三元操作符(`<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(PROJECTNAME{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 <BUILDLOCALINTERFACE:{CMAKE_CURRENT_SOURCE_DIR}/include/>

<BUILDLOCALINTERFACE:{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 "<BUILDLOCALINTERFACE:{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

CMAKECOMMANDEcopyifdifferent<TARGET_FILE:mc-gol>

<TARGETFILEDIR:{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(

PROJECTNAMEBUILDRPATHORIGIN) # 仅限 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 选择显示它,在 命令面板 中显示:

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/min-cmk/img/B21152_05_1.jpg

图 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 时,你会看到类似于以下的界面:

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/min-cmk/img/B21152_05_2.jpg

图 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 变量发生变化;所有红色高亮应随之消失:

https://github.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/min-cmk/img/B21152_05_3.jpg

图 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 图形界面,以便更好地理解它的工作原理以及我们可以用它做什么。

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

posted @   绝不原创的飞龙  阅读(15)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 提示词工程——AI应用必不可少的技术
· Open-Sora 2.0 重磅开源!
· 周边上新:园子的第一款马克杯温暖上架
历史上的今天:
2024-03-03 OpenDocCN 20240303 更新
2024-03-03 笨办法学 Python3 第五版(预览)(三)
2024-03-03 笨办法学 Python3 第五版(预览)(二)
2024-03-03 笨办法学 Python3 第五版(预览)(一)
2023-03-03 PyTorch 1.0 中文官方教程:用 numpy 和 scipy 创建扩展
点击右上角即可分享
微信分享提示