覆盖依赖项
覆盖依赖项的需求可能出现在多种场景中。然而,大多数场景都归结为在 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 将使用 uuid
的 2.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 = ...
指令表明 serde
的 1.*
版本应该从 git 仓库使用(拉取我们需要应用的 bug 修复),第二个 serde2 = ...
指令表明 serde
包也应该从 https://github.com/example/serde
的 v2
分支拉取。我们这里假设该分支上的 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-branch
为 https://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。