第14章:编译和连接

前一章讨论了构建类型,以及与选择的编译器和链接器行为之间的关系。本章讨论如何控制编译器和链接器的行为。本章提到的概念是每个CMake开发人员应该熟悉的主题和技术。

深入研究之前,随着CMake的发展,控制编译器和链接器的行为有所改进。焦点可以从构建全局的视角,转移到可以控制每个单独目标的需求的视角,以及这些需求应该或不应该传递到依赖于的其他目标上。这需要思维上进行转变,会影响项目如何有效地确定建立目标。CMake成熟的特性可以用在控制粗粒度行为上,但代价是失去定义目标之间的关系。以目标为中心的特性通常应该优先考虑使用,因为它们极大地提高了构建的健壮性,并提供了对编译器和链接器行为更精确的控制。更新的特性在其行为和使用方式上也趋于一致。

14.1. 目标属性

CMake的属性系统中,目标属性是控制编译器和链接器标志的主要机制。一些属性提供指定标志的能力,而其他属性则专注于特定的功能,因此它们可以抽象出平台或编译器的差异。本章重点介绍更通用的属性,后面的章节将介绍一些具体的属性。

14.1.1. 编译器标示

控制编译器标示的基本目标属性如下,每个属性都包含一个列表:

INCLUDE_DIRECTORIES

用作头文件搜索路径的目录列表,都必须是绝对路径。CMake会为每条路径添加一个编译器标记,并在前面加上适当的前缀(通常是-I或/I)。创建目标时,此目标属性的初始值从同名的文件夹属性中获取。

COMPILE_DEFINITIONS

保存要在编译命令行上设置的定义列表。定义的形式为VAR或VAR=VALUE, CMake将其转换为编译器适用的形式(通常为-DVAR…或/DVAR…)。创建目标时,此目标属性的初始值为空。有一个同名的目录属性,但不为这个目标属性提供初始值。相反,目录和目标属性将会在最终的编译器命令行中合并。

COMPILE_OPTIONS

此属性中提供的既不是头搜索路径,也不是符号定义的编译器标志。创建目标时,此目标属性的初始值从同名的文件夹属性中获取。

较旧的、现在已不推荐使用的目标属性,其名称为COMPILE_FLAGS,与COMPILE_OPTIONS类似。COMPILE_FLAGS属性视为直接包含在编译器命令行中的单个字符串。因此,可能需要手工转义,而COMPILE_OPTIONS是一个列表,CMake会自动执行任何需要的转义或引号。

INCLUDE_DIRECTORIES和COMPILE_DEFINITIONS属性实际上只是方便为项目经设置经常使用的编译器标示。所有的编译器特定标志都在COMPILE_OPTIONS属性中提供。

上面的三个目标属性都有一个同名的目标属性,只是前面加上了INTERFACE 前缀。这些接口属性执行相同的操作,只是不应用于目标本身,而是应用于链接到的其他目标。换句话说,它们用于指定的目标应该会继承的编译器标志。由于这个原因,通常称为使用需求,而非INTERFACE属性的构建需求。在第16章目标类型中将讨论两种特殊库类型:IMPORTED和INTERFACE。这些特殊的库类型只支持`INTERFACE目标属性,而不支持非INTERFACE_…`属性。

与非接口对应项不同,上面的INTERFACE_…属性都不是从目录属性初始化的。他们开始都为空,当设置头文件搜索路径后,会定义和传播编译器标志到相应的目标上。

上述所有目标属性都支持生成器表达式。这对于COMPILE_OPTIONS属性特别有用,因为它只允许在满足某些条件的情况下添加特定的标志,比如:针对特定的编译器进行操作。另一种常见用法是获取与其他目标相关的路径,并将其用作包含目录的一部分。

