(五)变量、字符串、列表和科学计算

前言

前面的章节介绍了如何定义基本目标以及如何生成构建输出。单就这一点而言,已经够用了,但 CMake 还具备一系列其他功能,这些功能带来了极大的灵活性和便利性。

本章介绍了 CMake 中最为基础的部分之一,即变量的使用。

变量基础

与任何计算机编程语言一样,在 CMake 中,变量是完成其他工作的基石。定义变量最基本的方式是使用 set() 命令。在 CMakeLists.txt 文件中,可以这样定义一个普通变量:

set(varName value... [PARENT_SCOPE])

变量的名称 varName 可以包含字母、数字和下划线,且区分大小写。该名称还可以包含字符“./-+”,但在实际应用中这类字符很少见。此外,通过间接方式也可以使用其他字符,但同样正常使用中并不常见。

在 CMake 中,变量具有特定的作用域,这与其他语言中的变量作用域(例如局限于特定函数、文件等)类似。变量不能在其作用域之外被读取或修改。与其他语言相比,CMake 中的变量作用域稍微更具灵活性,但目前,将变量的作用域视为其定义所在的文件即可。

CMake 将所有变量都视为字符串。在不同的环境中,变量可能会被解释为不同的类型,但最终它们都只是字符串。在设置变量值时,CMake 不要求这些值进行引号括起(除非值中包含空格)。如果给出多个值,这些值将通过一个分号分隔的方式连接在一起——所得到的字符串就是 CMake 表示列表的方式。以下内容有助于说明。

set(myVar a b c)# myVar = "a;b;c"
set(myVar a;b;c)# myVar = "a;b;c"
set(myVar "a b c")# myVar = "a b c"
set(myVar a b;c)# myVar = "a;b;c"
set(myVar a "b c")# myVar = "a;b c"

变量的值通过 ${myVar} 来获取,该变量可在任何需要字符串或变量的位置使用。CMake 的灵活性尤其体现在它可以递归地使用这种形式,或者指定要设置的另一个变量的名称。此外,CMake 不要求在使用变量之前先对其进行定义。未定义变量的使用只会替换为空字符串,这与 Unix 脚本的行为方式类似。默认情况下,对于未定义变量的使用不会发出警告,但可以给 cmake 命令传递 --warn-uninitialized 选项来设置是否提示警告。不过要注意的是,这种使用方式非常常见,而且不一定意味着存在什么问题,所以该选项的实用性可能会非常有限。

set(foo ab)# foo   = "ab"
set(bar ${foo}cd)# bar   = "abcd"
set(baz ${foo} cd)# baz   = "ab;cd"
set(myVar ba)# myVar = "ba"
set(big "${${myVar}r}ef")# big   = "${bar}ef" = "abcdef"set(${foo} xyz)# ab    = "xyz"
set(bar ${notSetVar})# bar   = ""

字符串并不局限于只能是一行内容,它们可以包含嵌入的换行符。此外,它们还可以包含引号,但这些引号则需要使用反斜杠进行转义。

set(myVar "goes here")
set(multiLine "First line ${myVar}
Second line with a \"quoted\" word")

如果使用 CMake 3.0 或更高版本,可以采用一种替代引号的方式,即使用类似 Lua 的方括号语法。其中,内容的起始用“[=[”标记,结束用“]=]”标记。方括号内的“=”字符可以有任意数量,也可以完全没有,但开头和结尾处的“=”字符数量必须相同。如果方括号后面紧接着是换行符,那么第一个换行符会被忽略,但后续的换行符则不会。此外,对方括号内的内容不会进行任何进一步的转换(即不会进行变量替换或转义)。

