CMake(十三):编译器和链接器的要点

前一章讨论了构建类型,以及它与选择特定的编译器和链接器行为之间的关系。本章讨论如何控制编译器和链接器行为的基本原理。这里提供的材料涵盖了每个CMake开发人员都应该熟悉的一些最重要的主题和技术。

​ 在深入研究细节之前,需要注意的是,随着CMake的发展,控制编译器和链接器行为的可用方法也得到了改进。重点已经从一个更加构建全局的视图转移到一个可以控制每个单独目标的需求的视图,以及这些需求应该或不应该被携带到依赖于它的任何其他目标。这是思想上的一个重要转变,因为它影响了项目如何最有效地定义应该建立的目标的方式。CMake更成熟的特性可以用来在粗略的层次上控制行为,但代价是失去了定义目标之间关系的能力。通常应该优先使用更近期的以目标为中心的特性,因为它们极大地提高了构建的健壮性,并提供了对编译器和链接器行为更精确的控制。新特性在行为和使用方式上也趋于一致。

13.1 目标属性

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

(1)编译器标志

​ 下面是控制编译器标志的最基本的目标属性,每个属性都包含一个列表:

  • INCLUDE_DIRECTORIES

    这是一个目录列表,用来作为header搜索路径,所有的必须是绝对路径。CMake会为每个路径添加一个编译器标记,并添加适当的前缀(通常是-I或/I)。创建目标时,该目标属性的初始值取自同名的目录属性。

  • COMPILE_DEFINITIONS

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

  • COMPILE_OPTIONS

    此属性提供了既不是头文件搜索路径也不是符号定义的任何编译器标志。创建目标时,该目标属性的初始值取自同名的目录属性。

​ INCLUDE_DIRECTORIES和COMPILE_DEFINITIONS属性实际上只是方便,为项目通常想要设置的最常见的东西负责编译器的特定标志。所有剩下的编译器特定标志都在COMPILE_OPTIONS属性中提供。

​ 上面三个目标属性都有一个同名的相关目标属性,只是INTERFACE_ prepended。这些接口属性做了完全相同的事情,只是它们应用于直接链接到目标的任何其他目标,而不是应用于目标本身。换句话说,它们被用来指定消费目标应该继承的编译器标志。由于这个原因,它们通常被称为使用需求,而与interface属性有时被称为构建需求不同。

​ 不像non-interface的同类,上面的INTERFACE_…属性都不是从目录属性初始化的。相反,它们都是空的,因为只有项目知道头文件的搜索路径、定义和编译器标志应该传播到消费目标。

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

​ 如果编译器标志需要在单独的源文件级别上操作,目标属性的粒度不够细。对于这种情况,CMake提供了COMPILE_DEFINITIONS, COMPILE_FLAGS和COMPILE_OPTIONS源文件属性(COMPILE_OPTIONS源文件属性只在CMake 3.11中添加)。除了它们只应用于设置它们的源文件之外,它们都类似于它们同名的目标属性。注意,它们对生成器表达式的支持落后于目标属性,COMPILE_DEFINITIONS源文件属性在CMake 3.8和3.11中获得了对生成器表达式的支持。此外,Xcode项目文件格式根本不支持配置特定的源文件属性,所以如果针对苹果平台,$ <CONFIG>或$ <CONFIG:…>不应该在源文件属性中使用。

(2)链接器标志

​ 与链接器标志相关联的目标属性与编译器标志相类似,但涉及的属性更少:

  • LINK_LIBRARIES

    这个属性包含目标应该直接链接到的所有库的列表。在创建目标时,它最初是空的,并且支持生成器表达式。列出的每个库可以是以下其中之一:

    • 库的路径,通常指定为绝对路径。
    • 只有库名,没有路径,通常也没有任何特定于平台的文件名前缀(例如lib)或后缀(例如.a, .so, .dll)。
    • CMake库目标的名称。当生成链接器命令时,CMake会将其转换为构建库的路径,包括为文件名提供适合平台的任何前缀或后缀。因为CMake代表项目处理所有不同平台的差异和路径,所以使用CMake目标名通常是首选方法。

    CMake将使用适当的链接器标志来链接LINK_LIBRARIES属性中列出的每个项目。

  • LINK_FLAGS

    它包含一个要传递给链接器的标志列表,用于可执行文件、共享库或模块库。对于构建为静态库的目标,它会被忽略。此属性用于通用链接器标志,而不是那些指定要链接到的其他库的标志。生成器表达式没有被记录为受支持。当创建目标时,此属性将为空。

  • STATIC_LIBRARY_FLAGS

    这是与LINK_FLAGS相对应的,只应用于作为静态库构建的目标。它将用于librarian或archiver工具。

