第9章:属性

属性几乎影响构建过程的方方面面,从源文件如何编译为目标文件,到构建的二进制文件在打包安装程序中的安装位置。它们会附加到一个特定的实体,无论是目录、目标、源文件、测试用例、缓存变量,甚至是整个构建过程本身。属性不像变量那样保存独立的值,而是提供特定的附加信息。

对于CMake的新手来说,属性有时会与变量混淆。虽然在功能和特性方面,两者看起来很相似,但属性的用途却截然不同。变量不依附于任何特定的实体,项目定义和使用变量是很常见的。将其与CMake通常定义良好并记录在案的属性进行比较,这些属性总是适用于特定实体。造成两者混淆的可能原因是属性的默认值有时是由变量提供的。CMake用于相关属性和变量的名称通常遵循相同的模式,即变量名是CMAKE_ 为前缀的属性名。

9.1. 通用命令属性

CMake的命令提供了许多用于操作属性。其中最通用的是set_property()和get_property(),可以在任何类型的实体上设置和获取任何属性。这些命令要求将实体的类型指定为命令参数以及一些特定于实体的信息。

set_property(entitySpecific
 [APPEND] [APPEND_STRING]
 PROPERTY propName [value1 [value2 [...]]])

entitySpecific定义了设置属性的实体。它必须是以下属性之一:

GLOBAL
DIRECTORY [dir]
TARGET [target1 [target2 [...]]]
SOURCE [source1 [source2 [...]]]
INSTALL [file1 [file2 [...]]]
TEST [test1 [test2 [...]]]
CACHE [var1 [var2 [...]]]

上面每个词的第一个词定义了设置属性的实体的类型。GLOBAL表示构建本身,因此不需要特定的实体名称。对于DIRECTORY,如果没有名为dir的目录,则使用当前源目录。对于所有其他类型的实体,可以列出该类型的任意数量的项。

PROPERTY关键字将剩下的参数标记为定义属性名和值。propName通常会匹配CMake文档中定义的一种属性,值是特定于属性的。除了CMake已经定义的属性外,还允许项目创建新的属性。这些特定于项目的属性意味着什么,以及如何影响构建,这将取决于项目。如果选择这样做,对于项目来说,明智的做法是在属性名称上使用一些特定于项目的前缀,以避免与CMake或其他第三方包定义的属性发生名称冲突。

使用APPEND和APPEND_STRING关键字来控制已有值(的命名)属性的更新方式。如果这两个关键字都没有指定,则将值替换为之前的(任意)值。APPEND关键字可以将行为的附加属性值累加形成一个列表,而APPEND_STRING关键词需要现有价值和附加新的价值,这里可以连接两个字符串而不是作为一个列表(参见下面还特别注意继承属性进一步)。下表展示了不同之处。

get_property()命令的参数列表如下:

get_property(resultVar entitySpecific
 PROPERTY propName
 [DEFINED | SET | BRIEF_DOCS | FULL_DOCS])

PROPERTY关键字是必需的,并且后面要跟检索属性的名称。检索的结果存储在一个变量中,该变量的名称由resultVar给出。entitySpecific部分类似于set_property(),必须是以下内容之一:

GLOBAL
DIRECTORY [dir]
TARGET target
SOURCE source
INSTALL file
TEST test
CACHE var
VARIABLE

与前面一样,GLOBAL将构建作为整体来引用。DIRECTORY可以与指定目录一起使用,也可以不指定特定目录,如果没有提供目录,则默认为当前源目录。大多数作用域,必须命名该作用域内的特定实例,并将检索附加到该实例的属性请求中。

VARIABLE略有不同,变量名指定为propName,而不是附加到VARIABLE关键字上。这有点不直观,可以考虑一下如果变量使用VARIABLE关键字命名为实体。在这种情况下,属性名没有任何内容需要指定,将VARIABLE看作指定作用范围可能会有所帮助。以这种方式理解变量时,其与其他类型变量的处理方式是一致的。

如果没有提供任何可选关键字,则检索命名属性的值。这是get_property()命令的使用方式。在实践中,VARIABLE作用域与get_property()使用比较少。变量值可以直接通过${}获得,它比使用get_property()更清晰、更简单。

可选关键字可用于检索有关属性的详细信息,而不仅仅是其值:

DEFINED

