SemVer 2.0.0 优先级比较:alpha.10 排在 alpha.9 之前,以及五个实现坑
规范原文:,英文版 。下面只挑和「比较 / 解析」直接相关的部分。
规范里的 MUST
- 版本号 MUST 是
X.Y.Z,X/Y/Z 为非负整数,MUST NOT 带前导零。依次是主版本、次版本、修订号。 - 修订号 Z 只在做向后兼容的 bug 修复时递增;次版本 Y 在引入向后兼容的新功能、或标记公共 API 弃用时递增;主版本 X 在任何不兼容变更时递增。
- 每次递增高段,低段 MUST 归零:
1.9.1 -> 1.10.0 -> 1.11.0。 - 版本发行后 MUST NOT 修改内容,任何改动只能发新版本。
- 预发布版本:在修订号后接
-和一串点分标识符。标识符只允许 ASCII 字母数字和连字符,MUST NOT 为空;纯数字标识符 MUST NOT 带前导零。例:1.0.0-alpha、1.0.0-alpha.1、1.0.0-0.3.7。预发布优先级低于对应的正式版。 - 构建元数据:在正式版或预发布版后接
+和一串点分标识符。比较优先级时 MUST 忽略构建元数据;仅构建元数据不同时视为同级。 0.y.z表示初始开发阶段,API 可能随时变更;从1.0.0起才承诺兼容。
优先级比较算法
把版本拆成 主、次、修订、预发布 四段,构建元数据直接丢掉。
- 主、次、修订按数值比较。
- 主次修订相同、且存在预发布时,预发布优先级低于正式版。
- 两个预发布比较:从左到右逐段比标识符。纯数字段按数值比,带字母或连字符的段按 ASCII 词法比。
- 数字标识符优先级总是低于非数字标识符。
- 前面全相同,字段更多的预发布优先级更高。
规范给的完整链条:
1.0.0-alpha 9,顺序反过来。同一类错误还有 `1.0.0` 与 `1.0.0-alpha`:前几字符相同、后者更长,字符串比较会把正式版排在预发布之后,但规范要求正式版优先级更高。
自己写比较函数时,预发布段为空要特判:
```python
def cmp_pre(a, b): # a、b 为点分预发布串,空串代表正式版
if a == b:
return 0
if a == "":
return 1 # 正式版 > 预发布
if b == "":
return -1
for x, y in zip(a.split("."), b.split(".")):
if x == y:
continue
if x.isdigit() and y.isdigit():
return -1 if int(x) len(b.split("."))) - (len(a.split(".")) < len(b.split(".")))
坑二:前导零非法,解析器应拒绝而不是归一化
1.02.3、1.0.0-01、1.0.0-alpha.00 都不是合法 SemVer。正则里用 0|[1-9]\d* 而不是 \d+ 就是为了挡这类输入。官方推荐的正则:
^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$
注意 v1.0.0 不匹配这个正则,Go 生态里带 v 的写法要单独处理。
坑三:构建元数据不参与比较,但去重时会被字符串相等误伤
1.0.0+build1 和 1.0.0+build2 在优先级上是同级。如果缓存 key、制品库索引、依赖去重直接用版本字符串做键,这两个会被当成不同版本,结果是同一份 1.0.0 被构建、下载或缓存两次。做集合运算前先剥掉 + 之后的部分。
坑四:0.x 的语义会让 ^ 退化
npm 的 semver 范围:~ 只允许修订号升级,^ 允许次版本升级但不改主版本。但在 0.x 下 ^ 会退化成只允许修订升级:
^0.2.3等价于~0.2.3^0.0.3只匹配精确的0.0.3^1.2.3不会匹配预发布版本,比如2.0.0-beta.1不在范围内
最后一类是「预发布默认不被范围匹配」的通用规则,不是 0.x 专属。
坑五:Go 的 v 前缀与 golang.org/x/mod/semver
Go 模块在版本号前统一加 v,形如 v1.4.2;同时在主版本升到 v2 及以上时强制语义化导入版本,模块路径必须带主版本后缀,如 example.com/mod/v2。
golang.org/x/mod/semver 只认可带 v 前缀的形式,v1 和 v1.0 会被规范化为 v1.0.0:
import (
"fmt"
"sort"
"golang.org/x/mod/semver"
)
func main() {
vs := []string{"v1.0.0-alpha.10", "v1.0.0-alpha.9", "v1.0.0", "v1.0.0-beta.2"}
sort.Slice(vs, func(i, j int) bool { return semver.Compare(vs[i], vs[j]) < 0 })
fmt.Println(vs)
// [v1.0.0-alpha.9 v1.0.0-alpha.10 v1.0.0-beta.2 v1.0.0]
fmt.Println(semver.IsValid("1.0.0")) // false,缺 v 前缀
fmt.Println(semver.Max("v1.2.3", "v1.10.0")) // v1.10.0
}
写 go.mod 时 require 必须带 v,require example.com/mod v1.2.3 合法,require example.com/mod 1.2.3 不合法。
落地检查
拿一串真实版本跑一遍排序,确认 1.0.0-alpha.9 < 1.0.0-alpha.10 < 1.0.0;对每个输入先过正则再过比较函数;比较函数里构建元数据一律剔除。这三步能挡掉上面大部分问题。