在软件管理世界里存在着被称作“依赖地狱”的死亡之谷,系统规模越大,引入的程序包越多,你就越有可能在不久的将来发现自己深陷绝望之中。
在多依赖的系统中发布新版本程序包很快成为噩梦。如果依赖关系过紧密,可能面临版本控制被锁死的风险(必须对每一个依赖程序包改版才能完成某次升级)。而如果依赖关系过于松散,又无法避免版本混乱(产生超过合理值的版本数
)。当你项目的进展由于版本控制被锁死和/或版本混乱变得不那么简便和可靠,也就意味着你正处于依赖地狱之中。
为了解决这个问题,我提议通过一些规则和约束来表述版本号如何命名及何时更新。要使此系统正常运作,你首先需要声明一个公共应用程序接口(以下简称API)。可以以文档形式或代码形式实施
。需要注意的是,这个API必须是清晰和明确的。一旦公共API确定下来,你将通过版本号增量来描述版本修改。形如X.Y.Z(主版本号.副版本号.补丁号)这样的版本格式。通过增加补丁号来表示不影响API的错误修复,增加副版本号来表示兼容现有API的扩展/修改,而增加主版本号则表示不兼容现有API的修改。
我称这套系统为“语义化的版本控制”,在这套约定下,版本号及其更新方式 包含了相邻版本间的底层
代码和修改部分的信息。
文档中出现的“必须”,“禁止”,“要求”,“应该”,“不该”,“可能”,“可能不”,“建议”,“也许”,和“可选”按RFC 2119规范解读。
-
使用语义化版本控制的软件必须 声明公共API。该API可以在代码中声明也可以固化为文档。无论何种形式,API
应该
是明确而全面的。 -
标准的版本号必须采用X.Y.Z的格式,其中X,Y,和Z为非负的整数。X是主版本号,Y是副版本号,而Z为补丁号。每个元素必须 取数值1为增量。例如:1.9.1->1.10.0->1.11.0。
-
标记版本号的软件包发布后,禁止改变该版本软件包的内容。任何修改都必须以新版本发布。
-
主版本号为0(0.y.z)的软件处于开发初始阶段,一切都可能随时被改变。这样的公共API不应该被视为稳定版。
-
1.0.0版本用于
界定
公共API的形成。这一版本之后所有的版本号更新都基于公共API及其修改。 -
补丁号Z(x.y.Z | x>0)必须增加,仅在兼容原有接口的错误修复被引入时。错误修复指的是对不正确反应修复而进行的
内部修改
。 -
副版本号Y (x.Y.z | x>0)必须增加,如果新的、兼容原有接口的功能被引入公共API;或者任何公共API被标记为弃用。副版本号可能增加,如果大量新功能或者改进被通过私有代码引入。这一过程中可能包含补丁级别的改变。当副版本号增加时补丁号必须置零。
-
主版本号X (X.y.z | X>0)必须增加,如果任何不兼容原有接口的改变被引入到公共API,这一过程中可能包含副版本级别和补丁级别的改变。当主版本号增加时补丁号和副版本号必须置零。
-
预发布版本可以通过紧跟在补丁号后的一个破折号和一系列点号分隔的标识符来修饰。这些标识符必须由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.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 。
这并不是一个新的或者革命性的主意。实际上,你可能已经在做一些近似的事情了。问题在于光“近似”还不够。如果没有一些正式的规范可循,版本号对于依赖管理并无实质意义。通过为这一主意命名并给予清晰的定义,让你向软件使用者传达意向变得更为轻松。一旦这些意向变得清晰,灵活(而不过于灵活)依赖关系就能最终确定。
举个例子来展示语义化的版本控制如何让依赖地狱成为过去。假设有一个名为“救火车”的库依赖另一个名为“梯子”的已纳入语义版本控制的库。救火车创建时,梯子的版本号为3.1.0。因为救火车调用的是3.1.0版本中的一些功能函数,你可以放心的指定梯子依赖为版本号大等于3.1.0而小于4.0.0。这样,当梯子版本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的合理定义,可以保证每个人每件事运行顺畅。
一旦发现自己破坏了语义化版本控制规则,要尽快修复问题,并发布一个纠正问题且兼容原有接口的副版本。记住,修改一个已发布版本的内容是不可接受的,即便在这种情况下。合适的情况下,将犯错的版本写入文档,告诉使用者问题所在,让他们能够意识到这是有问题的版本。
如果不影响公共API则认为此次更新是兼容的。和你的程序包有相同依赖关系的软件应该会有它自己的依赖关系说明,如有冲突软件作者会在其中提出。此次改变是补丁级别还是副版本级别则取决于你更新依赖关系是想要修复bug还是引入新功能。后一种情况通常伴随着附加代码,这显然应该被判为副版本级别的更新。
依靠你的判断,如果你有一个大用户会由于行为回滚到公共API文档而彻底困扰
,那么你可能应该进行一次主版本级别的更新,尽管这些修复从严格意义上来说是补丁级别发布。记住,语义化的版本控制的全部精义在于通过版本号的变化来传达意向。如果这些改变对于用户来说是重要的,请通过变更版本号来通知他们。
弃用现存的功能是软件开发中的家常便饭,也通常是向前发展所必须的。但当你弃用公共API的一部分时,你应该做两件事:(1)更新文档以便使用者知道这个变化。(2)发行不包含弃用功能的副版本。在新主版本中完全移除弃用功能前,至少应有一个不包含弃用功能的副版本发布,以便使用者能够平滑过渡到新API。
语义化的版本控制说明由Gravatars创办者兼Github共同创办者 Tom Preston-Werner 所著。
如需反馈,请在Github上创建issue。
创作共用 - CC BY 3.0 http://creativecommons.org/licenses/by/3.0/