第十二章:策略

Posted by lili on

CMake经过长时间的发展,引入了新功能,修复了错误,并改变了某些功能的行为,以解决问题或引入改进。虽然引入新功能不太可能对使用CMake构建的现有项目造成问题,但任何行为的更改都有可能破坏项目,特别是如果项目依赖于旧的行为。因此,CMake开发人员非常小心,确保更改以保持向后兼容性,并为项目更新到新行为提供直接、可控的迁移路径。

CMake的策略机制用于控制是使用旧行为还是新行为。一般来说,开发人员并不经常接触到策略,主要是当CMake发出关于项目依赖于旧版本行为的警告时。当开发人员转移到更近期的CMake版本时,新版本有时会发出这样的警告,以突出项目应如何更新以使用新行为。

12.1. 策略控制

CMake的策略功能与cmake_minimum_required()命令密切相关,该命令在第3章“最小项目”中引入。这个命令不仅指定项目需要的最低CMake版本,还设置CMake的行为以匹配给定版本。因此,当项目以cmake_minimum_required(VERSION 3.2)开始时,它表示至少需要CMake 3.2,并且项目期望CMake的行为与3.2版本相同。这使得项目有信心,开发人员应该能够方便地更新到任何更新版本的CMake,而项目仍将像以前一样构建。

然而,有时项目可能需要比cmake_minimum_required()命令提供的更精细的控制。考虑以下情况:

  • 项目希望设置较低的最低CMake版本,但如果有新的行为可用,它也希望利用它。
  • 项目的一部分无法修改(例如,它可能来自外部只读代码仓库),并且它依赖于在更新的CMake版本中已更改的旧行为。然而,项目的其余部分希望转移到新的行为。
  • 项目在某个旧行为上有很大的依赖,需要相当数量的工作才能进行更新。项目的某些部分希望利用最近的CMake功能,但该特定更改的旧行为需要保留,直到可以抽出时间更新项目为止。

这些是一些常见的例子,cmake_minimum_required()命令本身提供的高级别控制不足以满足需求。更具体的对策略的控制是通过cmake_policy()命令实现的,它有多种形式,作用于不同程度的粒度。

cmake_policy(VERSION major[.minor[.patch[.tweak]]])

作用于最粗粒度的形式与cmake_minimum_required()几乎相同,除了在项目的顶部,调用cmake_minimum_required()是强制执行最低CMake版本的必需的。在顶层CMakeLists.txt文件之外,使用cmake_policy()通常在项目需要强制执行某个版本的行为时更清晰地传达意图,如下例所示:

cmake_minimum_required(VERSION 3.7)
# 使用最新的CMake功能
add_subdirectory(modernDir)
# 从另一个项目导入,依赖于旧行为
cmake_policy(VERSION 2.8.11)
add_subdirectory(legacyDir)

CMake 3.12通过在不破坏向后兼容性的情况下可选地允许项目向cmake_minimum_required()或cmake_policy(VERSION)指定版本范围来扩展此功能。使用三个点…在最小和最大版本之间指定范围,没有空格。范围表示正在使用的CMake版本至少必须是最小版本,并且行为应该与指定的最大版本和运行时CMake版本的最低版本相匹配。这允许项目有效地表示:“我至少需要CMake X,但可以与CMake Y的策略一起使用”。以下示例展示了一种项目只需要CMake 3.7的两种方式,但如果运行的CMake版本支持它们,则仍支持所有策略直到CMake 3.12的新行为:

cmake_minimum_required(VERSION 3.7...3.12)
cmake_policy(VERSION 3.7...3.12)

在CMake版本3.12之前,将有效地看到只有一个版本号,并且会忽略…3.12部分,而3.12及更高版本会理解它表示一个范围。

CMake还提供了使用SET形式分别控制每个行为更改的能力:

cmake_policy(SET CMPxxxx NEW)
cmake_policy(SET CMPxxxx OLD)

每个个体行为更改都有一个形式为CMPxxxx的策略号,其中xxxx始终是四位数字。通过指定NEW或OLD,项目告诉CMake对于该特定策略使用新行为或旧行为。CMake文档提供了策略的完整列表,以及每个策略的旧行为和新行为的解释。

例如,在版本3.0之前,CMake允许项目调用get_target_property()并提供一个不存在的目标的名称。在这种情况下,如果遇到这种情况,属性的值将返回为-NOTFOUND而不是发出错误,但很可能项目包含不正确的逻辑。因此,从版本3.0开始,如果遇到这种情况,CMake将出现错误。

在项目依赖于旧行为的情况下,可以通过设置策略CMP0045来继续使用旧行为,如下所示:

