覆盖依赖

覆盖依赖的需求可能源于多种场景。然而,它们中的大多数都归结为能够在某个 crate 发布到 crates.io 之前使用它。例如:

  • 您正在开发的 crate 也用于您正在开发的一个更大的应用程序中,并且您想在更大的应用程序中测试该库的 bug 修复。
  • 您不参与开发的上游 crate 在其 git 仓库的 master 分支上有一个新功能或 bug 修复,您想测试一下。
  • 您即将发布一个新主版本的 crate,但您希望在整个软件包中进行集成测试,以确保新的主版本能够正常工作。
  • 您已经为发现的 bug 向上游 crate 提交了修复,但您希望立即让您的应用程序开始依赖于已修复版本的 crate,以避免因 bug 修复未合并而受阻。

这些场景可以使用 [patch] 清单部分来解决。

本章将介绍几个不同的用例,并详细介绍覆盖依赖的不同方法。

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

测试 bug 修复

假设您正在使用 uuid crate,但在使用过程中发现了一个 bug。然而,您非常积极,所以您决定也尝试修复这个 bug!最初,您的清单将如下所示:

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

[dependencies]
uuid = "1.0"

我们要做的第一件事是通过以下方式在本地克隆 uuid 仓库

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

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

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

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

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

这意味着本地结帐的版本号非常重要,并且会影响是否使用该补丁。我们的清单声明了 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 上不存在,因此我们在清单的 [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 的依赖。 uuid crate 将为此整个 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 的清单,如下所示:

[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 清单:

[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 有一个我们希望在其 1.* 系列中使用的 bug 修复,但我们也想原型化使用我们在 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 = ... 指令表示应从 git 仓库中使用 serde 1.*(引入我们需要的 bug 修复),第二个 serde2 = ... 指令表示也应从 https://github.com/example/serdev2 分支中提取 serde 包。我们假设该分支上的 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 文件中或像 --config 'patch.crates-io.rand.path="rand"' 这样的 CLI 选项。这对于您不想提交的本地更改或临时测试补丁非常有用。

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

这些表中的每个条目都是一个普通的依赖项规范,与清单的 [dependencies] 部分中找到的规范相同。 [patch] 部分中列出的依赖项将被解析并用于修补指定 URL 的源。上面的清单代码段使用 foo crate 和 bar crate 修补了 crates-io 源(例如 crates.io 本身)。它还使用来自其他地方的 my-branch 修补了 https://github.com/example/baz 源。

可以使用不存在的 crate 版本修补源,也可以使用已经存在的 crate 版本修补源。如果使用源中已存在的 crate 版本修补源,则该源的原始 crate 将被替换。

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

[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 规范,它允许任意选择依赖关系图中的一个节点进行覆盖(需要 3 部分的版本号)。每个键的值与用于指定依赖项的 [dependencies] 语法相同,但您不能指定功能。请注意,当一个 crate 被覆盖时,用来覆盖它的副本必须具有相同的名称和版本,但它可以来自不同的来源(例如,git 或本地路径)。

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

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]。因此,路径覆盖的使用通常仅限于快速修复错误,而不是更大的更改。

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