覆盖依赖项

覆盖依赖项的需求可能出现在多种场景中。然而,大多数场景都归结为在 crate 发布到 crates.io 之前对其进行处理的能力。例如

  • 你正在开发的一个 crate 也被你正在开发的一个更大的应用使用,你想在那个更大的应用内部测试该库的 bug 修复。
  • 你不参与开发的某个上游 crate,在其 git 仓库的 master 分支上有新功能或 bug 修复,你想测试一下。
  • 你即将发布你的 crate 的一个新主版本,但你想在整个包中进行集成测试,以确保新主版本能正常工作。
  • 你为你发现的 bug 向一个上游 crate 提交了修复,但你想让你的应用立即开始依赖这个修复后的 crate 版本,以免等待 bug 修复被合并而阻塞。

这些场景可以通过 [patch] manifest section 来解决。

本章将介绍一些不同的用例,并详细说明覆盖依赖项的不同方法。

注意:另请参阅通过 多个位置 指定依赖项,这可用于覆盖本地包中单个依赖项声明的源。

测试 bug 修复

假设你正在使用 uuid crate,但在使用过程中发现了 bug。然而,你很有进取心,所以决定尝试修复这个 bug!最初你的 manifest 会是这样

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

[dependencies]
uuid = "1.0"

首先我们要做的就是通过以下命令将 uuid 仓库 克隆到本地

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

接下来我们将编辑 my-library 的 manifest,使其包含

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

在这里,我们声明正在用一个新的依赖项补丁crates-io。这将有效地将本地检出的 uuid 版本添加到我们本地包的 crates.io 注册表中。

接下来我们需要确保我们的 lock 文件已更新,以便使用这个新版本的 uuid,这样我们的包就会使用本地检出的副本而不是来自 crates.io 的副本。[patch] 的工作方式是,它会加载 ../path/to/uuid 处的依赖项,然后无论何时向 crates.io 查询 uuid 的版本,它会返回本地版本。

这意味着本地检出的版本号很重要,会影响补丁是否被使用。我们的 manifest 中声明了 uuid = "1.0",这意味着我们将只解析到 >= 1.0.0, < 2.0.0 的版本,而 Cargo 的贪婪解析算法也意味着我们将解析到该范围内的最高版本。通常这并不重要,因为 git 仓库中的版本通常会大于或等于 crates.io 上发布的最高版本,但记住这一点很重要!

无论如何,现在你通常只需要这样做

$ cargo build
   Compiling uuid v1.0.0 (.../uuid)
   Compiling my-library v0.1.0 (.../my-library)
    Finished dev [unoptimized + debuginfo] target(s) in 0.32 secs

就是这样!你现在正在使用本地版本的 uuid 构建(注意构建输出中括号里的路径)。如果你没有看到本地路径版本被构建,那么你可能需要运行 cargo update uuid --precise $version,其中 $version 是本地检出的 uuid 副本的版本号。

一旦你修复了最初发现的 bug,下一步你想做的很可能是将它作为 pull request 提交给 uuid crate 本身。完成此操作后,你还可以更新 [patch] 部分。[patch] 内部的列表就像 [dependencies] 部分一样,所以一旦你的 pull request 被合并,你可以将你的 path 依赖项更改为

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

使用未发布的次要版本

现在让我们把重心从 bug 修复转移到添加功能。在开发 my-library 时,你发现 uuid crate 需要一个全新的功能。你已经实现了这个功能,并在上面使用 [patch] 在本地进行了测试,并提交了一个 pull request。让我们看看在它实际发布之前如何继续使用和测试它。

我们还假设 crates.io 上的 uuid 当前版本是 1.0.0,但从那时起 git 仓库的 master 分支已经更新到 1.0.1。这个分支包含了你之前提交的新功能。要使用这个仓库,我们将编辑我们的 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.git' }

请注意,我们对 uuid 的本地依赖项已更新到 1.0.1,因为一旦该 crate 发布,我们将实际需要这个版本。然而,这个版本在 crates.io 上并不存在,所以我们在 manifest 的 [patch] 部分提供了它。

现在当我们的库被构建时,它将从 git 仓库获取 uuid 并解析到仓库内的 1.0.1 版本,而不是尝试从 crates.io 下载版本。一旦 1.0.1 在 crates.io 上发布,[patch] 部分就可以删除了。

值得注意的是,[patch]传递性地应用的。假设你在一个更大的包中使用了 my-library,例如

