【CMake】《CMake构建实战:项目开发卷》笔记-Chapter10-策略与向后兼容
第10章 策略与向后兼容
CMake相当重视向后兼容性,并且受益于此,能够持续不断地改进和增加新特性,而几乎不会破坏古老的代码仓库。这一点在第2章介绍CMake的特点时已经提到过。CMake的策略机制就是为解决向后兼容问题而生的。
有了策略机制,CMake可以基本确保基于旧版本CMake编写的目录程序可以被新版本的CMake配置生成,同时,如果程序中使用了已经弃用的特性,它也能针对性地给出警告信息,鼓励或要求用户重构CMake程序。
CMake不是一味地妥协,也会在较长时间后逐渐移除这些“遗产”,使得CMake的代码能够有机会焕然一新,而不是总在积累技术债务。当然,策略机制也一定会告诉我们是否使用了已被彻底移除的特性,从而可以让我们根据产生的错误信息,对CMake程序进行重构。
是不是感到有些神奇,CMake的策略机制到底是如何实现的呢?下面一起来了解一下吧!
10.1 CMake策略(以CMP0115为例)
CMake策略(policy)的名称以CMP开头,后面跟着一个代表策略编号的四位整数,如CMP0115就是第115号策略。每一个策略都对应着新旧两种不同的行为:NEW行为和OLD行为。若想了解这些策略新旧行为的具体区别,可以查阅官方文档。
借助官方文档查阅CMake策略
CMake官方文档依次列举CMake从新到旧的各个版本引入的策略,每个策略都会提供一段摘要以便于查阅。读者可以尝试去官方文档中看一下CMake 3.20版本引入的CMP0115策略,它的摘要是“Source file extensions must be explicit”,即“源文件扩展名必须显式指定”。
点击文档中这个策略的超链接,即可进入其详情页面。在CMP0115的详情页面中可以了解到,在CMake 3.19及以前的版本中,为add_executable
等命令指定<源文件>参数时,可以省略源文件的扩展名,如add_executable(A main)
,CMake会自动根据项目采用的编程语言尝试为其添加扩展名并查找对应的源文件。然而在CMake 3.20及以后的版本中,CMake要求<源文件>参数必须显式指定文件的扩展名,如add_executable(A main.cpp)
。
对于CMP0115这个策略而言,OLD行为即允许CMake隐式地为<源文件>参数追加扩展名,NEW行为即要求<源文件>参数中必须显式指定扩展名。那么,CMake是如何决定到底采用哪一种行为的呢?它又是如何在CMake版本升级后,发现新版带来的行为改变可能造成CMake程序的不兼容问题呢?这首先要归功于每一个CMake目录程序中最开始调用的命令——cmake_minimum_required
命令。
10.2 指定CMake最低版本要求:cmake_minimum_required
事实上,该命令不仅适用于目录程序。在CMake脚本程序、模块程序中都可以调用该命令设定对CMake版本的最低要求。这里暂不考虑该命令对策略的影响,先来熟悉一下它的参数形式及基本功能:
cmake_minimum_required(VERSION <最低版本>)
该命令用于指定执行当前CMake程序所需CMake的<最低版本>。<最低版本>参数格式为通用的版本号格式,即主版本号.次版本号[.补丁版本号[.修订版本号]]
,其中主版本号和次版本号是必需的。如果该命令检测到当前CMake的版本低于<最低版本>的要求,CMake会报告致命错误,终止执行。
该命令相当于设置CMake变量CMAKE_MINIMUM_REQUIRED_VERSION
的值为<最低版本>,通过获取该变量的值可以获取最近一次指定的最低版本要求。
另外,该命令应当在CMake程序文件的最开始处调用,甚至要早于project命令。毕竟,project命令也说不定会有不兼容的改变。
最低版本要求与策略设置
cmake_minimum_required
命令被调用时,除了检测当前CMake版本,还会设置当前CMake程序的策略行为:<最低版本>及之前版本的CMake引入的策略将全部被设置为NEW行为,而<最低版本>之后的CMake引入的策略将不会被设置。未被设置的策略会默认采用OLD行为,但会同时产生警告信息。
以省略源文件扩展名这一行为为例,它在CMake 3.19及以前的版本中是受到支持的,但在CMake 3.20版本中被废弃了。这一行为的变化属于破坏性变化,可能导致CMake目录程序无法成功配置生成,因而CMake为该行为引入了策略,即CMP0115。如下所示的实例中设置的CMake<最低版本>为3.19,低于正在使用的3.20版本。同时,add_executable
命令的<源文件>参数省略了main.cpp的扩展名,直接给定其基本文件名部分main。
cmake_minimum_required(VERSION 3.19)
project(min-ver)
add_executable(A main)
由于该实例中设置的<最低版本>为3.19,CMP0115策略不会被设置为NEW行为,而是处于未被设置的状态。此时,CMake 3.20版本仍会默认采用OLD行为,为<源文件>参数补全扩展名。不过,由于CMP0115策略未被显式设置,CMake在执行时会输出如下警告信息:
Policy CMP0115 is not set: Source file extensions must be explicit.
Run "cmake --help-policy CMP0115" for policy details.
Use the cmake_policy command to set the policy and suppress this warning.
该警告信息的翻译如下:
策略CMP0115未被设置:源文件扩展名必须显式指定。
执行“cmake --help-policy CMP0115”命令行查看该策略的详细信息。
调用cmake_policy命令显式设置该策略的行为以屏蔽该警告。
警告信息中提到可以使用cmake_policy
命令显式指定策略的行为。10.3节中将会介绍这一命令。
10.3 管理策略行为:cmake_policy
cmake_policy
命令有多个子命令,可以分别用于获取和显式指定策略的行为等,下面将对它们进行逐一介绍。
10.3.1 按策略名称设置策略行为
cmake_policy(SET <策略名称> NEW|OLD)
该命令可以设置<策略名称>对应的CMake策略的行为为NEW或OLD。<策略名称>即CMP加四位数字,如CMP0001。
如果我们已经对CMake程序进行了代码重构,使其兼容新版的某个策略行为,那么就可以使用该命令设置该策略采用NEW行为。例如,可以检查所有创建构建目标的命令的<源文件>参数,若均不存在省略扩展名的写法,那么就可以调用cmake_policy(SET CMP0115 NEW)
,将CMP0115策略切换到新版的行为。
当然,有时候可能没有时间去重构代码,又不想总是看到CMake输出的警告信息——此时可以显式地将策略行为设置为OLD行为,这样CMake就不会再产生警告了。
10.3.2 获取策略行为
cmake_policy(GET <策略名称> <结果变量>)
该命令可以将<策略名称>对应的CMake策略的行为获取到<结果变量>中。如果指定的策略未被设置,<结果变量>会被赋空值,否则它会被赋值为NEW或OLD。
10.3.3 按CMake版本设置策略行为
cmake_policy(VERSION <最低版本>[...<策略兼容的最高版本>])
该命令可以根据指定的CMake<最低版本>和<策略兼容的最高版本>设置全部已知策略的行为。
<最低版本>最低不能低于2.4,最高不能超过当前运行的CMake的版本号。<最低版本>及其以前的版本引入的策略将会被设置为NEW行为。
<策略兼容的最高版本>最低不能低于<最低版本>,最高没有限制,可以设置为未来的版本号。如果指定了<策略兼容的最高版本>,那么<策略兼容的最高版本>及其以前的版本引入的策略也都会被设置为NEW行为。
用更容易理解的话说,<最低版本>可以用于限制能够运行CMake程序的最低版本,如果使用的CMake版本比它低,就会产生致命错误;而<策略兼容的最高版本>表示该程序可以一直兼容到不高于<策略兼容的最高版本>的新版CMake,因此这些新版引入的新策略都可以被设置为采用NEW行为。当用户使用了比<策略兼容的最高版本>还高的新版CMake,那么从<策略兼容的最高版本>之后的版本引入的CMake策略的行为都将处于未被设置的状态,也就会产生相关的警告信息。如果用户使用的版本比<策略兼容的最高版本>更低,那么后续版本的策略实际上都还不存在,也就等同于使用OLD行为。
<策略兼容的最高版本>参数是可选参数,如果不打算兼容旧版的CMake了,就无须指定它,直接提高<最低版本>就可以了。
其实cmake_minimum_required
命令就是通过该命令来完成对CMake策略行为设置的,它同样支持指定<策略兼容的最高版本>参数,如下所示:
cmake_minimum_required(VERSION <最低版本>...<策略兼容的最高版本>)
10.3.4 管理CMake策略栈
每一个子目录的目录程序,以及include和find_package
引用的模块程序都会创建一个新的策略作用域,并将其追加到策略栈中。当我们使用cmake_policy
设置策略行为时,实际上只是在修改栈顶的策略行为。也就是说,在某个子目录或模块程序中定义的策略行为,不会影响其他子目录或其他模块程序中定义的策略行为。不过有一个例外:当调用include和find_package
命令时若指定了NO_POLICY_SCOPE
参数,它们引用的模块程序不会拥有一个新的策略栈。
cmake_policy命令提供了如下子命令用于维护策略栈:
cmake_policy(PUSH) # 入栈
cmake_policy(POP) # 出栈
这两个命令分别用于将当前设置的策略行为入栈和出栈。如果需要对CMake程序进行重构升级,这一对命令将会非常有帮助。
10.4 渐进式重构CMake程序
策略能够很好地让CMake兼容古老的代码,同时输出友好的警告信息,帮助及时偿还技术债务,重构代码以兼容新版。每当新版CMake带来破坏性改变时,它都会引入一个新策略,允许切换新旧行为。理想情况下,我们应当尽快重构代码,使全部策略均采用NEW行为。
然而偿还技术债务往往是一个艰巨的任务,因此通常会采用渐进式重构的方式。当切换到新版CMake后,为了继续兼容以前的程序,先不要修改cmake_minimum_required
中指定的最低版本。此时,所有新版CMake引入的策略都处于未定义的状态,也就会默认采用OLD行为并产生警告信息。
10.4.1 局部代码重构并启用新行为
首先应当对部分CMake代码进行重构以采用NEW行为,另外新的代码也应直接按照NEW行为来编写。此时,我们需要对这几部分代码启用NEW行为,而不影响其他部分的代码行为。这里将借助策略栈来实现细粒度的策略行为设置和恢复,如下所示。
cmake_policy(PUSH) # 入栈,即保存原先的策略行为设置
cmake_policy(SET <策略名称> NEW) # 设置策略行为为NEW
# 这里是采用该策略的NEW行为的CMake程序代码
# ...
cmake_policy(POP) # 出栈,即恢复最近一次保存的设置
当然,随着重构的不断进行,可能某个子目录下所有目录程序均已完成重构,那么就可以直接调用cmake_policy(SET <策略名称> NEW)
命令对整个目录程序采用指定策略的NEW行为。
10.4.2 禁用警告信息
在介绍cmake_policy
命令时提到过,只要将对应策略的行为显式设置为OLD行为,CMake就不会再产生警告信息了。当我们想推迟重构而又不希望被警告信息干扰时,这很有用。
cmake_policy(SET <策略名称> OLD)
如果想对局部CMake程序进行该设置,也可以借助策略栈来完成。
10.4.3 同时兼容旧版CMake
有些用户往往并不积极地升级CMake版本,因此需要让CMake程序能够同时兼容旧版和新版的CMake。策略行为总是二选一的,因此只能通过条件判断来完成新旧版本的同时兼容。
if命令支持判断策略是否存在,借助它可以根据当前CMake版本是否存在指定的策略,来进行不同的项目配置,如下所示。
cmake_minimum_required(VERSION 3.19)
project(if-policy)
if(POLICY CMP0115)
# 支持策略CMP0115,肯定是CMake 3.20及以上版本
cmake_policy(SET CMP0115 NEW)
add_executable(A main.cpp) # 必须显式指定扩展名
else()
# 仍是CMake 3.19
add_executable(A main) # 仍可使用省略扩展名的写法
endif()
本例主要演示了如何根据不同版本来进行不同的项目配置,以同时兼容多个CMake版本。其实其中大可不必使用条件判断,毕竟显式指定扩展名旧版中也支持,因此重构后完全可以仅保留新行为的写法。
10.4.4 为全部策略采用新行为
重构阶段是相当痛苦的,很可能必须同时兼容新旧两个版本,需要逐一添加条件分支。当然,最终一定会重构好全部的代码,此时就可以对全部代码的全部策略采用NEW行为了。另外,为了给其他用户升级CMake版本留下足够的过渡时间,不必急于移除这些if(POLICY)条件分支。在这种情形下,可以直接调节cmake_minimum_required
命令的<策略兼容的最高版本>参数来实现新旧版本的同时兼容,如下所示:
cmake_minimum_required(VERSION 3.19...3.20)
这样一来,使用CMake 3.19,程序仍然能够正常执行,只不过涉及新版策略的if(POLICY)条件不成立,会采用else分支中的旧版设置。如果使用CMake 3.20,则会自动地将全部策略设置为NEW行为。
这样一来,if(POLICY)条件成立的分支中的cmake_policy(SET <策略名称> NEW)
就不需要了,有助于清理冗余的代码。
10.4.5 完全切换到新版CMake
最终当然要摆脱一切技术债务,修改全部程序中cmake_minimum_required
命令的<最低版本>参数,强制所有用户使用新版CMake。如此一来,所有的if(POLICY)条件分支就可以删除了,代码焕然一新。
10.5 小结
本章围绕CMake策略这一重要特性,并以CMP0115这一策略为例,介绍了相关的概念、命令使用等。正是CMake策略保证了CMake能够长盛不衰,相信读者一定也对该特性的巧妙设计深有感触。
本章最后还从实用的角度介绍了渐进式重构CMake程序的最佳实践,希望能够帮助读者告别技术债,保持项目的活力。
至此,本书对CMake的介绍就全部结束了。第11章是本书的最后一个实践章节,我们会一起对CMake进行综合应用,完成一个相对完整的人工智能项目库——手写数字识别库。