入门 The Cargo Team 2026-09-13 15:48:21 · 0 阅读

第16章 指定 Rust Cargo 依赖

你的 crate 可以依赖来自 crates.io 或其他注册中心的库、git 仓库,或本地文件系统中的子目录。你还能临时覆盖依赖来源位置——例如,为了测试自己正在本地开发的某个 bug 修复。此外,你可以为不同平台配置不同的依赖项,也可以指定仅在开发阶段使用的依赖。下面介绍上述各项的具体操作方法。

指定来自 crates.io 的依赖

Cargo 默认在 crates.io 上查找依赖。此时只需提供名称和一个版本字符串即可。在Cargo 指南中,我们指定了对 time crate 的依赖:

[dependencies]
time = "0.1.12"

版本字符串 "0.1.12" 被称为版本需求。它定义了解析依赖时可选的版本范围。在此例中,"0.1.12" 代表版本范围 >=0.1.12, <0.2.0。只要更新后的版本在该范围内,就允许更新。也就是说,如果运行 cargo update time,且 0.1.13 是最新的 0.1.z 版本,Cargo 会将其更新到该版本,但不会更新到 0.2.0

版本需求语法

默认需求

默认需求 指定了一个最低版本,并允许更新到 SemVer 兼容的版本。若 major/minor/patch 最左侧非零部分相同,则视为版本兼容。这与 SemVer 标准不同,后者认为所有 1.0.0 之前的包都不兼容。

1.2.3 是一个默认需求的示例。

1.2.3  :=  >=1.2.3, <2.0.0
1.2    :=  >=1.2.0, <2.0.0
1      :=  >=1.0.0, <2.0.0
0.2.3  :=  >=0.2.3, <0.3.0
0.2    :=  >=0.2.0, <0.3.0
0.0.3  :=  >=0.0.3, <0.0.4
0.0    :=  >=0.0.0, <0.1.0
0      :=  >=0.0.0, <1.0.0

Caret 依赖

Caret 依赖是默认的版本要求策略。这种策略允许接受与 SemVer 兼容的更新。写法上就是在版本号前加一个插入符(^)。

^1.2.3 就是一个 caret 依赖的例子。

省略插入符是 caret 依赖的简化等价写法。虽然 caret 依赖是默认策略,但建议尽量使用简化写法。

log = "^1.2.3"log = "1.2.3" 完全等价。

Tilde 依赖

Tilde 依赖指定一个最低版本,并允许一定范围内的更新。如果指定了主、次、补丁三段版本号,或只指定主、次两段,则只允许补丁级别的更新;如果只指定主版本号,则允许次版本和补丁级别的更新。

~1.2.3 就是一个 tilde 依赖的例子。

~1.2.3  := >=1.2.3, <1.3.0
~1.2    := >=1.2.0, <1.3.0
~1      := >=1.0.0, <2.0.0

通配符依赖

通配符依赖表示通配符所在的位置可以匹配任意版本号。

*1.*1.2.* 都是通配符依赖的例子。

*     := >=0.0.0
1.*   := >=1.0.0, <2.0.0
1.2.* := >=1.2.0, <1.3.0

注意crates.io 不允许单独使用 *

比较依赖

比较依赖允许手动指定一个版本范围或精确版本。

以下是一些比较依赖的例子:

>= 1.2.0
> 1
< 2
= 1.2.3

多重版本依赖

如上面的例子所示,多个版本要求可以用逗号分隔,例如 >= 1.2, < 1.5。所有条件都必须满足,因此像 <1.2, ^1.2.2 这样互不重叠的要求将匹配不到任何版本。

预发布版本

版本要求默认排除预发布版本(如 1.0.0-alpha),除非明确要求。例如,若包 foo 发布了 1.0.0-alpha,则要求 foo = "1.0" 不会匹配,并会返回错误。必须显式指定预发布版本,例如 foo = "1.0.0-alpha"。同理,cargo install 在未明确要求时也会避免安装预发布版本。

Cargo 允许自动使用“更新”的预发布版本。例如,若发布了 1.0.0-beta,那么要求 foo = "1.0.0-alpha" 将允许更新到 beta 版本。注意,这仅适用于同一发布版本,foo = "1.0.0-alpha" 不允许更新到 foo = "1.0.1-alpha"foo = "1.0.1-beta"

从预发布版本升级到 Semver 兼容的正式版本时,Cargo 也会自动处理。要求 foo = "1.0.0-alpha" 允许更新到 foo = "1.0.0" 以及 foo = "1.2.0"

需注意,预发布版本可能不稳定,使用时应谨慎。部分项目可能会在预发布版本之间引入破坏性变更。建议不要在库中使用预发布依赖,除非该库本身也是预发布版本。此外,更新 Cargo.lock 时也需小心,并准备好应对预发布版本更新可能引发的问题。

版本元数据

版本元数据(如 1.0.0+21AF26D3)会被忽略,不应在版本要求中使用。

建议: 如有疑问,请使用默认版本要求运算符。