如果需要对单个源文件操作编译器标志,那么目标属性的粒度就不够用了。这种情况下,CMake提供了COMPILE_DEFINITIONS、COMPILE_FLAGS和COMPILE_OPTIONS源文件属性(COMPILE_OPTIONS源文件属性仅在CMake 3.11中添加),这些类似于目标中的同名属性。注意,他们支持生成器表达式已经落后于目标属性,与COMPILE_DEFINITIONS源文件属性获得生成器表达式CMake 3.8,而其他两个则在3.11的支持。此外,Xcode项目文件格式不支持配置特定的源文件属性,所以针对Apple 平台时,$<CONFIG>$<CONFIG:…>不应该用于源文件属性。还要记住9.5节中讨论的警告,“源属性”是关于在使用源文件属性时,会产生性能问题。

14.1.2. 连接标示

与链接器标记相关的目标属性与编译器标示相似,但涉及的属性较少:

LINK_LIBRARIES

此属性包含目标直接链接的所有库的列表。创建目标时为空,并且支持生成器表达式。列出的库可以是下列之一:

  • 库的路径,通常为绝对路径。

  • 只有库名而没有路径,通常也没有任何特定于平台的文件名前缀(如lib)或后缀(如:.a、.so、.dll)。

  • CMake库目标的名称。当使用链接器命令时,CMake会将其转换为已构建库的路径,包括适合平台的文件名的前缀或后缀。因为CMake会代表项目处理不同的平台差异和路径,所以使用CMake目标名称通常是首选方法。

CMake使用适当的链接器标示连接LINK_LIBRARIES属性中的每个链接。

LINK_FLAGS

包含要传递给链接器的标志列表,用于可执行程序、动态库或模块库目标。对于静态库构建目标将忽略该属性。此属性用于一般链接器标记,而不用于指定其他链接库的标记。不确定生成器表达式是否能支持。当创建目标时,此属性将为空。

STATIC_LIBRARY_FLAGS

与LINK_FLAGS类似,不过用于静态库构建的目标。

与编译器属性不同,LINK_LIBRARIES只有一个等效的接口属性INTERFACE_LINK_LIBRARIES。没有与LINK_FLAGS或STATIC_LIBRARY_FLAGS等效的接口。

在一些较老的项目中,可能会遇到名为LINK_INTERFACE_LIBRARIES的目标属性,它是INTERFACE_LINK_LIBRARIES的前身。这个旧属性自CMake 2.8.12已经弃用,如果需要,可以启用策略CMP0022来使用旧属性。新项目更推荐使用INTERFACE_LINK_LIBRARIES。

LINK_FLAGS和STATIC_LIBRARY_FLAGS属性不支持生成器表达式,但确有相关的配置属性:

  • LINK_FLAGS_<CONFIG>

  • STATIC_LIBRARY_FLAGS_<CONFIG>

<CONFIG>匹配正在构建的配置时,除了非配置标记外,还将使用上面两个标记。

14.1.3. 目标属性的命令

上述目标属性通常不直接进行操作。CMake提供了专门的功能,以一种更方便、更健壮的方式修改它们,也鼓励明确依赖和目标之间传递行为的规范。4.3节在介绍target_link_libraries()命令时,解释了对的内部目标使用PRIVATE、PUBLIC和INTERFACE的依赖关系是如何表示的。前面的讨论集中在目标之间的依赖关系上,但这现些关键字的效果可以变得更为精确。

target_link_libraries(targetName
 <PRIVATE|PUBLIC|INTERFACE> item1 [item2 ...]
 [<PRIVATE|PUBLIC|INTERFACE> item3 [item4 ...]]
 ...
)

PRIVATE

PRIVATE后面的项只影响targetName的行为。只有非INTERFACE_…的目标属性会改变影响范围(例如:LINK_LIBRARIES, LINK_FLAGS和STATIC_LIBRARY_FLAGS)。

INTERFACE

这是对PRIVATE的补充,INTERFACE关键字后面的项只对链接到targetName的目标有影响。只有INTERFACE_…的目标属性会改变影响范围(例如:INTERFACE_LINK_LIBRARIES)。

PUBLIC