​ 与编译器属性不同,只有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>

​ 当匹配正在构建的配置时,除了非配置特定的标志外,还将使用这些标志。

(3)目标属性命令

​ 通常不直接操作上述目标属性。CMake提供了专用函数,以一种更方便、更健壮的方式修改它们,这也鼓励明确说明依赖关系和目标之间的传递行为。“连接目标”,我们介绍了target_link_libraries()命令,并解释了如何使用PRIVATE、PUBLIC和INTERFACE规范来表达目标间的依赖关系。前面的讨论集中于目标之间的依赖关系,但是在前面讨论了目标属性之后,现在可以更加精确地描述这些关键字的确切效果。

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

    在PRIVATE后面列出的项只影响targetName本身的行为。只有non-interface_…目标属性被修改(例如LINK_LIBRARIES, LINK_FLAGS和STATIC_LIBRARY_FLAGS)。

  • INTERFACE

    这是对PRIVATE的补充,INTERFACE关键字后面的项只对链接到targetName的目标有影响。只有targetName的INTERFACE_…目标属性被修改(即INTERFACE_LINK_LIBRARIES)。

  • PUBLIC

    这相当于将PRIVATE和INTERFACE的效果结合起来。

​ 大多数时候,开发人员可能会发现“链接目标”中的解释更直观,但上面更精确的描述可以帮助解释在更复杂的项目中,属性可能被以不寻常的方式操作的行为。上面的描述也恰好与其他操作编译器标志的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关键字后面的目录将添加到两者中。

​ 通常,每次调用target_include_directories()时,指定的目录被附加到相关的目标属性。这使得以自然、渐进的方式添加多个路径变得很容易。如果需要,可以使用BEFORE关键字将列出的目录前置到目标属性的现有内容。

​ 如果指定了SYSTEM关键字,编译器将把列出的目录视为某些平台上的系统包含路径。这样做的影响可能包括跳过某些编译器警告或改变处理文件依赖关系的方式。它还会影响一些编译器搜索头路径的顺序。开发人员有时倾向于使用SYSTEM来屏蔽来自标题的警告,而不是直接处理这些警告。如果这样的头文件是项目的一部分,通常使用SYSTEM不是一个合适的选项。通常,SYSTEM用于项目之外的路径,但即使这样,也很少需要它。

​ 同样值得注意的是,由导入的目标的INTERFACE_INCLUDE_DIRECTORIES属性指定的路径在使用目标时,默认情况下将被视为系统路径。这是因为导入的目标被假定来自于项目之外,因此它们的相关标头应该以与其他系统提供的标头类似的方式处理。项目可以通过设置消费目标的NO_SYSTEM_FROM_IMPORTED属性为true来覆盖此行为,这将防止所有它消费的导入目标被视为SYSTEM。

​ 与直接操作目标属性相比,target_include_directories()命令提供了另一个优势。项目也可以指定相对目录,而不仅仅是绝对目录。相对路径将在需要时自动转换为绝对路径,路径被视为相对于当前源目录。

​ 由于target_include_directories()命令基本上只是填充相关的目标属性,因此应用这些属性的所有常见特性。特别是,可以使用生成器表达式,这一特性在安装目标和创建包时变得更加重要。 $<BUILD_INTERFACE:…>和$<INSTALL_INTERFACE:…>生成器表达式允许指定不同的构建和安装路径。对于已安装的目标,通常使用相对路径,它们将被解释为相对于基本安装位置而不是源目录。

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

​ target_compile_definitions()命令非常简单,每个条目的形式为VAR或VAR=VALUE。私有项填充COMPILE_DEFINITIONS目标属性,而接口项填充INTERFACE_COMPILE_DEFINITIONS目标属性。公共项填充两个目标属性。可以使用生成器表达式,但通常不需要以不同的方式处理构建和安装情况。

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

