npm、Cargo、Composer 和 pip 对相同的 semver 简写解释各不相同,在我测试的 13 种范围表达式中,只有两种在所有四个工具中的表现完全一致。
我为每个管理器编写了独立的解析器,遵循其官方规范,然后将每个范围与一个包含 19 个版本号的矩阵进行对比。这个 13 × 19 的网格(共 247 个单元格)呈现出了一幅支离破碎的图景:只有 17 个单元格在所有工具中都给出了相同的“是”或“否”答案,而且这 17 个中大部分都是一致的拒绝。简而言之,这些工具在“否”上的达成共识的频率远高于“是”。
为什么工具之间存在分歧
这四个管理器都声称遵循语义化版本控制(Semantic Versioning),但每个管理器都加入了各自的简写规则。
- Caret (^) 和 dot-x – npm 支持 caret;pip 则忽略 caret 和 dot-x。仅此一项就在 npm 和 pip 之间造成了 49 个单元格的分歧。
- 裸版本号 (Bare versions) – Cargo 将纯
1.2.3视为 caret 范围,意味着“与 1.x 兼容”。而 npm 和 Composer 将相同的字符串视为精确匹配,仅接受1.2.3。 - Tilde (~) – Cargo 的 tilde 锁定的是 次要版本 (minor version)(
~1.2匹配1.2.*但不匹配1.3.0)。Composer 的 tilde 锁定的则是 主版本 (major version)(~1.2匹配1.*)。这两种解释在任何改变次要部分的版本上都会产生分歧。
预发布版本(Pre-release)的处理方式带来了另一个意外。像 ^1.2.3 这样的范围并不包含 2.0.0-rc.1,尽管其数字部分低于上限。规则是:除非范围明确提到了预发布标识符,否则预发布版本会被排除在外。
唯一安全的跨工具语法
实验表明,显式的范围不等式——例如 >=1.2.0 <2.0.0——在 npm、Cargo、Composer 和 pip 中的表现是一致的。每个管理器都将这两个边界视为字面上的数值限制,没有隐藏的 caret 或 tilde 语义。
如果你需要表达“任何至少为 1.2 的 1.x 版本”,请完整地写出来。虽然这会多出几个字符,但它能保证无论代码在哪里运行,解析结果都是可预测的。
简写在何时仍有意义
对于单一语言的项目,简写仍然具有吸引力。在纯 npm 生态系统中,^1.2.3 简洁地表达了“与未来的次要版本和修订版本兼容”。当你确定使用的工具不会改变时,Cargo 的 caret 或 Composer 的 tilde 也是如此。危险在于当同一个清单(manifest)被跨语言边界复用时,或者当 CI 任务从多个生态系统中拉取依赖时。
总结
依靠 semver 简写来实现跨语言兼容性是一场赌博。唯一可靠的保证版本约束在任何地方都具有相同含义的方法,就是编写显式的范围不等式。当你需要简洁时,请保持在单一生态系统中;当你需要一致性时,请用清晰度换取简写。