这相当于将PRIVATE和INTERFACE的结合在一起。

大多数情况下,开发人员可能会感觉第4.3节“链接目标”的解释更加直观,但是上面更精确的描述可以用在更复杂的项目中,属性可能会以不同寻常的方式操纵行为。上面的描述也恰好与操纵编译器标志的target_…()命令行为非常接近。实际上,它们都遵循相同的模式,以相同的方式应用于PRIVATE、PUBLIC 和INTERFACE 。

target_include_directories(targetName [BEFORE] [SYSTEM]
 <PRIVATE|PUBLIC|INTERFACE> dir1 [dir2 ...]
 [<PRIVATE|PUBLIC|INTERFACE> dir3 [dir4 ...]]
 ...
)

target_include_directories()命令将头文件搜索路径添加到INCLUDE_DIRECTORIES和INTERFACE_INCLUDE_DIRECTORIES目标属性中。使用PRIVATE关键字时目录添加到INCLUDE_DIRECTORIES目标属性中,使用INTERFACE关键字时目录添加到INTERFACE_INCLUDE_DIRECTORIES目标属性中。使用PUBLIC关键字,则会将目录添加到INCLUDE_DIRECTORIES和INTERFACE_INCLUDE_DIRECTORIES目标属性中。

通常,调用target_include_directories()指定的目录都会附加到相关的目标属性中,这使得添加多个路径变得很容易。如果需要,可以使用BEFORE关键字将目录添加到目标属性的内容列出。

如果指定了SYSTEM关键字,编译器将在某些平台上将目录视为系统包含路径。这样做可能会跳过某些编译器警告,或改变文件依赖关系的处理方式,还会影响某些编译器头路径搜索的顺序。开发人员有时会尝试使用SYSTEM关键字“沉默”来自头文件的警告,而不是直接解决这些警告。如果这些头文件是项目的一部分,那么SYSTEM通常不是一个合适的选项。通常,SYSTEM用于项目外部的路径(但即使这样也很少需要它)。

值得注意的是路径由导入指定目标INTERFACE_INCLUDE_DIRECTORIES属性将目标置于SYSTEM的默认路径。因为导入的目标假定来自项目外部,因此相关联的头文件应该以类似于其他系统提供的头文件的方式处理。项目可以通过将目标NO_SYSTEM_FROM_IMPORTED属性设置为true来覆盖这一行为,这将避免所有导入目标成为系统默认路径。导入目标的话题将在第16章中详细介绍。

target_include_directories()命令提供了另一种直接操纵目标属性的方式。项目也可以指定相对路径,而不仅是绝对路径。在需要的地方,相对路径将自动转换为绝对路径,并将路径视为相对于当前源目录的路径。

因为target_include_directories()命令基本上只是填充相关的目标属性,所以这些属性的所有常见特性都会应用。特别是可以使用生成器表达式,这一特性在安装目标和创建包时很重要。$<BUILD_INTERFACE:…>$<INSTALL_INTERFACE:…>生成器表达式允许为构建和安装指定不同的路径。安装目标通常用相对路径,将被解释为相对于基地安装位置而不是源目录。第25.2.1节,“接口属性”详细介绍了指定头文件搜索路径的内容。

target_compile_definitions(targetName
 <PRIVATE|PUBLIC|INTERFACE> item1 [item2 ...]
 [<PRIVATE|PUBLIC|INTERFACE> item3 [item4 ...]]
 ...
)

target_compile_definitions()命令非常简单,每个项都具有VAR或VAR=VALUE的形式。PRIVATE项填充COMPILE_DEFINITIONS目标属性,而INTERFACE项填充INTERFACE_COMPILE_DEFINITIONS目标属性。PUBLIC项会填充这两个目标属性。可以使用生成器表达式,但通常以相同的方式处理构建和安装情况。

target_compile_options(targetName [BEFORE]
 <PRIVATE|PUBLIC|INTERFACE> item1 [item2 ...]
 [<PRIVATE|PUBLIC|INTERFACE> item3 [item4 ...]]
 ...
)

