我们用不同的版本号来区分软件或程序库的版本差异,使用版本号的目的,根本上是想要进行版本管理,开发者应建立对版本号的共同认识,赋予号码间有明确的界线与意义,并进一步对版本之间的功能差异与兼容性负起责任。在各平台、软件、程序库之间,确实充满了各式不同的版本号,然而混乱之间倒也存在若干交集,尽管交集的界线模糊,开发者认识这些交集,对于版本辨识仍有帮助。粗浅地先从软件版本周期来看,开发者通常会看到 Alpha、Beta、Release Candidate(RC)、Stable 等名词。
版本名称 | 版本说明 |
---|---|
Alpha | Alpha 是希腊字母中的第一个字母,因而 Alpha 版本是软件发布周期中的第一个阶段,功能必然未完善,主要做为内部测试,或邀请外部相关人员(客户、合作伙伴等)参与测试之用。 |
Beta | Beta 通常是最早对外公开、可公众参与测试的版本,亦称为 Preview、Early Access Releases 等,这个阶段的版本包括承诺的功能,不过会有未修复的 BUG 或安全问题,通常用来大规模测试程序、收集使用者经验或测试市场反应,进而反馈作为修正软件的依据。 |
Release_Candidate | Release Candidate 是正式版发布前的候选版本,如果没有问题,也有可能成为正式版本,有时会有 RC1、RC2 等两个或更多。 |
Stable | Stable 就是正式稳定的发布版本,有时也常称为 General Availability(GA)。 |
版本名词中也常见 SNAPSHOT,就字词为「快照」之意,在软件开发中是用来进行版本修订控制的一种概念,用以表示某特定时间特定容器(Repository)下的原始码状态。
SNAPSHOT 代表处于开发中且经常更新的不稳定版本,在一些工具中,SNAPSHOT 有着具体之意义。例如 Maven 中,SNAPSHOT 版本号与每个阶段发布的版本号对应,如果程序库版本被设置为 Lib-0.3-SNAPSHOT,发布版本号就是 0.3;然而若程序库原始码变更、建构、重新部署时,Maven 都会加上时间戳,像是 Lib-0.3-20130103-115825-12.jar;如果有其他应用程序依赖在 Lib-0.3-SNAPSHOT,那就可以根据时间戳来判断、下载最新的 JAR 包。这可以避免版本号升得太快,或使用单一版本号无法取得更新的问题;相对地,对于应用程序正式环境来说,不得使用 SNAPSHOT 版本。
混乱版本号的交集
最常见的版本号制订方式,就是 major.minor(.build) 方式。
字段 | 字段说明 |
---|---|
major | major 字面意义是重大之意,正式版本前通常为 0.x 形式,首个正式版本则为 1.0,当有重大功能变更、架构变更时会增加 major 号。 |
minor | minor 字面意义是较小之意,用于小规模的功能增加、调整或变动,像是小部份 API 功能新增或加强时,会变更 minor 号。 |
build | 如果有 build 版本部份,通常是指 BUG 修正后,重新建构发布的版本变更。 |
在最常见的 major.minor 版本号制订方式之下,可以看到各种形式的混搭变化。Linux 核心版本有个不成文规范:minor 若是奇数,代表着不稳定版本,偶数则表示稳定版本;有时号码后面会有 a、b、rc 等字样,代表着 alpha、beta、release candidate,像是 1.0a、2.0b1、3.0rc2 等;build 版本号之后,可能结合日期或时间戳,或者直接以日期时间作为 build 版本号区段,像是 App-1.3.20130103。
对于具有标准规范的技术,由于同一规范下可以有不同的作品,规格书版本也必须有所规范,在 Oracle 官方《The Java Tutorial》中〈Setting Package Version Information〉就可以看到,它建议了在 JAR 档案的 manifest 中应该包括的版本信息,并建议应该区别规格书版本(Specification-Version)与实施版本(Implementation-Version)。
〈Java Product Versioning〉建议规格书版本号的格式为 major.minor.micro,只不过,对 major、minor、micro 没有清楚的定义,major.minor(.other) 似乎是版本号格式共同的交集,只不过什么是重大功能变更?哪些又该视为小规模功能增加呢?许多软件或程序库即使有专门文件说明版本号格式,然而,递增各区段版本号的递增规则都没有清楚地定义。
语意化版本规范
鉴于对版本号没有严谨的定义,Gravatars 创始人兼 GitHub 共同创办者 Tom Preston-Wernere 建立了〈Semantic Versioning〉,试着对版本号格式与递增方式进行清楚的规范。
这份规范是「根据(但不局限于)已经被各种封闭、开放源码软体所广泛使用的惯例所设计」,版本号格式为 MAJOR.MINOR.PATCH,版号递增规则中,定义「MAJOR」为做了不相容的 API 修改,「MINOR」为做了向下相容的功能性新增,「PATCH」则为做了向下相容的问题修正。这个规范中提到:「为了让这套理论运作,你必须先有定义好的公共API」,只有这样才可以「透过修改相应的版号来向大家说明你的修改」。
必须先有定义好的公共 API,等于要求你必须有清楚的规格,无论是通过文件定义或程序代码强制要求来实现。以 Java 来看,〈Semantic Versioning〉的 MAJOR.MINOR 就相当于〈Java Product Versioning〉中定义的规格书版本号码,也就是说,程序库中有了任何不兼容的修改,必须递增规格书的 MAJOR,只是在程序库中新增 API 的话则必须递增规格书的 MINOR,〈Semantic Versioning〉的 PATCH,则可对应至 JAR 档案中 manifest 的实际版本信息,因为 BUG 修正属于实际的处理范围。
从另一个角度来看,若只是递增 MINOR.PATCH 版本号,表示没有破坏兼容性,遵守〈Semantic Versioning〉的规范,可以解决「相依性地狱(Dependency hell)」的问题。以 Java 来说,如果应用程序依于程序库 ABC 2.1.0,而程序库版本号遵守〈Semantic Versioning〉规范,那么将来可以放心地使用 2.1.0 至小于 3.0.0 的 ABC 程序库版本,也就是说,只要 MAJOR 号码不递增,API都是相容的。
在版本号码上清楚地表达出兼容性,是〈Semantic Versioning〉要达到的目的之一,另一目的就是建立对版本号码的共同认知,这个认知必须简单但没有岐义,在程序库的 README 中,若看到有声明使用并遵循这些规则时,开发者在未确认版本号相关文件,也能清楚了解版本号代表的兼容性问题。
版本变更需要明确与责任
〈Semantic Versioning〉的规范并不长,我在阅读规范的过程中,脑海浮现了 OpenStack 上〈GIT Commit Good Practice〉谈到,Commit 时最重要的原则就是「确定每个 Commit 只会有一个逻辑上的变更」,并应避免「混杂两个无关的功能变更」,或是「在单一个巨大 Commit 中送出了一堆新特性」。虽然,是从更微观的角度来看待版本变更这回事,不过似乎与〈Semantic Versioning〉规范有相同的道理,也就是版本变更必须有明确的界线。
〈Semantic Versioning〉表面看来只是将各软件与程序库在版本号上做的事情,取得大的交集,实际上,它要求开发者审慎思考规格建立与版本兼容性问题,规范中第一条要求,必须定义精确且稳定的公共 API,就是在要求开发者制订规则,如果开发者难以制订规则,每天都在改变 API,「那么你应该仍在主版号为零的阶段(0.yz) 」,只有规格明确且稳定的 API,才可以是 1.0.0 版本。
有了第一个公共 API 之后,每个版号的增加就都要有明确决策,像 Maven 这类可管理相依性的工具,只是忠实地依据算法及给定的版本号进行管理,只有开发者自己才能知道「这只是个BUG修正?」、「这是个新特性?」、「这个变更是否破坏兼容性?」等问题。
任何公共 API 的不兼容无论幅度多小,都得变更 MAJOR 号码,这说明了公共 API 任何程度的不兼容,都会是巨大的变更,就如同〈Semantic Versioning〉中〈FAQ〉提到的: 「这是开发的责任感和前瞻性的问题。不兼容的改变不应该轻易被加入到有许多相依性程序代码的软件中」。