# 允许使用 get_target_property() 获取不存在的目标
cmake_policy(SET CMP0045 OLD)
# 在上述策略更改之前,这将因错误而停止
get_target_property(outVar doesNotExist COMPILE_DEFINITIONS)

将策略设置为NEW的需求相对较少。一种情况是,项目希望设置较低的最低CMake版本,但如果使用了较新的版本,仍然希望利用后续的功能。例如,在CMake 3.2中,引入了策略CMP0055,用于对break()命令的使用进行严格检查。如果项目仍希望支持使用较早版本的CMake构建,那么在使用较新版本的CMake时,必须显式启用额外的检查。

cmake_minimum_required(VERSION 3.0)
if(CMAKE_VERSION VERSION_GREATER 3.1)
    # 启用对 break() 命令使用的更强检查
    cmake_policy(SET CMP0055 NEW)
endif()

测试CMAKE_VERSION变量是确定策略是否可用的一种方式,但if()命令提供了一种更直接的方法,使用if(POLICY…)形式。上述示例可以以另一种方式实现:

cmake_minimum_required(VERSION 3.0)
# 仅在使用的CMake版本知道该策略编号时设置该策略
if(POLICY CMP0055)
    cmake_policy(SET CMP0055 NEW)
endif()

还可以获取特定策略的当前状态。可能需要读取当前策略设置的主要情况是在模块文件中,这可能是由CMake本身或项目提供的模块文件。然而,对于项目模块根据策略设置更改其行为的情况通常较少见。

cmake_policy(GET CMPxxxx outVar)

outVar中存储的值将是OLD、NEW或空字符串。cmake_minimum_required(VERSION…)和cmake_policy(VERSION…)命令会重置所有策略的状态。那些在指定的CMake版本或更早引入的策略将被重置为NEW。在指定版本之后添加的策略将有效地被重置为空。

如果CMake检测到项目正在执行依赖于旧行为、与新行为冲突或其行为不明确的操作,它可能会在相关策略未设置时发出警告。这些警告是开发人员接触到CMake策略功能的最常见方式。它们旨在嘈杂但富有信息,鼓励开发人员更新项目以使用新行为。在某些情况下,即使已经明确设置了策略,也可能会发出弃用警告,但这通常只发生在策略已经被长时间记录为弃用的情况下(很多版本)。

有时策略警告不能立即解决,但不希望出现这些警告。处理这种情况的首选方法是将策略显式设置为所需的行为(OLD或NEW),以停止警告。然而,这并不总是可能,例如当项目的较深部分发出自己的cmake_minimum_required(VERSION…)或cmake_policy(VERSION…)调用时,从而重置策略状态。作为解决这种情况的临时方法,CMake提供了CMAKE_POLICY_DEFAULT_CMPxxxx和CMAKE_POLICY_WARNING_CMPxxxx变量,其中xxxx是通常的四位策略编号。这些变量不打算由项目设置,而是由开发人员作为缓存变量临时启用/禁用警告或检查项目在启用特定策略时是否发出警告。最终,长期解决方案是解决警告突出的底层问题。尽管如此,有时对于项目设置这些变量以消除已知不会有害的警告可能是适当的。

12.2. 策略范围

有时候,策略设置只需要应用到文件的特定部分。与其要求项目手动保存它想要临时更改的任何策略的现有值不同,CMake提供了一个策略栈,可用于简化这个过程。将设置推送到策略栈上实质上创建了当前设置的副本,并允许项目在该副本上操作。弹出栈会丢弃当前的策略设置,并回到栈上的先前设置。

有两种方法可用于保存和恢复(推送和弹出)策略栈:

# 需要 CMake 3.25 或更高版本
block(SCOPE_FOR POLICIES)
 
 # 在此处更改策略。块外的策略设置不受影响。
 
endblock()
# 适用于任何 CMake 版本
cmake_policy(PUSH)
 
 # 在此处更改策略。策略更改适用于
 
 # 直到弹出策略栈为止(见下文)。
 
cmake_policy(POP)

在这两种方法中,使用block()的第一种方法更为稳健。无论控制流如何离开块,策略都会被还原为进入块之前的先前状态。这意味着在块内可以自由使用return()、break()和continue()等命令。

使用cmake_policy()的第二种方法更加脆弱,因为它依赖于项目确保对于每个cmake_policy(PUSH)都会调用一次cmake_policy(POP)。在跨多行推送和弹出策略状态时,这可能会很具有挑战性。一个常见的错误来源是在长文件的开头推送策略,然后在文件的结尾弹出,但在文件的中间的某个地方提前返回而没有弹出策略栈。这经常发生在最初没有return()语句的文件在添加了return()后。

模块文件是策略栈可能被这样操纵的更常见的地方,也是这种错误经常发生的地方。