target_compile_options()命令也非常简单。每一项都视为一个编译器选项,PRIVATE项填充COMPILE_OPTIONS目标属性和INTERFACE项填充INTERFACE_COMPILE_OPTIONS目标属性。通常,PUBLIC项会填充两个目标属性。对于所有情况,每个项都附加到现有的目标属性值上,可以使用BEFORE关键字作为前缀。可以使用生成器表达式,但通常以相同的方式处理构建和安装情况。

14.2. 目录属性和命令

CMake 3.0及以后版本中,目标属性是用来指定编译器和链接器标志的首选项,因为它能够定义如何与相互链接的目标交互。CMake的早期版本中,目标属性不那么突出,通常用在指定目录级别的属性。这些目录属性和通常用于操作它们的命令缺乏基于目标的等价物,从而显示的结果一样,这是项目应该尽可能避免使用它们的另一个原因。尽管如此,由于许多在线教程和示例仍然使用它们,开发人员应该了解一下目录级属性和命令。

include_directories([AFTER | BEFORE] [SYSTEM] dir1 [dir2...])

简单地说,include_directories()命令将头文件搜索路径添加到在当前目录范围。默认情况下,路径会附加到现有的目录列表中,可以通过将CMAKE_INCLUDE_DIRECTORIES_BEFORE变量设置为ON来改变默认设置。还可以通过BEFORE和AFTER选项对每次调用进行控制,以显式地指示应该如何处理该调用的路径。项目应该小心设置CMAKE_INCLUDE_DIRECTORIES_BEFORE,因为大多数开发人员可能会假定附加目录的默认行为是起作用的。SYSTEM关键字与target_include_directories()命令具有相同的效果。

为include_directories()提供的路径可以是相对的,也可以是绝对的。相对路径转换为绝对路径自动被视为相对于当前的源目录。路径也可能包含生成器表达式。

include_directories()实际的细节比上面简单的解释要复杂得多。首先,调用include_directory()有两个效果:

  • 将列出的路径添加到当前CMakeLists.txt文件的INCLUDE_DIRECTORIES目录属性中。这意味着在当前目录和以下目录中创建的所有目标,都会将目录添加到INCLUDE_DIRECTORIES目标属性中。

  • 当前CMakeLists.txt文件中创建的任何目标(或者更准确地说,当前目录范围)都将添加路径到其INCLUDE_DIRECTORIES目标属性中,即使这些目标是在调用INCLUDE_DIRECTORIES()之前创建的。这只适用于当前CMakeLists.txt中创建的目标文件或通过include()包含的其他文件。

上面的第二点往往会让许多开发人员感到惊讶。为了避免可能导致这种混乱的情况,如果必须使用include_directory()命令,最好在CMakeLists.txt文件中使用。创建任何目标或使用include()或add_subdirectory()拉入任何子目录之前,最好在CMakeLists.txt文件中调用include_directory()。

add_definitions(-DSomeSymbol /DFoo=Value ...)
remove_definitions(-DSomeSymbol /DFoo=Value ...)

add_definitions()和remove_definitions()命令添加和删除COMPILE_DEFINITIONS目录属性中的条目。每个条目都应该以-D或/D开头,这是绝大多数编译器使用的两种标记方式。在定义COMPILE_DEFINITIONS目录属性之前,CMake会去掉这个标志前缀,因此使用哪个前缀无关紧要(无论项目构建在哪个编译器或平台上)。

像include_directory()一样,这两个命令会影响当前CMakeLists.txt文件中创建的所有目标,甚至会影响那些在调用add_definitions()或remove_definitions()之前创建的目标。在子目录范围中创建的目标只有在调用之后才会受到影响。这是CMake使用COMPILE_DEFINITIONS目录属性的结果。

