依赖覆盖

依赖覆盖对于本地开发来说,是很常见的,大部分原因都是我们希望在某个包发布到 crates.io 之前使用它,例如:

  • 你正在同时开发一个包和一个项目,而后者依赖于前者,你希望能在该项目中对正在开发的包进行测试
  • 你引入的一个依赖包在 master 分支发布了新的代码,恰好修复了某个 bug,因此你希望能单独对该分支进行下测试
  • 你即将发布一个包的新版本,为了确保新版本正常工作,你需要对其进行集成测试
  • 你为项目的某个依赖包提了一个 PR 并解决了一个重要 bug,在等待合并到 master 分支,但是时间不等人,因此你决定先使用自己修改的版本,等未来合并后,再继续使用官方版本

下面我们来具体看看类似的问题该如何解决。

上一章节中我们讲了如果通过多种引用方式来引入一个包,其实这也是一种依赖覆盖。

测试 bugfix 版本

假设我们有一个项目正在使用 uuid 依赖包,但是却不幸地发现了一个 bug,由于这个 bug 影响了使用,没办法等到官方提交新版本,因此还是自己修复为好。

我们项目的 Cargo.toml 内容如下:

[package]
name = "my-library"
version = "0.1.0"

[dependencies]
uuid = "0.8.2"

为了修复 bug,首先需要将 uuid 的源码克隆到本地,笔者是克隆到和项目同级的目录下:

git clone https://github.com/uuid-rs/uuid

下面,修改项目的 Cargo.toml 添加以下内容以引入本地克隆的版本:

[patch.crates-io]
uuid = { path = "../uuid" }

这里我们使用自己修改过的 patch 来覆盖来自 crates.io 的版本,由于克隆下来的 uuid 目录和我们的项目同级,因此通过相对路径 "../uuid" 即可定位到。

在成功为 uuid 打了本地补丁后,现在尝试在项目下运行 cargo build,但是却报错了,而且报错内容有一些看不太懂:

$ cargo build
    Updating crates.io index
warning: Patch `uuid v1.0.0-alpha.1 (/Users/sunfei/development/rust/demos/uuid)` was not used in the crate graph.
Check that the patched package version and available features are compatible
with the dependency requirements. If the patch has a different version from
what is locked in the Cargo.lock file, run `cargo update` to use the new
version. This may also occur with an optional dependency that is not enabled.

具体原因比较复杂,但是仔细观察,会发现克隆下来的 uuid 的版本是 v1.0.0-alpha.1 (在 "../uuid/Cargo.toml" 中可以查看),然后我们本地引入的 uuid 版本是 0.8.2,根据之前讲过的 crates.io版本规则,这两者是不兼容的,0.8.2 只能升级到 0.8.z,例如 0.8.3

既然如此,我们先将 "../uuid/Cargo.toml" 中的 version = "1.0.0-alpha.1" 修改为 version = "0.8.3" ,然后看看结果先:

$ cargo build
    Updating crates.io index
   Compiling uuid v0.8.3 (/Users/sunfei/development/rust/demos/uuid)

大家注意到最后一行了吗?我们成功使用本地的 0.8.3 版本的 uuid 作为最新的依赖,因此也侧面证明了,补丁 patch 的版本也必须遵循相应的版本兼容规则!

如果修改后还是有问题,大家可以试试以下命令,指定版本进行更新:

% cargo update -p uuid --precise 0.8.3
    Updating crates.io index
    Updating uuid v0.8.3 (/Users/sunfei/development/rust/demos/uuid) -> v0.8.3

修复 bug 后,我们可以提交 pr 给 uuid,一旦 pr 被合并到了 master 分支,你可以直接通过以下方式来使用补丁:

[patch.crates-io]
uuid = { git = 'https://github.com/uuid-rs/uuid' }

等未来新的内容更新到 crates.io 后,大家就可以移除这个补丁,直接更新 [dependencies] 中的 uuid 版本即可!

使用未发布的小版本

还是 uuid 包,这次假设我们要为它新增一个特性,同时我们已经修改完毕,在本地测试过,并提交了相应的 pr,下面一起来看看该如何在它发布到 crates.io 之前继续使用。

再做一个假设,对于 uuid 来说,目前 crates.io 上的版本是 1.0.0,在我们提交了 pr 并合并到 master 分支后,master 上的版本变成了 1.0.1,这意味着未来 crates.io 上的版本也将变成 1.0.1

为了使用新加的特性,同时当该包在未来发布到 crates.io 后,我们可以自动使用 crates.io 上的新版本,而无需再使用 patch 补丁,可以这样修改 Cargo.toml

[package]
name = "my-library"
version = "0.1.0"

[dependencies]
uuid = "1.0.1"

[patch.crates-io]
uuid = { git = 'https://github.com/uuid-rs/uuid' }

