第19章:指定版本
版本控制是一件应该重视的事情。版本号传达给用户的信息的重要性经常被低估,导致用户无法满足预期,或者对版本变化感到困惑。市场营销和版本控制策略如何影响构建、打包等的技术之间也不可避免地存在着看微妙的关系。早期思考和建立这些项目的定位,好于交付之后再思考。本章探讨了实现有效版本控制策略的方法,利用CMake特性来提供健壮、高效的过程。
19.1. 项目版本
项目版本通常需要在顶级CMakeLists.txt文件的开始定义,以便构建的各个部分可以引用它。源代码可能想要嵌入项目版本,以便显示给用户或记录在日志文件中,打包步骤可能需要它来定义发布版本的细节等等。可以简单地在CMakeLists.txt文件的开头附近设置一个变量,记录需要的版本号,如下所示:
如果需要提取单独的组件,可能需要定义复杂的变量集:
不同的项目可能对变量的命名使用不同的规定。版本号的结构也因项目而异,因此缺乏一致性使得将许多项目合并成一个更大的集合或超级构建(28.1节“超级构建结构”中讨论)变得更加难以理解。CMake 3.0引入了新的功能,可以更容易地指定版本细节,并为项目版本编号带来了一致性。VERSION关键字添加到project()命令中,格式为major.minor.patch.tweak。根据该信息,可以自动填充一组变量,使完整的版本字符串以及每个版本组件对其余部分单独可用。如果提供的版本字符串省略了某些部分(例如,经常省略微调部分),则相应的变量将保留为空。下表显示了当VERSION关键字与project()命令一起使用时,如何自动填充的版本变量:
PROJECT_VERSION
projectName_VERSION
PROJECT_VERSION_MAJOR
projectName_VERSION_MAJOR
PROJECT_VERSION_MINOR
projectName_VERSION_MINOR
PROJECT_VERSION_PATCH
projectName_VERSION_PATCH
PROJECT_VERSION_TWEAK
projectName_VERSION_TWEAK
这两组变量的用途略有不同,项目的projectName_…
变量可以用于从当前目录范围或以下的任何地方获取版本细节。像project(FooBar VERSION 2.7.3)这样的调用会产生名为FooBarVERSION、FooBar_VERSION_MAJOR等变量。不能以相同的参数project()调用两次,所以这些特定于项目的变量不会在其他project()调用时覆盖。另一方面,`PROJECT…`变量在每次调用project()时都会更新,所以它们可以用来提供当前或以上范围内对project()最近一次调用的版本细节。CMake 3.12在顶级CMakeLists.txt文件调用project()时,还提供了一组相似的变量版本。这些变量是:
CMAKE_PROJECT_VERSION
CMAKE_PROJECT_VERSION_MAJOR
CMAKE_PROJECT_VERSION_MINOR
CMAKE_PROJECT_VERSION_PATCH
CMAKE_PROJECT_VERSION_TWEAK
同样的模式也用于为项目名称、描述和主页url提供变量,后两者分别在CMake版本3.9和3.12中添加。作为一个通用的指南,PROJECT_…
变量对于泛型代码(尤其是模块)很有用,可以用来为打包或文档之类的定义默认值。CMAKE_PROJECT_…
变量有时也用作默认值,因为它们通常使用于项目顶部,所以可靠性没那么高。projectName_…
变量是最健壮的,因为在提供项目细节时总是明确无误的。
当处理支持CMake 3.0之前版本时,有时会定义自己的版本变量,这与CMake 3.0及以后版本自动定义的变量有冲突。这可能导致CMP0048策略警告,会显示冲突。下面是导致这种警告的代码示例:
上面的示例中,显式设置FooBar_VERSION变量,但是该变量名与project()命令将与自动定义的变量冲突。生成策略警告旨在鼓励项目使用不同的变量名,或者将CMake的最小版本更新到3.0,并在project()命令中设置版本。
19.2. 让源码了解版本信息
CMakeLists.txt文件中定义了版本细节之后,非常常见的需求是使它们可用于编译的源代码。可以使用许多不同的方法,每一种都有自己的优点和缺点。CMake最常用的技术是在项目的顶层添加一个编译器定义:
这使得版本信息作为一个原始字符串,可以这样使用:
虽然这种方法简单,但将定义添加到项目中每个单独文件的编译中会有一些缺点。除了对每个文件的执行命令行编译,这也意味着任何时候版本号变化,会让整个项目重新构建。这可能是一个次要的问题,但是定期在源代码控制系统的不同分支之间切换的开发人员,肯定会对不必要的重新编译感到恼火。稍微好一点的方法是,使用源属性为需要FOOBAR_VERSION符号的文件进行定义。例如:
避免了向每个文件添加编译器定义,而只是将其添加到需要它的文件中。然而,当设置单独的源属性时,会对构建依赖关系产生负面影响,这将导致重新构建的文件比需要的要多。因此,这种方法可能看起来是一种改进,但通常不可行。
与在命令行上传递版本信息不同,另一种常见方法是使用configure_file()编写提供版本细节的头文件。例如:
foobar_version.h.in
main.cpp
CMakeLists.txt
foobar_version.h.in中的+0,对于minor、patch和tweak部件来说是必要的,以便在忽略版本组件的情况下允许对应的变量为空。
通过头文件提供版本信息是对以前技术的改进。版本信息不包含在任何源文件编译的命令行中,只有那些#include foobar_version.h头文件的源文件,在版本信息更改时才需要重新编译。提供不同的版本组件,而不仅是版本字符串也不会对命令行产生影响。然而,如果在许多不同的源文件中都需要版本号,这仍然可能导致需要更多源文件进行重新编译。通过将实现从头文件移到.cpp文件中,并将其编译为库,可以进一步改进这种方法。
foobar_version.h
foobar_version.cpp.in
CMakeLists.txt
这种安排没有前面方法的缺点。当版本信息发生变化时,只需要重新编译一个源文件(生成的foobar_version.cpp文件),而foobar和fooToolkit目标只需要重新链接。foobar_version.h永远不会改变,所以当版本细节改变时,任何依赖于它的文件都不会重编。也没有向任何源文件的编译命令行添加任何选项,因此不会因更改版本信息而触发重新编译。
项目提供库和头文件作为发布包的一部分,上述方法也是健壮的。头文件不包含版本细节,而库中包含。因此,使用库的代码可以调用版本函数,并且确认它们接收到的细节是构建库时使用的。这在复杂的环境中很有帮助,这种环境中,可能会安装项目的多个版本,而不一定按照项目的预期结构进行构造。
这种方法的一种变体使foobar_version成为一个对象库,而不是一个静态库。最终的结果或多或少是相同的,但是从中获得的好处并不多。使其成为动态库会失去一些健壮性优势,并且会增加复杂性,收效甚微,因此通常建议将这些版本库设置为静态。
19.3. 源代码控制提交
对于项目来说,记录与源代码控制系统相关的详细信息很正常。这包括构建时源的修订或提交哈希、当前分支的名称或最近的标记等等。上面介绍的方式,是通过专用的.cpp文件提供版本详细信息,很适合添加更多函数来返回这些信息。例如,当前的git的哈希值可以相对容易的提供:
foobar_version.cpp.in
CMakeLists.txt
有趣一点的示例,查看指定文件自创建以来,产生了多少次提交。考虑将项目的版本信息会嵌入到一个单独的文件中,这个单独的文件中只有项目版本号。这样就可以合理地假设文件只在版本号改变时才会改变。因此,查看自当前分支上的文件更改提交数量,通常是统计自上一个版本更新以来的提交数量的好方法。
下面的示例将项目版本移出到一个名为projectVersionDetails.cmake的单独文件中,并通过生成的foobar_version.cpp文件中的新函数提供提交的数量。它演示了一种模式,这种模式适用于通过顶级project()设置版本信息,但如果父项目合并到更大的项目层次结构中,这不会干扰父项目。
foobar_version.cpp.in
projectVersionDetails.cmake
CMakeLists.txt
上述方法输出文件最后修改版本的git哈希,然后使用git rev-list
获取整个存储库的提交列表。提交最初是作为一个哈希的字符串找到的,然后通过使用列表分隔符(;)替换换行字符,转换为CMake列表。然后,list()命令简单地计算列表中有多少项,给出提交的数量。一种更简单的方法是使用git rev-list --count
直接获取数字,但是旧版本的git不支持--count
选项,因此如果需要支持旧版本的git,最好使用上述方法。
一些项目使用git描述来提供各种细节,包括分支名称、最近的标签等等,但请注意,标签和分支细节可以在不改变提交的情况下改变。如果分支或标记移动或重命名,构建不可重复。如果版本信息只依赖于文件提交哈希,则不会产生这样的缺陷。构建确认提交没有错误之后,这也为项目提供了创建、重命名或删除标签的自由(想象一下在持续集成构建、测试等确认没有问题之后,应该要提交发布标签)。
像Subversion这样的源代码控制系统还面临其他挑战。一方面,Subversion为整个存储库维护一个全局修订号,因此不需要首先获取提交哈希值,然后对其进行计数。Subversion还有一个复杂之处,它允许混合不同文件的不同版本。因此,如果开发人员签出文件的不同版本,而不去管项目版本文件,那么上面为git列出的方法就会失效。这不是自动化持续集成预期的方式,但对于在自己的本地工作的开发人员则有可能,这取决于他们偏好的工作方式。
对上述技术的另一个考虑是,将什么强制更新到生成的.cpp文件版本。如果项目版本文件改变了,CMake可以确保配置步骤重新运行,因为它是以include()的形势进入主CMakeLists.txt文件的。如果对其他文件进行了提交,CMake将不会了解。版本控制系统中实现钩子(比如git的post-commit钩子)来强制CMake重新运行是可能的,但这更可能让开发人员烦恼。最终,会在便利性和健壮性之间做出妥协。也就是说,源代码控制细节的准确性可能只对发布至关重要,而且应该足够简单,以确保发布过程可以显式地调用CMake。
19.4. 推荐
项目不需要遵循任何特定的版本控制系统,但是需要遵循major.minor.patch.tweak格式。CMake的某些功能是免费的,新开发人员可以更容易地理解项目使用的版本。后面的章节中,版本格式在打包发布时更加重要,但是许多项目在运行时报告他们自己的版本号,版本格式也会影响构建。
组成版本格式的每个数字的含义由项目决定,但有一些约定是用户期望的。例如,major通常意味着一个重要的发布,通常涉及到不向后兼容的变更或者代表项目方向上的变更。minor发生了变化,用户会倾向于将其视为一个增量版本,可能是在不破坏现有行为的情况下添加新特性。只修改patch时,用户不会认为这是一个特别重要的更改,希望它是相对较小的更改,比如修复一些bug,但不引入新功能。tweak会经常忽略,除了比patch更不重要之外,它往往没有一个常见的解释。请注意,这些只是一般性的观察结果,项目可以给予版本号完全不同的含义。为了最终的简单性,一个项目可能只使用一个单独的数字,有效地指定每个版本作为一个新的major。虽然这很容易实现,但无法给用户提供更多的指导,并且需要高质量的发布说明来管理每个版本之间的区别,从而满足用户的期望。
project()命令的VERSION关键字是CMake在major.minor.patch.tweak中提供的一个例子。项目提供一个版本字符串,project()命令自动定义一组变量,使版本号的各个部分可用。一些CMake模块可以使用这些变量作为某些元数据的默认值,所以通常建议使用project()命令中的VERSION关键字来设置项目版本。这个关键字是CMake 3.0中添加,但是如果支持旧的CMake版本,这个功能仍然需要考虑。项目不应该定义与自动定义的名称冲突的变量,否则之后的CMake版本会发出警告。避免显式地设置名称为xxx_VERSION或xxx_VERSION_yyy的变量,避免此类警告。
定义版本号时,考虑在自己的专用文件中这样做,然后CMake通过include()命令将其拉入。这使得项目可以利用版本号的变化,这些版本号与项目的源代码控制系统所看到的文件中的变化保持一致。为了减少版本更改时不必要的重新编译,可以生成一个.c或.cpp文件,其中包含返回版本细节的函数,而不是将这些细节嵌入生成在头文件中,或者作为编译器定义在命令行上传递。还要确保这些函数提供的名称包含特定于项目的内容,或者将它们放置在特定于项目的命名空间中。这允许在许多项目之间可以使用相同的模式,这些项目也可合并到单个构建中,而不会导致名称冲突。
项目的早期建立版本控制策略和实现模式,有助于开发人员清楚地了解如何更新版本细节,并鼓励在第一次交付的压力到来前就考虑发布的过程。还允许较低效率的方法(需要尽早淘汰),以便在版本号变更,以及相似构建时间变得更重要前,最大化构建的效率。
Last updated
Was this helpful?