虽然不推荐,但也可以使用这些命令指定(除定义之外的)编译器标志。如果CMake不能识别出编译器定义的特定项,那么该项将会不加修改地添加到COMPILE_OPTIONS目录属性中。这种行为是由于历史原因而出现的,但是新项目应该避免这种行为(参见下面的add_compile_options()命令以获得另一种选择)。

由于底层目录属性支持生成器表达式,所以可以执行这两个命令,但也有一些注意事项。生成器表达式只能用于定义有值的部分,而不能用于名称部分(即只能在-DVAR=值项中的“=”之后使用,或者对于-DVAR是无法用的)。这与CMake如何解析每个项,来检查它是否是一个编译器定义有关。还要注意,这些命令只修改目录属性,它们不影响COMPILE_DEFINITIONS的目标属性。

add_definitions()命令有许多缺点。要求在每个条目前面加上-D或/D以将其视为定义,这与其他CMake行为不一致。忽略前缀使得命令将项作为通用选项处理,这一事实对于命令的名称来说也是违反直觉的。此外,对生成器表达式的限制只支持KEY=VALUE定义的值部分,这也是对前缀有要求的原因。CMake 3.12引入了add_compile_definitions()命令作为add_definitions()的替代:

add_compile_definitions(SomeSymbol Foo=Value ...)

新命令只处理编译定义,不需要在每个项上使用任何前缀,并且可以使用生成器表达式。新命令的名称和定义项的处理与target_compile_definitions()一致。add_compile_definitions()仍然会影响在同一个目录中创建的所有目标,无论这些目标创建在add_compile_definitions()之前或之后,因为这是COMPILE_DEFINITIONS目录属性的操作,而不是命令本身的操作。

add_compile_options(opt1 [opt2 ...])

add_compile_options()命令用于提供任意的编译器选项。与include_directory()、add_definitions()、remove_definitions()和add_compile_definitions()命令不同,它的行为非常简单。add_compile_options()的每个选项都会添加到COMPILE_OPTIONS目录属性中。随后在当前目录范围及后续创建的每个目标,都将在它们自己的COMPILE_OPTIONS目标属性中继承这些选项。调用之前创建的任何目标都不会受到影响。与其他目录属性命令相比,这种行为更接近开发人员的预期。此外,生成器表达式是由底层目录和目标属性控制,所以add_compile_options()命令也支持生成器表达式。

link_libraries(item1 [item2 ...] [ [debug | optimized | general] item] ...)
link_directories(dir1 [dir2 ...])

早期的CMake版本中,这两个命令是告诉CMake将库链接到其他目标的主要方式。它们会影响当前目录范围内,以及命令调用后创建的所有目标,但现有的目标不会受到影响(即类似于add_compile_options()的行为)。link_libraries()命令中指定的项可以是CMake目标、库名、库的完整路径,甚至是链接器标志。

简单地说,可以通过在调试构建类型前面加上关键字Debug,使一个项只适用于调试构建类型,或者通过在构建类型前面加上关键字optimized使其适用于除调试之外的所有构建类型。项前面可以加上关键字general,表明适用于所有构建类型,但是由于general是默认的,这样做没有什么意义。所有三个关键字只影响它后面的单个项,而不是下一个关键字之前的所有项。强烈建议不要使用这些关键字,因为生成器表达式可以更好地控制应该何时添加项。要考虑自定义生成类型,如果生成类型在DEBUG_CONFIGURATIONS全局属性中列出,则将其视为调试配置。

link_directory()所添加的目录只有在给CMake一个要链接的库名时才会起作用。CMake将提供的路径添加到链接器命令行中,并让链接器自己查找这些库。如果给出了相对路径,将视为相对于当前源目录(CMake的早期版本有不同的行为,请参阅策略CMP0015的文档了解详细信息)。完整的路径或CMake目标的名称通常是首选,而且一旦link_directory()添加了链接器搜索目录,项目就没有方法来删除搜索路径(如果需要的话)。由于这些原因,应该尽可能避免添加链接器的搜索目录。

14.3. 编译器和链接器变量

