步骤 1:CMake 入门¶
CMake 教程的第一个步骤旨在帮助您快速上手为小型项目编写有用的 CMake 构建。到最后,您将能够使用 CMake 描述可执行文件、库、源文件和头文件,以及它们之间的链接关系。
此步骤中的每个练习都将从讨论练习所需的概念和命令开始。然后,提供一个目标和有用的资源列表。Files to Edit 部分中的每个文件都位于 Step1 目录中,并包含一个或多个 TODO 注释。每个 TODO 代表一两行需要更改或添加的代码。TODOs 按数字顺序完成,先完成 TODO 1,然后是 TODO 2,依此类推。
注意
教程中的每个步骤都建立在前一个步骤的基础上,但步骤并非严格连续。与学习 CMake 无关的代码,例如 C++ 函数实现或教程范围之外的 CMake 代码,有时会插入到步骤之间。
Getting Started 部分将提供一些有用的提示,并引导您完成练习。然后 Build and Run 部分将逐步介绍如何构建和测试练习。最后,在每个练习结束时,将回顾预期的解决方案。
背景¶
CMake 的典型用法围绕一个或多个名为 CMakeLists.txt 的文件。此文件有时被称为“列表文件”或“CML”。在一个给定的软件项目中,CMakeLists.txt 将存在于我们希望为 CMake 提供有关如何处理该目录或子目录中的本地文件和操作的说明的任何目录中。每个文件都包含一组命令,这些命令描述了与构建软件项目相关的某些信息或操作。
并非软件项目中的每个目录都需要 CML,但强烈建议项目根目录包含一个。这将作为 CMake 在配置期间进行初始设置的入口点。此根 CML 应始终在文件顶部或附近包含相同的两个命令。
cmake_minimum_required(VERSION 3.23)
project(MyProjectName)
命令 cmake_minimum_required() 是 CMake 为项目开发者提供的兼容性保证。调用它时,可以确保 CMake 采用所列版本的行为。如果调用包含上述代码的 CML 的 CMake 版本较晚,其行为将与 CMake 3.23 完全相同。
命令 project() 是一个概念上简单的命令,但功能复杂。它通知 CMake,接下来的内容是描述一个具有给定名称的独立软件项目(而不是类 shell 脚本)。当 CMake 看到 project() 命令时,它会执行各种检查以确保环境适合构建软件;例如,检查编译器和其他构建工具,并发现主机和目标机器的字节序等属性。
注意
虽然为每个命令都提供了指向完整文档的链接,但并不期望读者理解他们使用的每个 CMake 命令的全部语义。像学习任何软件一样,有效学习 CMake 是一个循序渐进的过程。
本教程此步骤的其余部分将主要关注另外四个命令的用法。add_executable() 和 add_library() 命令用于描述软件项目希望生成的输出构件,target_sources() 命令用于将输入文件与相应的输出构件关联,以及 target_link_libraries() 命令用于将输出构件彼此关联。
这四个命令是大多数 CMake 用法的骨干。正如我们将学习到的,它们足以描述典型项目的大部分需求。
练习 1 - 构建可执行文件¶
最基本的 CMake 项目是由单个源文件构建的可执行文件。对于像这样的简单项目,只需要一个仅包含四个命令的 CMakeLists.txt 文件。
注意
虽然 CMake 支持大写、小写和混合大小写的命令,但在教程中将优先使用小写命令。
我们已经介绍了前两个命令:cmake_minimum_required() 和 project()。在 CMake 的任何用法中,根 CML 中的第一个命令都将是 cmake_minimum_required()。在某些高级用法中,project() 可能不是 CML 中的第二个命令,但就我们的目的而言,它始终是。
我们需要下一个命令是 add_executable()。此命令创建一个目标。在 CMake 的术语中,目标是开发者为一组属性指定的名称。
- 目标可能要跟踪的一些属性示例是
构件种类(可执行文件、库、头文件集合等)
源文件
包含目录
可执行文件或库的输出名称
依赖项
编译器和链接器标志
CMake 的机制通常最好理解为描述和操作目标及其属性。这里列出的属性远不止这些。CMake 命令的文档通常会以它们操作的目标属性来讨论其功能。
目标本身只是名称,是此属性集合的句柄。使用 add_executable() 命令就像指定我们想为目标使用的名称一样简单。
add_executable(MyProgram)
现在我们有了目标名称,就可以开始为其关联属性,例如我们想构建和链接的源文件。为此主要命令是 target_sources(),它接受目标名称后跟一个或多个文件集合作为参数。
target_sources(MyProgram
PRIVATE
main.cxx
)
注意
CMake 中的路径通常是绝对路径,或者相对于 CMAKE_CURRENT_SOURCE_DIR。我们还没有讨论这样的变量,所以您可以将其理解为“相对于当前 CML 的位置”。
每个文件集合都以一个作用域关键字为前缀。当我们讨论链接目标时,我们将详细讨论这些关键字的完整语义,但快速解释是它们描述了属性应如何被我们的目标的依赖项继承。
通常,没有什么依赖于可执行文件。其他程序和库不需要链接到可执行文件,也不需要继承头文件或其他类似的东西。因此,最适合在此处使用的作用域是 PRIVATE,它通知 CMake 此属性仅属于 MyProgram 且不可继承。
注意
这条规则几乎在所有地方都适用。除了高级和晦涩的用法之外,可执行文件的作用域关键字应始终为 PRIVATE。对于实现文件,无论目标是可执行文件还是库,情况都是如此。唯一需要“看到”.cxx 文件的是构建它们的那个目标。
目标¶
了解如何使用单个可执行文件创建一个简单的 CMake 项目。
有用资源¶
要编辑的文件¶
CMakeLists.txt
开始¶
Tutorial.cxx 的源代码在 Help/guide/tutorial/Step1/Tutorial 目录中提供,可用于计算数字的平方根。在此练习中,无需编辑此文件。
在父目录 Help/guide/tutorial/Step1 中,有一个 CMakeLists.txt 文件,您将对其进行完善。从 TODO 1 开始,完成到 TODO 4。
构建并运行¶
完成 TODO 1 到 TODO 4 后,我们就准备好构建和运行我们的项目了!首先,运行 cmake 可执行文件或 cmake-gui 来配置项目,然后使用您选择的构建工具进行构建。
例如,从命令行,我们可以导航到 Help/guide/tutorial/Step1 目录并如下调用 CMake 进行配置:
cmake -B build
-B 标志告诉 CMake 使用给定的相对路径作为在构建过程中生成文件和存储构件的位置。如果省略,则使用当前工作目录。将这些生成的文件放在源树本身中执行“源内”构建通常被认为是不好的做法。
接下来,使用 cmake --build 告诉 CMake 构建项目,并将我们用 -B 标志传递的相同相对路径传递给它。
cmake --build build
名为 Tutorial 的可执行文件将被构建到 build 目录中。对于多配置生成器(例如 Visual Studio),它可能位于一个子目录中,例如 build/Debug。
最后,尝试使用新构建的 Tutorial
Tutorial 4294967296
Tutorial 10
Tutorial
注意
根据 shell 的不同,正确的语法可能是 Tutorial、./Tutorial、.\Tutorial,甚至 .\Tutorial.exe。为简单起见,练习中将始终使用 Tutorial。
解决方案¶
如上所述,一个包含四个命令的 CMakeLists.txt 文件是我们入门所需的一切。第一行应该是 cmake_minimum_required(),以将 CMake 版本设置为如下:
TODO 1:点击显示/隐藏答案
cmake_minimum_required(VERSION 3.23)
下一步是使用 project() 命令来设置项目名称,并通知 CMake 我们打算使用此 CMakeLists.txt 构建软件,如下所示:
TODO 2:点击显示/隐藏答案
project(Tutorial)
现在我们可以使用 add_executable() 为 Tutorial 设置我们的可执行目标。
TODO 3:点击显示/隐藏答案
add_executable(Tutorial)
最后,我们可以使用 target_sources() 将我们的源文件与 Tutorial 可执行目标关联起来。
TODO 4:点击显示/隐藏答案
target_sources(Tutorial
PRIVATE
Tutorial/Tutorial.cxx
)
练习 2 - 构建库¶
要构建库,我们只需要引入一个命令:add_library()。它的工作方式与 add_executable() 完全相同,但用于库。
add_library(MyLibrary)
然而,现在是时候介绍头文件了。头文件不直接作为翻译单元构建,也就是说它们不是构建要求。它们是使用要求。我们需要了解头文件才能构建给定目标的其他部分。
因此,头文件的描述方式与实现文件(如 tutorial.cxx)略有不同。它们还需要与我们到目前为止使用的 PRIVATE 关键字不同的 作用域关键字。
为了描述一组头文件,我们将使用所谓的 FILE_SET。
target_sources(MyLibrary
PRIVATE
library_implementation.cxx
PUBLIC
FILE_SET myHeaders
TYPE HEADERS
BASE_DIRS
include
FILES
include/library_header.h
)
这涉及很多复杂性,但我们将逐点进行。首先,请注意我们将实现文件作为 PRIVATE 源,这与之前用于可执行文件的文件相同。但是,我们现在对头文件使用了 PUBLIC。这允许库的消费者“看到”库的头文件。
注意
我们还没有准备好讨论作用域关键字的完整语义。我们将在练习 3 中更全面地介绍它们。
作用域关键字之后是一个 FILE_SET,这是一组被描述为一个单元的文件。一个 FILE_SET 由以下部分组成:
FILE_SET <name>是FILE_SET的名称。这是一个句柄,我们可以在其他上下文中用它来描述这个集合。TYPE <type>是我们正在描述的文件类型。最常见的是头文件,但较新版本的 CMake 支持其他类型,如 C++20 模块。BASE_DIRS是文件的“基”位置。这可以最容易地理解为通过-I标志向编译器描述的用于头文件发现的位置。FILES是文件列表,与之前的实现源列表相同。
信息量很大,所以有一些有用的快捷方式我们可以采用。特别是,如果 FILE_SET 的名称与类型相同,我们就不需要提供 TYPE 字段。
target_sources(MyLibrary
PRIVATE
library_implementation.cxx
PUBLIC
FILE_SET HEADERS
BASE_DIRS
include
FILES
include/library_header.h
)
还有其他快捷方式我们可以采用,但我们将在后面的步骤中讨论它们。
目标¶
构建一个库。
有用资源¶
要编辑的文件¶
CMakeLists.txt
开始¶
继续编辑 Step1 目录中的文件。从 TODO 5 开始,完成到 TODO 6。
构建并运行¶
让我们再次构建我们的项目。由于我们已经创建了构建目录并为练习 1 运行了 CMake,我们可以跳到构建步骤:
cmake --build build
我们应该能看到我们的库与 Tutorial 可执行文件一起被创建。
解决方案¶
我们首先以与 Tutorial 可执行文件相同的方式添加库目标。
TODO 5:点击显示/隐藏答案
add_library(MathFunctions)
接下来我们需要描述源文件。对于实现文件 MathFunctions.cxx,这很简单;对于头文件 MathFunctions.h,我们将需要使用 FILE_SET。
我们可以给这个 FILE_SET 它自己的名字,或者使用将其命名为 HEADERS 的快捷方式。在本教程中,我们将使用快捷方式,但两种解决方案都是有效的。
对于 BASE_DIRS,我们需要确定一个目录,该目录将允许所需的 #include <MathFunctions.h> 指令。为了实现这一点,MathFunctions 文件夹本身将是一个基目录。如果我们想要的 include 指令是 #include <MathFunctions/MathFunctions.h> 或类似的,我们会做出不同的选择。
TODO 6:点击显示/隐藏答案
target_sources(MathFunctions
PRIVATE
MathFunctions/MathFunctions.cxx
PUBLIC
FILE_SET HEADERS
BASE_DIRS
MathFunctions
FILES
MathFunctions/MathFunctions.h
)
练习 3 - 链接库和可执行文件¶
我们准备好将我们的库与我们的可执行文件结合起来,为此我们必须引入一个新命令:target_link_libraries()。这个命令的名字可能有点误导,因为它做的远不止调用链接器。它通常描述目标之间的关系。
target_link_libraries(MyProgram
PRIVATE
MyLibrary
)
我们终于可以讨论 作用域关键字 了。它们有三个:PRIVATE、INTERFACE 和 PUBLIC。它们描述了属性如何提供给目标。
PRIVATE属性(也称为“非接口”属性)仅可供拥有它的目标使用,例如PRIVATE头文件将仅对附加到它们的目标可见。一个
INTERFACE属性仅对链接拥有目标的那些目标可用。拥有目标本身无法访问这些属性。一个仅限头文件的库是INTERFACE属性集合的一个例子,因为仅限头文件的库本身不构建任何内容,也不需要访问自己的文件。PUBLIC不是一种独立的属性类型,而是PRIVATE和INTERFACE属性的并集。因此,使用PUBLIC描述的需求对于拥有目标和消费目标都可用。
考虑以下具体示例:
target_sources(MyLibrary
PRIVATE
FILE_SET internalOnlyHeaders
TYPE HEADERS
FILES
InternalOnlyHeader.h
INTERFACE
FILE_SET consumerOnlyHeaders
TYPE HEADERS
FILES
ConsumerOnlyHeader.h
PUBLIC
FILE_SET publicHeaders
TYPE HEADERS
FILES
PublicHeader.h
)
注意
我们为每个文件集排除了 BASE_DIRS,这是另一种快捷方式。当排除时,BASE_DIRS 默认为当前源目录。
调用 target_sources() 时,MyLibrary 目标有几个属性将被修改。到目前为止,我们使用了“属性”这个词,但属性本身是我们可以推理的命名值。这里将被修改的两个特定属性是 HEADER_SETS 和 INTERFACE_HEADER_SETS,它们都包含通过 target_sources() 添加的头文件集合列表。
internalOnlyHeaders 的值将添加到 HEADER_SETS,consumerOnlyHeaders 将添加到 INTERFACE_HEADER_SETS,而 publicHeaders 将添加到两者。
当构建给定目标时,它将使用自己的非接口属性(例如,HEADER_SETS),结合它链接的任何目标的接口属性(例如,INTERFACE_HEADER_SETS)。
注意
没有必要在这个细节程度上推理 CMake 属性。以上内容是为了完整性而描述的。大多数时候您不需要关心命令正在修改的特定属性。
作用域关键字与它们的考虑有着简单的直观关联,从应用该命令的目标的角度来看:PRIVATE 是给我的,INTERFACE 是给别人的,PUBLIC 是给所有人的。
目标¶
在 Tutorial 可执行文件中,使用 MathFunctions 库提供的 sqrt() 函数。
有用资源¶
要编辑的文件¶
CMakeLists.txtTutorial/Tutorial.cxx
入门¶
继续编辑 Step1 中的文件。从 TODO 7 开始,完成到 TODO 9。在此练习中,我们需要使用 target_link_libraries() 将 MathFunctions 目标添加到 Tutorial 目标的链接库中。
修改 CML 后,将 tutorial.cxx 更新为使用 mathfunctions::sqrt() 函数而不是 std::sqrt。
构建和运行¶
让我们再次构建我们的项目。和以前一样,我们已经创建了一个构建目录并运行了 CMake,所以我们可以跳到构建步骤:
cmake --build build
验证输出是否与您从 MathFunctions 库预期的一致。
解决方案¶
在此练习中,我们通过将 MathFunctions 添加到 Tutorial 的链接库中,将 Tutorial 可执行文件描述为 MathFunctions 目标的消费者。
为了实现这一点,我们修改 CMakeLists.txt 文件以使用 target_link_libraries() 命令,将 Tutorial 作为要修改的目标,将 MathFunctions 作为我们想添加的库。
TODO 7:点击显示/隐藏答案
target_link_libraries(Tutorial
PRIVATE
MathFunctions
)
注意
这里的顺序只是大致相关。我们调用 target_link_libraries() 的时间早于 add_library() 定义 MathFunctions 这一点 CMake 不在意。我们正在记录 Tutorial 依赖于一个名为 MathFunctions 的东西,但 MathFunctions 的含义在此阶段尚未解析。
在调用 target_sources() 或 target_link_libraries() 等 CMake 命令时,唯一需要定义的 target 是正在修改的 target。
最后,所要做的就是修改 Tutorial.cxx 以使用新提供的 mathfunctions::sqrt 函数。这意味着添加适当的头文件并修改我们的 sqrt() 调用。
练习 4 - 子目录¶
随着我们继续进行教程,我们将添加更多命令来操作 Tutorial 可执行文件和 MathFunctions 库。我们希望确保将命令保留在它们所处理的文件本地。虽然对于这样一个小型项目来说这不是主要问题,但对于拥有许多目标和数千个文件的庞大项目来说,它可能非常有用。
命令 add_subdirectory() 允许我们包含位于项目子目录中的 CML。
add_subdirectory(SubdirectoryName)
当子目录中的 CMakeLists.txt 被 CMake 处理时,子目录 CML 中描述的所有相对路径都相对于该子目录,而不是顶层 CML。
目标¶
使用 add_subdirectory() 来组织项目。
有用资源¶
要编辑的文件¶
CMakeLists.txtTutorial/CMakeLists.txtMathFunctions/CMakeLists.txt
入门¶
此步骤中的 TODOs 分布在三个 CMakeLists.txt 文件中。在将 target_sources() 命令移到子目录时,请务必注意必要的路径更改。
注意
先前我们说过 BASE_DIRS 默认为当前源目录。由于 MathFunctions 的所需包含目录将与调用 target_sources() 的 CML 相同,因此我们应该删除 BASE_DIRS 关键字和参数。
完成 TODO 10 到 TODO 13。
构建和运行¶
由于进行了重组,我们需要在重新构建之前清理旧的构建目录(否则我们新的 Target 构建文件夹将与我们之前创建的 Target 可执行文件冲突)。我们可以使用 --clean-first 标志来实现这一点。
无需重新配置。由于 CML 中的更改,CMake 将自动重新配置自身。
cmake --build build --clean-first
注意
我们的可执行文件和库将输出到构建树中的新位置。一个子目录,镜像 add_executable() 和 add_library() 在源树中调用时的位置。在以后的步骤中,您需要导航到构建树中的此子目录以运行教程可执行文件。
您可以通过删除旧的 Tutorial 可执行文件来验证此行为,并观察新的可执行文件已在 Tutorial/Tutorial 生成。
解决方案¶
我们需要将所有关于 Tutorial 可执行文件的命令移到 Tutorial/CMakeLists.txt 中,并用一个 add_subdirectory() 命令替换它们。我们还需要更新 Tutorial.cxx 的路径。
TODO 10-11: 点击显示/隐藏答案
add_executable(Tutorial)
target_sources(Tutorial
PRIVATE
Tutorial.cxx
)
target_link_libraries(Tutorial
PRIVATE
MathFunctions
)
add_subdirectory(Tutorial)
我们需要对 MathFunctions 的命令做同样的事情,相应地更改相对路径,并删除 BASE_DIRS,因为它不再是必需的,默认值即可。