面向 C++ 的现代 CMake 教程(一)(2)https://developer.aliyun.com/article/1526963
CMakeLists.txt
CMakeLists.txt
列表文件用于配置 CMake 项目。你必须在源树根目录中提供至少一个。这样的顶级文件在配置阶段是第一个被执行的,它至少应该包含两个命令:
cmake_minimum_required(VERSION )
:设置 CMake 的预期版本(隐含地告诉 CMake 如何应用与遗留行为相关的策略)。project( )
:用于命名项目(提供的名称将存储在PROJECT_NAME
变量中)并指定配置选项(我们将在第二章 CMake 语言中进一步讨论)。
随着你的软件的增长,你可能会希望将其划分为更小的单元,可以单独配置和推理。CMake 通过子目录及其自己的CMakeLists.txt
文件支持这一点。你的项目结构可能类似于以下示例:
CMakeLists.txt api/CMakeLists.txt api/api.h api/api.cpp
然后可以使用一个非常简单的CMakeLists.txt
文件将其全部整合在一起:
CMakeLists.txt
cmake_minimum_required(VERSION 3.20) project(app) message("Top level CMakeLists.txt") add_subdirectory(api)
项目的主要方面在顶级文件中涵盖:管理依赖项,声明要求,以及检测环境。在此文件中,我们还有一个add_subdirectory(api)
命令,以从api
目录中包含另一个CMakeListst.txt
文件,执行与应用程序的 API 部分相关的特定步骤。
CMakeCache.txt
缓存变量将在第一次运行配置阶段时从listfiles
生成,并存储在CMakeCache.txt
中。此文件位于构建树的根目录中,格式相当简单:
# This is the CMakeCache file. # For build in directory: c:/Users/rapha/Desktop/CMake/empty_project/build # It was generated by CMake: C:/Program Files/CMake/bin/cmake.exe # You can edit this file to change values found and used by cmake. # If you do want to change a value, simply edit, save, and exit the editor. # The syntax for the file is as follows: # KEY:TYPE=VALUE # KEY is the name of a variable in the cache. # TYPE is a hint to GUIs for the type of VALUE, DO NOT EDIT TYPE!. # VALUE is the current value for the KEY. ######################## # EXTERNAL cache entries ######################## //Flags used by the CXX compiler during DEBUG builds. CMAKE_CXX_FLAGS_DEBUG:STRING=/MDd /Zi /Ob0 /Od /RTC1 // ... more variables here ... ######################## # INTERNAL cache entries ######################## //Minor version of cmake used to create the current loaded cache CMAKE_CACHE_MINOR_VERSION:INTERNAL=19 // ... more variables here ...
正如你在标题中的注释所观察到的,这个格式相当简单易懂。EXTERNAL
部分中的缓存条目是为了让用户修改,而INTERNAL
部分由 CMake 管理。请注意,不建议您手动更改它们。
以下是一些要点:
- 你可以通过调用
cmake
手动管理此文件(请参阅缓存选项在精通命令行部分),或者通过ccmake
/cmake-gui
。 - 通过删除此文件,你可以将项目重置为其默认配置;它将从列表文件中重新生成。
- 缓存变量可以从列表文件中读写。有时,变量引用评估有点复杂;然而,我们将在第二章 CMake 语言中更详细地介绍。
包的配置文件
CMake 生态系统的大部分包括项目可以依赖的外部包。它们允许开发人员以无缝、跨平台的方式使用库和工具。支持 CMake 的包应提供配置文件,以便 CMake 了解如何使用它们。
我们将在第十一章中学习如何编写这些文件,安装和打包。同时,这里有一些有趣的细节要注意:
- 配置文件(原名)包含有关如何使用库二进制文件、头文件和辅助工具的信息。有时,它们暴露出 CMake 宏,可以在您的项目中使用。
- 使用
find_package()
命令来包含包。 - 描述包的 CMake 文件名为
-config.cmake
和Config.cmake
。 - 使用包时,您可以指定需要的包的哪个版本。CMake 会在关联的
Version.cmake
文件中检查这个版本。 - 配置文件由支持 CMake 生态系统的包供应商提供。如果一个供应商没有提供这样的配置文件,可以用 Find-module(原名)来替换。
- CMake 提供了一个包注册表,用于在系统范围内和每个用户处存储包。
cmake_install.cmake、CTestTestfile.cmake 和 CPackConfig.cmake 文件
这些文件由 cmake
可执行文件在生成阶段在构建树中生成。因此,不建议手动编辑它们。CMake 使用它们作为 cmake
安装操作、CTest 和 CPack 的配置。如果您实现源代码构建(不建议),添加到 VCS 忽略文件中可能是个不错的主意。
CMakePresets.json 和 CMakeUserPresets.json
当我们需要具体设置诸如缓存变量、选择生成器、构建树路径等事物时,项目的配置可能会变得相对繁琐——尤其是当我们有多种构建项目的方式时。这时预设就派上用场了。
用户可以通过 GUI 选择预设,或者使用命令行 --list-presets
并使用 --preset=
选项为构建系统选择一个预设。您可以在本章的 精通命令行 部分找到更多详细信息。
预设以相同的 JSON 格式存储在两个文件中:
CMakePresets.json
:这是为了让项目作者提供官方预设。CMakeUserPresets.json
:这是专为希望按自己的喜好自定义项目配置的用户准备的(您可以在 VCS 忽略文件中添加它)。
预设是项目文件,所以它们的解释属于这里。然而,在项目中它们并非必需,只有在完成初始设置后它们才变得有用。所以,如果您愿意,可以跳到下一节,需要时再回来:
chapter-01/02-presets/CMakePresets.json
{ "version": 1, "cmakeMinimumRequired": { "major": 3, "minor": 19, "patch": 3 }, "configurePresets": [ ], "vendor": { "vendor-one.com/ExampleIDE/1.0": { "buildQuickly": false } } }
CMakePresets.json
指定以下根字段:
Version
:这是必须的,总是1
。cmakeMinimumRequired
:这是可选的。它以散列形式指定 CMake 版本,包含三个字段:major
、minor
和patch
。vendor
:IDE 可以使用这个可选字段来存储其元数据。它是一个以供应商域和斜杠分隔的路径为键的映射。CMake 实际上忽略这个字段。configurePresets
:这是一个可选的可用预设数组。
让我们向我们的configurePresets
数组中添加两个预设:
chapter-01/02-presets/CMakePresets.json:my-preset
{ "name": "my-preset", "displayName": "Custom Preset", "description": "Custom build - Ninja", "generator": "Ninja", "binaryDir": "${sourceDir}/build/ninja", "cacheVariables": { "FIRST_CACHE_VARIABLE": { "type": "BOOL", "value": "OFF" }, "SECOND_CACHE_VARIABLE": "Ninjas rock" }, "environment": { "MY_ENVIRONMENT_VARIABLE": "Test", "PATH": "$env{HOME}/ninja/bin:$penv{PATH}" }, "vendor": { "vendor-one.com/ExampleIDE/1.0": { "buildQuickly": true } } },
此文件支持树状结构,其中子预设从多个父预设继承属性。这意味着我们可以创建先前预设的副本,并只覆盖我们需要的字段。以下是一个子预设可能的样子:
chapter-01/02-presets/CMakePresets.json:my-preset-multi
{ "name": "my-preset-multi", "inherits": "my-preset", "displayName": "Custom Ninja Multi-Config", "description": "Custom build - Ninja Multi", "generator": "Ninja Multi-Config" }
注意
CMake 文档只将一些字段明确标记为必需的。然而,还有一些其他字段被标记为可选的,这些字段必须在预设中提供,或者从其父预设继承。
预设被定义为具有以下字段的映射:
name
:这是一个必需的字符串,用于标识预设。它必须对机器友好,并且在两个文件中唯一。Hidden
:这是一个可选的布尔值,用于隐藏预设,使其不在 GUI 和命令行列表中显示。这样的预设可以是另一个预设的父预设,并且不需要提供除其名称以外的任何内容。displayName
:这是一个可选的字符串,有一个人类可读的名字。description
:这是一个可选的字符串,用于描述预设。Inherits
:这是一个可选的字符串或预设名称数组,用于从其中继承。在冲突的情况下,早期预设的值将被优先考虑,每个预设都可以覆盖任何继承的字段。此外,CMakeUserPresets.json
可以继承项目预设,但反之则不行。Vendor
:这是一个可选的供应商特定值的映射。它遵循与根级vendor
字段相同的约定。Generator
:这是一个必需或继承的字符串,用于指定预设要使用的生成器。architecture
和toolset
:这些是用于配置支持这些选项的生成器的可选字段(在生成项目构建系统部分提到)。每个字段可以是一个简单的字符串,或者一个带有value
和strategy
字段的哈希表,其中strategy
是set
或external
。当strategy
字段配置为set
时,将设置字段值,如果生成器不支持此字段,则会产生错误。配置为external
意味着字段值是为外部 IDE 设置的,CMake 应该忽略它。binaryDir
:这是一个必需或继承的字符串,提供了构建树目录的路径(相对于源树是绝对路径或相对路径)。它支持宏扩展。cacheVariables
:这是一个可选的缓存变量映射,其中键表示变量名。接受的值包括null
、"TRUE"
、"FALSE"
、字符串值,或具有可选type
字段和必需value
字段的哈希。value
可以是"TRUE"
或"FALSE"
的字符串值。除非明确指定为null
,否则缓存变量会通过并集操作继承——在这种情况下,它将保持未设置。字符串值支持宏扩展。Environment
: 这是一个可选的环境变量映射,其中键表示变量名。接受的值包括null
或字符串值。除非明确指定为null
,否则环境变量会通过并集操作继承——在这种情况下,它将保持未设置。字符串值支持宏扩展,变量可以以任何顺序相互引用,只要没有循环引用即可。
以下宏将被识别和评估:
${sourceDir}
:这是源树的位置。${sourceParentDir}
:这是源树父目录的位置。${sourceDirName}
: 这是${sourceDir}
的最后一个文件名组件。例如,对于/home/rafal/project
,它就是project
。${presetName}
: 这是预设的名称字段的值。${generator}
:这是预设的生成器字段的值。${dollar}
: 这是一个字面意义上的美元符号($)。$env{}
:这是一个环境变量宏。如果预设中定义了该变量,它将返回预设中的变量值;否则,它将从父环境返回值。请注意,预设中的变量名是区分大小写的(与 Windows 环境不同)。$penv{}
:这个选项与$env
类似,但总是从父环境返回值。这允许您解决预设环境变量中不允许的循环引用问题。$vendor{}
:这使得供应商能够插入自己的宏。
在 Git 中忽略文件
有很多版本控制系统;其中最流行的一种是 Git。每当我们开始一个新项目时,确保我们只将需要存在于仓库中的文件提交到仓库中是很重要的。如果我们只是将生成的、用户或临时文件添加到.gitignore
文件中,项目卫生更容易维护。这样,Git 就知道在构建新提交时自动跳过它们。这是我在我项目中使用的文件:
chapter-01/01-hello/.gitignore
# If you put build tree in the source tree add it like so: build_debug/ build_release/ # Generated and user files **/CMakeCache.txt **/CMakeUserPresets.json **/CTestTestfile.cmake **/CPackConfig.cmake **/cmake_install.cmake **/install_manifest.txt **/compile_commands.json
在您的项目中使用前面的文件将为您和其他贡献者和用户带来更多的灵活性。
项目文件的未知领域现在已经绘制成图。有了这张地图,你很快就能编写自己的列表文件,配置缓存,准备预设,等等。在你扬帆远航项目编写之前,让我们来看看您可以使用 CMake 创建的其他类型的自包含单元。
发现脚本和模块
与 CMake 一起工作的主要焦点是构建的项目以及生产供其他系统(如 CI/CD 管道和测试平台)消费的工件,或者部署到机器或工件仓库。然而,CMake 还有两个其他概念可以用其语言创建:脚本和模块。让我们仔细看看。
脚本
为了配置项目构建,CMake 提供了一种与平台无关的编程语言。这带有许多有用命令。你可以使用这个工具来编写随项目提供或完全独立的脚本。
把它当作一种一致的跨平台工作方式:不用在 Linux 上使用 bash 脚本,在 Windows 上使用批处理或 PowerShell 脚本,你可以有一个版本。当然,你可以引入外部工具,如 Python、Perl 或 Ruby 脚本,但这又是另一个依赖,将增加 C/C++项目的复杂性。是的,有时这将是唯一能完成工作的事情,但更多的时候,我们可以用一些更简单的东西应付过去。
我们已经从掌握命令行部分了解到,我们可以使用-P
选项执行脚本:cmake -P script.cmake
。但是提供的脚本文件的实际要求是什么?并不多:脚本可以像你喜欢的那么复杂,也可以是一个空文件。然而,建议你在脚本的开始处调用cmake_minimum_required()
命令。这个命令告诉 CMake 应该对项目中的后续命令应用哪些策略(更多详情请参阅第三章,设置你的第一个 CMake 项目)。
chapter-01/03-script/script.cmake
# An example of a script cmake_minimum_required(VERSION 3.20.0) message("Hello world") file(WRITE Hello.txt "I am writing to a file")
当运行脚本时,CMake 不会执行任何常规阶段(如配置或生成),也不会使用缓存。由于脚本中没有源/构建树的概念,通常持有这些路径引用的变量将包含当前工作目录:CMAKE_BINARY_DIR
、CMAKE_SOURCE_DIR
、CMAKE_CURRENT_BINARY_DIR
和CMAKE_CURRENT_SOURCE_DIR
。
快乐脚本编程!
实用模块
CMake 项目可以利用外部模块来增强其功能。模块是用 CMake 语言编写的,包含宏定义、变量和执行各种功能的命令。它们从相当复杂的脚本(CPack
和CTest
也提供模块!)到相对简单的脚本,如AddFileDependencies
或TestBigEndian
。
CMake 分发版包含了几乎 90 个不同的实用模块。如果这还不够,你可以在浏览精选列表,如在github.com/onqtam/awesome-cmake
找到的列表上互联网下载更多,或者从头开始编写一个模块。
要使用一个实用模块,我们需要调用一个include()
命令。下面是一个简单项目展示了这个动作:
chapter-01/04-module/CMakeLists.txt
cmake_minimum_required(VERSION 3.20.0) project(ModuleExample) include (TestBigEndian) TEST_BIG_ENDIAN(IS_BIG_ENDIAN) if(IS_BIG_ENDIAN) message("BIG_ENDIAN") else() message("LITTLE_ENDIAN") endif()
我们将在它们与主题相关时学习有哪些模块可供使用。如果你好奇,可以找到包含模块的完整列表在cmake.org/cmake/help/latest/manual/cmake-modules.7.html
。
查找模块
在包的配置文件部分,我提到 CMake 有一个机制,允许它找到属于外部依赖项的文件,这些依赖项不支持 CMake 并且没有提供 CMake 配置文件(或者还没有)。查找模块就是为了这个目的。CMake 提供了 150 多个模块,能够定位系统中的不同包。和实用模块一样,网络上还有更多的查找模块可供选择,另一种选择是编写自己的模块,作为最后的手段。
你可以通过调用find_package()
命令并提供相关包的名称来使用它们。这样的查找模块将然后玩一场捉迷藏游戏,并检查它所寻找的软件的所有已知位置。在此之后,它定义了变量(如该模块手册中所指定的)允许你针对该依赖项进行构建。
例如,FindCURL
模块搜索一个流行的客户端 URL库,并定义了以下变量:CURL_FOUND
、CURL_INCLUDE_DIRS
、CURL_LIBRARIES
和CURL_VERSION_STRING
。
我们将在第七章更深入地讨论查找模块,使用 CMake 管理依赖项。
总结
现在你已经了解了 CMake 是什么以及它是如何工作的;你学习了 CMake 工具家族的关键组成部分以及如何在各种系统中安装它们。像真正的功率用户一样,你知道通过命令行运行 CMake 的所有方式:生成构建系统、构建项目、安装、运行脚本、命令行工具和打印帮助。你知道 CTest、CPack 和 GUI 应用程序。这将帮助你为用户和其他开发者创建项目,并从正确的角度出发。此外,你还学会了组成一个项目的内容:目录、列表文件、配置文件、预设和帮助文件,以及在 VCS 中应该忽略哪些内容。最后,你简要地查看了其他非项目文件:独立的脚本和模块。
在下一章中,我们将深入探讨 CMake 的编程语言。这将使你能够编写自己的列表文件并打开编写第一个脚本、项目和模块的大门。
进一步阅读
更多信息,你可以参考以下资源:
- 官方 CMake 网页和文档:
cmake.org/
- 单配置生成器:
cgold.readthedocs.io/en/latest/glossary/single-config.html
- CMake GUI 中阶段的分离:
stackoverflow.com/questions/39401003/why-there-are-two-buttons-in-gui-configure-and-generate-when-cli-does-all-in-one
第二章:CMake 语言
在CMake 语言中写作有点棘手。当你第一次阅读 CMake 列表文件时,你可能会觉得其中的语言如此简单,以至于不需要任何特殊培训或准备。接下来的内容经常是尝试引入变化和实验代码的实际尝试,而没有彻底理解它是如何工作的。我们程序员通常非常忙碌,并且过于热衷于用最小的投入解决任何与构建相关的问题。我们倾向于基于直觉进行更改,希望它们可能管用。解决技术问题的这种方法称为巫术编程。
CMake 语言看起来很简单:在我们完成小的添加、修复或黑客攻击,或者添加了一行代码之后,我们意识到有些事情不对劲。调试时间通常比实际研究主题的时间还要长。幸运的是,这不会是我们的命运——因为本章涵盖了实践中使用 CMake 语言所需的大部分关键知识。
在本章中,我们不仅将学习 CMake 语言的构建块——注释、命令、变量和控制结构,还将提供必要的背景知识,并在一个干净现代的 CMake 示例中尝试它们。CMake 让你处于一个独特的位置。一方面,你扮演着构建工程师的角色;你需要理解编译器、平台以及中间所有事物的复杂性。另一方面,你是一名开发者;你在编写生成构建系统的代码。编写好的代码是困难的,并要求同时思考多个层面——它应该能工作且容易阅读,但它也应该容易分析、扩展和维护。这正是我们将在这里讨论的内容。
最后,我们将介绍 CMake 中一些最有用和常见的命令。不经常使用的命令将放在附录部分(这将包括字符串、列表和文件操作命令的完整参考指南)。
在本章中,我们将涵盖以下主要主题:
- CMake 语言基础语法
- 使用变量
- 使用列表
- 理解 CMake 中的控制结构
- 有用命令
技术要求
你可以在 GitHub 上找到本章中存在的代码文件:github.com/PacktPublishing/Modern-CMake-for-Cpp/tree/main/examples/chapter02
。
为了构建本书中提供的示例,总是使用推荐的命令:
cmake -B <build tree> -S <source tree> cmake --build <build tree>
请确保将占位符和
替换为适当的路径。作为提醒:构建树是目标/输出目录的路径,源树是您的源代码所在的路径。
CMake 语言基础语法
编写 CMake 代码与编写其他命令式语言的代码非常相似:代码从上到下、从左到右执行,偶尔会进入一个被包含的文件或一个被调用的函数。根据模式(参见第一章中的掌握命令行部分,CMake 的初学者指南),执行从源树根文件(CMakeLists.txt
)或作为一个参数传递给cmake
的.cmake
脚本文件开始。
正如我们在上一章中讨论的,脚本支持 CMake 语言的大部分(排除任何与项目相关的功能)。因此,它们是开始练习 CMake 语法的好方法,这就是为什么我们会在这里使用它们。在熟练编写基本列表文件之后,我们将在下一章开始准备实际的项目文件。如果你还记得,脚本可以用以下命令运行:
cmake -P script.cmake
注意
CMake 支持\n
或\r\n
行结束。UTF-8支持可选的字节顺序标记(BOMs)的 CMake 版本 above 3.0,并且UTF-16在 CMake 版本 above 3.2 中得到支持。
CMake 列表文件中的所有内容要么是命令调用,要么是注释。
注释
就像在**C++**中一样,有两种注释——单行注释和方括号 (多行)注释。但与 C++不同,方括号注释可以嵌套。让我给你展示一下语法:
# single-line comments start with a hash sign "#" # they can be placed on an empty line message("Hi"); # or after a command like here. #[=[ bracket comment #[[ nested bracket comment #]] #]=]
多行注释因其符号而得名——它们以一个开口方括号([
)开始,后面跟着任意数量的等号(=
)和一个另一个方括号:[=[
。要关闭方括号注释,请使用相同数量的等号,并像这样反转方括号:]=]
。
在方括号标记前面加上#
是可选的,允许你通过在方括号注释的第一行添加另一个#
来快速禁用多行注释,像这样:
##[=[ this is a single-line comment now no longer commented #[[ still, a nested comment #]] #]=] this is a single-line comment now
那是个巧妙的技巧,但我们在 CMake 文件中什么时候以及如何使用注释呢?由于编写列表文件本质上是一种编程,因此最好也将我们最好的编程实践应用到它们上。遵循此类实践的代码通常被称为干净——这个术语多年来被软件开发大师如 Robert C. Martin, Martin Fowler,以及其他许多作者使用。认为有帮助和有害的东西常常激烈争论,正如你所猜的,注释也没有被排除在这些争论之外。
一切都应该根据具体情况来判断,但通常公认的指导原则说,好的注释至少提供以下之一:
- 信息:它们可以解释像正则表达式模式或格式化字符串这样的复杂性。
- 意图:它们可以在代码的实现或接口不明显时解释代码的意图。
- 阐明:它们可以解释难以重构或更改的概念。
- 警告后果:它们可以提供警告,尤其是关于可能破坏其他内容的代码。
- 放大:它们可以强调难以用代码表达的想法。
- 法律条款:它们可以添加这个必要的恶棍,这通常不是程序员的领域。
如果你可以,避免添加注释并采用更好的命名约定,或者重构或修正你的代码。如果你可以,避免添加以下类型的注释:
- 强制:这些是为了完整性而添加的,但它们并不是非常重要。
- 冗余:这些重复了代码中已经清晰写明的内容。
- 误导:如果它们没有跟随代码更改,它们可能是过时的或不正确的。
- 日志:这些记录了更改的内容和时间(使用版本控制系统代替)。
- 分隔符:这些用于标记章节。
不带注释编写优雅的代码很难,但它可以提高读者的体验。由于我们花在阅读代码上的时间比编写代码的时间多,我们总是应该努力编写可读的代码,而不仅仅是尝试快速编写它。我建议在本章末尾查看进一步阅读部分,那里有一些关于整洁代码的好参考资料。如果你对注释特别感兴趣,你会在其中一个 YouTube 视频中找到一个深入讨论这个主题的链接,这个视频是我关于这个主题的众多视频之一。
命令调用
是时候动手了!调用命令是 CMake 列表文件的核心。要执行一个命令,你必须提供其名称,后面跟着括号,其中你可以包含一个空格分隔的命令参数列表。
[外链图片转存中…(img-J5wSbjjz-1716544491730)]
图 2.1 – 命令示例
命令名称不区分大小写,但 CMake 社区有一个约定,在命令名称中使用蛇形大小写(即,使用下划线连接的小写字母单词)。你也可以定义自己的命令,这部分将在本章的理解 CMake 中的控制结构部分进行介绍。
与 C++ 相比,特别引人注目的是 CMake 中的命令调用不是表达式。你不能将另一个命令作为参数传递给被调用的命令,因为所有括号内的内容都被解释为该命令的参数。
更加令人沮丧的是,CMake 命令在调用结束时不需要分号。这可能是因为源代码的每一行可以包含多达一个命令调用,后面可以跟一个可选的单行注释。或者,整个行必须是括号注释的一部分。所以,这些是唯一允许的格式:
command(argument1 "argument2" argument3) # comment [[ multiline comment ]]
在括号注释后放置命令是不允许的:
[[ bracket ]] command()
在删除任何注释、空格和空行之后,我们得到一个命令调用的列表。这创造了一个有趣的视角——CMake 语法真的很简单,但这是一件好事吗?我们是如何处理变量的?或者,我们是如何指导执行流程的?
CMake 提供了这些操作以及更多命令。为了简化事情,我们将随着不同示例的推进介绍相关命令,并将它们分为三个类别:
- 脚本命令:这些命令始终可用,用于改变命令处理器的状态、访问变量,并影响其他命令和环境。
- 项目命令:这些命令在项目中可用,用于操作项目状态和构建目标。
- CTest 命令:这些命令在 CTest 脚本中可用,用于管理测试。
在本章中,我们将介绍最有用的脚本命令(因为它们在项目中也非常有用)。项目和 CTest 命令将在我们引入与构建目标相关的概念(第三章,设置你的第一个 CMake 项目)和测试框架(第八章,测试框架)时讨论。
几乎每个命令都依赖于语言的其他元素才能正常工作:变量、条件语句,以及最重要的,命令行参数。让我们看看应该如何使用这些元素。
命令参数
许多命令需要用空格分隔的参数来指定它们的行为方式。正如你在图 2.1中看到的,引号周围的参数有些奇怪。一些参数有引号,而其他参数没有——这是怎么回事?
在底层,CMake 唯一能识别的数据类型是一个字符串。这就是为什么每个命令都期望其参数为零个或多个字符串。但是,普通的、静态的字符串并不非常有用,尤其是当我们不能嵌套命令调用时。参数就派上用场了——CMake 将评估每个参数为静态字符串,然后将它们传递给命令。评估意味着字符串插值,或将字符串的一部分替换为另一个值。这可以意味着替换转义序列,展开变量引用(也称为变量插值),以及解包列表。
根据上下文,我们可能需要启用这种评估。为此,CMake 提供了三种参数类型:
- 方括号参数
- 引号参数
- 未引用的参数
每种参数类型提供不同级别的评估,并且有一些小怪癖。
方括号参数
方括号参数不进行评估,因为它们用于将多行字符串作为单个参数传递给命令,而不做任何更改。这意味着它会包括制表符和换行符形式的空白。
这些参数的结构与注释完全一样——也就是说,它们以[=[
开头,以]=]
结尾,其中开头和结尾标记中的等号数量必须匹配(省略等号也是可以的,但它们仍然必须匹配)。与注释的区别在于,你不能嵌套方括号参数。
下面是使用此类参数与message()
命令的一个例子,该命令将所有传递的参数打印到屏幕上:
chapter02/01-arguments/bracket.cmake
message([[multiline bracket argument ]]) message([==[ because we used two equal-signs "==" following is still a single argument: { "petsArray" = [["mouse","cat"],["dog"]] } ]==])
在上面的例子中,我们可以看到不同形式的括号参数。第一个省略了等号。注意把闭合标签放在单独一行上,在输出中会显示为一个空行:
$ cmake -P chapter02/01-arguments/bracket.cmake multiline bracket argument because we used two equal-signs "==" following is still a single argument: { "petsArray" = [["mouse","cat"],["dog"]] }
第二种形式在传递包含双括号(]]
)的文本时很有用(在代码片段中突出显示),因为它们不会被解释为参数的结束标记。
这类括号参数的用途有限——通常,用来包含较长的文本块。在大多数情况下,我们需要一些更动态的内容,比如引号参数。
引号参数
引号参数类似于普通的 C++字符串——这些参数将多个字符(包括空格)组合在一起,并将展开转义序列。和 C++字符串一样,它们用双引号字符("
)打开和关闭,所以为了在输出字符串中包含一个引号字符,你必须用反斜杠(\"
)进行转义。也支持其他一些著名的转义序列:\\
表示一个字面反斜杠,\t
是一个制表符,\n
是一个换行符,\r
是一个回车符。
这就是 C++字符串相似之处的结束。引号参数可以跨越多行,并且它们将插值变量引用。可以认为它们内置了sprintf
函数从std::format
中${name}
。我们将在使用变量部分更多地讨论变量引用。
让我们尝试这些参数的实际应用:
chapter02/01-arguments/quoted.cmake
message("1\. escape sequence: \" \n in a quoted argument") message("2\. multi... line") message("3\. and a variable reference: ${CMAKE_VERSION}")
你能猜到前面脚本的输出将有多少行吗?
$ cmake -P chapter02/01-arguments/quoted.cmake 1\. escape sequence: " in a quoted argument 2\. multi... line 3\. and a variable reference: 3.16.3
没错——我们有一个转义的引号字符,一个转义的换行符,和一个字面的换行符。它们都将被打印在输出中。我们还访问了内置的CMAKE_VERSION
变量,我们可以在最后一行正确地看到它被插值。
面向 C++ 的现代 CMake 教程(一)(4)https://developer.aliyun.com/article/1526966