属性是项目修改编译器和链接器标志的主要方式。用户不能直接操作属性,因此项目可以完全控制如何设置属性。但在某些情况下,用户需要添加他们自己的编译器或链接器标志。他们可能希望添加更多的警告选项,打开特殊的编译器特性,如sanitizer或调试开关等等。在这些情况下,使用变量更合适。

CMake提供了一组变量,用于指定编译器和链接器标志,并将各种目录、目标和源文件属性提供的标志合并。它们通常是缓存变量,以便用户方便地查看和修改,但也可以在项目的CMakeLists.txt文件中设置为常规的CMake变量(这是项目应该避免的)。CMake在第一次在构建目录中运行时,会为缓存变量提供合适的默认值。

直接影响编译器标示的主要变量有以下形式:

  • CMAKE_<LANG>_FLAGS

  • CMAKE_<LANG>_FLAGS_<CONFIG>

这个变量族中,对应于编译的语言,典型的值有C、CXX、Fortran、Swift等。部分是一个大写字符串,对应于其中一种构建类型,如DEBUG、RELEASE、RELWITHDEBINFO或MINSIZEREL。第一个变量将应用于所有构建类型,包括CMAKE_BUILD_TYPE为空的单个配置生成器,而第二个变量应用于由指定的构建类型。因此,使用调试配置构建的C++文件将具有来自CMAKE_CXX_FLAGS和CMAKE_CXX_FLAGS_DEBUG的编译器标记。

第一个project()命令会为这些不存在的缓存变量创建缓存变量(这有点简化了,更完整的解释在第21章中给出)。因此,第一次运行CMake之后,它们的值很容易在CMake GUI应用程序中找到。举个例子,对于一个特定的编译器,下面C++语言变量是默认的:

链接器标志的处理也是类似的。它们由以下变量族控制:

  • CMAKE_<TARGETTYPE>_LINKER_FLAGS

  • CMAKE_<TARGETTYPE>_LINKER_FLAGS_<CONFIG>

这些变量都用于特定类型的目标,每一种都在第4章中介绍过。变量名的部分必须是以下类型之一:

EXE

目标由add_executable()创建。

SHARED

用add_library(name SHARED…)或等效的方法创建的目标,比如省略SHARED关键字,但将BUILD_SHARED_LIBS变量设置为true。

STATIC

用add_library(name STATIC…)或等效的方法创建的目标,比如省略STATIC关键字,但BUILD_SHARED_LIBS变量设置为false或未定义。

MODULE

目标使用add_library(name MODULE …)创建。

如编译器标志一样,CMAKE_<TARGETTYPE>_LINKER_FLAGS在链接构建配置时使用,而CMAKE_<TARGETTYPE>_LINKER_FLAGS_<CONFIG>标志只在相应的配置中使用。某些平台上,部分或整个链接器标志为空很常见。

CMake教程和示例代码经常使用上述变量来控制编译器和链接器标志。这在CMake 3.0之前是相当普遍的做法,但是随着CMake 3.0及以后的关注焦点转移到以目标为中心的模型上,这样的例子不再推荐。它们经常会导致一些非常常见的错误,下面列出了一些比较常见的错误。

编译器/链接器变量是单个字符串,而不是列表

如果需要设置多个编译器标志,需要将它们指定为单个字符串,而不是列表。如果标记变量的内容包含分号,CMake将不能正确处理分号变量,如果项目指定了分号,列表将转换成分号。

# Wrong, list used instead of a string
set(CMAKE_CXX_FLAGS -Wall -Werror)

# Correct, but see later sections for why appending would be preferred
set(CMAKE_CXX_FLAGS "-Wall -Werror")

# Appending to existing flags the correct way (two methods)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Werror")
string(APPEND CMAKE_CXX_FLAGS " -Wall -Werror")

区分缓存和非缓存变量

