覆盖依赖

在很多场景下可能会希望覆盖一个依赖。大部分情况是在包发布到 crates.io 之前覆盖。比如:

  • 你正在写的一个crate被另一个更大的应用(app)使用,而你希望在这个app中测试对该crate的一个bug修复。
  • 一个不属于你的上游crate在其git仓库的master分支增加了一些新特性,或修复了一些bug,你想要测试一下。
  • 你准备给自己的crate发布一个新的主版本(major version),但是你想先在整个包上做一下集成测试来确保正确性。
  • 你已经给一个上游crate提交了bug修复,但是现在想立刻让你的app使用这个修复后的crate,避免等待这个bug修复合并。

这些问题都可以通过配置清单中的 [patch] 部分来解决。

本章将从几种不同使用案例出发,详细介绍各种覆盖依赖的方法。

注意: 也可以参考使用 multiple locations 多位置依赖来指定依赖条目,这个方法可以在本地覆盖依赖的源。

测试bugfix

例如,你正在使用 uuid crate 却发现bug。你打算自己修复这个bug!配置清单初始可能是这样:

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

[dependencies]
uuid = "1.0"

我们首先要做的是把 uuid 仓库 clone下来:

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

然后修改自己的 my-library 包的配置清单来包含clone下来的uuid:

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

这里我们声明用新的依赖 覆盖 (patching) crates-io 源。 这会将我们刚下载到本地的 uuid 添加到 crates.io 中(仅仅针对这个本地项目)。

接着我们需要更新 lock file ,以保证我们使用的是下载到本地的 uuid[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 -p uuid --precise $version$version 是本地版本 uuid 的版本号。

当你解决了之前发现的bug,接下来要做的事可能是将其通过 pull request 提交给 uuid 仓库。 之后,pull request 被合并后,可以更新 [patch] 部分,可以把 path 依赖改为 git 依赖, [patch] 中条目的功能和 [dependencies] 一样:

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

使用未发布的crate次要版本

现在让我们从修复bug转到添加特性。在写 my-library 时你发现需要在 uuid 里加新特性。 你已经实现了这个特性并在本地通过 [patch] 进行了测试,然后提交了 pull request。 现在我们来看,怎样在这个crate实际发布前继续测试和使用它。

假设 uuid 目前在crates.io上的最新版为 1.0.0,但 git 仓库上的最新版是 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] 来声明。

现在,当我们构建库时,Cargo会从git仓库fetch这个 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 同时作用于 uuidmy-library -> uuid 这两个依赖。 整个依赖图中,uuid 只使用了我们从git仓库拉下来的 1.0.1 这个版本。

覆盖git仓库的URL

如果你想要覆盖的依赖不在crates.io上,那么就需要改变使用 [patch] 的方法。比如说一个位于git仓库的依赖,你需要这样覆盖为本地路径:

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

OK了。

预发布一个破坏性更新

更新主版本一般来说会有破坏性的更新(breaking change)。拿之前的crate举例,我们创建 uuid 的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" }

很好。就像之前的例子一样,因为crates.io中实际上没有2.0.0版,我们实际上通过 [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' }

注意,这里会解析出两个版本的 uuidmy-binary 会继续使用 1.x.y 版的 uuid ,而 my-library 会使用 2.0.0 版。 这种解析逻辑允许你在依赖图中逐步应用某个crate的破坏性更新,而不是强制把这个破坏性更新一次性应用到整个依赖图。

多版本情况下使用 [patch]

你可以通过 package 重命名来对同一个crate有多个patch。比如说我们想使用 serde 一个 1.* 版的bug修复(还没发布到crates.io),同时也想使用git仓库中 serde2.0.0 版来构建原型。这时可以设置:

[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的 2.0.0 版 (我们假设git仓库中该分支中的 Cargo.toml 中声明这个crate的版本是 2.0.0)。

注意,这里 serde2 用到了 package ,从而会忽略 serde2 标识符,只是一个和其他patch不冲突的占位。

[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] 也可以被设置为 configuration option ,比如 .cargo/config.toml 文件或者命令行选项,如 --config 'patch.crates-io.rand.path="rand"'。 这对于你不打算 commit 的本地修改,或是临时测试某个 patch 很方便。(译者注:因为这样就不用写到Cargo.toml里面,不修改现有文件的内容)。

[patch] 表和 dependencies 表很类似。[patch] 的key是源的URL,或者是注册中心的名称。 crates-io 用于覆盖默认的注册中心 crates.io。 上面的例子里第一个 [patch] 用于展示对 crates.io 的覆盖,第二个 [patch] 展示对一个git源的覆盖。

[patch] 表中的条目就是普通的依赖项,与[dependencies] 里的一样。这些依赖用于覆盖 URL 指定源中的相应crate。 上面例子中覆盖了 crates-io 源中的 foo crate 和 bar brate。 同时,其也用另一个git仓库中的 my-branch 分支覆盖了 https://github.com/example/baz 源。

可以对源中某个crate尚不存在的版本进行覆盖,也可以对存在的版本进行覆盖。 如果覆盖的是源中crate的已存在版本,则实际上是把这个版本替换掉了。

Cargo 只读取工作空间中顶层 Cargo.toml 中的 [patch] 设置,会忽略依赖中的patch设置。

[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] 表中的每个key都是 package ID specification,其可以覆盖依赖图中的任意一个节点 (需要以三数字格式标明版本号)。 每个key对应的value与 [dependencies] 中依赖格式相同 (除了不能使用 feature 字段以外)。用于覆盖的crate必须与被覆盖的crate版本相同,但是可以来自不同的源 (比如git或本地路径)。

Cargo 仅读取工作空间中顶层 Cargo.toml 中的 [replace] 设置,会忽略依赖中的设置。

paths 覆盖

有时你仅仅想临时使用一个crate,而不想修改 Cargo.toml (比如[patch] 字段),为此 Cargo 提供了一个更简单但功能有限的 路径覆盖 (path overrides) 功能。

路径覆盖可以通过 .cargo/config.toml 而不是 Cargo.toml 来指定。在 .cargo/config.toml 中,你可以指定一个名为 paths 的key:

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

这个数组中应该填写一系列包含一个 Cargo.toml 文件的路径。在这个例子中,我们只添加了 uuid ,所以只有这个ctate被覆盖。这个路径可以是绝对地址,也可以是 包含.cargo目录 那个文件夹的相对路径。

译者注:举例来说:

比如你的路径格式为:

rust
├── .cargo
│   └── config.toml
├── .rustup
└── overrides
    └── uuid
        └── Cargo.toml

那么相应的在 config.toml 中应该写:

paths = ["overrides/uuid"]

而且paths字段应写在文件的最上方,不在任何一个 [xxx] 的下面,才能生效。

路径覆盖比 [patch] 更严格,其无法改变依赖图的结构。替换路径后,之前所有的依赖必须满足新 Cargo.toml 的要求。 这意味着路径覆盖无法在测试时给某个crate添加依赖,这种情况只能使用 [patch] 。因此,路径覆盖一般只用于孤立地修改某个crate,以快速修复其中的bug,而不应用于更大规模的修改。

注意:用本地设置来进行路径覆盖,仅能用于那些已经发布在 crates.io 上的包。你无法把这个功能用于让Cargo查找本地的未发布crate。