检索的结果将是一个布尔值,确定命名的属性是否定义。在VARIABLE的作用域内查询,只有在指定的变量使用define_property()命令显式定义时(见下面),结果才为真。

SET

检索的结果将是一个布尔值,确定命名的属性是否已经设置。这是一个比DEFINED的更强的测试,测试命名的属性是否已经实际给定了一个值(值本身是不相关的)。

BRIEF_DOCS

检索已命名属性的字符串描述。如果没有为该属性定义字符串描述,结果将是字符串NOTFOUND。

FULL_DOCS

检索已命名属性的完整文档。如果没有为该属性定义字符串描述,结果将是字符串NOTFOUND。

可选关键字中,除了SET之外的所有关键字都用的不多,除非项目显式地调用define_property()来填充特定实例。这个很少使用的命令有以下形式:

define_property(entityType
 PROPERTY propName [INHERITED]
 BRIEF_DOCS briefDoc [moreBriefDocs...]
 FULL_DOCS fullDoc [moreFullDocs...])

重要的是,这个命令不设置属性的值,只有文档和是否从别处继承值(如果它尚未确定)。entityType必须为GLOBAL,DIRECTORY,TARGET,SOURCE,TEST,VARIABLE 或CACHED_VARIABLE 之一和propName用于指定属性定义。尽管与get_property()命令一样,对于VARIABLE,变量名指定为propName,但没有指定任何实例。简短描述通常应该为单行,而完整的文档可以更长,如果需要的话可以跨越多行。

如果在定义属性时使用INHERITED选项,如果该属性没有在指定的范围中设置,get_property()命令将连接到父作用域,例如:请求了一个目录属性,但没有为指定的目录设置该属性,那么父目录作用域的属性将在目录作用域结构中递归查询,直到找到该属性或到达源的顶层。如果在顶级目录中仍然找不到,将会搜索全局作用域。类似地,如果一个TARGET、SOURCE或TEST 属性要求但没有设置为指定的实例,该目录将搜索范围(包括递归目录结构和GLOBAL范围,如果必要的话)。没有为变量或缓存提供这样的链接功能,因为VARIABLE或CACHE已经根据涉及到父变量范围,所以没有提供这样的连接父作用域的功能。

INHERITED属性的继承行为仅适用于特定属性类型的getproperty()命令及类似的get…函数(将在下面的小节中介绍)。当使用APPEND或APPEND_STRING选项调用set_property()时,只考虑属性的当前值(即在计算要追加的值时不发生继承)。

CMake对每种类型都有大量预定义的属性。开发人员应查阅CMake参考文档,可以了解可用属性及其预期用途。后面的章节中,我们将讨论许多这些属性,以及与其他CMake命令、变量和特性的关系。

9.2. 全局属性

全局属性与整体构建相关。它们通常用于修改构建工具,如何启动或工具行为的其他方面、定义项目文件如何构造的方面,以及提供某种程度的构建级别的信息。

除了set_property()和get_property()命令可以操作属性外,CMake还提供了get_cmake_property()用于查询全局实例。它不仅仅是get_property()的简写,还可以简单地用于检索任何全局属性的值。

get_cmake_property(resultVar property)

像get_property()一样,resultVar是一个变量的名称,当命令返回时,请求的属性的值将存储在该变量中。属性参数可以是任何全局属性的名称,也可以是以下伪属性之一:

VARIABLES

返回所有常规(即非缓存)变量的列表。

CACHE_VARIABLES

返回所有缓存变量的列表。

COMMANDS

返回所有已定义的命令、函数和宏的列表。命令由CMake预定义,而函数和宏可以由CMake(通常通过模块)或项目本身定义。一些返回的名称会对应于未归档的或不打算直接用于项目的内部实例。返回的名称可能与最初定义的名称的大小写不同。

MACROS

返回已定义宏的列表。这将是伪属性返回COMMANDS的子集,但请注意名称的大小写可能与伪属性报告的COMMANDS不同。

COMPONENTS

返回由install()命令定义的所有组件的列表,这在第25章中有介绍。

从技术上讲,这些只读的伪属性不是全局属性(例如,不能使用get_property()检索它们),但是理论上非常相似。这些属性只能通过get_cmake_property()检索。

9.3. 目录属性

目录还支持自己的属性集。从逻辑上讲,目录属性位于作用域的全局属性,并且只影响单个目标的属性。因此,目录属性主要关注于设置目标属性的默认值和覆盖全局属性或当前目录的默认值。一些只读目录属性也提供了一定程度的信息,保存有关构建如何到达目录、在此点定义了哪些内容等信息。

