3个不稳定版本
使用旧的Rust 2015
0.3.0 | 2017年7月9日 |
---|---|
0.2.1 | 2017年7月9日 |
0.2.0 | 2017年7月9日 |
#6 in #breaking
2KB
semver技巧
semver技巧指的是在Rust库中发布破坏性更改,而不需要在其下游依赖图中进行协调升级。这个技巧的核心是让库的一个版本声明依赖于相同库的新版本。
示例说明
Rust库生态系统有一个痛苦的库升级历史。2015年,从0.1到0.2的升级被称为“libcpocalypse”。另一个常见的罪魁祸首是1.0之前的Serde,从0.7到0.8、0.9再到1.0的升级需要整个生态系统的努力。
困难的原因是许多crate在其公共API中使用这些库的类型。
例如,考虑一个简化版本的libc
crate,它只暴露了两件事:来自NetBSD的c_void
类型和EVFILT_AIO
常量。
// libc 0.2.0
pub type c_void = /* it's complicated */;
pub const EVFILT_AIO: i32 = 2;
c_void
类型被广泛使用,因为数百个库都希望暴露与C的void *
类型ABI兼容的函数。同时,EVFILT_AIO
常量较少使用,且从未出现在下游crate的公共API中。
extern "C" {
// Usable from C as:
//
// void qsort(
// void *base,
// size_t nitems,
// size_t size,
// int (*compar)(const void *, const void*));
//
// The `c_void` type is now part of the public API of this crate.
pub fn qsort(
base: *mut c_void,
nitems: usize,
size: usize,
compar: Option<unsafe extern fn(*const c_void, *const c_void) -> c_int>,
);
}
一段时间后,发现应该将EVFILT_AIO
定义为uint32_t
而不是int32_t
,以匹配NetBSD头文件中其他地方的使用方式(rust-lang/libc#506)。
这个修复将是对libc
crate的破坏性更改。将libc::EVFILT_AIO
传递给接受类型为int32_t
的函数的现有代码将失效,并且这需要在libc
crate的semver版本中反映出来。
问题就出在这里。
协调升级
假设我们进行了修复并将其作为破坏性更改发布。
// libc 0.3.0
pub type c_void = /* it's complicated */;
pub const EVFILT_AIO: u32 = 2;
尽管c_void
的定义没有改变,但从技术上讲,libc 0.2中的c_void
和libc 0.3中的c_void
是不同的类型。在Rust中(也适用于C,实际上),两个结构体只是因为它们有相同的字段,所以是不可互换的;将一个传递给声明为接受另一个的结构体的函数会导致编译错误。
这意味着,如果crate A依赖于依赖于libc
的crate B,并且B在A调用的某个公共API中使用c_void
,那么A在B升级到libc 0.3之前不能升级到libc 0.3。如果A在B之前升级,那么A将尝试将libc 0.3的c_void
传递给B的函数,该函数仍然期望使用libc 0.2的c_void
,这将导致无法编译。
需要做的事情是首先B升级到libc 0.3,将其作为B的主要版本升级发布(因为其公共API以破坏性的方式更改),然后A可以升级到B的新版本。
对于更长的依赖链,这是一个巨大的麻烦,需要几十个开发者的协调努力。在最近的一次libcpocalypse中,Servo发现自己协调了52个库在三个月内的升级(servo/servo#8608)。
技巧
问题的核心是广泛使用的API被不广泛使用的API的破坏所困扰。Rust和Cargo能够以更好的方式处理这种困境。
我们需要的只是一项对上述c_void
/ EVFILT_AIO
示例的修改。
在发布破坏性更改并将其作为libc 0.3.0发布后,我们发布0.2系列的最后一个次要版本,并从0.3重新导出未更改的API。
在Cargo.toml中
[package]
name = "libc"
version = "0.2.1"
[dependencies]
libc = "0.3" # future version of itself
并且在lib.rs中
// libc 0.2.1
pub use libc::c_void; // reexport from libc 0.3, as per Cargo.toml
pub const EVFILT_AIO: i32 = 2;
这样我们避免了有两个看似相同但不可互换的c_void
类型的问题。在这里,libc 0.2.1中的c_void
和libc 0.3.0中的c_void
是精确相同的类型。
避免了libcpocalypse场景,因为libc
的用户可以随意地从0.2升级到0.3,以任何顺序,而不需要提高他们自己的semver主要版本。
高级技巧
通过一些细心和创造力,上述技术可以推广到许多不同的破坏性更改场景。这个repo中包含的semver-trick
示例crate演示了一些可以适应的变化类型。
semver_trick::Unchanged
在0.2和0.3之间是可互换的。semver_trick::Removed
存在于0.2中,但在0.3中不存在。semver_trick::Added
存在于0.3中,但在0.2中不存在。semver_trick::before::Moved
已移动到semver_trick::after::Moved
。
限制
这并不是解决所有依赖地狱出现的银弹。
基本上,当crate需要破坏很少使用的API而留下广泛使用的API不变,或者当crate想要在其模块层次结构中重新排列类型时,semver技巧是有益的。
此技巧无法帮助大多数其他类型的破坏,包括以下具体示例
- 向广泛使用的trait添加一个非密封的新方法,
- 提升一个公共依赖项的主版本号,而该依赖项本身并未使用semver技巧。
- 提高rustc的最小支持版本。
许可证
在构成可版权作品范围内,依赖相同库的将来版本的思路在CC0 1.0通用许可证下授权(LICENSE-CC0)并可无需署名使用。本文档和随附的semver-trick
示例crate根据您的选择,受Apache License 2.0许可证(《LICENSE-APACHE》)或MIT许可证(《LICENSE-MIT》)约束。
除非您明确表示,否则,您提交给本代码库的任何贡献,如Apache-2.0许可证中定义,将根据上述方式双重许可,无任何附加条款或条件。