​ target_compile_options()命令也非常简单。每个项都被视为编译器选项,私有项填充COMPILE_OPTIONS目标属性,接口项填充INTERFACE_COMPILE_OPTIONS目标属性。通常,公共项填充两个目标属性。对于所有情况,每个项都被附加到现有的目标属性值,但可以使用BEFORE关键字代替。生成器表达式在所有情况下都受到支持,通常不需要以不同的方式处理构建和安装情况。

13.2 目录属性和命令

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

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

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

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

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

  • 列出的路径被添加到当前CMakeLists.txt文件的INCLUDE_DIRECTORIES目录属性中。这意味着在当前目录和以下目录中创建的所有目标都将把目录添加到它们的INCLUDE_DIRECTORIES目标属性中。
  • 任何在当前CMakeLists.txt文件中创建的目标(或者更准确地说,当前目录范围)也会将路径添加到它们的INCLUDE_DIRECTORIES目标属性中,即使这些目标是在调用INCLUDE_DIRECTORIES()之前创建的。这只适用于在当前CMakeLists.txt文件中创建的目标或其他通过include()拉进来的文件,但不适用于在父目录或子目录范围中创建的任何目标。

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

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

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

​ 就像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行为是不一致的。省略前缀使命令将item作为通用选项来处理,这一事实也是反直觉的,因为命令的名称。此外,对生成器表达式的限制只支持KEY=VALUE定义的VALUE部分,这也是前缀需求的直接结果。认识到这一点,CMake 3.12引入了add_compile_definitions()命令来替代add_definitions():

add_compile_definitions(SomeSymbol Foo=Value ...)

​ 新的命令只处理编译定义,它不需要在每个项上添加任何前缀,并且生成器表达式可以在没有value only限制的情况下使用。新命令的名称和定义项的处理方法与类似的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(dir1 [dir2 ...])

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

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

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

13.3 编译器和链接器变量

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

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

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

  • CMAKE_<LANG>_FLAGS
  • CMAKE_<LANG>_FLAGS_<CONFIG>

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

​ 遇到的第一个project()命令将为它们创建缓存变量(如果它们不存在的话)。因此,在第一次运行CMake之后,它们的值很容易检入CMake GUI应用程序。例如,对于一个特定的编译器,c++语言默认定义了以下变量:

CMAKE_CXX_FLAGS
CMAKE_CXX_FLAGS_DEBUG -g -O0
CMAKE_CXX_FLAGS_RELEASE -g -O0
CMAKE_CXX_FLAGS_RELWITHDEBINFO -O2 -g -DNDEBUG
CMAKE_CXX_FLAGS_MINSIZEREL -O2 -g -DNDEBUG

​ 链接器标志的处理是类似的。它们由以下一系列变量控制:

  • CMAKE_<TARGETTYPE>_LINKER_FLAGS
  • CMAKE_<TARGETTYPE>_LINKER_FLAGS_<CONFIG>

这些都是特定于特定类型的目标。变量名的部分必须是以下内容之一:

  • EXE

    用add_executable()创建的目标。

  • SHARED

    使用add_library创建的目标(名称SHARED…)或等效的目标,例如省略SHARED关键字,但将BUILD_SHARED_LIBS变量设置为true。

  • STATIC

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

  • MODULE

    Targets created with 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的首次运行可能因此出现做开发人员计划(如果放置在任何项目()命令),但如果是改变指定其他的Flag,这种变化不会被应用到现有的构建,因为变量将已经在缓存中。发现这一点后,通常的反应是使用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 ...)
    

    开发人员遇到这种行为的主要方法之一是,将编译器和链接器变量视为它们可以立即应用于创建的任何目标。另一个相关的陷阱是在目标创建后使用include(),所包含的文件修改了编译器或链接器变量。这也会改变当前目录作用域中已经定义的目标的编译器和链接器标志。由于编译器和连接器变量的这种延迟特性,它们在使用时可能很脆弱。理想情况下,项目应该只在CMakeLists.txt文件的早期修改它们,如果有的话,这样可以减少误用和开发人员惊喜的机会。
    相关代码:https://gitee.com/jiangli01/cmake-learning
    更多请关注微信公众号【Hope Hut】:
    CMake(十三):编译器和链接器的要点

上一篇:Android JNI学习(二)——实战JNI之“hello world”


下一篇:Centos/Linux 源码安装wireshark与tshark任意版本