cmake_policy(PUSH)
cmake_policy(SET CMP0085 NEW)
# ...
if("Foo" IN_LIST SomeList)
 
 # 错误:在没有弹出策略栈的情况下返回
 
 return()
endif()
# ...
cmake_policy(POP)

用block()替换上述代码会避免这个问题:

block(SCOPE_FOR POLICIES)
cmake_policy(SET CMP0085 NEW)
# ...
if("Foo" IN_LIST SomeList)
 
 # OK:无需手动弹出策略栈
 
 return()
endif()
# ...
endblock()

有些命令隐式地在返回到调用者之前将新的策略状态推送到栈上,并在弹出之前再次弹出它。add_subdirectory()、include()和find_package()命令是这一点的重要示例。include()和find_package()命令还支持一个NO_POLICY_SCOPE选项,该选项可阻止自动推送-弹出策略栈(add_subdirectory()没有此选项)。

在CMake的早期版本中,include()和find_package()不会自动在策略栈上推送和弹出一个条目。NO_POLICY_SCOPE选项被添加为一种方式,使使用较新CMake版本的项目可以针对项目的特定部分恢复到旧行为,但不鼓励使用,并且对于新项目来说是不必要的。

12.3. 最佳实践

在可能的情况下,项目应该更喜欢在CMake版本级别上处理策略,而不是操纵特定的策略。将策略设置为与特定CMake版本的行为相匹配使项目更易于理解和更新,而对个别策略的更改可能更难追踪通过多个目录层次的影响,特别是由于它们与总是被重置的版本级别策略更改的交互。

在选择如何指定符合的CMake版本时,通常会选择在cmake_minimum_required(VERSION)和cmake_policy(VERSION)之间进行选择,通常会选择后者。两个主要的例外情况是在项目的顶级CMakeLists.txt文件的开头和可能在多个项目中重复使用的模块文件的顶部。对于后一种情况,最好使用cmake_minimum_required(VERSION),因为使用该模块的项目可能会强制执行其自己的最低CMake版本,但模块可能具有自己的特定的最低版本要求。除了这些情况外,cmake_policy(VERSION)通常更清晰地表达了意图,但两个命令从策略的角度来看通常会实现相同的事情。

在项目确实需要操纵特定策略的情况下,应该使用if(POLICY…)来检查策略是否可用,而不是测试CMAKE_VERSION变量。这可以使代码更加一致。比较下面两种设置策略行为的方式,注意检查和执行使用了一致的方法:

# 版本级别的策略强制执行
if(NOT CMAKE_VERSION VERSION_LESS 3.4)
    cmake_policy(VERSION 3.4)
endif()
# 个别策略级别的强制执行
if(POLICY CMP0055)
    cmake_policy(SET CMP0055 NEW)
endif()

如果项目需要在本地操作多个个别策略,请将该部分用block(SCOPE_FOR POLICIES)和endblock()括起来。如果必须支持CMake 3.24或更早版本,请使用cmake_policy(PUSH)和cmake_policy(POP)。用任一命令对代码进行括起来确保其余范围与更改隔离开来。如果使用cmake_policy()定义这些区域,请特别注意可能退出该代码部分的任何return()、break()或continue()语句,并确保没有推送没有相应的弹出。

还要注意,add_subdirectory()、include()和find_package()都会自动在策略栈上推送和弹出一个条目。不需要显式使用block或push-pop来将它们的策略更改与调用范围隔离开。项目应该避免使用这些命令的NO_POLICY_SCOPE关键字,因为它仅用于解决非常早期CMake版本行为的更改。对于新项目来说,NO_POLICY_SCOPE很少是合适的。

在函数内部修改策略设置时,应该避免除非在函数体内使用适当的block()或cmake_policy() push-pop。由于函数不引入新的策略范围,策略更改可能会影响调用者,如果更改没有使用适当的逻辑进行隔离。此外,函数实现的策略设置取自定义函数被定义的范围,而不是从它被调用的范围。因此,最好在定义函数的范围内调整任何策略设置,而不是在函数内部。作为最后的手段,CMAKE_POLICY_DEFAULT_CMPxxxx 和 CMAKE_POLICY_WARNING_CMPxxxx 变量可以允许开发人员或项目解决一些特定的与策略相关的情况。开发人员可以使用这些变量来临时更改特定策略设置的默认值,或者防止关于特定策略的警告。项目通常应该避免设置这些变量,以便开发人员可以在本地进行控制。尽管如此,在某些情况下,它们可以用于确保特定策略的行为或警告持续存在,即使通过调用 cmake_minimum_required() 或 cmake_policy(VERSION) 进行调用。在可能的情况下,项目应该尝试更新到较新的行为,而不是依赖于这些变量。