第 1 步:CMake 入门¶
CMake 教程的第一步旨在作为小型项目编写有效构建脚本的快速入门。学完这一步后,你将能够使用 CMake 来描述可执行文件、库、源文件与头文件,以及它们之间的链接关系。
本步骤中的每个练习都将从讨论该练习所需的概念和命令开始。然后,提供目标和有用的资源列表。在 Files to Edit 部分中的每个文件都位于 Step1 目录中,并包含一个或多个 TODO 注释。每个 TODO 代表需要更改或添加的一两行代码。建议按照数字顺序完成 TODO,先完成 TODO 1,然后是 TODO 2,依此类推。
注意
本教程的每个步骤都建立在前一个步骤的基础上,但这些步骤并非严格连续。一些与学习 CMake 无关的代码,例如 C++ 函数实现或教程范围之外的 CMake 代码,有时会添加在步骤之间。
入门部分将提供一些有用的提示并指导你完成练习。然后,构建与运行部分将逐步介绍如何构建和测试练习。最后,在每个练习结束时,会对预期的解决方案进行回顾。
背景¶
CMake 的典型用法围绕一个或多个名为 CMakeLists.txt 的文件进行。该文件有时被称为“列表文件”或“CML”。在给定的软件项目中,任何我们希望向 CMake 提供如何处理该目录或其子目录中的本地文件和操作说明的目录中,都会存在一个 CMakeLists.txt。每个文件都由一组命令组成,这些命令描述了与构建软件项目相关的信息或操作。
软件项目中的并非每个目录都需要 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() 命令时,它会执行各种检查以确保环境适合构建软件;例如检查编译器和其他构建工具,并发现主机和目标机器的大小端(endianness)等属性。
注意
虽然为每个命令提供了完整文档的链接,但并不打算让读者理解他们使用的每个 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()。此命令创建一个目标(target)。在 CMake 的术语中,目标是开发人员赋予属性集合的一个名称。
- 目标可能想要跟踪的一些属性示例如下:
产物类型(可执行文件、库、头文件集合等)
源文件
包含目录
可执行文件或库的输出名称
依赖项
编译器和链接器标志
CMake 的机制通常最好理解为对目标及其属性的描述和操作。属性远不止此处列出的这些。CMake 命令的文档通常会根据它们所操作的目标属性来讨论其功能。
目标本身仅仅是名称,是该属性集合的句柄。使用 add_executable() 命令就像指定我们要用于该目标的名称一样简单。
add_executable(MyProgram)
现在我们有了目标的名称,就可以开始将属性(如我们要构建和链接的源文件)与它相关联。主要的命令是 target_sources(),它接受一个目标名称作为参数,后跟一个或多个文件集合。
target_sources(MyProgram
PRIVATE
main.cxx
)
注意
CMake 中的路径通常要么是绝对路径,要么是相对于 CMAKE_CURRENT_SOURCE_DIR 的路径。我们还没有讨论过这样的变量,所以你可以将其理解为“相对于当前 CML 的位置”。
每个文件集合前都带有一个 作用域关键字(scope keyword)。当我们讨论将目标链接在一起时,我们将讨论这些关键字的完整语义,但简短的解释是:它们描述了属性应如何由我们目标的依赖项继承。
通常,没有什么会依赖于可执行文件。其他程序和库不需要链接到可执行文件,也不需要继承头文件或任何此类性质的内容。因此,此处使用的适当作用域是 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 使用给定的相对路径作为在构建过程中生成文件和存储产物的位置。如果省略它,则使用当前工作目录。通常认为进行“源内(in-source)”构建(即将这些生成的文件放置在源代码树本身中)是一种糟糕的做法。
接下来,通过 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 <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 默认为当前源目录。
MyLibrary 目标有几个属性将通过此对 target_sources() 的调用进行修改。到目前为止,我们一直泛指“属性”,但属性本身是可以推理的命名值。此处将修改的两个特定属性是 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
)
注意
这里的顺序仅与此松散相关。在定义 MathFunctions(使用 add_library())之前调用 target_link_libraries() 对 CMake 而言并不重要。我们只是记录了 Tutorial 对名为 MathFunctions 的事物有依赖关系,但 MathFunctions 意味着什么,在现阶段尚未解析。
当调用像 target_sources() 或 target_link_libraries() 这样的 CMake 命令时,唯一需要定义的目标是正在被修改的目标。
最后,剩下的就是修改 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
入门¶
此步骤的 TODO 分布在三个 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,因为它不再需要,默认值将起作用。