第十五章:编译器和链接器基础

Posted by lili on

前一章讨论了构建类型及其与选择特定编译器和链接器行为的关系。本章讨论了控制编译器和链接器行为的基础知识。这里介绍的材料涵盖了一些每个 CMake 开发者都应该熟悉的最重要的主题和技术。

在继续之前,请注意随着 CMake 的发展,控制编译器和链接器行为的可用方法也得到了改进。焦点已经从更全局的构建视图转移到了可以控制每个单独目标要求的地方,以及这些要求如何影响依赖于它的其他目标。这是一种重要的思维转变,因为它影响了项目如何最有效地定义目标的构建方式。CMake的更成熟的功能可以用于在粗略层面上控制行为,但会失去定义目标之间关系的能力。相反,应该更优先选择最近引入的以目标为中心的功能,因为它们极大地提高了构建的稳健性,并且在控制编译器和链接器行为方面提供了更精确的控制。新的功能在行为和使用方式上也更一致。

15.1. 目标属性

在 CMake 的属性系统中,目标属性是控制编译器和链接器标志的主要机制。一些属性提供了指定任何任意标志的能力,而其他属性专注于特定功能,以便可以抽象掉平台或编译器的差异。本章重点介绍了一些更常用和通用的属性,后续章节将涵盖一些更具体的属性。

在继续之前,应该注意以下几点:在下面的部分中讨论的目标属性通常不会直接修改。CMake提供了专用命令,通常比直接属性操作更方便、更健壮。然而,了解涉及的基本属性可以帮助开发者理解某些命令的特性和限制。

15.1.1. 编译器标志

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

INCLUDE_DIRECTORIES:这是一个用作头文件搜索路径的目录列表,其中所有路径都必须是绝对路径。CMake会为每个路径添加一个带有适当前缀的编译器标志(通常是 -I 或 /I)。创建目标时,此目标属性的初始值取自同名目录属性。

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

COMPILE_OPTIONS:此属性提供任何既不是头文件搜索路径也不是符号定义的编译器标志。创建目标时,此目标属性的初始值取自同名目录属性。请注意,此属性也会受到去重的影响(有关详细信息,请参见第15.4节“去重选项”)。

旧的、现在已弃用的目标属性名 COMPILE_FLAGS 曾经具有与 COMPILE_OPTIONS 类似的作用。COMPILE_FLAGS 属性被视为一个直接包含在编译器命令行上的单个字符串。因此,它可能需要手动转义,而 COMPILE_OPTIONS 是一个列表,CMake会自动执行任何所需的转义或引用。

INCLUDE_DIRECTORIES 和 COMPILE_DEFINITIONS 属性实际上只是一些方便之处,负责为项目通常想要设置的最常见的事务处理编译器特定标志。然后,COMPILE_OPTIONS 属性中提供所有剩余的编译器特定标志。

上述三个目标属性有同名的 INTERFACE_… 属性,其工作方式与非 INTERFACE 版本相同,但不是应用于目标自身,而是应用于直接链接到它的目标。换句话说,它们指定了消费目标继承的编译器标志。因此,它们通常被称为使用要求,与非 INTERFACE 属性相对,后者有时被称为构建要求。本章后面的两个特殊库类型 IMPORTED 和 INTERFACE 将在第18章“目标类型”中进行讨论。这两个特殊库类型仅支持 INTERFACE_… 目标属性,而不支持非 INTERFACE_… 属性。第15.7.2节“系统头文件搜索路径”讨论了与 INTERFACE_INCLUDE_DIRECTORIES 如何使用的相关功能。

与非接口对应物不同,上述 INTERFACE_… 属性中的任何一个都不是从目录属性初始化的。相反,它们都是空的,因为只有项目知道头文件搜索路径、定义和编译器标志应该传播到消费目标。

除了 COMPILE_FLAGS 外,上述所有目标属性都支持生成器表达式。生成器表达式对于 COMPILE_OPTIONS 属性特别有用,因为它们使得仅在满足某些条件时添加特定标志成为可能,比如仅对特定的编译器或语言。如果需要在单个源文件级别操作编译器标志,则目标属性不够精细。对于这种情况,CMake提供了 COMPILE_DEFINITIONS、COMPILE_FLAGS 和 COMPILE_OPTIONS 源文件属性(COMPILE_OPTIONS 源文件属性仅在 CMake 3.11 中添加)。它们分别与它们同名的目标属性类似,只是它们仅适用于设置它们的单个源文件。请注意,它们对生成器表达式的支持落后于目标属性的支持,COMPILE_DEFINITIONS 源文件属性在 CMake 3.8 中获得了对生成器表达式的支持,而其他属性在 3.11 中获得了支持。此外,Xcode 项目文件格式根本不支持特定于配置的源文件属性,因此如果目标是苹果平台,就不应该在源文件属性中使用 \$<CONFIG> 或 \$<CONFIG:…>。

15.1.2. 链接器标志

与链接器标志相关联的目标属性与编译器标志类似,但一些是在更近期的 CMake 版本中添加的。特别是,CMake 3.13 添加了许多用于链接器控制的改进。还要注意,仅有一些与链接器相关的属性具有相关的接口属性,并且并非所有属性都支持生成器表达式。

LINK_LIBRARIES:此目标属性保存目标应直接链接到的所有库的列表。在创建目标时,它最初为空,并支持生成器表达式。支持相关的接口属性 INTERFACE_LINK_LIBRARIES。列出的每个库可以是以下之一:

  • 库的路径,通常指定为绝对路径。
  • 仅是库名称,不包含路径,通常也不包含任何平台特定的文件名前缀(例如 lib)或后缀(例如 .a、.so、.dll)。
  • CMake 库目标的名称。CMake 将在生成链接器命令时将其转换为构建库的路径,包括根据平台适当地提供文件名的任何前缀或后缀。因为 CMake 代表项目处理所有各种平台差异和路径,使用 CMake 目标名称通常是首选方法。CMake 将使用适当的链接器标志来链接 LINK_LIBRARIES 属性中列出的每个项目。在某些情况下,链接器标志也可能存在于此属性中,但通常更倾向于使用下面的其他目标属性保存这些选项。

LINK_OPTIONS:在 CMake 3.13 中添加了对此属性的支持。它保存要传递给链接器的标志列表,适用于正在构建为可执行文件、共享库或模块库的目标。对于正在构建为静态库的目标,它将被忽略。此属性用于通用的链接器标志,而不是指定其他要链接的库的标志。创建目标时,此目标属性的初始值取自同名目录属性。支持生成器表达式,该属性也受去重影响(有关详细信息,请参见第15.4节“去重选项”)。

还支持关联的接口属性 INTERFACE_LINK_OPTIONS。请注意,即使设置 INTERFACE_LINK_OPTIONS 的目标是静态库,此接口属性的内容也将应用于消费目标。这是因为接口属性指定了消费者应该使用的链接器标志,因此被消费的库的类型不是一个因素。

LINK_FLAGS:此属性与 LINK_OPTIONS 具有类似的作用,但存在一些区别。首要区别是它保存一个字符串,该字符串将直接放在链接器命令行上,而不是链接器标志的列表。另一个区别是它不支持生成器表达式。此外,没有关联的接口属性,且在创建目标时其值被初始化为空。总体而言,LINK_OPTIONS 更健壮并提供更广泛的功能,因此仅在必须支持早于 3.13 版本的 CMake 时使用 LINK_FLAGS。

STATIC_LIBRARY_OPTIONS:这是 LINK_OPTIONS 的对应物。它仅对正在构建为静态库的目标有意义,并将用于库管理员或归档工具。它保存一个选项列表,支持生成器表达式,且受去重影响(有关详细信息,请参见第15.4节“去重选项”)。与 LINK_OPTIONS 类似,STATIC_LIBRARY_OPTIONS 的支持也仅在 CMake 3.13 中添加,但请注意没有相关的接口属性。目标不能规定其消费者的库管理员/归档工具标志,只能规定链接器标志(请参阅上面关于 INTERFACE_LINK_OPTIONS 的注释)。