上面提到的所有变量都是缓存变量。可以定义同名的非缓存变量,它们将覆盖当前目录范围及其子目录(即add_subdirectory()创建的子目录)的缓存变量。但当项目试图强制更新缓存变量时,可能会出现问题。下面的代码往往会使项目更难以工作,并可能导致开发人员感觉在与项目战斗,当他们想要改变标志时,可以通过CMake GUI或类似方法:

# Case 1: Only has an effect if the variable isn't already in the cache
set(CMAKE_CXX_FLAGS "-Wall -Werror" CACHE STRING "C++ flags")

# Case 2: Using FORCE to always update the cache variable, but this overwrites
# any changes a developer might make to the cache
set(CMAKE_CXX_FLAGS "-Wall -Werror" CACHE STRING "C++ flags" FORCE)

# Case 3: FORCE + append = recipe for disaster (see discussion below)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Werror" CACHE STRING "C++ flags" FORCE)

上面的第一个例子是初学CMake的开发人员常见的疏忽。如果没有FORCE关键字,set()命令只更新尚未定义的缓存变量。CMake的首次运行可能因此出现做开发人员计划(如果放置在任何project()命令),但如果是改变指定其他的标示,因为变量将已经在缓存中,所以这种变化不会应用到现有的构建。发现这一点后,通常的反应是使用FORCE来确保缓存变量的更新,如第二种情况所示,这又会产生另一个问题。缓存是开发人员在本地更改变量而无需编辑项目文件的主要手段。如果项目以这种方式使用FORCE单方面的设置缓存变量,开发人员对该缓存变量所做的修改都会丢失。第三种情况的问题更大,因为每次运行CMake时,都会再次添加标志,导致标志集不断增长和重复。为编译器和链接器标记使用FORCE来更新缓存不是一个好主意。

正确的行为不是简单地删除FORCE关键字,而是设置一个非缓存变量。然后,将标志附加到当前值,因为缓存变量保持不变,因此每次CMake运行都从缓存变量的相同标志集开始,而不管CMake被调用的频率有多高。开发人员对缓存变量所做的任何更改也将保留。

# Preserves the cache variable contents, appends new flags safely
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Werror")

选择附加而不是替换

如上所述,开发人员有时会试图单方面在CMakeLists.txt文件中设置编译器标示,如下所示:

# Not ideal, discards any developer settings from cache
set(CMAKE_CXX_FLAGS "-Wall -Werror")

因为这个设置会丢弃已设置的缓存变量,开发人员很容易地使用自己的标示。像这样替换现有的标志,迫使开发人员深入到项目文件中,以找到在哪里以及如何修改修改相关标志的行。对于有许多子目录的复杂项目,是相当头痛的。可能的情况下,项目应该倾向于向现有值添加标志。

合理的例外原则可能是,如果项目需要执行一组特定的编译器和链接器标示。在这种情况下,一个可行的方法是尽早将变量值设置在顶层CMakeLists.txt文件,最好在顶端cmake_minimum_required()命令后。请记住,随着时间的推移,项目可能成为另一个项目的子项,此时它将不再是构建的最高级别,这种折衷的适用性可能会减少。

了解什么时候使用变量值

更模糊的方面是编译器和链接器标志变量构建的过程,会使用它们的值会。人们会期望以下代码的行为如注释描述一样:

# Save the original set of flags so we can restore them later
set(oldCxxFlags "${CMAKE_CXX_FLAGS}")

# This library has stringent build requirements, so enforce them just for it alone
# WARNING: This doesn't do what it may appear to do!
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Werror")
add_library(strictReq STATIC ...)

# Less strict requirements from here, so restore the original set of compiler flags
set(CMAKE_CXX_FLAGS "${oldCxxFlags}")
add_library(relaxedReq STATIC ...)

令人惊讶的是,按照上面的安排,strictReq库将不会使用-Wall -Werror标志来构建。直观地说,我们可能期望CMake使用的是add_library()时的变量值,但实际上是在处理结束时使用的该目录作用域的变量值。换句话说,重要的是该变量在该目录的CMakeLists.txt文件末尾持有的值。对于不知情的人来说,这可能会在各种情况下导致意想不到的结果。