方便起见,CMake提供了用于设置和获取目录属性的专属命令。设置命令的定义如下:

set_directory_properties(PROPERTIES prop1 val1 [prop2 val2 ...])

虽然更加简洁,但是这个特定于目录的设置命令缺少APPEND或APPEND_STRING选项。只能用于设置或替换属性,不能直接添加到现有属性。与通用的set_property()相比,此命令的限制是只适用于当前目录。项目可以选择在方便的地方使用这种更具体的方式,而在其他地方使用通用方式,或者为了一致性,任何地方都使用通用方式用。没有正确不正确的问题,这只是个人偏好的问题。

指定目录属性获取命令有两种形式:

get_directory_property(resultVar [DIRECTORY dir] property)
get_directory_property(resultVar [DIRECTORY dir] DEFINITION varName)

第一种形式用于从特定目录或从当前目录(如果没有使用DIRECTORY参数)获取属性的值。第二种形式检索变量的值,看起来可能不是那么有用,但是提供了一种从当前目录范围以外的其他目录范围(当使用目录参数时)获取变量值的方法。实践中,第二种形式应该很少需要,其应避免调试以外的场景构建。

对于get_directory_property()命令的任何一种形式,如果使用了DIRECTORY参数,则指定的目录必须已经处理过。CMake不可能知道它尚未遇到的目录作用域的属性。

9.4. 目标属性

CMake中很少有东西能如此强烈而直接地影响目标如何构建为目标属性。他们通过编译选项控制编译源文件,从而构建指定位置上的二进制类型文件和中间文件。一些目标属性会影响IDE中的显示,而其他属性会影响编译/链接时使用的工具。简而言之,目标属性是收集将源文件实际转换为二进制文件细节的地方。

CMake中已经有许多用于操作目标属性的方法。除了通用的set_property()和get_property()命令,CMake还为特定目标提供了一些等价操作:

set_target_properties(target1 [target2...]
 PROPERTIES
 propertyName1 value1
 [propertyName2 value2] ... )
get_target_property(resultVar target propertyName)

set_target_properties()缺乏set_property()的灵活性,但为常见情况提供了更简单的语法。set_target_properties()命令不支持直接将属性添加到现有属性中,如果需要为给定的属性提供列表值,需要在set_target_properties()命令中以字符串形式指定该值,例如:“this;is;a;list”。

get_target_property()命令是get_property()的简化版本。它只关注于提供一种简单的方法来获取目标属性的值,是通用命令的简写版本。

除了通用和特定于目标的属性获取器和设置器之外,CMake还有许多修改目标属性的命令。特别是target_…()命令是CMake的一个关键部分,除了琐碎的CMake项目之外,所有的CMake项目都会使用它们。这些命令不仅定义了特定目标的属性,还定义了如何将这些信息传播到与其相关的其他目标。第14章基本内容涵盖了这些命令,以及它们如何与其他目标属性进行关联。

9.5. 源码属性

CMake还支持单个源文件的属性。其允许在逐个文件的基础上对编译器标志进行细粒度操作。也允许告诉CMake或构建工具如何对待这个文件,如指示是否生成作为构建的一部分,用什么编译器,或选择与编译器无关的工具处理文件等等。

项目很少需要查询或修改源文件属性,但对于需要的情况,CMake提供专用的setter和getter命令,这使得任务更容易。这些命令与其他特定于属性的setter和getter命令有类似的模式:

set_source_files_properties(file1 [file2...]
 PROPERTIES
 propertyName1 value1
 [propertyName2 value2] ... )
get_source_file_property(resultVar sourceFile propertyName)

这里,依旧是没有提供附加功能的setter,而getter只是语法简写为通用get_property()命令,并没有提供新的功能。

源码属性仅对相同目录范围内的目标可见。如果一个源文件的设置属性出现在不同的目录范围,目标不会看到改变的属性,因此编译,以及相应源文件将不受影响。一个源文件可以编译成多个目标,因此设置的任何源码属性都应该对添加该源码的所有目标有意义。