STATIC_LIBRARY_FLAGS:这是 LINK_FLAGS 的对应物,只有在必须支持早于 3.13 版本的 CMake 时才应使用。它是一个字符串而不是列表,并且不支持生成器表达式。没有相关的接口属性。

在一些较老的项目中,可能偶尔会遇到一个名为 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> 与正在构建的配置匹配时,这些标志将与非配置特定标志一起使用。只有在项目必须支持早于 3.13 版本的 CMake 时,才应使用这些标志。对于 3.13 或更高版本,建议使用 LINK_OPTIONS 和 STATIC_LIBRARY_OPTIONS,并使用生成器表达式表示特定于配置的内容。 向链接器传递标志的一个困难之处在于,通常通过编译器前端调用链接器,但每个编译器都有自己的语法来传递链接器选项。例如,通过 gcc 调用 ld 链接器需要使用形式 -Wl,…,而 clang 期望形式 -Xlinker …. 在 CMake 3.13 或更高版本中,可以通过在 LINK_OPTIONS 和 INTERFACE_LINK_OPTIONS 属性中的每个链接器标志前添加 LINKER: 前缀来自动处理此差异。这将导致链接器标志被转换为正在使用的编译器前端所需的形式。例如:

set_target_properties(Foo PROPERTIES LINK_OPTIONS LINKER:-stats)

使用 gcc 编译器,这将添加 -Wl,-stats,而使用 clang 时将添加 -Xlinker -stats。有关 LINKER: 前缀的更多相关讨论,请参见第15.4节“去重选项”。

15.1.3. 源文件

与编译器和链接器标志类似,与目标相关的源文件遵循类似的模式。一个 SOURCES 属性列出了将用于编译的目标的所有源文件。这不仅包括像 .cpp 或 .c 文件之类的文件,还可能包括头文件、资源和其他不需要进行编译的文件。如果列出了不会被编译的文件,可能会显得没啥用,但在某些情况下是有用的。如果构建生成了任何文件,将它们列为源文件将使它们成为目标的依赖项,并确保在构建目标时生成这些文件。这对于生成的头文件特别有用(参见第19.3节“生成文件的命令”)。将非编译文件列为源文件还是一种常见的技术,使它们出现在某些IDE工具的文件列表中。

目标的 INTERFACE_SOURCES 属性列出要添加到该目标的消费者的源文件。在实践中,任何此属性中的文件都是可编译源文件的情况非常不寻常。更典型的用例是列出接口库的头文件(参见第18.2.4节“接口库”)。另一个潜在的用例可能是添加需要成为同一翻译单元一部分的资源,但这样的情况可能不太常见。

SOURCES 和 INTERFACE_SOURCES 都支持生成器表达式。它们的常见用例包括指定仅对某些配置、平台或编译器进行编译的源文件。另一个常见的例子是使用 \$<TARGET_OBJECTS:targetName> 生成器表达式。在 CMake 对对象库的支持成熟之前(参见第18.2.2节“对象库”),无法直接链接到对象库。相反,项目必须使用 \$<TARGET_OBJECTS:objectLib> 将该对象库的对象直接添加到消费目标的 SOURCES 属性中。使用 CMake 3.14 或更高版本,这不再是必要的,可以通过直接链接到对象库来稳健地处理。

还请注意,关于实现细节导致源文件属性使用时可能引发性能问题的警告,这些警告在第9.5节“源属性”中有讨论。

15.2. 目标属性命令

正如前文提到的,本章讨论的目标属性通常不会直接操纵。CMake 提供了专用命令以更方便和稳健的方式修改它们。这些命令还鼓励清晰地指定目标之间的依赖关系和传递行为。

15.2.1. 链接库

在第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 本身的行为。这些项将添加到 LINK_LIBRARIES 目标属性。

INTERFACE: 这是 PRIVATE 的补充,后面跟着 INTERFACE 关键字的项将添加到目标的 INTERFACE_LINK_LIBRARIES 属性。链接到 targetName 的任何目标都将这些项应用于它们,就像这些项在它们自己的 LINK_LIBRARIES 属性中列出一样。

PUBLIC: 相当于结合了 PRIVATE 和 INTERFACE 的效果。

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

CMake 3.12 及更早版本禁止 target_link_libraries() 在不同目录中定义的目标上操作。如果子目录需要使目标链接到某些内容,它不能在该子目录内执行此操作。必须在调用 add_executable() 或 add_library() 的同一目录中进行 target_link_libraries() 的调用。第34.5.1节“在不同目录中构建目标”更详细地讨论了这个限制。CMake 3.13 解除了这个限制。

15.2.2. 链接器选项

CMake 3.13 添加了一个专用命令来指定链接器选项。与在 target_link_libraries() 中指定链接器选项不同,它允许项目更清晰、更准确地表达正在添加链接器选项。它还避免了将 LINK_LIBRARIES 属性填充到链接器标志中,而是填充了专门用于这些标志的相关目标属性。

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

target_link_options() 命令使用 PRIVATE 项填充 LINK_OPTIONS 目标属性,使用 INTERFACE 项填充 INTERFACE_LINK_OPTIONS 目标属性。正如预期的那样,PUBLIC 项会添加到两个目标属性中。由于这些属性支持生成器表达式,因此 target_link_options() 命令也支持。

通常情况下,每次调用 target_link_options(),指定的项都会追加到相关目标属性中。这使得以一种自然、渐进的方式添加多个选项变得容易。如果需要,可以使用 BEFORE 关键字将列出的选项预先添加到目标属性的现有内容中。

即使 targetName 是一个静态库,也可以使用此命令,但请注意,PRIVATE 和 PUBLIC 项将填充 LINK_OPTIONS,而不是 STATIC_LIBRARY_OPTIONS。要填充 STATIC_LIBRARY_OPTIONS,唯一的选择是直接使用 set_property() 或 set_target_properties() 修改目标属性。

对于静态库目标使用 target_link_options() 添加 INTERFACE 项仍然很有用,因为 INTERFACE_LINK_OPTIONS 的内容将应用于消费目标。

由于 target_link_options() 向 LINK_OPTIONS 和 INTERFACE_LINK_OPTIONS 属性添加项,该命令还支持使用 LINKER: 前缀处理编译器前端的差异。因此,第15.1.2节“链接器标志”的示例可以更好地实现为:

target_link_options(Foo PRIVATE LINKER:-stats)

15.2.3. 头文件搜索路径

有多个命令可用于管理目标的与编译器相关的属性。将目录添加到编译器的头文件搜索路径是最常见的需求之一。

