在软件管理过程中,随着系统规模的扩大,会有越来越多的模块被加入进来,包与包之间的依赖关系往往会让我们陷入痛苦的境地。
当系统中有很多依赖关系存在时,发布新版本的包会成为一场恶梦。如果依赖过于紧密,就可能会产生“版本锁”,即发布包的新版本时需要发布其依赖包的新版本;如果依赖关系很松散,这样还是会陷入混乱,因为需要对更多的包进行兼容。这些问题往往会成为项目向前推进的绊脚石。
为了解决这个问题,我拟定了一组简单的规则,用来规范软件发布中的版本号命名。在实施这组规则之前,我们需要一套API接口。这个接口可以是一组文档,也可以是代码实现。最重要的是,这套API接口必须是明确和精准的。在确认了这套API接口之后,对它的任何改动都要通过特定的版本号递增规则来进行。假设版本号的格式为X.Y.Z(主版本.次版本.补丁)。修复Bug会递增补丁版本,向前兼容的API修改或新增功能会递增次版本,向前不兼容的API修改会递增主版本。
我给这套规则起名为“版本号的语义命名法”。在这种规则下,版本号的递增过程是带有特定含义的。
下文中出现的关键字“必须”、“禁止”、“应该”、“不应该”、“建议”、“可以”等名词的含义与RFC 2119规范中定义的一致。
-
使用版本号语义命名法的软件必须提供一组公共的API接口。这套API接口可以是由代码实现的,也可以是由文档所规范的。无论是哪一种情况,API都应该要精确。
-
普通的版本号必须以X.Y.Z的形式命名。X、Y、Z都是正整数。X表示主版本,Y表示次版本,Z表示补丁版本。每个版本号元素必须以1为单位递增,如1.9.0 -> 1.10.0 -> 1.11.0。
-
当某个版本的软件包发布后,该包的内容禁止再做更改。每一次修改都必须发布一个新的版本。
-
主版本号为0的版本(0.y.z)是初始开发版本,任何内容都可以随时做出修改。此时的API应视为不稳定的。
-
1.0.0版定义了公共API接口。此后的版本号递增规则就需要按照API的修改或新增内容来区别对待。
-
当发生BUG修复操作,且该修复是向前兼容的,则递增补丁版本Z(x.y.Z,其中x大于0)。BUG修复是指修正软件不正确行为的内部修改。
-
当公共API添加了新功能,且该修改是向前兼容的,则递增次版本(x.Y.z,其中x大于0)。当公共API的某个功能被标记为弃用,则必须递增次版本。当有全新的功能或优化被引入私有代码,则可以递增次版本。当补丁版本级别的变动发生时,也可以选择递增次版本。次版本递增时,补丁版本必须重置为0。
-
当公共API发生任何向前不兼容的变动时,必须递增主版本(X.y.z,其中X大于0)。当发生补丁版本级别或次版本级别的变动时,也可以选择递增主版本。主版本递增时,次版本和补丁版本必须都重置为0。
-
表示预发布版本号时,可以在补丁版本号之后添加一个破折号,以及一组由点号相连的标识符。标识符必须由ASCII码表中的字母、数字、以及破折号组成,即[0-9A-Za-z-]。预发布版本号的优先级要比相应的正常版本号低。预发布版本号示例:1.0.0-alpha, 1.0.0-alpha.1, 1.0.0-0.3.7, 1.0.0-x.7.z.92。
-
表示构建版本号时,可以在补丁版本号或预发布版本号之后添加一个加号,以及一组由点号相连的标识符。标识符必须由ASCII码表中的字母、数字、以及破折号组成,即[0-9A-Za-z-]。构建版本号的优先级要比相应的正常版本号高。构建版本号示例:1.0.0+build.1, 1.3.7+build.11.e0f985a。
-
版本号优先级的比较方式为:将版本号按主版本、次版本、补丁版本、预发布版本、构建版本分割;主版本、次版本、补丁版本按数字大小比较;预发布版本和构建版本需要进一步按句点分割,每一部分使用如下方式比较:若为数字,则按照数字大小比较;若为字母或破折号,则按照其在ASCII码表中的顺序比较;若对应部分分别为数字和字母,则数字一方的优先级低。示例:1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-beta.2 < 1.0.0-beta.11 < 1.0.0-rc.1 < 1.0.0-rc.1+build.1 < 1.0.0 < 1.0.0+0.3.7 < 1.3.7+build < 1.3.7+build.2.b8f12d7 < 1.3.7+build.11.e0f985a。
这种命名方法并不是一个全新的概念,事实上,你目前使用的版本号命名方法可能已经与上文中的规范相近。但是,“相近”还不够,如果不能严格按照某个标准来进行命名,这样的版本号在依赖管理中就是无用的。在对版本号有了清晰的定义后,就能很方便地和软件用户进行交流。一旦版本号的变化规律得以明确,那依赖管理也能够较好地进行。
为什么说版本号的语义命名可以让依赖管理更好地进行?以下举一个简单的示例。有一个名为“Firetruck”的库,需要调用名为“Ladder”的包,该包的版本号使用了语义命名法。Firetruck创建之初,Ladder的版本号是3.1.0。Firetruck中使用了Ladder在3.1.0版本才开始有的新特性,因此可以确定只要Ladder的版本号在3.1.0和4.0.0之间(不含4.0.0),都是可以被Firetruck使用的。当Ladder发布了3.1.1和3.2.0版本时,你可以很放心地将它们添加到依赖管理系统中。
作为一个责任心强的开发人员,当有新包发布时,你一定会对照它的更新日志,验证其功能的完整性。现实世界是混乱的,除了谨小慎微,我们别无他法。语义命名法所能帮到你的,就是在发布或升级软件包时,不用逐一检验依赖关系,从而节省时间。
如果以上所说都符合你的需求,你所要做的就是对外声明自己在使用语义命名法,并在项目的README中添加通往本站点的链接,那么他人就能了解这一规范,并从中受益。
最简单的方法是,将最初的开发版本号定为0.1.0,并在后续的开发中逐一递增次版本号。
如果你的软件已经在产品中投入使用,那它应该已经是1.0.0版本了;如果你提供的公共API已经有用户使用,那也应该发布1.0.0版本;如果你已经开始担心向前兼容的问题,那这时你也应该已经发布了1.0.0版本。
主版本为0的版本号就是为了敏捷开发而存在的。如果你每天都需要更新API,那就只有两种可能:一是它仍在0.x.x版本下;二是你们创建了一个独立的开发分支,准备发布下一个主版本。
这个问题涉及到的是如何负责任地开发,以及开发的计划性。如果软件包被很多其他代码依赖,那么轻易地引入向前不兼容的改动是很不合理的,会有很高的代价。因此,在递增主版本之前,你需要详细考量变动所带来的影响,它的成本和收益。
如何恰当地为软件撰写使用文档,这是开发者的责任。正确管理软件的复杂性往往决定了项目开发是否高效。如果没有人知道如何使用你的软件,哪些方法是能够正确地调用的,那就无法管理项目了。以长远的眼光来看,版本号的语义命名法配合详尽的软件说明文档,可以让项目运作得更为顺畅。
一旦发现破坏了语义命名的规则,你就应该立即发布一个新的次版本,重新恢复向前兼容。记住,即使在这样的失误中,对已发布的版本进行修改也是严格禁止的。如果可以,应该在错误版本的发布说明中向用户解释这次错误,从而避免其他问题。
这种行为是向前兼容的,因为并没有影响到公共API。含有依赖项的软件包应该在发布说明中注明其所依赖的软件包清单,并对可能出现的包冲突做出提示。至于这次更新应递增补丁版本还是次版本,取决于对该依赖项的升级是为了修复BUG还是添加新功能。如果是添加新功能,就意味着代码量的增加,显然应该递增次版本。
这时你就需要判断清楚,如果此次修改会对该API的用户群造成很大的影响,那最好还是递增主版本号,即使只是修复一个BUG。记住,版本号语义命名法的目的就是为了让版本号的递增规律携带特定的含义,如果软件包的变化会影响到用户,就应该使用正确的版本号变动方式来告知他们。
软件开发中,弃用现存的功能是很常见的。在你决定弃用公共API中的某项功能时,需要做两件事情:1)更新API文档,让用户知道本次修改;2)递增次版本。通常来说,当决定弃用某个API时,在递增主版本之前,应至少发布一个次版本,从而让用户能够平滑地切换到新API中。
版本号语义命名法的规范文档由Gravatar的发明者、GitHub的合伙创始人Tom Preston-Werner撰写。
如果你想给我一些反馈,可以在GitHub中创建一个话题.
Creative Commons - CC BY 3.0 http://creativecommons.org/licenses/by/3.0/