开始使用源属性之前,开发人员应该了解实现细节。使用一些CMake生成器(特别是Unix Makefiles生成器)时,源和源属性之间的依赖性比预期的要强。特别是源属性用于修改特定源文件(而不是整个目标)的编译器标记,那么更改源的编译器标记会导致重新构建目标的所有源。这是在Makefile设置中处理依赖项细节的一个限制,在Makefile中,测试每个单独源代码的编译器标志是否改变会带来极大的性能损失,因此CMake开发人员选择在目标级别实现依赖项。项目可能会通过版本细节,只是一个或者两个源码文件用编译器编译,但正如19.2节中所讨论的,有更好的替代方式来应对源属性不同的副作用。

9.6. 缓存变量属性

缓存变量上的属性与其他属性类型的目的略有不同。大多数情况下,缓存变量属性更多的是在CMake GUI和基于控制台的ccmake工具中处理缓存变量,而不是以任何有形的方式影响构建。也没有额外的命令可以操纵他们,所以通用set_property()和get_property()命令修改时,必须使用CACHE关键字。

第5.3节“缓存变量”中,我们讨论了缓存变量的许多方面,这些方面反映在缓存变量的属性中。

  • 每个缓存变量都有一个类型,类型必须是BOOL、FILEPATH、PATH、STRING或INTERNAL。可以使用带有属性名称类型的get_property()来获得此类型。类型会影响CMake GUI和ccmake在UI中显示缓存变量的方式,以及用于编辑其值部件的类型。任何内部类型的变量都不会在CMake GUI或ccmake中显示。

  • 可以使用mark_as_advanced()命令将缓存变量标记为advanced,该命令实际上只是设置了布尔型ADVANCED缓存变量属性。CMake GUI和ccmake工具都提供了显示或隐藏高级缓存变量的选项,允许用户选择,是只关注主要的基本变量,还是查看全部的非内部变量。

  • 缓存变量的描述字符串通常是在调用set()命令时设置,也可以使用HELPSTRING缓存变量属性修改或读取。这个描述字符串在CMake GUI中用作工具提示,在ccmake工具中用作单行的帮助提示。

  • 如果缓存变量的类型是STRING,CMake GUI将查找名为STRING的缓存变量属性。如果不是空的,应该是变量的有效值列表,然后CMake GUI会将该变量显示为这些值的组合框,而不是任意的文本输入的部件。ccmake中,在缓存变量上按输入顺序遍历。注意CMake并没有强制要求缓存变量必须是字符串属性中的一个值,这只是为了方便CMake GUI和ccmake工具。当CMake运行configure步骤时,仍然将缓存变量视为一个任意字符串,因此仍然可以在CMake命令行或通过项目中的set()命令为缓存变量提供任何值。

9.7. 其他属性类型

CMake还支持单个属性的测试,并提供了属性setter和getter命令的通常测试特定版本:

set_tests_properties(test1 [test2...]
 PROPERTIES
 propertyName1 value1
 [propertyName2 value2] ... )

get_test_property(resultVar test propertyName)

这些只是通用命令的更简洁的版本,它们缺乏附加功能,但在某些情况下可能使用起来更方便。测试将在第24章中详细讨论。

CMake支持的另一种属性是针对安装文件。这些属性特定于打包类型,大多数项目通常不需要这些属性。

9.8. 推荐

属性是CMake的重要组成部分。一系列命令能够设置、修改或查询各种类型的属性,其中一些属性进一步影响了项目之间的依赖关系。

  • 除了特殊的全局伪属性外,所有属性都可以使用set_property()进行操作,这使得开发人员可以预测它,并在需要时提供灵活的APPEND功能。具体属性可能在某些情况下更方便进行操作,比如允许多个属性设置,不过缺少APPEND功能可能引导一些项目只是用set_property()。虽然常见的错误是使用特定于属性的命令替换而不是附加到属性值中,但这两种方法都不是正确的,只能算是个人偏好。

  • 对于目标属性,强烈建议使用target_…()命令,而不是直接操作相关的目标属性。这些命令不仅可以操作特定目标上的属性,还可以在目标之间建立依赖关系,以便CMake可以自动传播一些属性。第14章会讨论这一系列的主题,强调了对target_…()命令的强烈偏好。

  • 在编译器选项的控制级别上,源码属性提供了更细粒度的操作,这些对项目的构建行为有潜在的影响。特别是,当编译选项只有少数源文件改变时,一些CMake生成器可能会重新构建一些不必要的东西。项目应该考虑使用其他可用的替代方案,如第19.2节提供的技术。

Last updated