开发人员会让这种行为欺骗的主要原因是,把编译器和链接器变量当作立即应用到任何创建的目标上。另一个相关的陷阱,是在创建目标并且所包含的文件修改了编译器或链接器变量之后使用include(),这会改变当前目录范围中已定义目标的编译器和链接器标志。由于编译器和链接器变量有这种延迟特性,使用它们可能会让项目变得很脆弱。理想情况下,项目只会在顶级CMakeLists.txt文件的修改它们(如果要修改的话),以减少误用的机会。

14.4. 推荐

本章涵盖CMake早期版本以来中经历的一些最重要的改进。读者可以在网上和其他地方找到大量的例子和教程,它们仍然推荐使用使用变量和目录属性命令的旧方法的模式和方法,但是应该理解target_…()命令应该是CMake 3.0+时代的首选方法。

项目应该使用targetlink_libraries()命令定义目标之间的所有依赖关系。这清楚地表达了目标之间的关系,并明确地向项目的所有开发人员说明了目标是如何关联的。target_link_libraries()命令应该优于link_libraries()命令或直接操作目标或目录属性。类似地,target…()命令提供了一种比变量、目录属性命令或直接操作属性更干净、更一致和更健壮的操作编译器和链接器标志的方法。以下的指南可能会有用:

  • 如果可能,最好使用target_…()命令来描述目标之间的关系,并修改编译器和链接器的行为。

  • 一般情况下,最好避免使用目录属性命令。虽然target…()命令在一些特定的环境中很方便,但是使用target…()命令将建立一个项目中所有开发人员都可以遵循的清晰模式。如果必须使用目录属性命令,请尽早在CMakeLists.txt文件中使用,以避免在前面几节中描述的一些不太直观的行为。

  • 避免直接操作会影响编译器和链接器行为的目标和目录属性,理解属性的作用,以及不同的命令如何操作它们,最好使用专门的目标和目录特定命令。不过,在调试意外的编译器或链接器命令行标志时,查询目标属性可能会很有用。

  • 最好避免修改各种CMAKE…标志变量和特定配置的对应项,可能开发人员只希望在本地环境下改变他们。如果需要在整个项目的基础上应用更改,可以考虑在项目的顶层使用一些策略目录属性命令,但要考虑是否真的应该单方面应用这些设置。一个例外是在工具链文件中定义了初始的默认值(参见第21章会对这个领域进行详细的讨论)。

开发人员应该熟悉PRIVATE、PUBLIC和INTERFACE关系的概念。他们是target_…()命令集的关键部分,并且他们在项目的安装和包装阶段也很重要。PRIVATE是指目标本身,INTERFACE是指与目标相链接的东西,而PUBLIC是前两者行为的结合。虽然将所有东西都标记为PUBLIC可能很诱人,但这可能会暴露出超出目标范围的依赖关系,构建时间可能会受到影响,PRIVATE依赖关系可能会强加到其他不应该知道它们的目标上。这反过来会对其他领域产生强烈的影响,比如符号可见性(在20.5节“符号可见性”中详细讨论)。因此,倾向于明确需要依赖其他目标时,将目标的PRIVATE改为PUBLIC。

INTERFACE关键字通常用于导入或接口库目标,或者偶尔用于向某些(项目中不允许开发人员更改的部分中定义的)目标添加缺少的依赖项。这样的例子包括为旧CMake版本编写的项目的子部分,因此不使用target_…()命令,或者导入外部目标库,这些库忽略了目标所需要的一些重要标志。对于targetlink_libraries()之外的所有`target…()命令,指定的目标可以在项目的任何地方定义,唯一的要求是目标已经在target_…()`命令调用之前创建。因此,可以从项目的任何部分将编译器接口依赖项附加到目标,但链接器接口依赖项只能在创建目标时在所在的目录范围内完成。CMake开发人员正在积极讨论这一限制,并可能在未来的版本中删除这个限制。

Last updated