#semver #upgrade #trick #breaking #version #change #across

semver-trick

防止困难协调升级的semver技巧示例

3个不稳定版本

使用旧的Rust 2015

0.3.0 2017年7月9日
0.2.1 2017年7月9日
0.2.0 2017年7月9日

#6 in #breaking

MIT/Apache

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演示了一些可以适应的变化类型。


限制

这并不是解决所有依赖地狱出现的银弹。

基本上,当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许可证中定义,将根据上述方式双重许可,无任何附加条款或条件。

无运行时依赖