第12章:策略

CMake已经发展了很长一段时间,引入了很多新的功能,修复了不少bug,并改变了某些特性的行为以解决缺陷或引入改进。虽然新功能的引入不太可能给现有项目的构建带来问题,但如果依赖于老式行为,任何改变都有可能破坏项目。由于这个原因,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_minimum_required()的效果非常接近:

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

在这种方式中,命令更改了CMake的行为以匹配指定版本的行为,cmake_minimum_required()命令会隐式设置CMake的行为。除了项目顶部强制调用cmake_minimum_required()来强制最低CMake版本外,这两种方式在很大程度上是可互换的。除了顶级CMakeLists.txt文件的开始部分,使用cmake_policy()通常可以更清楚地传达意图,当项目需要对项目的某个部分执行特定版本的行为时,就如下面的例子所示:

cmake_minimum_required(VERSION 3.7)
project(WithLegacy)

# Uses recent CMake features
add_subdirectory(modernDir)

# Imported from another project, relies on old behavior
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 3.12之前所有策略的新式行为(如果运行的CMake版本支持的话):

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

3.12之前的CMake版本实际上只看到一个版本号,并忽略…3.12部分,而3.12之后的版本可以理解为一个范围。

CMake还提供了能力集,可以单独的控制每个行为变化与SET的形式:

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

每个单独的行为更改都有自己的策略号CMPxxxx,其中xxxx总是四位数。通过指定NEW或OLD,项目告诉CMake为特定策略使用NEW或OLD行为。CMake文档提供了策略的完整列表,并解释了每种策略的新旧行为。

作为一个例子,3.0版本之前CMake允许项目将get_target_property()与不存在名称目标的一起使用。这种情况下,属性的值被返回为NOTFOUND,而不是直接报错,但项目很可能包含不正确的逻辑。因此,从3.0版本开始,如果遇到这样的情况,CMake会在出现该错误时停止。如果一个项目依赖于旧式行为,它可以继续使用策略CMP0045,像这样:

# Allow non-existent target with get_target_property()
cmake_policy(SET CMP0045 OLD)

# Would halt with an error without the above policy change
get_target_property(outVar doesNotExist COMPILE_DEFINITIONS)

为NEW操作制定策略的需求就不那么常见了。一种情况是,项目想要设置一个较低的CMake版本,但如果使用的是较新的版本,仍然可以利用后续的特性。例如,在CMake 3.2中引入了策略CMP0055,提供对break()命令使用情况的检查。如果项目仍然想要支持在早期CMake版本中构建,在运行新的CMake版本时必须显式启用检查。

cmake_minimum_required(VERSION 3.0)
project(PolicyExample)

if(CMAKE_VERSION VERSION_GREATER 3.1)
 # Enable stronger checking of break() command usage
 cmake_policy(SET CMP0055 NEW)
endif()

测试CMAKE_VERSION变量是确定策略是否可用的一种方法,而if()命令提供了一种更直接的方法,直接使用if(POLICY…)形式。上述方式也可以这样执行:

cmake_minimum_required(VERSION 3.0)
project(PolicyExample)

# Only set the policy if the version of CMake being used
# knows about that policy number
if(POLICY CMP0055)
 cmake_policy(SET CMP0055 NEW)
endif()

还可以获得特定策略的当前状态。当前策略设置可能在模块文件中,它可能是由CMake本身或项目提供。然而,项目模块根据策略设置改变其行为是不常见的。

cmake_policy(GET CMPxxxx outVar)

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

如果CMake检测到项目正在依赖一些于旧式行为、并与新式行为有冲突,或其行为含糊不清时,在相关策略未设置时会发出警告。这些警告是向开发人员展示CMake策略的最常见方式。它们的设计为噪音大且信息量大,其鼓励开发商更新项目以适应新的行为。某些情况下,即使策略已明确设置,也可能会发出弃用警告,但这通常只针对已弃用很长时间(许多版本)的策略。