在极少数情况下,某个包通过公共 API 重新导出依赖项或与其交互(即拥有“公共依赖”),且该包能兼容多个不兼容的 semver 版本(例如,仅使用了一个在不同发布版本间未发生变化的简单类型,如 Id),此时可以允许用户自行选择“公共依赖”的版本。在这种情况下,类似 ">=0.4, <2" 的版本约束可能具有一定的参考价值。然而,由于 Cargo 在解析依赖版本时可能会为“公共依赖”选取不同的版本,包的用户很可能会遇到错误,并需要通过 cargo update 手动指定“公共依赖”的版本(参见 #10599)。

避免将版本的上限设置为低于下一个不兼容 semver 版本的值(例如,避免使用 ">=2.0, <2.4""2.0.*"~2.0),因为依赖树中的其他包可能需要更新版本,从而导致无法解决的冲突(参见 #9029)。请考虑控制 Cargo.lock 中的版本是否更为合适。

在某些情况下,这种做法可能影响不大或收益大于成本,包括:

  • 当没有其他包依赖你的包时;例如,它仅包含一个 [[bin]] 目标
  • 当依赖预发布包且希望避免破坏性变更时,使用完全指定的 "=1.2.3-alpha.3" 可能是合理的(参见 #2222
  • 当一个库重新导出 proc-macro,但该 proc-macro 生成的代码又回调到重新导出它的库中时,使用完全指定的 =1.2.3 可能是必要的,以确保 proc-macro 版本不高于重新导出的库版本,从而避免生成使用当前版本中不存在的 API 部分的代码

指定来自其他注册表的依赖

若要指定来自 crates.io 以外注册表的依赖,请将 registry 键设置为要使用的注册表名称:

[dependencies]
some-crate = { version = "1.0", registry = "my-registry" }

其中 my-registry 是在 .cargo/config.toml 文件中配置的 registry 名称。详情请参阅 registry 文档

注意crates.io 不允许发布的包依赖其之外托管的代码。

git 仓库指定依赖

要依赖某个 git 仓库中的库,最少只需通过 git 键指定仓库位置:

[dependencies]
regex = { git = "https://github.com/rust-lang/regex.git" }

Cargo 会从该位置拉取 git 仓库,并遍历整个文件树,在仓库的任意位置查找所需 crate 的 Cargo.toml 文件。例如,regex-literegex-syntax 都是 rust-lang/regex 仓库的成员,无论它们位于文件树的哪个位置,都可以直接通过仓库根 URL(https://github.com/rust-lang/regex.git)来引用。

regex-lite   = { git = "https://github.com/rust-lang/regex.git" }
regex-syntax = { git = "https://github.com/rust-lang/regex.git" }

这条规则不适用于path 依赖

commit 的选择

如果像上面的例子那样只指定仓库 URL,Cargo 会默认使用默认分支上的最新 commit 来构建我们的包。

你还可以将 git 键与 revtagbranch 键组合使用,以更精确地指定要使用的 commit。例如,使用名为 next 分支上的最新 commit:

[dependencies]
regex = { git = "https://github.com/rust-lang/regex.git", branch = "next" }

任何既不是分支也不是标签的引用,都归属于 rev 键。这可以是一个提交哈希,如 rev = "4c59b707",也可以是远程仓库暴露的具名引用,如 rev = "refs/pull/493/head"

rev 键可用的引用取决于仓库托管位置。
GitHub 会暴露每个 Pull Request 最新提交的引用,如上文示例所示。其他 Git 托管服务可能以不同的命名规范提供等效功能。

更多 git 依赖示例:

# .git 后缀可以省略,如果宿主接受此类 URL - 两个示例效果相同
regex = { git = "https://github.com/rust-lang/regex" }
regex = { git = "https://github.com/rust-lang/regex.git" }

# 具有特定标签的提交
regex = { git = "https://github.com/rust-lang/regex.git", tag = "1.10.3" }

# 通过 SHA1 哈希指定提交
regex = { git = "https://github.com/rust-lang/regex.git", rev = "0c0990399270277832fbb5b91a1fa118e6f63dba" }

# PR 493 的 HEAD 提交
regex = { git = "https://github.com/rust-lang/regex.git", rev = "refs/pull/493/head" }

# 无效示例

# 在 # 后指定提交会忽略提交 ID 并产生警告
regex = { git = "https://github.com/rust-lang/regex.git#4c59b70" }

# git 和 path 不能同时使用
regex = { git = "https://github.com/rust-lang/regex.git#4c59b70", path = "../regex" }

Cargo 会在添加 git 依赖时锁定其提交,并将其记录在 Cargo.lock 文件中,仅当运行 cargo update 命令时才会检查更新。

version 键的作用

只要包含 version 键,就表示该包在注册表中可用,这与是否存在 gitpath 键无关。

version不会影响 Cargo 获取 git 依赖时使用哪个提交,但 Cargo 会检查依赖项 Cargo.toml 文件中的版本信息是否与 version 键匹配,若不匹配则报错。

在此示例中,Cargo 从 Git 获取名为 next 的分支的 HEAD 提交,并检查 crate 的版本是否与 version = "1.10.3" 兼容:

[dependencies]
regex = { version = "1.10.3", git = "https://github.com/rust-lang/regex.git", branch = "next" }

versiongitpath 键被视为解析依赖的独立来源。有关详细说明,请参阅下文多个来源章节。

注意crates.io 不允许发布依赖于 crates.io 外部代码的包(开发依赖除外)。如需替代方案处理 gitpath 依赖的降级策略,请参见多个来源章节。

Git 子模块

克隆 git 依赖时,Cargo 会自动递归获取其子模块,以确保构建所需的所有代码均可用。

若要跳过与构建无关的子模块获取,可在依赖仓库的 .gitmodules 文件中设置 submodule.<name>.update = none。此操作需要对该仓库有写入权限,并会全局禁用子模块更新。

访问私有 Git 仓库

关于私有仓库的 Git 身份验证帮助,请参阅Git 身份验证

指定路径依赖

随着时间推移,我们在指南中创建的 hello_world 包体积显著增大!到了需要将其拆分为独立 crate 供他人使用的阶段。为此,Cargo 支持路径依赖,这类依赖通常是位于同一仓库内的子 crate。让我们在 hello_world 包内部创建一个新的 crate 作为起点:

# 在 hello_world/ 内部
$ cargo new hello_utils

这会在当前目录下新建一个 hello_utils 文件夹,里面已有配置好的 Cargo.tomlsrc 目录。要让 Cargo 认识它,请打开 hello_world/Cargo.toml,把 hello_utils 加入依赖:

[dependencies]
hello_utils = { path = "hello_utils" }

这告诉 Cargo:我们依赖一个叫 hello_utils 的 crate,它位于相对当前 Cargo.toml 所在位置的 hello_utils 文件夹中。

下次执行 cargo build 时,会自动构建 hello_utils 及其所有依赖。

不会遍历本地路径

本地路径必须精确指向包含依赖 Cargo.toml 的那个文件夹。与 git 依赖不同,Cargo 不会遍历本地路径。例如,如果 regex-literegex-syntax 是本地克隆的 rust-lang/regex 仓库中的成员,就必须写完整路径:

# git 键只需填写仓库根 URL,Cargo 会遍历目录树找到 crate
[dependencies]
regex-lite   = { git = "https://github.com/rust-lang/regex.git" }
regex-syntax = { git = "https://github.com/rust-lang/regex.git" }

# path 键要求路径中包含成员名
[dependencies]
regex-lite   = { path = "../regex/regex-lite" }
regex-syntax = { path = "../regex/regex-syntax" }

发布 crate 中的本地路径

只使用 path 指定依赖的 crate 不允许发布到 crates.io

如果想发布 hello_world 这个 crate,就需要先把某个版本的 hello_utils 作为独立 crate 发布到 crates.io,然后在 hello_world 的依赖中指定其版本:

[dependencies]
hello_utils = { path = "hello_utils", version = "0.1.0" }

pathversion 键同时使用的说明见 Multiple locations 一节。

注意crates.io 不允许发布依赖 crates.io 外部代码的包,dev-dependencies 除外。关于 gitpath 依赖的替代方案,请参阅 多个位置 章节。

Multiple locations

可以同时指定注册表版本以及 gitpath 位置。在本地使用时,会使用 gitpath 指定的依赖(此时会检查本地副本是否符合 version 要求);而发布到 crates.io 等注册表时,则会使用注册表版本。其他组合不被允许。示例:

[dependencies]
# 本地使用时用 `my-bitflags`,发布时用 crates.io 的 1.0 版本。
bitflags = { path = "my-bitflags", version = "1.0" }

# 本地使用时用指定的 git 仓库,发布时用 crates.io 的 1.0 版本。
smallvec = { git = "https://github.com/servo/rust-smallvec.git", version = "1.0" }

# 注意:如果版本不匹配,Cargo 编译将失败!

这种用法在一个典型场景中非常有用:当你将库拆分为同一工作区(workspace)内的多个包时。此时可用 path 依赖指向工作区内的本地包,以便在开发阶段使用本地版本;待这些包发布后,再使用 crates.io 上的版本。这类似于指定 依赖覆盖,但仅针对当前这一条依赖声明生效。

Platform specific dependencies

平台特定依赖采用相同的格式,但需列在 target 节下。通常使用 Rust 风格的 #[cfg] 语法 来定义这些代码段:

[target.'cfg(windows)'.dependencies]
winhttp = "0.4.0"

[target.'cfg(unix)'.dependencies]
openssl = "1.0.1"

[target.'cfg(target_arch = "x86")'.dependencies]
native-i686 = { path = "native/i686" }

[target.'cfg(target_arch = "x86_64")'.dependencies]
native-x86_64 = { path = "native/x86_64" }

与 Rust 代码类似,这里的语法支持 notanyall 操作符,用于组合不同的 cfg 名称/值对。

如果想查看当前平台支持哪些 cfg 目标,请在命令行运行 rustc --print=cfg。如果想查看其他平台(如 64 位 Windows)支持的 cfg 目标,运行 rustc --print=cfg --target=x86_64-pc-windows-msvc

与 Rust 源代码不同,你不能使用 [target.'cfg(feature = "fancy-feature")'.dependencies] 来根据可选功能添加依赖。请改用 [features] 部分

[dependencies]
foo = { version = "1.0", optional = true }
bar = { version = "1.0", optional = true }

[features]
fancy-feature = ["foo", "bar"]

同样的情况也适用于 cfg(debug_assertions)cfg(test)cfg(proc_macro)。 这些值不会按预期工作,并且始终返回由 rustc --print=cfg 给出的默认值。 目前没有办法基于这些配置值添加依赖。

除了 #[cfg] 语法外,Cargo 还支持列出依赖项适用的完整目标:

[target.x86_64-pc-windows-gnu.dependencies]
winhttp = "0.4.0"

[target.i686-unknown-linux-gnu.dependencies]
openssl = "1.0.1"

自定义目标规范

如果你正在使用自定义目标规范(例如 --target foo/bar.json),请去掉 .json 扩展名,使用基础文件名:

[target.bar.dependencies]
winhttp = "0.4.0"

[target.my-special-i686-platform.dependencies]
openssl = "1.0.1"
native = { path = "native/i686" }

注意:稳定版通道不支持使用自定义目标规范。

开发依赖

你可以在 Cargo.toml 中添加一个 [dev-dependencies] 段,其格式与 [dependencies] 相同:

[dev-dependencies]
tempdir = "0.3"

dev-dependencies 不会在构建包本身时使用,但会用于编译测试、示例和基准测试。

这些依赖不会传递给依赖此包的其他包。

你还可以通过在 target 段的头部用 dev-dependencies 替代 dependencies,来声明特定平台的开发依赖。例如:

[target.'cfg(unix)'.dev-dependencies]
mio = "0.0.1"

注意:发布包时,只有指定了 version 的 dev-dependencies 才会包含在发布的 crate 中。大多数情况下发布时并不需要 dev-dependencies,但有些用户(比如操作系统打包者)可能希望在 crate 内运行测试,因此如果条件允许,提供 version 仍然有好处。

构建依赖(Build dependencies)

你可以在构建脚本中使用其他基于 Cargo 的 crate。这类依赖通过 manifest 中的 build-dependencies 段声明:

[build-dependencies]
cc = "1.0.3"

同样,你也可以在 target 段的头部用 build-dependencies 替代 dependencies,声明特定平台的构建依赖。例如:

[target.'cfg(unix)'.build-dependencies]
cc = "1.0.3"

这样,只有当宿主平台与指定的 target 匹配时,该依赖才会被构建。

构建脚本无法访问 dependenciesdev-dependencies 段中列出的依赖;同样,构建依赖对包本身也不可用,除非它同时也列在 dependencies 段中。包本身和它的构建脚本是分开构建的,因此二者的依赖不必相同。让不同的用途使用各自独立的依赖,可以让 Cargo 保持简单和清晰。

选择 features

如果你依赖的包提供了条件性的 feature,你可以指定使用哪些:

[dependencies.awesome]
version = "1.3.5"
default-features = false # 不启用默认特性,并可按需
                         # 单独选择特定特性
features = ["secure-password", "civet"]

关于特性的详细信息,请参阅特性章节

Cargo.toml 中重命名依赖项

Cargo.toml 中编写 [dependencies] 部分时,依赖项的键名通常与代码中导入的 crate 名称保持一致。但在某些项目中,你可能希望在代码中使用不同的名称来引用 crate,即使它在 crates.io 上的发布名称保持不变。例如,你可能希望:

  • 避免在 Rust 源代码中编写 use foo as bar
  • 依赖某个 crate 的多个版本。
  • 依赖来自不同注册表但名称相同的 crate。

为了支持这种需求,Cargo 允许在 [dependencies] 部分使用 package 键来指定实际依赖的包名称:

[package]
name = "mypackage"
version = "0.0.1"

[dependencies]
foo = "0.1"
bar = { git = "https://github.com/example/project.git", package = "foo" }
baz = { version = "0.1", registry = "custom", package = "foo" }

在此示例中,你的 Rust 代码现在可以访问三个 crate:

extern crate foo; // crates.io
extern crate bar; // git 仓库
extern crate baz; // 注册表 `custom`

这三个 crate 各自在 Cargo.toml 中定义的包名称均为 foo。因此,我们需要显式使用 package 键告知 Cargo,尽管我们在本地使用了不同的名称(如 bar 和 baz),但我们实际想要依赖的是 foo 包。如果未指定 package 键,其默认值即为请求的依赖项名称。

请注意,如果你有一个可选依赖项,例如:

[dependencies]
bar = { version = "0.1", package = 'foo', optional = true }

假设你依赖来自 crates.io 的 `foo` 库,但你的库中定义的特性名是 `bar` 而非 `foo`。也就是说,当依赖项被重命名时,特性名称沿用的是原依赖名称,而不是包名称。

启用传递依赖项的方式类似。例如,我们可以在上述清单中添加以下内容:

[features]
log-debug = ['bar/log-debug'] # 使用 'foo/log-debug' 会报错!

从工作区继承依赖项

可以在工作区的 [workspace.dependencies] 表中定义依赖项,从而让成员从工作区继承它们。定义后,在 [dependencies] 表中添加该依赖项并设置 workspace = true

除了 workspace 键,继承的依赖项还可以包含以下键:

  • optional:注意,[workspace.dependencies] 表不允许指定 optional
  • features:这些特性会与 [workspace.dependencies] 中声明的特性相加。

optionalfeatures 外,继承的依赖项不能使用其他任何依赖项键(例如 versiondefault-features)。

[dependencies][dev-dependencies][build-dependencies] 以及 [target."...".dependencies] 部分支持引用 [workspace.dependencies] 中的依赖项定义。

[package]
name = "bar"
version = "0.2.0"

[dependencies]
regex = { workspace = true, features = ["unicode"] }

[build-dependencies]
cc.workspace = true

[dev-dependencies]
rand = { workspace = true, optional = true }

覆盖依赖项

在许多场景下,你可能会希望覆盖某个依赖项。不过,这些场景大多归结为在库发布到 crates.io 之前进行联调。例如:

  • 你正在开发的 crate 同时被一个更大的应用使用,你想在这个大应用中测试对库的 bug 修复。
  • 某个你不参与维护的上游 crate,其 git 仓库的 master 分支上有新功能或 bug 修复,你想先试用一下。
  • 你即将发布 crate 的一个新主版本,想对整个包做集成测试,确保新主版本能正常工作。
  • 你向上游 crate 提交了一个 bug 修复,但希望应用能立即改用修复后的版本,而不用等修复被合并。

这些场景都可以用 [patch] 清单配置段来解决。

本章会介绍几个不同的使用场景,并详细说明覆盖依赖的各种方式。

注意:另见多位置指定依赖,它可用于覆盖本地包中单个依赖声明的来源。

测试 bug 修复

假设你在使用 uuid crate,但使用过程中发现了一个 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 注册表中。

下一步,我们需要确保锁定文件已更新,以便使用这个新版本的 uuid,从而让包使用本地检出的副本,而不是来自 crates.io 的版本。[patch] 的工作原理是加载位于 ../path/to/uuid 处的依赖,随后当查询 uuid 在 crates.io 上的版本时,也会同时返回本地版本。

这意味着本地检出代码的版本号很重要,并会影响是否使用该补丁。我们的清单中声明了 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 之后,你很可能下一步就会向 uuid crate 提交 pull request。完成这一步后,就可以更新 [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 仓库的主干分支已更新至 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 这条依赖链。整个依赖图中 uuid 会被统一解析为 1.0.1 版本,并从 git 仓库拉取。

覆盖仓库 URL

如果你想覆盖的依赖不是从 crates.io 加载的,那么 [patch] 的用法需要稍微调整一下。比如依赖来自 git,可以这样用本地路径覆盖它:

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

就这么简单!

提前发布破坏性变更

下面看看如何使用一个新的大版本 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" }

就这样!和前面的例子一样,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' }

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

使用 [patch] 处理多个版本

你可以使用 package 键来重命名依赖项,从而修补同一个 crate 的多个版本。例如,假设 serde 有一个我们想用于其 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 = ... 指令表示 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 源。

这些表中的每个条目都是常规的依赖规范,与清单文件的 [dependencies] 部分相同。[patch] 部分列出的依赖会被解析并用于修补指定 URL 的源。上面的清单片段使用 foobar 这两个 crate 修补 crates-io 源(例如 crates.io 本身),并使用来自其他地方的 my-branch 修补 https://github.com/example/baz 源。

可以使用不存在的 crate 版本来修补源,也可以使用已存在的版本。如果用于修补的 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] 表中的每个键都是一个包 ID 规范,可以任选依赖图中的某个节点进行覆盖(三段式版本号必须写全)。每个键对应的值与 [dependencies] 中声明依赖的语法相同,但不能指定 features。注意,覆盖某个 crate 时,用来替换的副本必须具有相同的名称和版本,但可以来自不同的源(比如 git 或本地路径)。

Cargo 只会读取工作区根目录下 Cargo.toml 中的 replace 配置,依赖包里定义的 replace 配置会被忽略。

paths 覆盖

有时你只是临时改一下某个 crate,不想像上面的 [patch] 那样去修改 Cargo.toml。针对这种场景,Cargo 提供了一种功能更受限的覆盖方式,叫做路径覆盖(path overrides)。

路径覆盖不是写在 Cargo.toml 里,而是通过.cargo/config.toml 来指定。在 .cargo/config.toml 中添加一个 paths 键:

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

这个数组里应填入包含 Cargo.toml 的目录。这里我们只加了 uuid,所以它就是唯一被覆盖的包。路径可以是绝对路径,也可以是相对于 .cargo 文件夹所在目录的相对路径。

不过路径覆盖比 [patch] 受限得多:它不能改变依赖图的结构。使用路径替换时,原有的依赖集合必须与新 Cargo.toml 中的声明完全匹配。也就是说,路径覆盖不能用来测试给某个 crate 新增依赖,这种情况必须用 [patch]。因此路径覆盖通常只用于快速修复 bug,而不是做较大改动。

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

来源替换

本文档旨在说明如何将 注册表基于 git 的依赖仓库 的通信重定向到其他数据源,例如原始注册表的镜像服务器或完全一致的本地副本。

如果需要修补个别依赖项,请参见本文档的覆盖依赖项部分。如需控制 Cargo 发起网络请求的方式,请参见 [http][net] 配置。

来源(Source)是提供可作为包依赖项的 crate 的服务方。Cargo 支持将一个来源替换为另一个,以实现以下策略:

  • vendoring —— 可以定义代表本地文件系统上 crate 的自定义来源。这些来源是其所替换来源的子集,如有必要,可以检入包中。

  • 镜像 —— 可以用作为 crates.io 缓存的等效版本来替换来源。

Cargo 在来源替换上的核心假设是:两个来源中的源代码必须完全相同。这意味着替换来源不允许包含原始来源中不存在的 crate。

因此,来源替换并不适用于修补依赖项或使用私有注册表等场景。Cargo 支持通过[patch]来修补依赖项,而有关私有注册表支持的内容在注册表章节中有详细说明。

使用来源替换时,运行需要直接联系注册表的命令1需要传递 --registry 选项。这有助于避免关于要联系哪个注册表的歧义,并将使用指定注册表的身份验证令牌。

配置

替换源通过 .cargo/config.toml 进行配置,可用的完整键列表如下:

# 所有与源替换相关的键都存储在 `source` 表中。
[source]

# 在 `source` 表下还有多个表,其键为相关源的名称。
# 例如,本节定义了一个名为 `my-vendor-source` 的新源,
# 它来自相对于包含此 `.cargo/config.toml` 文件的目录的 `vendor` 目录。
[source.my-vendor-source]
directory = "vendor"

# crates.io 默认源的名称为 "crates-io",
# 这里使用 `replace-with` 键指示其被上方定义的源替换。
#
# `replace-with` 键也可以引用在 `[registries]` 表中定义的备用注册表名称。
[source.crates-io]
replace-with = "my-vendor-source"

# 每个源都有自己独立的表,键为源名称
[source.the-source-name]

# 指示 `the-source-name` 将被替换为在其他位置定义的 `another-source`
replace-with = "another-source"

# 可以指定多种类型的源(详见下文说明):
registry = "https://example.com/path/to/index"
local-registry = "path/to/registry"
directory = "path/to/vendor"

# Git 源还可以可选地指定分支/标签/修订版本
git = "https://example.com/path/to/repo"
# branch = "master"
# tag = "v1.0.1"
# rev = "313f44e8"

注册表源

“注册表源”类似于 crates.io 本身。它是一个符合 https://doc.rust-lang.org/cargo/reference/registry-index.html 规范的索引,并包含指示从何处下载 crate 的配置文件。

注册表源可以使用 Git 或稀疏 HTTP 协议

# Git 协议
registry = "ssh://git@example.com/path/to/index.git"

# 稀疏 HTTP 协议  
registry = "sparse+https://example.com/path/to/index"

# HTTPS Git 协议
registry = "https://example.com/path/to/index"

本地注册表源

“本地 registry 源”是某个 registry 源的子集,放在本地文件系统上(也就是常说的 vendoring)。本地 registry 需要提前下载好,通常与 Cargo.lock 保持同步,其组成和普通 registry 一样:一组 *.crate 文件加上一份索引。

管理和创建本地 registry 源的主要方式是通过 cargo-local-registry 子命令,它发布在 crates.io 上,可以用 cargo install cargo-local-registry 安装。

本地 registry 包含在一个目录里,里面有若干从 crates.io 下载的 *.crate 文件,以及一个 index 目录,格式与 crates.io-index 项目相同(只包含现有 crate 对应的条目)。

目录源

“目录源”与本地 registry 源类似,也是在本地文件系统上存放若干 crate,同样适用于依赖的 vendoring。目录源主要由 cargo vendor 子命令管理。

不过目录源和本地 registry 有所不同:它存放的是 *.crate 文件解包后的内容,因此在某些情况下更适合整体纳入版本控制。目录源就是一个目录,里面包含若干子目录,每个子目录存放一个 crate 的源码(即 *.crate 文件解包后的内容)。目前对子目录的命名没有任何限制。

目录源中的每个 crate 还有一个关联的元数据文件 .cargo-checksum.json,用于防止意外修改。它并非安全机制,无法防范恶意篡改。

Git 源

Git 源表示 基于 git 的依赖所使用的仓库,用于指定哪些基于 git 的依赖应被替换为其他源。

Git 源与 git registry 无关,也不能用来替换 registry 源。

  1. 相关命令示例参见发布命令

依赖解析

Cargo 的核心职责之一,是根据各包中指定的版本要求,确定所依赖项的具体版本。这一过程称为“依赖解析”(dependency resolution),由“解析器”(resolver)执行。解析结果被记录在 Cargo.lock 文件中,将依赖项锁定到特定版本,并保证其在后续构建中保持不变。 你可以使用 cargo tree 命令来可视化解析器的输出结果。

约束与启发式策略

在许多情况下,并不存在唯一的“最优”依赖解析方案。解析器在多种约束和启发式规则的指导下,寻找一个通用性较好的解析结果。 要理解这些机制的相互作用,有必要粗略了解依赖解析的基本工作原理。

以下伪代码近似描述了 Cargo 解析器的工作过程:

#![allow(unused)]
fn main() {
pub fn resolve(workspace: &[Package], policy: Policy) -> Option<ResolveGraph> {
    let dep_queue = Queue::new(workspace);
    let resolved = ResolveGraph::new();
    resolve_next(dep_queue, resolved, policy)
}

fn resolve_next(dep_queue: Queue, resolved: ResolveGraph, policy: Policy) -> Option<ResolveGraph> {
    let Some(dep_spec) = policy.pick_next_dep(&mut dep_queue) else {
        // Done
        return Some(resolved);
    };

    if let Some(resolved) = policy.try_unify_version(dep_spec, resolved.clone()) {
        return Some(resolved);
    }

    let dep_versions = dep_spec.lookup_versions()?;
    let mut dep_versions = policy.filter_versions(dep_spec, dep_versions);
    while let Some(dep_version) = policy.pick_next_version(&mut dep_versions) {
        if policy.needs_version_unification(&dep_version, &resolved) {
            continue;
        }

        let mut dep_queue = dep_queue.clone();
        dep_queue.enqueue(&dep_version.dependencies);
        let mut resolved = resolved.clone();
        resolved.register(dep_version);
        if let Some(resolved) = resolve_next(dep_queue, resolved, policy) {
            return Some(resolved);
        }
    }

    // No valid solution found, backtrack and `pick_next_version`
    None
}
}

关键步骤:

  • 遍历依赖项(pick_next_dep):遍历依赖项的顺序会影响同一依赖项的相关版本要求如何被解决,以及版本统一的情况,并会影响解析器回溯的程度,从而影响解析器的性能。
  • 统一版本(try_unify_versionneeds_version_unification):Cargo 尽可能复用版本以减少构建时间,并允许公共依赖项的类型在 API 之间传递。如果多个版本本可以被统一,但它们的依赖规范之间存在冲突,Cargo 会进行回溯,如果在未找到解决方案时则报错,而不是选择多个版本。一个依赖规范或 Cargo 可能会认为某个版本不理想,宁愿回溯或报错也不愿使用它。
  • 优先选择特定版本(pick_next_version): Cargo 可能决定优先使用某个版本,在回溯时再退而选择下一个版本。
  • 版本号

    通常情况下,Cargo 会优先选择当前可用的最高版本。

    例如,假设依赖解析图中的某个包声明了:

    [dependencies]
    bitflags = "*"
    

    如果生成 Cargo.lock 文件时,bitflags 的最高版本是 1.2.1,那么这个包就会使用 1.2.1

    关于可能的例外情况,请参阅Rust 版本

    版本要求

    包通过版本要求来声明自己支持的版本范围,其余版本会被一律拒绝。

    例如,假设依赖解析图中的某个包声明了:

    [dependencies]
    bitflags = "1.0"  # 意为 `>=1.0.0,<2.0.0`
    

    如果生成 Cargo.lock 文件时,bitflags 的最高版本是 1.2.1,那么这个包就会使用 1.2.1,因为它在兼容范围内的版本最高。即使之后发布了 2.0.0,它仍会继续使用 1.2.1,因为 2.0.0 被视为不兼容。

    SemVer 兼容性

    Cargo 假定所有包都遵循 SemVer,并会在依赖版本满足插入符版本要求所定义的 SemVer 兼容关系时进行统一。如果两个本应兼容的版本因版本要求冲突而无法统一,Cargo 会报错。

    关于什么样的变更算是“兼容”变更,请参阅 SemVer Compatibility 章节的指引。

    示例:

    下面两个包对 bitflags 的依赖会被统一,因为无论最终选定哪个版本,二者都彼此兼容。

    # Package A
    [dependencies]
    bitflags = "1.0"  # 意为 `>=1.0.0,<2.0.0`
    
    # Package B
    [dependencies]
    bitflags = "1.1"  # 意为 `>=1.1.0,<2.0.0`
    

    如果两个包的版本要求存在冲突,无法选定同一个兼容版本,则会导致错误。

    # Package A
    [dependencies]
    log = "=0.4.11"
    
    # Package B
    [dependencies]
    log = "=0.4.8"
    

    以下两个包对 rand 的依赖无法统一,因为各自只有互不兼容的版本可用。 此时,解析器会分别选定两个不同的版本(例如 0.6.5 和 0.7.3)并进行编译。 这可能导致潜在问题,详见 版本不兼容风险 章节。

    # Package A
    [dependencies]
    rand = "0.7"  # 意为 `>=0.7.0,<0.8.0`
    
    # Package B
    [dependencies]
    rand = "0.6"  # 意为 `>=0.6.0,<0.7.0`
    

    通常,如果存在满足版本要求但不兼容的多个版本,以下两个包的依赖也不会被统一: 相反,解析器会选定两个不同的版本(例如 0.6.5 和 0.7.3)并进行编译。 应用其他约束或启发式策略可能会强制统一它们,从而选定其中一个版本(例如 0.6.5)。

    # Package A
    [dependencies]
    rand = ">=0.6,<0.8.0"
    
    # Package B
    [dependencies]
    rand = "0.6"  # 意为 `>=0.6.0,<0.7.0`
    

    版本不兼容风险

    当一个 crate 的多个版本出现在依赖解析图中,如果使用该 crate 的其他 crate 将其类型暴露给外部,就可能引发问题。 这是因为即使名称相同,Rust 编译器也会将它们视为不同的类型和项。 在发布 SemVer 不兼容版本时(例如在 1.0.0 被广泛使用后发布 2.0.0),库作者需格外谨慎,尤其是那些被广泛使用的库。

    SemVer 技巧”是解决此问题的变通方案:在发布包含破坏性变更的新版本时,保持与旧版本的兼容性。 链接页面详细解释了问题的本质及应对方法。简言之,当库需要发布 SemVer 不兼容的新版本时,除了发布该新版本外,还应发布旧版本的一个点版本(point release),该点版本重新导出(re-export)新版本的类型。

    这些不兼容问题通常表现为编译时错误,但有时也会以运行时的异常行为呈现。举例来说,假设名为 foo 的公共库在依赖解析图中同时出现了 1.0.02.0.0 两个版本。若代码中调用 downcast_ref,且该对象由 1.0.0 版本的库创建,而调用者试图将其向下转换(downcast)为 2.0.0 版本中的类型,那么该向下转换操作将在运行时失败。

    当项目中引入了多个版本的同一库时,务必确保正确使用它们,尤其是当不同版本的类型可能会在代码中混用时。可以使用 cargo tree -d 命令来识别重复的版本及其来源。同样,若计划发布某个流行库的不符合 SemVer 的版本,也需慎重考虑其对整个生态系统的影响。

    Lock file

    在使用 Cargo.lock 文件时,Cargo 会赋予其中包含的版本最高优先级。这种设计旨在平衡可复现构建(reproducible builds)的需求与清单(manifest)变更带来的调整。

    假设依赖解析图中的某个包包含如下配置:

    [dependencies]
    bitflags = "*"
    

    若生成 Cargo.lock 文件时,bitflags 的最新版本为 1.2.1,那么该包将使用 1.2.1,并记录在 Cargo.lock 文件中。

    等到 Cargo 下次运行时,bitflags 已发布 1.3.5。在解析依赖时,由于 Cargo.lock 文件中已存在 1.2.1,系统仍会使用此版本。

    随后,该包被修改为:

    [dependencies]
    bitflags = "1.3.0"
    

    此时 bitflags 1.2.1 不再匹配新的版本要求,因此 Cargo.lock 中对应的条目将被忽略,系统将改用并记录 1.3.5 版本。

    Rust version

    为了支持在指定的最低 Rust 版本下开发软件,resolver 在选择依赖时会考虑依赖版本与你的 Rust 版本的兼容性。这由配置项 resolver.incompatible-rust-versions 控制。

    设置为 fallback 时,resolver 会优先选择 Rust 版本要求不高于你当前版本的包。例如,你正在用 Rust 1.85 开发下面这个包:

    [package]
    name = "my-cli"
    rust-version = "1.62"
    
    [dependencies]
    clap = "4.0"  # resolves to 4.0.32
    

    resolver 会选择 4.0.32,因为它要求的 Rust 版本是 1.60.0。

    • 不选 4.0.0,虽然它同样要求 Rust 1.60.0,但它的版本号更低
    • 不选 4.5.20,虽然它的版本号更高,且要求的 Rust 版本 1.74.0 与你的 1.85 工具链兼容,但它与 my-cli 声明的 Rust 版本 1.62 不兼容。

    如果版本要求中找不到兼容指定 Rust 版本的依赖版本,resolver 不会报错,而是照常选择一个版本,哪怕这个选择并不理想。例如,把对 clap 的依赖改成:

    [package]
    name = "my-cli"
    rust-version = "1.62"
    
    [dependencies]
    clap = "4.2"  # resolves to 4.5.20
    

    没有任何 clap 版本能同时满足这个版本要求和 Rust 版本 1.62。于是 resolver 会选择一个不兼容的版本,比如要求 Rust 1.74 的 4.5.20。

    resolver 在为某个包选择依赖版本时,并不知道最终会有哪些 workspace 成员通过传递依赖用到这个版本,因此无法只针对某一个依赖相关的 Rust 版本来做选择。当 workspace 中各成员的 Rust 版本不同时,resolver 会采用启发式策略来找到一个“足够好”的方案。这条规则同样适用于 workspace 中没有声明 Rust 版本的包。

    当 workspace 中各成员的 Rust 版本不一致时,resolver 可能会选出一个比实际需要的更低的依赖版本。例如,你有下面这些 workspace 成员:

    [package]
    name = "a"
    rust-version = "1.62"
    
    [package]
    name = "b"
    
    [dependencies]
    clap = "4.2"  # 解析到 4.5.20
    

    尽管包 b 没有指定 Rust 版本,本可以使用 4.5.20 这样更高的版本,但由于包 a 要求 Rust 1.62,解析器最终仍会选择 4.0.32。

    解析器也可能选择了过高的版本。例如,你拥有以下 workspace 成员:

    [package]
    name = "a"
    rust-version = "1.62"
    
    [dependencies]
    clap = "4.2"  # 解析到 4.5.20
    
    [package]
    name = "b"
    
    [dependencies]
    clap = "4.5"  # 解析到 4.5.20
    

    虽然每个包对 clap 的版本要求都符合其自身的 Rust 版本,但由于版本统一机制,解析器需要选择一个在两种情况下都可行的版本,结果会是像 4.5.20 这样的版本。

    Features

    为了生成 Cargo.lock,解析器会构建依赖图,假设所有 features 在所有 workspace 成员中均已启用。这确保了任何可选依赖在通过--features 命令行参数添加或移除 features 时,都能被正确发现并解析。随后,解析器会根据命令行选定的 features,第二次运行以确定编译 crate 时实际使用的 features。

    依赖解析基于所有已启用 features 的并集。例如,如果一个包依赖带有serde 依赖im 包,而另一个包依赖同一 im 包但启用了rayon 依赖,那么 im 将以这两个 features 同时启用进行构建,serderayon crate 都会包含在解析图中。如果没有任何包以这些 features 依赖 im,这些可选依赖将被忽略,不影响解析。

    在工作区(例如使用 --workspace 或多个 -p 标志)中构建多个包时,所有依赖包的特性(features)会进行统一处理。如果希望避免不同工作区成员之间的特性统一,需要分别通过独立的 cargo 命令进行构建。

    解析器(resolver)会跳过缺少所需特性的包版本。例如,如果一个包依赖于带有 perf 特性regex ^1 版本,那么它最低可选择的版本是 1.3.0,因为更早的版本不包含 perf 特性。同理,如果新发布版本移除了某个特性,依赖该特性的包将被锁定在仍包含该特性的旧版本上。不建议在符合 SemVer 规范的版本更新中移除特性。请注意,可选依赖项(optional dependencies)也隐式定义了一个特性,因此移除可选依赖项或将其改为非可选项可能会引发问题,参见移除可选依赖项

    特性解析器版本 2

    当在 Cargo.toml 中指定 resolver = "2" 时(见下文解析器版本),系统将使用不同的特性解析器,其特性统一算法也有所不同。版本 "1" 的解析器无论特性在哪里声明,都会对同一包的特性进行统一。版本 "2" 的解析器在以下情况下会避免统一特性:

    • 针对特定目标的依赖项的特性,如果当前构建目标不匹配,则不会被启用。例如:

      [dependencies.common]
      version = "1.0"
      features = ["f1"]
      
      [target.'cfg(windows)'.dependencies.common]
      version = "1.0"
      features = ["f2"]
      

      当在非 Windows 平台上构建此示例时,f2 特性将不会被启用。

    • 启用在 build-dependencies 或 proc-macros 上的特性,当这些相同的依赖项作为普通依赖项使用时,不会发生统一。例如:

      [dependencies]
      log = "0.4"
      
      [build-dependencies]
      log = {version = "0.4", features=['std']}
      

      构建 build script 时,log crate 会启用 std feature;而构建包本身的库时,则不会启用该 feature。

    • 如果某个依赖同时作为 dev-dependencies 和普通依赖出现,那么它在 dev-dependencies 中启用的 feature 不会被统一进来,除非当前正在构建的就是这些 dev-dependencies。例如:

      [dependencies]
      serde = {version = "1.0", default-features = false}
      
      [dev-dependencies]
      serde = {version = "1.0", features = ["std"]}
      

      在这个例子中,正常构建时库会链接不启用 std feature 的 serde;但构建测试或示例时会启用 std feature。比如执行 cargo testcargo build --all-targets 时,这些 feature 会被统一。注意,依赖包自身声明的 dev-dependencies 始终会被忽略,这条规则只对顶层包或 workspace 成员有意义。

    links 字段用于确保同一个原生库只会有一份被链接进二进制文件。resolver 在构建依赖图时,会尽量让每个 links 名称在图中只出现一次;如果找不到满足该约束的依赖图,就会报错。

    例如,如果一个包依赖 libgit2-sys0.11 版本,另一个包依赖 0.12 版本,就会出错——因为 Cargo 无法把它们统一,而它们又都链接到 git2 这个原生库。因此,如果你的库被广泛使用,在发布与 links 字段相关的 SemVer 不兼容版本时一定要格外谨慎。

    被撤下的版本

    被撤下的版本是指被标记为不应再使用的版本。resolver 在构建依赖图时会忽略所有被撤下的版本,除非它们已经存在于 Cargo.lock 文件中,或者通过 cargo update--precise 参数被明确指定。

    依赖更新

    Cargo 中需要了解依赖图的所有命令都会自动执行依赖解析。例如,cargo build 会运行解析器来发现需要构建的所有依赖。首次运行后,结果会被存储在 Cargo.lock 文件中。后续命令在运行时,解析器会尝试将依赖锁定在 Cargo.lock 文件中的版本上,前提是其版本满足要求。

    如果 Cargo.toml 中的依赖列表发生了变更,例如将某个依赖的版本从 1.0 改为 2.0,解析器会为该依赖选择符合新要求的新版本。如果该新依赖引入了新要求,这些新要求也可能触发额外的更新。Cargo.lock 文件会根据新结果进行更新。可以使用 --locked--frozen 标志来改变此行为,防止需求变更时自动更新,而是直接返回错误。

    cargo update 可用于在新版本发布时更新 Cargo.lock 中的条目。不加任何选项时,它会尝试更新锁文件中的所有包。可以使用 -p 标志针对特定包进行更新,也可以使用 --recursive--precise 等其他标志来控制版本选择。

    覆盖

    Cargo 提供了几种机制来覆盖依赖图中的依赖。覆盖依赖 章节详细介绍了如何使用这些覆盖机制。覆盖功能作为注册表的覆盖层,用新条目替换被修补的版本。除此之外,解析过程与往常一样正常进行。

    依赖种类

    包中主要有三种依赖:普通依赖、构建依赖开发依赖。从解析器的角度来看,这三种依赖大多被同等对待。唯一的区别在于,非工作区成员的 dev-dependencies 始终会被忽略,不会影响解析结果。

    平台相关依赖[target]表的解析方式是假设所有平台均已启用。换言之,解析器会忽略平台或cfg表达式。

    dev-dependency循环

    通常解析器不允许图中存在循环,但允许dev-dependencies存在循环。例如,项目“foo”对“bar”有dev-dependency,而“bar”对“foo”有普通依赖(通常表现为“path”依赖)。这是被允许的,因为从构建产物的角度来看,实际上并不存在循环。在此例中,“foo”库会先被构建(由于“bar”仅用于测试,“foo”的构建无需依赖“bar”),随后“bar”可以依赖“foo”进行构建,最后可以构建“foo”的测试并链接到“bar”。

    需注意,这可能引发令人困惑的错误。在构建库单元测试时,最终测试二进制文件中实际包含该库的两个副本:一个是与“bar”链接的,另一个是包含单元测试的。正如版本不兼容隐患部分所述的问题,这两个副本中的类型并不兼容。在这种情况下,若“bar”暴露了“foo”的类型,需谨慎处理,因为“foo”的单元测试不会将这些类型视为本地类型。

    如果可能,建议将包拆分为多个包并重构,以确保依赖关系严格保持无环。

    解析器版本

    可以通过Cargo.toml中的解析器版本指定不同的解析器行为:

    [package]
    name = "my-package"
    version = "1.0.0"
    resolver = "2"
    

    resolver 是影响整个 workspace 的全局选项。依赖中设置的 resolver 版本会被忽略,只有顶层包中的值才有效。如果使用的是虚拟 workspace,则应该在 [workspace] 表中指定版本,例如:

    [workspace]
    members = ["member1", "member2"]
    resolver = "2"
    

    MSRV: 需要 1.51+

    建议

    以下是一些关于如何设置包版本号以及声明依赖要求的建议。这些是针对常见情况的通用指南,某些特殊场景当然可能需要不同的做法。

    • 在决定如何更新版本号、以及是否需要进行不兼容 SemVer 的版本变更时,遵循 SemVer 指南

    • 大多数情况下,依赖应使用插入符要求(caret requirement),例如 "1.2.3"。这样可以在保持构建兼容的前提下,让 resolver 尽可能灵活地选择版本。

      • 版本号写全三个部分,填当前正在使用的版本。这样既设定了最低版本,也能保证其他用户不会拿到缺少你包所需功能的旧版本依赖。
      • 避免使用 * 要求:crates.io 不允许使用,而且普通的 cargo update 也可能因此引入破坏 SemVer 兼容性的变更。
      • 避免过于宽泛的版本要求。例如 >=2.0.0 可能匹配到任何不兼容 SemVer 的版本(比如 5.0.0),将来可能导致构建失败。
      • 尽量避免过于狭窄的版本要求。例如,你指定波浪符要求 bar="~1.3",而另一个包要求 bar="1.4",即使次版本发布本应互相兼容,依赖解析也会失败。
    • 尽量将依赖项的版本号维持在库实际所需的最低版本上。例如,如果当前要求是 bar="1.0.12",而后续版本开始使用 1.1.0 版本中新增的功能,那么应将依赖要求更新为 bar="1.1.0"

      如果未这样做,问题可能不会立即显现,因为当你运行整体 cargo update 时,Cargo 可能会顺势选择最新版本。但是,如果其他用户依赖你的库并运行 cargo update your-library,而他们的 Cargo.lock 中锁定的是旧版本的 “bar”,它不会自动更新 “bar”。只有当依赖声明也同步更新时,才会在那种情况下更新 “bar”。疏忽这一点会导致用户在运行 cargo update your-library 时遇到令人困惑的构建错误。

    • 如果两个包紧密耦合,使用 = 依赖要求有助于确保它们保持同步。例如,某个库与其配套的 proc-macro 库之间有时会做出假设,如果两个库版本不同步(且预期绝不会单独使用这两个库),这些假设将失效。主库可以使用 = 要求锁定 proc-macro 版本,并重导出这些宏以便轻松访问。

    • 0.0.x 版本可用于永久不稳定的包。

    通常,依赖要求越严格,解析器失败的可能性就越大。反之,如果要求过于宽松,新发布的版本可能会破坏构建过程。

    故障排除

    以下示例说明了你可能会遇到的一些问题及部分解决方案。

    为什么包含某个依赖?

    假设你在 cargo check 输出中看到依赖 rand,但不认为它是必需的,想要理解其被引入的原因。

    你可以运行:

    $ cargo tree --workspace --target all --all-features --invert rand
    rand v0.8.5
    └── ...
    
    rand v0.8.5
    └── ...
    

    为什么该依赖项的某个特性被启用了?

    你或许会发现,是某个已启用的 feature 导致 rand 出现在依赖树中。若想查明是哪个包启用了该 feature,可以添加 --edges features 参数

    $ cargo tree --workspace --target all --all-features --edges features --invert rand
    rand v0.8.5
    └── ...
    
    rand v0.8.5
    └── ...
    

    意外的依赖重复

    运行以下命令时,你看到 rand 出现了多个实例:

    $ cargo tree --workspace --target all --all-features --duplicates
    rand v0.7.3
    └── ...
    
    rand v0.8.5
    └── ...
    

    解析算法收敛到一个包含两份依赖副本的方案,尽管一份就足够了。例如:

    # Package A
    [dependencies]
    rand = "0.7"
    
    # Package B
    [dependencies]
    rand = ">=0.6"  # 注意:不鼓励使用这种开放式版本要求
    

    在这个例子中,Cargo 可能会构建两份 rand crate,即使单份 0.7.3 版本就能满足所有要求。这是因为解析算法倾向于为 Package B 构建当前可用的最新 rand 版本(即撰写本文时的 0.8.5),而这与 Package A 的规格不兼容。目前,解析算法并未尝试在此种情况下进行去重。

    Cargo 不鼓励使用类似 >=0.6 的开放式版本要求。但如果你遇到这种情况,可以使用带有 --precise 标志的 cargo update 命令手动移除这类重复。

    为什么未选择更新的版本?

    假设你运行 cargo update 后注意到依赖未选择最新版本:

    $ cargo update
    

    你可以启用额外的日志记录来查看原因:

    $ env CARGO_LOG=cargo::core::resolver=trace cargo update
    

    注意: Cargo 的日志目标和级别可能会随时间变化。

    破坏 SemVer 的补丁版本导致构建失败

    有时项目可能会不小心发布一个带有 SemVer 破坏性变更的小版本更新。用户执行 cargo update 后会拉取到这个新版本,构建就可能出问题。这种情况下,建议项目对该版本执行 yank(撤销),然后要么撤掉这个破坏性变更,要么以新的 SemVer 主版本号发布。

    如果变更发生在第三方项目里,条件允许的话,尽量(礼貌地!)与该项目沟通协作来解决问题。

    在等待该版本被撤销期间,可以根据具体情况采取一些临时应对措施:

    • 如果你的项目是最终产品(比如二进制可执行文件),只需在 Cargo.lock 中避免更新出问题的包即可,可以用 cargo update--precise 标志实现。
    • 如果你在 crates.io 上发布了二进制项目,可以临时给依赖加上 = 要求,将其锁定在某个正常的特定版本。
      • 二进制项目也可以建议用户在 cargo install 时使用 --locked 标志,以沿用原始 Cargo.lock 中已确认正常的版本。
    • 库项目也可以考虑临时发布一个新版本,用更严格的要求避开有问题的依赖。此时建议使用范围要求(而非 =),避免因要求过严而与使用同一依赖的其他包产生冲突。问题解决后,再发布一个小版本,把依赖要求放宽回 caret 要求即可。
    • 如果第三方项目看起来无法或不愿撤销该版本,一个选择是调整你的代码以兼容这些变更,并把依赖要求的最低版本更新为新版本。同时你还需要考虑这是否对你自己的库构成了 SemVer 破坏性变更,例如你的库是否对外暴露了该依赖中的类型。

    评论 (0)