注意,我们将 [dependencies] 中的 uuid 版本提前修改为 1.0.1,由于该版本在 crates.io 尚未发布,因此 patch 版本会被使用。

现在,我们的项目是基于 patch 版本的 uuid 来构建,也就是从 gihtubmaster 分支中拉取最新的 commit 来构建。一旦未来 crates.io 上有了 1.0.1 版本,那项目就会继续基于 crates.io 来构建,此时,patch 就可以删除了。

间接使用 patch

现在假设项目 A 的依赖是 Buuid,而 B 的依赖也是 uuid,此时我们可以让 AB 都使用来自 GitHubpatch 版本,配置如下:

[package]
name = "my-binary"
version = "0.1.0"

[dependencies]
my-library = { git = 'https://example.com/git/my-library' }
uuid = "1.0.1"

[patch.crates-io]
uuid = { git = 'https://github.com/uuid-rs/uuid' }

如上所示,patch 不仅仅对于 my-binary 项目有用,对于 my-binary 的依赖 my-library 来说,一样可以间接生效。

非 crates.io 的 patch

若我们想要覆盖的依赖并不是来自 crates.io ,就需要对 [patch] 做一些修改。例如依赖是 git 仓库,然后使用本地路径来覆盖它:

[patch."https://github.com/your/repository"]
my-library = { path = "../my-library/path" }

easy,轻松搞定!

使用未发布的大版本

现在假设我们要发布一个大版本 2.0.0,与之前类似,可以将 Cargo.toml 修改如下:

[dependencies]
uuid = "2.0"

[patch.crates-io]
uuid = { git = "https://github.com/uuid-rs/uuid", branch = "2.0.0" }

此时 2.0 版本在 crates.io 上还不存在,因此我们使用了 patch 版本且指定了 branch = "2.0.0"

间接使用 patch

这里需要注意,与之前的小版本不同,大版本的 patch 不会发生间接的传递!,例如:

[package]
name = "my-binary"
version = "0.1.0"

[dependencies]
my-library = { git = 'https://example.com/git/my-library' }
uuid = "1.0"

[patch.crates-io]
uuid = { git = 'https://github.com/uuid-rs/uuid', branch = '2.0.0' }

以上配置中, my-binary 将继续使用 1.x.y 系列的版本,而 my-library 将使用最新的 2.0.0 patch。

原因是,大版本更新往往会带来破坏性的功能,Rust 为了让我们平稳的升级,采用了滚动的方式:在依赖图中逐步推进更新,而不是一次性全部更新。

多版本[patch]

在之前章节,我们介绍过如何使用 package key重命名依赖包,现在来看看如何使用它同时引入多个 patch

假设,我们对 serde 有两个新的 patch 需求:

  • serde 官方解决了一个 bug 但是还没发布到 crates.io,我们想直接从 git 仓库的最新 commit 拉取版本 1.*
  • 我们自己为 serde 添加了新的功能,命名为 2.0.0 版本,并将该版本上传到自己的 git 仓库中

为了满足这两个 patch,可以使用如下内容的 Cargo.toml

[patch.crates-io]
serde = { git = 'https://github.com/serde-rs/serde' }
serde2 = { git = 'https://github.com/example/serde', package = 'serde', branch = 'v2' }

第一行说明,第一个 patch 从官方仓库 main 分支的最新 commit 拉取,而第二个则从我们自己的仓库拉取 v2 分支,同时将其重命名为 serde2

这样,在代码中就可以分别通过 serdeserde2 引用不同版本的依赖库了。

通过[path]来覆盖依赖

有时我们只是临时性地对一个项目进行处理,因此并不想去修改它的 Cargo.toml。此时可以使用 Cargo 提供的路径覆盖方法: 注意,这个方法限制较多,如果可以,还是要使用 [patch]

[patch] 修改 Cargo.toml 不同,路径覆盖修改的是 Cargo 自身的配置文件 $Home/.cargo/config.toml:

paths = ["/path/to/uuid"]

paths 数组中的元素是一个包含 Cargo.toml 的目录(依赖包),在当前例子中,由于我们只有一个 uuid,因此只需要覆盖它即可。目标路径可以是相对的,也是绝对的,需要注意,如果是相对路径,那是相对包含 .cargo$Home 来说的。

不推荐的[replace]

[replace] 已经被标记为 deprecated,并将在未来被移除,请使用 [patch] 替代

虽然不建议使用,但是如果大家阅读其它项目时依然可能会碰到这种用法:

[replace]
"foo:0.1.0" = { git = 'https://github.com/example/foo' }
"bar:1.0.2" = { path = 'my/local/bar' }

语法看上去还是很清晰的,[replace] 中的每一个 key 都是 Package ID 格式,通过这种写法可以在依赖图中任意挑选一个节点进行覆盖。