# Simple multi-line content with bracket syntax,
# no = needed between the square bracket markers
set(multiLine [[
First line
Second line
]])
# Bracket syntax prevents unwanted substitution
set(shellScript [=[
#!/bin/bash
[[ -n "${USER}" ]] && echo "Have USER"]=])
# Equivalent code without bracket syntax 
set(shellScript"#!/bin/bash

[[ -n \"\${USER}\" ]] && echo \"Have USER\"")

如上述示例所示,花括号语法特别适合用于定义诸如 Unix 脚本之类的内容。此类内容会出于自身目的使用 ${…} 语法,并且经常包含引号,但使用花括号语法意味着这些内容无需像定义 CMake 内容时的传统引号写法那样进行转义,这与传统的引号写法不同。使用 [ 和 ] 标记之间的任意数量的等号字符的灵活性也意味着嵌入的方括号不会被误认为是标记。

可以使用 unset() 函数或者通过向指定变量调用 set() 函数并传入空值的方式来取消变量的设置。以下两种方式是等效的,如果 myVar 不存在的话,也不会出现错误或警告:

set(myVar)
unset(myVar)

除了项目自身定义的变量外,CMake 的许多命令的行为还会受到在调用该命令时特定变量值的影响。这是 CMake 常用的一种模式,用于定制命令的行为或修改默认值,以便不必为每个命令、目标定义等重复定义这些默认值。每个命令的 CMake 参考文档通常会列出任何可能影响该命令行为的变量。本书后续章节还将重点介绍一些有用的变量以及它们如何影响构建过程或提供有关构建的信息。

环境变量

CMake 还允许通过一种改进的变量标记形式来获取和设置环境变量的值,该形式与 CMake 中的变量表示方式法类似。获取环境变量的值使用特殊标识 $ENV{varName},并且可以将其用于任何常规变量可以使用的地方。设置环境变量的方式也与设置常规变量类似,只是变量名使用的是 ENV{varName} 而不是简单的 varName。例如:

set(ENV{PATH} "$ENV{PATH}:/opt/myDir")

需要注意的是,像这样设置环境变量只会对当前正在运行的 CMake 实例产生影响。一旦当前实例运行结束,环境变量的值也会被丢弃。同时,对环境变量的修改在构建时是不可见的。因此,在 CMakeLists.txt 文件中像这样设置环境变量通常是没有太大用处的。

缓存变量

不同于常规变量的生命周期仅限于 CMakeLists.txt,缓存变量存储于构建目录下的 CMakeCache.txt中,并且在 CMake 运行过程中会一直存在。访问缓存变量的方式跟常规变量一致,但设置缓存变量的值有些许不同:

set(varName value... CACHE type "docstring" [FORCE])

相较于常规变量,缓存变量携带有更多信息,其中包括基本类型和文档描述字符串。尽管文档信息可以设置为空,但在设置缓存变量时也需要指定。文档描述字符串不会影响变量的行为,只是作为 CMake 图形界面提供帮助信息和提示等有用。在处理过程中 CMake 会将缓存变量视作字符串,而 type 在大多数情况下只是为了提升图界面的用户体验,type 必须为以下几种中的一种:BOOL、FILEPATH、PATH、STRING、INTERNAL。(每种类型的含义和作用暂不细讲,看后续需要)

图形界面使用文档描述字符串最典型的是作为缓存变量的提示信息或简短描述。因此应尽量使用纯文本,而不是 HTML 等标记语言。

由于设置一个布尔类型的缓存变量用处比较多,所以 CMake 为它单独提供了一个命令,来替代繁琐的 set():

option(optVar helpString [initialValue])

如果省略了初始值,则将使用默认值“OFF”。若提供了初始值,则该值必须符合 set() 命令所接受的布尔值之一。作为参考,上述内容大致相当于:

set(optVar initialValue CACHE BOOL helpString)

与 set() 相比,option() 命令能更清晰地表达布尔型缓存变量的意图,因此通常会是更推荐使用的命令。但需注意的是,在某些情况下,这两个命令的效果可能会有所不同。(后续会讲到)

常规变量与缓存变量最大的区别在于,常规变量在设置新值时总会替换掉旧的值,但缓存变量只有在给定了 FORCE 选项时才会覆盖旧值。在用于定义缓存变量时,set() 命令的运行方式更类似于“如果没有则设置”的操作,而 option() 命令(该命令不具备强制功能)也是如此。这样做的主要原因是缓存变量最初是为了给开发者进行自定义化而设计的。为了避免像常规变量一样在 CMakeLists.txt 文件中硬性指定变量的值,缓存变量可以使得开发者在不对 CMakeLists.txt 文件进行编辑的情况下修改变量的值。这些变量可以通过图形界面或脚本在不改变项目文件本身的前提下进行赋值或覆盖旧值。

块/局部作用域

前面变量基础章节中提到变量都有作用域,但是缓存变量的作用域是全局的,可以在任意位置被访问。到目前所有资料中提到的为止,所有非缓存变量的作用域是其被定义时所在 CMakeLists.txt 文件,也常被称作目录作用域。子目录或函数会从其父级作用域继承这些变量。

CMake 3.25及以后版本,加入的 block() 和 enblock() 命令可以用来定义一个本地变量作用域,即块作用域。当进入一个块,会获得其当时所在范围所有定义变量的一个副本,任意对这些变量的修改也仅作用于副本,而不会影响原来变量的值。一旦离开块所在区域,所有副本和在块内定义的变量都会被丢弃。这在隔离一些命令子集的时候会非常有用。如下所示:

set(x 1)

block()
	set(x 2)# Shadows outer "x"
	set(y 3)# Local, not visible outside the block 
endblock()
# Here, x still equals 1, y is not defined

当然,在使用块时并不总是想与其所在区域相隔离,这时使用 set() 和 unset() 的 PARENT_SCOPE 选项来修改块外部变量的值即可:

set(x 1)
set(y 3)

block()
	set(x 2 PARENT_SCOPE)
	unset(y PARENT_SCOPE)
	# x still has the value 1 here
	# y still exists and has the value 3
endblock()
# x has the value 2 here and y is no longer defined

当使用 PARENT_SCOPE 时,被设置或取消设置的变量是父作用域中的变量,而非当前作用域中的变量。重要的是,这并不意味着要在父作用域和当前作用域中都设置或取消设置该变量。这可能会使 PARENT_SCOPE 使用起来有些不便,因为当需要对两个不同的作用域都产生影响时,通常需要为这两个不同的作用域重复相同的命令。block() 命令支持一个 PROPAGATE 关键字,可以以更可靠和简洁的方式实现这种行为。当控制流离开块时,PROPAGATE 关键字之后列出的所有变量的值都会从块传播到其周围的作用域。如果在块内部取消设置一个被传播的变量,那么在离开块时,该变量在周围的作用域中也会被取消设置。

```shell
set(x 1)
set(z 5)
block(PROPAGATE x z)
	set(x 2)# Gets propagated back out to the outer "x"
	set(y 3)# Local, not visible outside the block
	unset(z)# Unsets the outer "z" too
endblock()
# Here, x equals 2, y and z are undefined

块不只用来控制变量的作用域,它的完整签名如下:

block([SCOPE_FOR [VARIABLES] [POLICIES]] [PROPAGATE var...])

SCOPE_FOR 关键字可用于指定该块应创建何种作用域。若省略 SCOPE_FOR,则 block() 会为变量和策略分别创建一个新的局部作用域(关于后者的内容,后续会讲到)。以下代码的效果与前面的示例相同,但仅创建变量作用域,而策略作用域保持不变:

set(x 1)
set(z 5)
block(SCOPE_FOR VARIABLES PROPAGATE x z)
	set(x 2)# Gets propagated back out to the outer "x"
	set(y 3)# Local variable, not visible outside the block 
	unset(z)# Unsets the outer "z" too
endblock()
# Here, x equals 2, y and z are undefined

虽然局部变量可能是项目中最常用的,而创建一个新的策略作用域也是可以的。使用 block() 在效率上可能稍低于 block(SCOPE_FOR VARIABLES),但因其简洁性更易被使用。

变量潜在行为

经常被忽略的一点是,常规变量和缓存变量其实是完全不同的两个事物。即使它们使用相同的变量名,却可以有着不同的值。这种情况下,如果使用 ${myVar} 则获取的是常规变量的值。换句话说,常规变量的优先级高于缓存变量。但也有例外,就是在给一个缓存变量赋值时,以下情况中,所有与该缓存变量同名的常规变量值会被移除:

  • 调用 set() 或 option() 命令时缓存变量不存在;
  • 调用 set() 或 option() 命令时缓存变量存在但未定义其类型;
  • set() 命令中使用了 FORCE 或 INTERNAL 选项。

在上述的前两种情况中,这意味着在第一次和后续的 CMake 运行之间可能会出现不同的行为。在第一次运行中,缓存变量将不存在或者其类型未定义,但在后续的运行中它将会存在并具有明确的类型。因此,在首次运行时,一个常规变量不可见,但在后续运行中,它就又变得可见。下面通过一个例子来说明这个问题:

set(myVar foo)# Local myVar
set(result ${myVar})# result = foo
set(myVar bar CACHE STRING "")# Cache myVar
set(result ${myVar})# First run:       result = bar
# Subsequent runs: result = foo set(myVar fred)
set(result ${myVar})# result = fred

在 CMake 3.13 版本中,option() 命令的行为有所改变,即如果存在同名的常规变量,该命令将不会执行任何操作。这种新的行为通常符合开发者的直观预期。在 CMake 3.21 版本中,set() 命令也进行了类似的更改,但请注意这两种命令的新行为存在以下差异:

  • 对于 set() 函数,即便之前该缓存变量不存在,它也会被重新设置;但对于 option() 函数,则不会重新设置该缓存变量。
  • 如果在使用 set() 时同时使用了 INTERNAL 或 FORCE,那么缓存变量将始终被设置或更新。

开发人员应当留意这些不一致之处以及提供新功能的各不相同的 CMake 版本。

缓存变量与非缓存变量之间的相互作用还可能导致其他一些可能出乎意料的行为。请看以下这三条命令:

unset(foo)
set(foo)
set(foo "")

有人可能会认为,在上述任何一种情况下, ${foo} 都始终返回一个空字符串,但实际上只有最后一种情况是这样。unset(foo) 和 set(foo) 都会从当前作用域中移除一个非缓存变量。如果还有一个名为 foo 的缓存变量,那么该缓存变量将保持不变,而 foo将提供该缓存变量的值。从这个意义上说,unset(foo)和set(foo)都能有效地解除foo缓存变量的“不可见状态”,前提是该变量存在。另一方面,set(foo"")并不会移除一个非缓存变量,而是明确将其设置为空值,因此无论是否还有名为foo的缓存变量,{foo} 将提供该缓存变量的值。从这个意义上说,unset(foo) 和 set(foo) 都能有效地解除 foo 缓存变量的“不可见状态”,前提是该变量存在。另一方面,set(foo "") 并不会移除一个非缓存变量,而是明确将其设置为空值,因此无论是否还有名为 foo 的缓存变量,foo将提供该缓存变量的值。从这个意义上说,unset(foo)set(foo)都能有效地解除foo缓存变量的不可见状态,前提是该变量存在。另一方面,set(foo"")并不会移除一个非缓存变量,而是明确将其设置为空值,因此无论是否还有名为foo的缓存变量,{foo} 都将始终为一个空字符串。因此,将变量设置为空字符串而不是将其删除,可能是实现开发人员意图的更稳健的方法。

或者在某些非常特殊的情况下,比如项目可能需要获取缓存变量的值,并忽略具有相同名称但非缓存类型的其他变量,可参考 CMake 3.13 对 $CACHE{someVar} 的相关文档说明。但大多数时候,项目不应使用此形式,除非是为了临时调试之用,因为这违背了长期以来形成的惯例,即常规变量会覆盖在缓存中设置的值。

操作缓存变量

通过使用 set() 和 option() 函数,一个项目能够为其开发人员自定义一套实用的构建方式。比如可以开启或关闭构建的不同部分,设置外部包的路径,修改编译器和链接器的标志等等。后续章节将介绍这些以及缓存变量的其他用途,但首先需要理解如何操作这些变量。开发人员有两种主要的方式来实现这一点,一种是从 cmake 命令行中进行操作,另一种是使用图形用户界面工具。

命令行

CMake 允许通过传递给 cmake 的命令行选项直接操作缓存变量。其中最主要的是 -D 选项,它用于定义缓存变量的值。

cmake -D myVar:type=someValue ...

someValue 将替换 myVar 缓存变量的任何先前值。其行为本质上就如同使用 set() 命令并结合 CACHE 和 FORCE 选项对变量进行赋值一样。命令行选项只需提供一次,因为其会被存储在缓存中以便后续运行使用,因此在每次运行 cmake 时无需每次都提供该选项。可以在 cmake 命令行中提供多个 -D 选项,以便一次设置多个变量。

通过这种方式定义缓存变量时,它们无需在 CMakeLists.txt 文件中进行设置(即无需使用相应的 set() 命令)。在命令行中定义的缓存变量没有文档字符串。类型也可以省略,此时变量将具有未定义的类型,或者更准确地说,它会被赋予一个类似于 INTERNAL 的特殊类型,但 CMake 会将其解释为未定义类型。以下展示了通过命令行设置缓存变量的各种示例。

cmake -D foo:BOOL=ON ...
cmake -D "bar:STRING=This contains spaces" ...
cmake -D hideMe=mysteryValue ...
cmake -D helpers:FILEPATH=subdir/helpers.txt ...
cmake -D helpDir:PATH=/opt/helpThings ...

请注意,如果要设置包含空格的缓存变量,那么使用 -D 选项给出的整个值都应进行引号包围。

对于在 CMake 命令行中初始声明但未指定类型的值,存在一种特殊处理方式。如果项目的 CMakeLists.txt 文件随后尝试设置相同的缓存变量,并且指定了 FILEPATH 或 PATH 类型,则如果该缓存变量的值是相对路径,CMake 会将其视为相对于调用 CMake 的目录进行相对处理,并自动将其转换为绝对路径。这种方法并不是特别可靠,因为 CMake 可以从任何目录中调用,而不仅仅是构建目录。因此,建议开发人员在 CMake 命令行中为表示某种路径的变量指定变量时,始终包含类型。无论如何,最好总是在命令行中明确指定变量的类型,以便在图形用户界面应用程序中以最合适的形式显示它。

还可以使用 -U 选项从缓存中移除变量,此选项可重复使用以移除多个变量。请注意,-U 选项支持 * 和 ? 通配符,但需要谨慎操作,以免删除超出预期的变量,并使缓存处于无法构建的状态。一般来说,建议仅移除不含通配符的特定条目,除非绝对确定所使用的通配符是安全的。

cmake -U 'help*' -U foo ...

CMake 图形工具

暂略

变量值打印

set(myVar HiThere)
message("The value of myVar = ${myVar}\nAnd this "
	"appears on the next line")

输出如下:

The value of myVar = HiThere
And this appears on the next line

字符串处理

列表

科学计算

建议

如果开发环境允许的话,CMake 图形用户界面工具是一种非常实用的方式,能够帮助您快速、轻松地了解项目的构建选项,并在开发过程中根据需要对其进行修改。花一点时间熟悉它,日后处理更复杂的项目时会变得简单许多。此外,它还能为开发者提供一个良好的工作基础,以便他们在需要时对诸如编译器设置之类的事项进行试验,因为这些设置很容易在图形用户界面环境中找到并进行修改。

更倾向于提供缓存变量来控制是否启用构建中的可选部分,而非将逻辑编码在 CMake 之外的构建脚本中。这样,通过 CMake 图形用户界面或其他能够处理 CMake 缓存的工具(越来越多的集成开发环境正在具备这一功能)来开启和关闭这些功能就变得非常简单。

尽量避免依赖于那些未被定义的环境变量,除非是那些常见的如 PATH 或类似的操作系统级别的变量。构建过程应当是可预测的、可靠的并且易于设置的,但如果它依赖于环境变量的设置才能正常运行,那么这可能会让新开发人员感到沮丧,因为他们会费力地去设置他们的构建环境。此外,当运行 CMake 时的环境与构建本身被调用时的环境相比可能会发生变化。因此,尽可能地通过缓存变量直接将信息传递给 CMake。

尽早确立变量命名规则。对于缓存变量,可以考虑将相关变量按共同前缀分组,并在前缀后添加下划线,以利用 CMake 图形用户界面根据相同前缀自动分组变量的功能。此外,考虑到该项目可能有一天会成为某个更大项目的子部分,因此以项目名称或与项目密切相关的名称开头的名称可能是理想的选择。

尽量避免在项目中定义与缓存变量同名的非缓存变量。对于初次接触 CMake 的开发人员来说,这两种类型的变量之间的相互作用可能会出乎意料。

CMake 提供了大量的预定义变量,这些变量能够提供有关系统的信息,或者影响 CMake 行为的某些方面。其中一些变量被众多项目广泛使用,例如那些仅在针对特定平台(如 WIN32、APPLE、UNIX 等)进行构建时才定义的变量。因此,建议开发人员偶尔快速浏览一下 CMake 文档页面中列出的预定义变量列表,以帮助自己熟悉可用的变量。

posted @ 2025-06-16 14:37  凉皮也是菜  阅读(7)  评论(0)    收藏  举报  来源