有时不能立即处理策略警告,这些警告可能也不需要处理。处理此问题的首选方法是显式地将策略设置为所需的行为(旧式或新式),这将关闭警告。但这中办法并不总是可行,比如项目的较深的部分调用了cmake_minimum_required(VERSION…)或cmake_policy(VERSION…),重新设置策略状态时。作为解决这种情况的临时方法,CMake提供了CMAKE_POLICY_DEFAULT_CMPxxxx和CMAKE_POLICY_WARNING_CMPxxxx变量,其中xxxx是通常的四位数策略号。这些不由项目设置,而是由开发人员临时作为缓存变量来启用/禁用警告,或者检查项目是否在启用特定策略时发出警告。最终,长期解决方案是解决上述警告所突显的根本问题。然而,对于项目来说,设置这些变量中的一个来屏蔽已知无害的警告是合适的。

12.2. 策略范围

有时策略设置只需要应用于文件的特定部分,所以CMake提供了一个策略堆栈,可以用来简化这个过程,而不需要手动保存任何想临时改变策略的现有值:

cmake_policy(PUSH)
cmake_policy(POP)

所有策略的现有状态可以通过PUSH操作保存,并通过相应的POP丢弃当前状态。每次PUSH都需要最终需要一个匹配的POP。这两者之间,项目可以修改它需要的任何策略的设置,而不必先显式地保存每个策略。同样,模块文件是策略堆栈操作的常见位置之一。为一个模块文件制定一些暂时策略:

# Save existing policy state
cmake_policy(PUSH)

# Set some policies to OLD to preserve a few old behaviors
cmake_policy(SET CMP0060 OLD) # Library path linking behavior
cmake_policy(SET CMP0021 OLD) # Tolerate relative INCLUDE_DIRECTORIES

# Do various processing here...

# Restore earlier policy settings
cmake_policy(POP)

有些命令隐式地将一个新的策略状态推送到栈上,在稍后某个定义的点再次弹出它。一个例子是add_subdirectory()命令,它在进入指定的子目录时将新的策略范围压入堆栈,并在命令返回时弹出它。include()命令执行类似的操作,在开始处理指定的文件之前推入一个新的策略范围,并在处理完该文件后再次弹出该文件。find_package()命令也对include()执行类似的操作,即在启动和完成与之相关的FindXXX.cmake模块文件的处理时,分别进行压入和弹出。

include()和find_package()命令还支持NO_POLICY_SCOPE选项,该选项可以防止策略栈的自动push-pop(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变量。比较以下两种设置策略行为的方法,并注意检查和执行如何使用策略一致的方法:

# Version-level policy enforcement
if(NOT CMAKE_VERSION VERSION_LESS 3.4)
 cmake_policy(VERSION 3.4)
endif()

# Individual policy-level enforcement
if(POLICY CMP0055)
 cmake_policy(SET CMP0055 NEW)
endif()

如果项目需要在本地操作多个单独的策略,那么应该使用cmake_policy(PUSH)和cmake_policy(POP)的包围该部分,以确保与作用域的其余部分隔离。特别注意任何可能退出该代码段的return()语句,并确保留下相应pop的push。还要注意的是,add_subdirectory()、include()和find_package()都自动在策略堆栈上推入和弹出策略,所以不需要显式推入和弹出,除非需要为拉入的文件的一小部分在本地更改策略设置。项目应该避免使用这些命令中的NO_POLICY_SCOPE关键字,因为它仅用于处理早期CMake版本的行为变化,已经很少使用于新项目了。

为了避免修改函数内的策略设置。函数没有引入新的策略范围,如果没有使用适当的push-pop正确地隔离更改,则策略更改可能会影响调用方。此外,函数实现的策略设置来自函数的范围,而不是调用函数的范围。因此,最好在定义函数的范围内调整策略设置,而不是在函数本身内。

作为最后的部分,允许开发人员或项目为解决一些具体政策的情况,使用CMAKE_POLICY_DEFAULT_CMPxxxx CMAKE_POLICY_WARNING_CMPxxxx变量。开发人员可以使用它们临时更改特定策略设置的默认值,或者防止有关特定策略的警告。项目应该避免设置这些变量,以便开发人员能够在本地进行控制,但在某些情况下,即使调用cmake_minimum_required()或cmake_policy(VERSION),也可以使用它们来确保特定策略的行为或警告持续存在。不过,在可能的情况下,项目应该尝试更新到新式行为,而非依赖于这些变量。

Last updated