target_include_directories(targetName [AFTER|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 关键字之后的目录将添加到 INTERFACE 和非 INTERFACE 属性中。SYSTEM 关键字影响这些搜索路径的使用,详见第15.7.2节“系统头文件搜索路径”。

BEFORE 关键字对应于 target_link_options() 中的 BEFORE 关键字。它导致指定的目录被预先添加到相关属性中,而不是追加。CMake 3.20 添加了对 AFTER 关键字的支持以实现对称性,但它无需使用,因为追加是默认行为。

target_include_directories() 命令相对于直接操纵目标属性具有另一个优势。项目可以指定相对目录,而不仅仅是绝对目录。除下文讨论的一个例外情况外,相对路径将自动转换为绝对路径,视其为相对于当前源目录。

由于 target_include_directories() 命令基本上只是填充相关目标属性,所以这些属性的所有常见特性都适用。这意味着可以使用生成器表达式,这在安装目标和创建软件包时变得更加重要。\$<BUILD_INTERFACE:…> 和 \$<INSTALL_INTERFACE:…> 生成器表达式允许为构建和安装指定不同的路径。对于已安装的目标,通常使用相对路径。它们将被解释为相对于基本安装位置而不是源目录。第27.2.1节“接口属性”更详细地讨论了在指定头文件搜索路径时的这一方面。对于构建目标,\$<BUILD_INTERFACE:…> 生成器表达式的展开发生在相对路径检查之后,因此这些表达式必须求值为绝对路径,否则 CMake 将发出错误。

15.2.4. 编译器定义

为目标设置编译器定义也有其专用命令,遵循通常的形式:

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 项填充两个目标属性。虽然支持生成器表达式,但通常不需要处理构建和安装情况的不同。

15.2.5. 编译器选项

除了定义之外,应该使用以下专用命令添加其他编译器选项:

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

按照通常的模式,PRIVATE 项填充 COMPILE_OPTIONS 目标属性,INTERFACE 项填充 INTERFACE_COMPILE_OPTIONS 目标属性,而 PUBLIC 项填充两个目标属性。对于所有情况,每个项都会附加到现有的目标属性值,但 BEFORE 关键字可以用于预先添加。在所有情况下都支持生成器表达式,通常不需要处理构建和安装情况的不同。

15.2.6. 源文件

向目标添加源文件的最直接方法是在 add_executable() 或 add_library() 调用中列出它们。这将这些文件添加到目标的 SOURCES 属性。使用 CMake 3.1 或更高版本,可以使用 target_sources() 命令在定义目标后向目标添加源文件。此命令与其他 target_…() 命令的使用方式相同,并支持相同的熟悉形式(第27.5.1节“文件集”还讨论了一种不同的形式):

target_sources(targetName
    <PRIVATE|PUBLIC|INTERFACE> file1 [file2 ...]
    [<PRIVATE|PUBLIC|INTERFACE> file3 [file4 ...]]
    ...
)

PRIVATE 源文件将添加到 SOURCES 属性中。INTERFACE 源文件将添加到 INTERFACE_SOURCES 属性中。PUBLIC 源文件将添加到两个属性中。更实用的思考方式是,PRIVATE 源文件将被编译到 targetName 中,INTERFACE 源文件将添加到链接到 targetName 的任何内容中,而 PUBLIC 源文件将添加到两者中。实际上,除了 PRIVATE 之外的任何选项都将是不寻常的。第18.2.4节“接口库”讨论了在支持 CMake 3.18 或更早版本时,使用 INTERFACE 列出头文件的情况。

add_executable(MyApp main.cpp)
if(WIN32)
    target_sources(MyApp PRIVATE eventloop_win.cpp)
else()
    target_sources(MyApp PRIVATE eventloop_generic.cpp)

在 CMake 3.13 之前的 target_sources() 命令的一个奇特之处是,如果使用相对路径指定源文件,则该路径被视为相对于将其添加到的目标的源目录。这造成了一些问题。

第一个问题是,如果使用 INTERFACE 添加了相对源文件,该路径将被视为相对于消费目标,而不是调用 target_sources() 的目标。显然,这可能会创建不正确的路径,因此非 PRIVATE 源文件需要使用绝对路径指定。

第二个问题是,当从不同于定义目标的目录中调用 target_sources() 时,相对路径的行为不直观。考虑一个修改前述示例的例子,其中平台特定代码分离到不同的目录中:

CMakeLists.txt

add_executable(MyApp main.cpp)
if(WIN32)
    add_subdirectory(windows)
else()
    add_subdirectory(generic)
endif()

windows/CMakeLists.txt:

# 警告:CMake 3.12 或更早版本的错误文件路径
target_sources(MyApp PRIVATE eventloop_win.cpp)

generic/CMakeLists.txt:

# 警告:CMake 3.12 或更早版本的错误文件路径
target_sources(MyApp PRIVATE eventloop_generic.cpp)

在上面的示例中,调用 target_sources() 的意图是从 windows 或 generic 子目录中添加源文件。但是在 CMake 3.12 或更早版本中,它们将被解释为相对于定义 MyApp 目标的顶层目录。

解决这两个问题的一个强大方法是使用\${CMAKE_CURRENT_SOURCE_DIR}或\${CMAKE_CURRENT_LIST_DIR}前缀,以确保它们始终使用正确的路径:

windows/CMakeLists.txt

target_sources(MyApp PRIVATE ${CMAKE_CURRENT_LIST_DIR}/eventloop_win.cpp)

在每个源文件前缀都使用\${CMAKE_CURRENT_SOURCE_DIR}或\${CMAKE_CURRENT_LIST_DIR}有些不便,也不太直观。为此,CMake 3.13 更改了行为,将相对路径视为相对于调用 target_sources() 的地方的 CMAKE_CURRENT_SOURCE_DIR,而不是目标定义时的源目录。策略 CMP0076 为那些依赖于旧行为的项目提供了向后兼容性。如果可能的话,项目应将其最低 CMake 版本设置为 3.13 或更高,并使用 CMP0076 策略的新行为。

CMake 3.20 添加了使用 target_sources() 向自定义目标添加源文件的功能(在第19章“自定义任务”中详细讨论)。在较早的 CMake 版本中,只能在 add_custom_target() 调用中向自定义目标添加源文件。

15.3. 目录属性和命令

从 CMake 3.0 开始,由于其能够定义它们与相互链接的目标之间的交互方式,强烈推荐使用目标属性来指定编译器和链接器标志。在较早版本的 CMake 中,目标属性不太突出,通常在目录级别而不是目标级别指定属性。这些目录属性和通常用于操作它们的命令在一致性方面不如它们的基于目标的等效项,这是另一个通常应该尽可能避免使用它们的原因。尽管如此,由于许多在线教程和示例仍在使用它们,开发人员至少应该了解目录级别的属性和命令。

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

简单来说,include_directories() 命令将头文件搜索路径添加到当前目录范围及其以下创建的目标中。默认情况下,路径将附加到现有目录列表,但可以通过将 CMAKE_INCLUDE_DIRECTORIES_BEFORE 变量设置为 true 来更改该默认值。还可以通过 BEFORE 和 AFTER 选项在每次调用时显式指定路径的处理方式。项目应该谨慎设置 CMAKE_INCLUDE_DIRECTORIES_BEFORE,因为大多数开发人员可能会认为默认行为是附加目录。

SYSTEM 关键字的效果在第15.7.2节“系统头文件搜索路径”中讨论。提供给 include_directories() 的路径可以是相对或绝对的。相对路径会自动转换为绝对路径,并被视为相对于当前源目录。路径还可以包含生成器表达式。

include_directories() 实际上的操作比上面的简单解释要复杂。主要地,调用 include_directories() 有两个主要影响:

  • 列出的路径被添加到当前 CMakeLists.txt 文件的 INCLUDE_DIRECTORIES 目录属性中。这意味着在当前目录和其以下创建的所有目标将这些目录添加到它们的 INCLUDE_DIRECTORIES 目标属性中。
  • 在当前 CMakeLists.txt 文件中创建的任何目标(更准确地说是当前目录范围)也将这些路径添加到它们的 INCLUDE_DIRECTORIES 目标属性中,即使这些目标在调用 include_directories() 之前已经创建。这仅严格适用于在当前 CMakeLists.txt 文件或通过 include() 引入的其他文件中创建的目标,而不适用于在父目录或子目录范围中创建的任何目标。

以上的第二点往往会让许多开发人员感到惊讶。为避免可能导致混淆的情况,如果必须使用 include_directories() 命令,请最好在 CMakeLists.txt 文件中的任何目标被创建或使用 include() 或 add_subdirectory() 引入之前调用它。

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

add_definitions() 和 remove_definitions() 命令向 COMPILE_DEFINITIONS 目录属性中添加和移除条目。每个条目应以 -D 或 /D 开头,这是绝大多数编译器使用的两种最常见的标志格式。CMake 在将定义存储到 COMPILE_DEFINITIONS 目录属性之前会剥离掉此标志前缀,因此不管项目在哪种编译器或平台上构建,都无关紧要。与 include_directories() 一样,这两个命令影响当前 CMakeLists.txt 文件中创建的所有目标,即使是在调用 add_definitions() 或 remove_definitions() 之前创建的。在子目录范围中创建的目标只有在调用之后创建才会受到影响。这是由 CMake 如何使用 COMPILE_DEFINITIONS 目录属性的直接后果。

尽管不建议,但也可以使用这些命令指定除定义以外的编译器标志。如果 CMake 无法识别特定的项看起来像编译器定义,那么该项将被不加修改地添加到 COMPILE_OPTIONS 目录属性中。由于历史原因存在这种行为,但新项目应避免使用此行为(请参阅稍后的 add_compile_options() 命令以获取替代方法)。由于底层目录属性支持生成器表达式,这两个命令也支持,但有一些注意事项。生成器表达式应仅用于定义的值部分,而不是名称部分(即在 =-DVAR=VALUE 项中仅在 = 后或在 =-DVAR 项中根本不使用)。这与 CMake 解析每个项以检查其是否是编译器定义的方式有关。还要注意,这些命令只修改目录属性,不影响 COMPILE_DEFINITIONS 目标属性。

add_definitions() 命令有一些缺点。必须在每个项目前面使用 -D 或 /D 为其添加前缀,这与其他 CMake 行为不一致。省略前缀会使命令将该项视为通用选项,这也与命令的名称相矛盾。此外,对于生成器表达式仅支持键=值定义的值部分的限制也是前缀要求的直接结果。为了解决这个问题,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_directories()、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( [ BEFORE | AFTER ] dir1 [dir2 ...])

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

宽泛地说,通过在关键字 debug 前面加上它,可以使项仅适用于 Debug 构建类型,或者通过在关键字 optimized 前面加上它,可以使项适用于除 Debug 之外的所有构建类型。可以在项前面加上关键字 general,表示它适用于所有构建类型,但由于 general 已经是默认值,因此这样做的好处很少。这三个关键字只影响它们后面的单个项,而不是到下一个关键字的所有项。强烈不建议使用这些关键字,因为生成器表达式提供了更好的控制项何时被添加的方式。为了考虑自定义构建类型,如果 DEBUG_CONFIGURATIONS 全局属性中列出了一个构建类型,则将其视为 Debug 配置。

由 link_directories() 添加的目录仅在 CMake 被给定一个裸库名称进行链接时才会产生效果。CMake 将提供的路径添加到链接器命令行,并让链接器自行查找这样的库。给定的目录应该是绝对路径,尽管在 CMake 3.13 之前允许使用相对路径(请参阅 CMP0081 策略,该策略控制是否在遇到相对路径时 CMake 会中止并显示错误)。BEFORE 和 AFTER 关键字是在 CMake 3.13 中引入的,并且与它们对 include_directories() 的影响相似,包括默认行为等效于在没有这两个关键字的情况下使用 AFTER。

出于健壮性原因,在使用 link_libraries() 时,请提供完整路径或 CMake 目标的名称。对于这两种情况,都不需要链接器搜索目录,而且库的确切位置将被提供给链接器。此外,一旦 link_directories() 添加了链接器搜索目录,项目将没有方便的方法来删除该搜索路径(如果需要的话)。通常应避免添加链接器搜索目录,并且通常情况下是不必要的。

CMake 3.13 还引入了 add_link_options() 命令。它类似于 target_link_options() 命令,只是作用于目录属性而不是目标属性。

add_link_options(item1 [item2...])

此命令将项附加到 LINK_OPTIONS 目录属性,用于初始化当前目录范围及其以下随后创建的所有目标的同名目标属性。与其他目录级别命令一样,通常应避免使用 add_link_options(),而改用目标级别命令。

15.4. 选项去重

当 CMake 构建最终的编译器和链接器命令行时,它会对标志进行去重操作。这可以极大地减少命令行长度,对于实现和开发人员尝试理解使用的最终选项集都有好处。然而,在某些情况下,去重可能是不希望的。例如,一个选项可能需要以不同的第二参数重复,比如使用 Clang 传递多个链接器选项:

# 这不会按预期工作
target_link_options(SomeTarget PRIVATE
    -Xlinker -z
    -Xlinker defs
)

经过去重后,第二个 -Xlinker 将被移除,导致错误的命令行选项 -Xlinker -z defs。对于编译器也存在类似的情况:

# 这也不会按预期工作
target_compile_options(SomeTarget PRIVATE
    -Xassembler --keep
    -Xassembler --no_esc
)

CMake 提供了 “SHELL:” 前缀作为防止选项组被去重拆分的方法。它自 CMake 3.12 版本开始支持用于编译器选项,对于链接器选项则是自 CMake 3.13 版本开始支持。要强制两个或多个选项被视为一个不应该被拆分的组,它们应该以 SHELL: 为前缀,并以由空格分隔的单引号字符串形式给出。

target_link_options(SomeTarget PRIVATE
    "SHELL:-Xlinker -z"
    "SHELL:-Xlinker defs"
)

target_compile_options(SomeTarget PRIVATE
    "SHELL:-Xassembler --keep"
    "SHELL:-Xassembler --no_esc"
)

对于链接器选项,LINKER: 前缀在去重后扩展。它也可以与 SHELL: 结合使用。以下两者是等效的:

target_link_options(SomeTarget PRIVATE "LINKER:-z,defs")
target_link_options(SomeTarget PRIVATE "LINKER:SHELL:-z defs")

在使用 Clang 时,”LINKER:-z,defs” 和 “LINKER:SHELL:-z defs” 都会扩展为 -Xlinker -z -Xlinker defs。-Xlinker 部分不会被去重。

SHELL:、LINKER: 和 LINKER:SHELL: 前缀在目标属性级别处理。这意味着它们可以与任何操纵目标属性的命令一起使用。LINK_OPTIONS 和 INTERFACE_LINK_OPTIONS 支持所有前缀。COMPILE_OPTIONS、INTERFACE_COMPILE_OPTIONS 和 STATIC_LIBRARY_OPTIONS 仅支持 SHELL:。由于所有这些目标属性都是从同名的目录属性初始化的,因此那些目录属性也可以使用这些前缀。

15.5. 编译器和链接器变量

属性是项目应该寻求影响编译器和链接器标志的主要方式。最终用户无法直接操纵属性,因此项目完全控制如何设置这些属性。然而,存在用户希望添加自己的编译器或链接器标志的情况。他们可能希望添加更多的警告选项,打开特殊的编译器功能,如检查器或调试开关等。对于这些情况,变量更为合适。

CMake提供了一组变量,用于指定将与各种目录、目标和源文件属性提供的编译器和链接器标志合并的变量。它们通常是缓存变量,以便用户可以轻松查看和修改它们,但它们也可以作为项目的CMakeLists.txt文件中的常规CMake变量设置(项目应该尽量避免这样做)。

CMake在构建目录中首次运行时为这些变量提供了合适的默认值。直接影响编译器标志的主要变量具有以下形式:

  • CMAKE_<LANG>_FLAGS
  • CMAKE_<LANG>FLAGS_<CONFIG>

这里,<LANG>对应于正在编译的语言,典型的值包括C、CXX、Fortran、Swift等。<CONFIG>部分是一个大写字符串,对应于构建类型之一,例如DEBUG、RELEASE、RELWITHDEBINFO或MINSIZEREL。第一个变量将应用于所有构建类型,包括带有空CMAKE_BUILD_TYPE的单配置生成器。第二个变量仅应用于由指定的构建类型。因此,使用Debug配置构建的C++文件将同时具有来自CMAKE_CXX_FLAGS和CMAKE_CXX_FLAGS_DEBUG的编译器标志。

遇到的第一个project()命令将为这些变量创建缓存变量(这有点简化,更完整的解释在第23章,“工具链和交叉编译”中给出)。因此,在第一次运行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> 仅在对应的 CONFIG 中添加。在某些平台上,一些或所有链接器标志为空字符串是不寻常的。

CMake 教程和示例代码经常使用上述变量来控制编译器和链接器标志。这在 CMake 3.0 以前是相当普遍的做法,但随着焦点转向以 CMake 3.0 及更高版本为中心的目标模型,这些示例不再是一个好的模型。它们经常导致一些非常常见的错误,以下是其中一些比较普遍的错误。

15.5.1. 编译器和链接器变量是单个字符串,而非列表

如果需要设置多个编译器标志,它们需要被指定为单个字符串,而非列表。如果其内容包含分号,CMake 将无法正确处理标志变量,而分号则是指定项目时列表将被转换为的符号。

# 错误,使用列表而非字符串
set(CMAKE_CXX_FLAGS -Wall -Wextra)
# 正确,但查看后续章节以了解为何最好使用追加的方法
set(CMAKE_CXX_FLAGS "-Wall -Wextra")
# 以正确方式追加到现有标志(两种方法)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra")
string(APPEND CMAKE_CXX_FLAGS " -Wall -Wextra")

15.5.2. 区分缓存和非缓存变量

上述所有变量都是缓存变量。可以定义同名的非缓存变量,它们将覆盖当前目录范围及其子目录(即通过 add_subdirectory() 创建的目录)的缓存变量。然而,当项目试图强制更新缓存变量而非局部变量时,问题可能会出现。以下代码经常使项目难以处理,并可能导致开发人员感觉在通过 CMake GUI 应用程序或类似方式更改其自己构建的标志时,他们正在与项目进行斗争:

# 情况1:仅在变量尚未在缓存中定义时起作用
set(CMAKE_CXX_FLAGS "-Wall -Wextra" CACHE STRING "C++ flags")
# 情况2:使用 FORCE 始终更新缓存变量,但这会覆盖
# 开发人员可能对缓存进行的任何更改
set(CMAKE_CXX_FLAGS "-Wall -Wextra" CACHE STRING "C++ flags" FORCE)
# 情况3:FORCE + 追加 = 灾难的食谱(请参阅下文的讨论)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra" CACHE STRING "C++ flags" FORCE)

上述情况中的第一个突显了对 CMake 新手经常犯的一个常见错误。没有 FORCE 关键字,set() 命令仅在变量尚未定义时更新缓存变量。因此,首次运行 CMake 可能会看起来符合开发人员的意图(如果放置在任何 project() 命令之前),但如果该行被更改以指定其他标志,那么在此时点,对现有构建不会应用该更改,因为变量在缓存中已经存在。

发现这一点的常见反应是使用 FORCE 确保缓存变量始终被更新,如第二种情况所示,但这又带来了另一个问题。缓存是开发人员在无需编辑项目文件的情况下本地更改变量的主要手段。如果项目使用 FORCE 以这种方式单方面设置缓存变量,开发人员对该缓存变量的任何更改都将丢失。

第三种情况更加问题复杂,因为每次运行 CMake 时都会再次追加标志,导致标志集不断增长和重复。使用 FORCE 在这样的情况下更新缓存,对于编译器和链接器标志来说通常都不是一个好主意。

与其简单地删除 FORCE 关键字,正确的做法是设置非缓存变量而非缓存变量。然后,可以安全地将标志追加到当前值,因为缓存变量保持不变。每次 CMake 运行都将始终从缓存变量开始,而不管 CMake 被调用的频率如何。开发人员选择对缓存变量进行的任何更改也将被保留。

# 保留缓存变量内容,安全地追加新标志
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra")

15.5.3. 在追加标志上而非替换标志

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

# 非理想,舍弃了缓存中的任何开发人员设置
set(CMAKE_CXX_FLAGS "-Wall -Wextra")

因为这会舍弃缓存变量设置的任何值,开发人员失去了轻松注入自己标志的能力。这迫使开发人员在整个项目中寻找并修改有问题的行。对于具有许多子目录的复杂项目,这可能很繁琐。在可能的情况下,项目应优先选择将标志追加到现有值。

对此指南的一个合理例外情况可能是,如果项目需要强制执行一组规定的编译器或链接器标志。在这种情况下,可行的妥协方案可能是尽早在顶层 CMakeLists.txt 文件中设置变量值,理想情况下在 cmake_minimum_required() 命令之后的最顶层(或者更好的是,在使用工具链文件的情况下 - 有关详细信息,请参见第 23 章,“工具链和交叉编译”)。但请注意,随着时间的推移,项目本身可能会成为另一个项目的子项目,在那种情况下,它将不再是构建的顶层,此妥协的适用性可能会降低。

15.5.4. 了解变量值何时被使用

编译器和链接器标志变量的一个较为隐蔽的方面是它们的值实际上在构建过程中的哪个时刻被使用。人们可能合理地期望以下代码的行为与内联注释中所述相符:

# 保存原始的标志集以便稍后恢复
set(oldCxxFlags "${CMAKE_CXX_FLAGS}")
# 该库有严格的构建要求,因此
# 仅对它本身进行强制执行。
# 警告:这不是它可能看起来所做的事情!
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra")
add_library(StrictReq STATIC ...)
# 从这里开始要求不那么严格,因此恢复
# 原始的编译器标志集
set(CMAKE_CXX_FLAGS "${oldCxxFlags}")
add_library(RelaxedReq STATIC ...)

令人惊讶的是,上述排列中,StrictReq 库将不会使用 -Wall -Wextra 标志进行构建。直观地,人们可能期望在调用 add_library() 时的变量值是 CMake 使用的,但事实上,使用的是该目录范围的 CMakeLists.txt 文件处理结束时的变量值。换句话说,重要的是该变量在该目录的 CMakeLists.txt 文件结束时的值。对于不知情的开发人员来说,这可能导致各种情况下的意外结果。

开发人员通常会被这种行为弄糊涂,误以为编译器和链接器变量立即应用于创建的任何目标。另一个相关的陷阱是在创建目标之后使用 include(),而包含的文件(或文件)修改编译器或链接器变量。这也将更改当前目录范围内已定义目标的编译器和链接器标志。这种延迟性质使得这些编译器和链接器变量在使用时变得脆弱。理想情况下,项目应仅在顶层 CMakeLists.txt 文件的早期修改它们,以最小化误用和开发人员的意外。

15.6. 特定语言的编译器标志

在设置应仅对特定语言应用的编译器标志时,需要注意一些限制。可以使用生成器表达式为特定目标设置特定语言的编译器标志,如下例所示(为简化起见,假设编译器支持 -fno-exceptions 选项):

target_compile_options(Foo PRIVATE
    $<$<COMPILE_LANGUAGE:CXX>:-fno-exceptions>
)

不幸的是,对于 Visual Studio 或 Xcode,这不会按预期工作。这些生成器的实现不支持在目标级别为不同语言设置不同的标志。相反,它们在评估生成器表达式时假定目标语言为 C++(如果目标有任何 C++ 源文件)或者为 C(如果没有 C++ 源文件)。这不仅适用于编译选项,还适用于编译定义和包含目录。这种限制是为了避免严重降低构建性能而需要的一种妥协。如果项目愿意接受较慢的构建速度,可以使用源文件属性来应用编译器标志。例如:

add_executable(Foo src1.c src2.cpp)
set_property(SOURCE src2.cpp APPEND PROPERTY
    COMPILE_OPTIONS -fno-exceptions
)

源文件属性也有其自身的限制,如在第 9.5 节“源属性”中讨论的。特别要注意的是,Xcode 生成器有一些限制,阻止其支持特定配置的源文件属性,因此需要避免使用像 $<CONFIG> 这样的生成器表达式。

更好的解决这些限制的方法是将不同的语言拆分到它们自己的独立目标中,而不是将它们合并到同一目标中。然后可以将编译器标志应用于整个目标,这对于所有 CMake 生成器都有效,而且不会降低构建性能。

add_library(Foo_c src1.c)
add_executable(Foo src2.cpp)
target_link_libraries(Foo PRIVATE Foo_c)
target_compile_options(Foo PRIVATE -fno-exceptions)

一种不太理想的解决方法是使用正确处理的 CMAKE_<LANG>_FLAGS 变量,但它们将不加选择地应用于目录范围内的所有目标,最好由开发人员保持不变。

15.7. 编译器选项抽象

CMake为各种不同的编译器功能提供了抽象。其中一些是为多个编译器实现的功能,而另一些则是为了使特定工具链的某项功能更易于使用。本节涵盖了一些更常见的通用目的的抽象。其他章节涵盖了与特定主题相关的其他情况。

在CMake为特定功能提供抽象的情况下,项目不应将使用该抽象与明确传递该功能的编译器标志混合使用。项目应该完全采用该抽象,或完全避免使用它。混合使用某个功能的显式编译器标志以及使用该功能的抽象可能导致意外结果,甚至导致构建失败。需要特别注意作为构建一部分的依赖项(请参阅第30章,FetchContent),因为它们需要使用或不使用与构建的其余部分相同的抽象。

15.7.1. 将警告视为错误

对于预计会无警告地构建的项目,将任何警告视为错误可能是一种可取的做法。这在持续集成构建中很常见,新引入的警告应该导致构建失败。这种机制鼓励开发人员在将其更改合并到主分支之前解决警告。

对于在本地构建的开发人员来说,是否应将警告视为错误并不那么明确。开发人员可能正在测试更新的编译器或不同的工具链,这意味着与持续集成构建相比可能会生成新的警告。如果项目硬编码了将警告视为错误,开发人员将不得不修改项目以阻止这种行为。如果导致警告的代码来自依赖项并且这些依赖项正在强制将警告视为错误,开发人员可能难以进行更改(请参阅第15.7.2节,“系统头文件搜索路径”以了解处理这类情况的常见方法)。

CMake 3.24添加了对处理是否将警告视为错误的支持。可以将 COMPILE_WARNING_AS_ERROR 目标属性设置为 true,以将该目标的所有编译器警告视为错误。CMake将为正在使用的编译器添加适当的标志,这使项目无需弄清要添加什么标志。并非所有编译器都支持此功能,但它适用于所有主流编译器。有关支持的编译器列表,请参阅 COMPILE_WARNING_AS_ERROR 属性文档。

该属性在创建目标时从 CMAKE_COMPILE_WARNING_AS_ERROR 变量初始化。一般来说,项目不应该直接设置目标属性,最好也不要设置变量。相反,是否将所有警告视为错误的决定应由开发人员或驱动构建的脚本做出。开发人员或脚本可以将 CMAKE_COMPILE_WARNING_AS_ERROR 设置为缓存变量,而无需修改项目:

cmake -DCMAKE_COMPILE_WARNING_AS_ERROR=YES ...

如果正在使用CMake预设(请参阅第33章,预设),则将其设置为此变量的理想位置。特别是在预设用于持续集成构建的情况下,这更为适用。

在某些情况下,项目可能会选择设置 CMAKE_COMPILE_WARNING_AS_ERROR 变量,以满足诸如认证要求或公司政策之类的需求。或者,某个依赖项可能会硬编码启用警告作为错误,尽管上述建议,可能是通过设置单个目标属性或 CMAKE_COMPILE_WARNING_AS_ERROR 变量来实现。在出现这种情况时,开发人员仍然可以通过在 cmake 命令行上使用 –compile-no-warning-as-error 选项来关闭将警告视为错误。此命令行选项会强制CMake在整个构建过程中忽略 COMPILE_WARNING_AS_ERROR 目标属性。

cmake --compile-no-warning-as-error ...

如果项目手动添加编译器标志以将警告转换为错误,CMake 将不会尝试删除这些标志。除非CMake未为正在使用的编译器实现警告作为错误的功能,否则应从项目中删除这些硬编码的标志。

15.7.2. 系统头文件搜索路径

大多数主流编译器支持指定系统头文件搜索路径。在历史上,这些路径位于系统位置(例如Unix系统上的/usr/include)或者是工具链的一部分,而不是由项目提供的。最近,这个术语有时也用于与项目的依赖关系相关联的头文件搜索路径。

系统头文件搜索路径基本上与常规头文件搜索路径类似,但可能会存在一些依赖于编译器的行为差异。例如:

  • 即使系统路径出现在命令行的前面,编译器可能会在搜索任何系统路径之前搜索所有非系统搜索路径。
  • 当在系统头文件位置找到头文件时,编译器通常会跳过来自该头文件的任何警告。一些编译器会自动执行此操作,而其他编译器则提供单独的警告控制标志,允许您实现相同的效果(Visual Studio工具链是后者的一个示例)。
  • 当编译器计算正在编译的文件的依赖关系时,它可能会省略系统位置的头文件的依赖关系。

CMake对系统头文件搜索路径的抽象有助于使行为在工具链和CMake生成器之间更加一致。以下描述了引入关键CMake版本时引入的相关行为:

CMake 3.12

系统搜索路径总是放置在编译器命令行的非系统路径之后。这意味着头文件搜索顺序将在所有工具链上保持一致。

CMake 3.22

使用Visual Studio工具链(VS 16.10或更高版本)时,使用Ninja或Makefiles生成器之一时,默认情况下会在默认编译器标志中包含/external:W0。这会关闭来自系统头文件的警告。与默认情况下始终执行此操作的gcc和clang相比。

CMake 3.24

在使用Visual Studio生成器与Visual Studio工具链(VS 16.11或更高版本)时,支持系统头文件。在使用Visual Studio生成器时,较早的CMake或Visual Studio工具链版本会将此类头文件视为常规非系统头文件。

CMake默认将导入目标上定义的头文件搜索路径视为系统搜索路径。第18.2.3节,“导入的库”更详细地讨论了导入目标,但现在知道这些通常表示由项目之外提供的库,通常是系统库或来自依赖项的库。它们通常表示已经存在于系统中的库,或者以某种方式在项目之外构建。

如果由于某种原因,使用者不应将导入目标的头文件搜索路径视为系统搜索路径,则可以将使用者的NO_SYSTEM_FROM_IMPORTED目标属性设置为true。请注意,此设置不区分不同的导入目标,它将应用于使用者链接到的所有导入目标。在实践中,使用此设置可能表明项目或其链接到的目标存在更深层次的问题。被消耗的目标的“系统性”不应由消耗它的事物确定。

CMake 3.25及更高版本提供了一个更好的方法。目标支持SYSTEM属性,CMake使用该属性来决定使用者是否应将该目标的头文件搜索路径视为系统或非系统。它不会影响设置SYSTEM的目标的构建,只影响它的使用者。对于导入目标,SYSTEM默认为true。对于非导入目标,默认值取自SYSTEM目录属性,如果未设置该目录属性,则为false。这些默认值与上述CMake 3.24及更早版本的行为相同。

对于导入目标,更改SYSTEM属性的需求很少见。如果导入目标表示项目的一部分,但必须使用某个外部构建系统构建,则可能需要这样做。除此之外,在导入目标上将SYSTEM设置为false是不寻常的。

对于非导入目标,在某些情况下将SYSTEM属性设置为true可能是合适的。一个例子是将第三方依赖关系的源代码直接添加到项目的构建中(在第30章“FetchContent”中讨论的FetchContent模块广泛使用了这种方法)。在这种情况下,为该依赖项创建了非导入目标,但主项目可能仍然希望将来自该依赖项的头文件视为系统头文件。

对于刚才描述的与依赖关系相关的情景,逐个为每个依赖项的每个目标显式设置SYSTEM属性将很繁琐。更方便的方法是使用SYSTEM目录属性来更改非导入目标的默认值。与直接修改该目录属性不同,操作它的最佳方式是通过在用于引入依赖项的add_subdirectory()或FetchContent_Declare()调用中添加SYSTEM关键字。这两者都将SYSTEM目录属性设置为对构建的子目录为true。

# 存储在项目中的供应商代码
add_subdirectory(third_party/somedep SYSTEM)
# 下载并添加到构建中的外部依赖项
include(FetchContent)
FetchContent_Declare(anotherdep
 GIT_REPOSITORY ...
 SYSTEM
)
FetchContent_MakeAvailable()

CMake 3.25还在目标被安装或导出时提供了对SYSTEM属性的控制(请参阅第27.2节“安装项目目标”和第27.3节“导出安装”)。在安装或导出时,它将被表示为导入目标。如上所述,该导入目标将默认情况下的SYSTEM属性设置为true。通常情况下,这是正确的行为,但在不应将该导入目标视为系统时,可以将EXPORT_NO_SYSTEM属性设置为true。

# 在构建过程中,这是普通的非导入目标
add_library(MyThing ...)
set_target_properties(MyThing PROPERTIES
 EXPORT_NO_SYSTEM TRUE
)
# 它在安装位置变成一个导入目标。
# 在安装时,其SYSTEM属性将为false。
install(TARGETS MyThing EXPORT MyProj ...)
install(EXPORT MyProj ...)
export(EXPORT MyProj ...)

CMake 3.23添加了对IMPORTED_NO_SYSTEM属性的支持,但在CMake 3.25中已被弃用。IMPORTED_NO_SYSTEM提供的功能已由SYSTEM和EXPORT_NO_SYSTEM属性取代。因此,请避免使用IMPORTED_NO_SYSTEM。

除了上述描述的方法外,target_include_directories()和include_directories()命令还接受SYSTEM关键字。这些命令中的该关键字的效果与上述有关,但机制不同。

在include_directories()中使用SYSTEM时,它会强制将列出的头文件搜索路径视为当前目录及其所有子目录的系统搜索路径。这不能被禁用,并且不受SYSTEM、EXPORT_NO_SYSTEM或IMPORTED_NO_SYSTEM等目标属性的影响。通常应避免使用include_directories()命令,而使用基于目标的命令,而在include_directories()中使用SYSTEM关键字更加不鼓励。

在target_include_directories()中使用SYSTEM只是略好一些。这些路径仍然添加到相同的目标属性中,因此PRIVATE、PUBLIC和INTERFACE关键字仍然具有其通常的含义。但是,在内部,CMake记录这些路径应被视为系统路径。在列为PUBLIC或INTERFACE的路径将被添加到目标的INTERFACE_SYSTEM_INCLUDE_DIRECTORIES属性中,但PRIVATE路径不会添加到任何项目可读的属性中。target_include_directories()的SYSTEM关键字也与include_directories()一样存在问题,即它不受目标属性(如SYSTEM、EXPORT_NO_SYSTEM或IMPORTED_NO_SYSTEM)的影响。

add_library(MyThing ...)
add_executable(Consumer ...)
target_link_libraries(Consumer PRIVATE MyThing)
target_include_directories(MyThing SYSTEM
 PRIVATE secret
 PUBLIC api
)

上述不是使用SYSTEM关键字的一个特别好的例子,但它演示了这种行为。在构建Consumer时,CMake将api添加为系统头文件搜索路径。在构建MyThing时,CMake将secret和api添加为系统头文件搜索路径。这使得这个例子相对较差的原因在于,项目提供的头文件通常不应被视为系统头文件。由于MyThing是由项目构建的,因此其secret或api目录中的头文件也可以被视为项目的一部分,因此不应将其视为系统头文件。

实际上,项目很少需要在target_include_directories()或include_directories()中使用SYSTEM关键字。CMake通常根据被消耗目标是导入还是非导入来自动进行适当的选择。如果默认行为不符合项目的需求,且项目必须支持较早版本的CMake,其中不支持SYSTEM目标属性,则可能适用于target_include_directories()中使用SYSTEM关键字。

15.7.3. 运行时库选择

在使用以MSVC ABI为目标的编译器时,必须选择运行时库。项目需要在静态链接或动态链接的运行时库之间进行选择。还需要选择是使用调试还是非调试的运行时库。在CMake 3.15或更高版本中,可以通过MSVC_RUNTIME_LIBRARY目标属性处理此选择。有效值包括:

  • MultiThreaded
  • MultiThreadedDLL
  • MultiThreadedDebug
  • MultiThreadedDebugDLL

以DLL结尾的值使用动态链接的运行时库,没有DLL的值使用静态链接的运行时库。当设置了MSVC_RUNTIME_LIBRARY时,CMake会为所使用的编译器选择适当的标志。这使项目无需了解针对MSVC ABI的各种工具链所需的所有不同选项。如果未设置MSVC_RUNTIME_LIBRARY属性,CMake将使用默认值,相当于生成表达式MultiThreaded$<$CONFIG:Debug:Debug>DLL。该属性从CMAKE_MSVC_RUNTIME_LIBRARY变量的值初始化,如果已设置。

在使用CMake 3.14或更早版本时,对于至少某些MSVC工具链,将使用类似的默认值。但是,与使用MSVC_RUNTIME_LIBRARY属性不同,这些默认值是通过在第15.5节“编译器和链接器变量”中讨论的变量中添加原始标志来实现的。这使得项目更难从默认值更改行为,因为它需要知道使用的标志并执行字符串替换。这相当脆弱,也不太方便。

为了让CMake使用MSVC_RUNTIME_LIBRARY属性,项目必须确保在第一个project()命令调用之前将策略CMP0091设置为NEW。最简单且最典型的确保方式是在顶层CMakeLists.txt的开头要求至少CMake 3.15或更高版本,类似以下语句:

# 需要至少 CMake 3.15 以使用 MSVC_RUNTIME_LIBRARY
cmake_minimum_required(VERSION 3.15)

如果指定的版本早于3.15,CMake将忽略MSVC_RUNTIME_LIBRARY属性,并退回到在标志变量中编码原始编译器标志的旧行为。

上述说明了如何指定MSVC运行时库,但对于大多数项目来说,默认值已经给出了适当的行为。调试构建将使用调试运行时库,并且二进制文件将是动态链接的。只有在有特定需要偏离这些默认值时,才应使用上面讨论的属性或变量。从默认值更改通常会导致其他需要链接到生成的二进制文件的项目之间的摩擦。因此,最好将选择权留给消费项目,并仅在顶层项目中有强烈需要时覆盖默认值。

15.7.4. 调试信息格式选择

CMake的默认编译器标志可能包含影响调试信息格式的选项。例如,在使用MSVC工具链时,像/Z7、/Zi或/ZI这样的标志会同时更改调试信息的存储位置以及在Visual Studio中调试体验的方面。在CMake 3.24及更早版本中,这些标志在第15.5节“编译器和链接器变量”中讨论的CMAKE_<LANG>FLAGS_<CONFIG>变量家族中指定。当开发人员或项目需要与默认值不同的东西时,必须对这些变量执行基于字符串的搜索和替换。由于多种原因,这样做并不方便:

  • 基于字符串的替换可能很脆弱。
  • 必须对每种语言和配置的每种组合分别执行此操作。
  • 需要了解工具链的所有可能标志的版本。

CMake 3.25引入了一种处理调试信息的抽象,当使用以MSVC ABI为目标的工具链时会使用这种抽象。该抽象使用MSVC_DEBUG_INFORMATION_FORMAT目标属性确定为该目标添加调试信息的标志。仅当策略CMP0141在第一个project()调用时设置为NEW时,才使用该抽象。如果该策略未设置或设置为OLD,各种CMAKE_<LANG>FLAGS_<CONFIG>变量的默认值将包含与CMake 3.24及更早版本一样的与调试相关的标志,并且将忽略MSVC_DEBUG_INFORMATION_FORMAT属性。

当CMP0141设置为NEW时,MSVC_DEBUG_INFORMATION_FORMAT目标属性必须评估为以下值之一或为空字符串。支持生成器表达式,因此可以为不同的配置使用不同的标志。

  • Embedded:编译器将直接将调试信息嵌入每个目标文件中。对于MSVC工具链,对应于/Z7标志。
  • ProgramDatabase:编译器将调试信息收集在单独的文件中(程序数据库)。对于MSVC工具链,对应于/Zi标志,并将调试信息存储在PDB文件中。
  • EditAndContinue:这类似于ProgramDatabase,但程序数据库支持Visual Studio的“编辑和继续”功能。对于MSVC工具链,对应于/ZI标志。

对于不是MSVC工具链的以MSVC ABI为目标的工具链,不是所有上述选项都受支持。开发人员有责任确保所请求的行为得到工具链的支持。例如,当使用以MSVC ABI为目标的Clang工具链时,唯一受支持的值是Embedded。

在CMP0141设置为NEW时,如果MSVC_DEBUG_INFORMATION_FORMAT评估为空字符串,则不会为该目标添加与调试相关的标志。这允许使用生成器表达式为某些配置提供与调试相关的标志,而对其他配置不提供。

# 注意:不建议直接设置此属性。
set_target_properties(SomeTarget
    PROPERTIES CMAKE_MSVC_DEBUG_INFORMATION_FORMAT
    $<$<CONFIG:Debug,RelWithDebInfo>:Embedded>
)

项目通常不会像上面的示例那样直接操作MSVC_DEBUG_INFORMATION_FORMAT目标属性。该属性的初始值取自CMAKE_MSVC_DEBUG_INFORMATION_FORMAT变量。该变量通常不应由项目设置,而应该由开发人员控制,以允许他们选择所需的调试支持类型。

当未设置CMAKE_MSVC_DEBUG_INFORMATION_FORMAT并且CMP0141设置为NEW时,CMake将使用一个默认值,该值产生与OLD行为相同的最终结果。对于支持ProgramDatabase的工具链,将是Debug和RelWithDebInfo配置的默认值。对于以MSVC ABI为目标但不支持ProgramDatabase的工具链,这些配置的默认值将是Embedded。

CMAKE_MSVC_DEBUG_INFORMATION_FORMAT可以明确设置为空字符串。在CMP0141设置为NEW时,这将导致在使用以MSVC ABI为目标的工具链时,不会为任何配置添加与调试相关的标志。可能需要这样做的一个示例场景是在使用工具链文件时(请参见第23.1节“工具链文件”),并且将与之一起使用的项目可能会将CMP0141设置为OLD或NEW,或者需要支持CMake 3.24或更早版本。在这种情况下,为了使工具链文件在所有情况下都能按预期工作,它将需要覆盖默认的编译器标志变量,并明确将调试相关的标志设置为防止CMake添加调试相关的标志。附录B,“完整的编译器缓存示例”展示了必须考虑CMP0141的OLD和NEW行为的另一种情况。

15.8. 推荐做法

本章覆盖了CMake的一些自早期版本以来经历了最显著改进的领域。读者应该期望在在线和其他地方找到大量仍然推荐使用变量和目录属性命令的旧方法的示例和教程。应该理解,在CMake 3.0+ 时代,target_…() 命令应该是首选的方法。

项目应该尽量使用 target_link_libraries() 命令来定义所有目标之间的依赖关系。这清晰地表达了目标之间的关系性质,并向项目的所有开发人员明确传达了目标之间的关系。与直接使用 link_libraries() 或直接操作目标或目录属性相比,应该优先使用 target_link_libraries() 命令。同样,其他 target_…() 命令提供了一种更清晰、更一致且更健壮的方式来操作编译器和链接器标志,而不是使用变量、目录属性命令或直接操作属性。

CMake 3.13引入了一些与链接器选项相关的新命令和属性,其中一些是为了保持一致性或解决特定用例而添加的。项目通常应避免使用新的 add_link_options() 目录级别命令,而应该优先使用新的 target_link_options() 命令。CMake 3.13还引入了一个新的目标级别命令 target_link_directories(),它是现有的目录级别 link_directories() 命令的补充。出于健壮性原因,这两个链接目录命令都应该避免使用。

建议项目链接到目标名称或使用库的绝对路径,如果这些库不在默认链接器搜索路径上预期的目录中。对于CMake 3.23或更高版本支持的 LINK_LIBRARIES_ONLY_TARGETS 目标属性可能在强制执行这一点时会有帮助(参见第16.1节“要求链接的目标”)。

以下指南可能有助于确定哪些方法适用于特定情况:

  • 在可能的情况下,优先使用 target_…() 命令来描述目标之间的关系,并修改编译器和链接器行为。
  • 一般来说,最好避免使用目录属性命令。虽然在一些具体情况下它们可能很方便,但是始终使用 target_…() 命令会建立所有项目开发人员都能遵循的清晰模式。如果必须使用目录属性命令,请尽早在CMakeLists.txt文件中使用,以避免在前面的章节中描述的一些较不直观的行为。
  • 避免直接操作影响编译器和链接器行为的目标和目录属性。了解属性的作用以及不同命令如何操作它们,但在可能的情况下,优先使用更专业的面向目标和目录的命令。然而,在调查意外的编译器或链接器命令行标志时,查询目标属性可能是有用的。
  • 在CMake为编译器或链接器功能提供抽象时,优先使用该抽象,而不是添加原始的编译器或链接器标志。确保在整个构建中一致使用该抽象,包括从源代码构建的任何依赖项。
  • 最好避免修改各种 CMAKE_…_FLAGS 变量及其特定配置的对应项。将这些变量视为开发人员可能随意在本地更改的保留变量。如果需要在整个项目范围应用更改,请考虑在项目顶层使用一些战略性的目录属性命令,但请考虑这样的设置是否真的应该一刀切。这在工具链文件中可能是一个部分例外,其中可以定义初始默认值(请参见第23章,“工具链和交叉编译”对于该领域的详细讨论)。

开发人员应熟悉 PRIVATE、PUBLIC 和 INTERFACE 关系的概念。它们是 target_…() 命令集的重要组成部分,对项目的安装和打包阶段变得更加重要。将 PRIVATE 视为针对目标本身的行为,将 INTERFACE 视为针对链接到目标的内容,将 PUBLIC 视为两者行为的结合。虽然将所有东西都标记为 PUBLIC 可能很诱人,但这可能会不必要地将依赖关系暴露到目标之外。这可能会影响构建时间,并可能强制将私有依赖关系强加到不必须了解它们的其他目标上。这反过来对其他方面产生强烈影响,比如符号可见性(在第22.5节“符号可见性”中详细讨论)。

INTERFACE 关键字主要用于导入或接口库目标。另一个较不常见的用法是向项目的开发人员可能无法直接更改的部分添加缺失的要求。示例包括为较旧的CMake版本编写的部分,不使用 target_…() 命令,或者具有导入目标的外部库,这些目标省略了链接到它们的目标所需的重要标志。CMake 3.13移除了 target_link_libraries() 不能被调用以在不同目录范围内定义的目标上操作的限制。对于所有其他 target_…() 命令,以前不存在此类限制,因此它们始终可用于扩展在项目中其他地方定义的目标的接口属性。第34.5.1节“跨目录构建目标”重新讨论了这个主题,演示了如何使用这些能力来促进更模块化的项目结构。

避免将警告硬编码为错误。让选择由 CMAKE_COMPILE_WARNING_AS_ERROR 变量是否启用(需要CMake 3.24或更高版本)来确定。项目不应该设置此变量,它是作为开发人员控制的,可以在命令行或可能在 CMake 预设中设置(请参见第33章,“预设”)。

有时开发人员会诱使使用 SYSTEM 目标属性或在 target_include_directories() 或 include_directories() 中使用 SYSTEM 关键字,以消除来自头文件的警告,而不是直接解决这些警告。如果这些头文件是项目的一部分,SYSTEM 通常不是使用的适当特性。一般来说,SYSTEM 是用于项目外部路径(例如,用于依赖项)的。