[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.git' }

请记住,[patch]传递性地应用的,但只能在顶层定义,所以如果需要,my-library 的消费者必须重复 [patch] 部分。不过在这里,新的 uuid crate 同时适用于我们对 uuid 的依赖以及 my-library -> uuid 的依赖。对于整个 crate 图,uuid crate 将解析到 1.0.1 这一个版本,并且会从 git 仓库拉取。

覆盖仓库 URL

如果你想覆盖的依赖项不是从 crates.io 加载的,你必须稍微改变一下使用 [patch] 的方式。例如,如果依赖项是 git 依赖项,你可以将其覆盖为本地路径,使用

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

就是这样!

预发布破坏性变更

让我们看看如何使用 crate 的新主版本,这通常伴随着破坏性变更。继续使用我们之前的 crate,这意味着我们将创建 uuid crate 的 2.0.0 版本。在我们将所有变更提交到上游后,我们可以更新 my-library 的 manifest,使其看起来像

[dependencies]
uuid = "2.0"

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

就是这样!就像前面的例子一样,2.0.0 版本实际上并不存在于 crates.io 上,但我们仍然可以通过使用 [patch] 部分将其作为 git 依赖项引入。作为一项思维练习,让我们再看看上面提到的 my-binary 的 manifest

[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.git', branch = '2.0.0' }

注意,这实际上会解析出两个版本的 uuid crate。my-binary crate 将继续使用 uuid crate 的 1.x.y 系列,但 my-library crate 将使用 uuid2.0.0 版本。这使得你可以在依赖图谱中逐步推广对 crate 的破坏性变更,而无需一次性更新所有内容。

[patch] 与多个版本一起使用

你可以使用用于重命名依赖项的 package 键来补丁同一 crate 的多个版本。例如,假设 serde crate 有一个 bug 修复,我们想将其用于 1.* 系列,但我们也想原型化使用我们 git 仓库中的 2.0.0 版本 serde。要配置这个,我们可以这样做

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

第一个 serde = ... 指令表明 serde1.* 版本应该从 git 仓库使用(拉取我们需要应用的 bug 修复),第二个 serde2 = ... 指令表明 serde 包也应该从 https://github.com/example/serdev2 分支拉取。我们这里假设该分支上的 Cargo.toml 提及了 2.0.0 版本。

注意,当使用 package 键时,这里的 serde2 标识符实际上是被忽略的。我们只需要一个唯一的名称,它不与其他补丁的 crate 冲突。

[patch] 部分

Cargo.toml 文件中的 [patch] 部分可用于用其他副本覆盖依赖项。其语法类似于 [dependencies] 部分

[patch.crates-io]
foo = { git = 'https://github.com/example/foo.git' }
bar = { path = 'my/local/bar' }

[dependencies.baz]
git = 'https://github.com/example/baz.git'

[patch.'https://github.com/example/baz']
baz = { git = 'https://github.com/example/patched-baz.git', branch = 'my-branch' }

注意[patch] 表也可以指定为配置选项,例如在 .cargo/config.toml 文件中或 CLI 选项,如 --config 'patch.crates-io.rand.path="rand"'。这对于你不想提交的本地更改或临时测试补丁很有用。

[patch] 表由类似依赖项的子表组成。[patch] 后面的每个键都是正在打补丁的源的 URL,或注册表的名称。名称 crates-io 可用于覆盖默认注册表 crates.io。上面示例中的第一个 [patch] 演示了覆盖 crates.io,第二个 [patch] 演示了覆盖 git 源。

这些表中的每个条目都是一个标准的依赖项规范,与 manifest 的 [dependencies] 部分中找到的相同。[patch] 部分中列出的依赖项将被解析并用于为指定的 URL 源打补丁。上面的 manifest 片段用 foo crate 和 bar crate 为 crates-io 源(即 crates.io 本身)打补丁。它还用来自其他地方的 my-branchhttps://github.com/example/baz 源打补丁。

源可以使用不存在的 crate 版本进行补丁,也可以使用已存在的 crate 版本进行补丁。如果源使用源中已存在的 crate 版本进行补丁,那么源的原始 crate 将被替换。

Cargo 只会查看工作区根目录下的 Cargo.toml manifest 中的补丁设置。依赖项中定义的补丁设置将被忽略。

[replace] 部分

注意[replace] 已被弃用。你应该改用 [patch] 表。

Cargo.toml 的这一部分可用于用其他副本覆盖依赖项。其语法类似于 [dependencies] 部分

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

[replace] 表中的每个键都是一个 包 ID 规范,允许任意选择依赖图中的一个节点进行覆盖(需要三部分的版本号)。每个键的值与 [dependencies] 中指定依赖项的语法相同,只是不能指定 features。请注意,当一个 crate 被覆盖时,用于覆盖的副本必须具有相同的名称和版本,但它可以来自不同的源(例如,git 或本地路径)。

Cargo 只会查看工作区根目录下的 Cargo.toml manifest 中的替换设置。依赖项中定义的替换设置将被忽略。

paths 覆盖

有时你只是临时处理一个 crate,不想像上面使用 [patch] 部分那样修改 Cargo.toml。对于这种情况,Cargo 提供了一种限制得多的覆盖方式,称为 路径覆盖

路径覆盖是通过 .cargo/config.toml 指定的,而不是 Cargo.toml。在 .cargo/config.toml 内部,你会指定一个名为 paths 的键

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

这个数组应该填充包含 Cargo.toml 的目录。在这个例子中,我们只添加了 uuid,所以只有它会被覆盖。这个路径可以是绝对路径,也可以是相对于包含 .cargo 文件夹的目录的相对路径。

然而,路径覆盖比 [patch] 部分受到的限制更多,因为它们不能改变依赖图的结构。使用路径替换时,先前的依赖项集必须与新的 Cargo.toml 规范完全匹配。例如,这意味着不能使用路径覆盖来测试向 crate 添加依赖项,在这种情况下必须使用 [patch]。因此,路径覆盖的使用通常仅限于快速的 bug 修复,而不是较大的更改。

注意:使用本地配置覆盖路径仅适用于已发布到 crates.io 的 crate。你不能使用此功能告诉 Cargo 如何查找本地未发布的 crate。