依赖项解析
Cargo 的主要任务之一是根据每个包中指定的版本要求确定要使用的依赖项版本。此过程称为“依赖项解析”,由“解析器”执行。解析结果存储在 Cargo.lock
文件中,该文件将依赖项“锁定”到特定版本,并随着时间的推移保持不变。
解析器尝试在考虑可能冲突的需求的同时统一公共依赖项。然而,事实证明,在许多情况下,没有单一的“最佳”依赖项解析,因此解析器必须使用启发式方法来选择首选解决方案。以下部分提供了一些有关如何处理需求以及如何使用解析器的详细信息。
有关如何指定依赖项要求的更多详细信息,请参阅指定依赖项一章。
可以使用cargo tree
命令可视化解析器的结果。
语义化版本兼容性
Cargo 使用语义化版本来指定版本号。这为包的不同版本之间兼容的内容建立了通用约定。有关什么被认为是“兼容”更改的指南,请参阅语义化版本兼容性一章。这种“兼容性”的概念很重要,因为 Cargo 假设在兼容性范围内更新依赖项应该是安全的,而不会破坏构建。
如果版本的第一个非零主版本号/次版本号/补丁版本号组件相同,则认为这些版本是兼容的。例如,1.0.3
和 1.1.0
被认为是兼容的,因此从旧版本更新到新版本应该是安全的。但是,不允许自动从 1.1.0
更新到 2.0.0
。此约定也适用于前导零的版本。例如,0.1.0
和 0.1.2
是兼容的,但 0.1.0
和 0.2.0
不兼容。类似地,0.0.1
和 0.0.2
也不兼容。
快速回顾一下,Cargo 用于依赖项的版本要求语法是
要求 | 示例 | 等效 | 描述 |
---|---|---|---|
插入符号 | 1.2.3 或 ^1.2.3 | >=1.2.3, <2.0.0 | 任何至少与给定值语义化版本兼容的版本。 |
波浪号 | ~1.2 |
| >=1.2.0, <1.3.0 |
最低版本,兼容性范围受限。 | 1.* | 通配符 | * 或 1.* 或 1.2.* |
>=1.0.0, <2.0.0 | =1.2.3 | =1.2.3 | * 位置的任何版本。 |
等于 | >1.1 | >=1.2.0 | =1.2.3 |
仅指定的确切版本。 | 比较 | >1.2.3, <1.2.3, =1.2.3 | 指定数字的简单数字比较。 |
复合
# Package A
[dependencies]
bitflags = "1.0"
# Package B
[dependencies]
bitflags = "1.1"
>=1.2, <1.5
>=1.2.0, <1.5.0
# Package A
[dependencies]
rand = "0.7"
# Package B
[dependencies]
rand = "0.6"
必须同时满足的多个要求。
当多个包为公共包指定依赖项时,解析器会尝试确保它们使用相同版本的公共包,只要它们在语义化版本兼容性范围内即可。它还尝试使用该兼容性范围内当前可用的最大版本。例如,如果解析图中有两个包具有以下要求
# Package A
[dependencies]
log = "=0.4.11"
# Package B
[dependencies]
log = "=0.4.8"
如果在生成 Cargo.lock
文件时,bitflags
的最大版本是 1.2.1
,则两个包都将使用 1.2.1
,因为它是兼容性范围内最大的版本。如果发布了 2.0.0
,它仍然会使用 1.2.1
,因为 2.0.0
被认为是不兼容的。
如果多个包对公共依赖项具有语义化版本不兼容的版本,则 Cargo 将允许这种情况,但会构建该依赖项的两个单独副本。例如
以上将导致包 A 使用最大的 0.7
版本(在撰写本文时为 0.7.3
),而包 B 将使用最大的 0.6
版本(例如 0.6.5
)。这可能会导致潜在问题,有关更多详细信息,请参阅版本不兼容性风险部分。
“semver 技巧”是解决在保留与旧版本兼容性的同时发布重大更改问题的一种方法。链接页面详细介绍了问题是什么以及如何解决它。简而言之,当一个库想要发布一个破坏 SemVer 的版本时,发布新版本,并发布先前版本的一个点版本,该版本重新导出新版本的类型。
这些不兼容性通常表现为编译时错误,但有时它们只会在运行时表现为错误行为。例如,假设有一个名为 foo
的公共库,它最终在解析图中同时出现了版本 1.0.0
和 2.0.0
。如果在使用版本 1.0.0
的库创建的对象上使用 downcast_ref
,并且调用 downcast_ref
的代码向下转换为版本 2.0.0
的类型,则向下转换将在运行时失败。
重要的是要确保,如果您有多个版本的库,您要正确使用它们,特别是当不同版本的类型可能一起使用时。可以使用 cargo tree -d
命令来识别重复版本及其来源。同样,如果您发布了一个流行库的 SemVer 不兼容版本,请务必考虑对生态系统的影响。
预发布版本
SemVer 具有“预发布版本”的概念,版本号中带有一个破折号,例如 1.0.0-alpha
或 1.0.0-beta
。除非明确要求,否则 Cargo 将避免自动使用预发布版本。例如,如果发布了 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"
。
Cargo 还将从预发布版本自动升级到语义化版本兼容的已发布版本。要求 foo = "1.0.0-alpha"
将允许更新到 foo = "1.0.0"
以及 foo = "1.2.0"
。
请注意,预发布版本可能不稳定,因此在使用它们时应格外小心。某些项目可能会选择在预发布版本之间发布重大更改。如果您的库不是预发布版本,则建议不要在库中使用预发布依赖项。更新 Cargo.lock
时也应格外小心,并为预发布更新导致问题做好准备。
预发布标记可以用句点分隔以区分不同的组件。数字组件将使用数字比较。例如,1.0.0-alpha.4
将对 4
组件使用数字比较。这意味着如果发布了 1.0.0-alpha.11
,它将被选为最大的版本。非数字组件按字典顺序比较。
版本元数据
SemVer 具有“版本元数据”的概念,版本号中带有一个加号,例如 1.0.0+21AF26D3
。此元数据通常被忽略,并且不应在版本要求中使用。您永远不应该发布仅元数据标记不同的多个版本。
其他约束
版本要求并不是解析器在选择和统一依赖项时考虑的唯一约束。以下部分介绍了可能影响解析的其他一些约束。
功能
为了生成 Cargo.lock
,解析器构建依赖关系图,就好像所有 工作区 成员的所有 功能 都已启用一样。这确保了在使用 --features
命令行标志 添加或删除功能时,任何可选依赖项都可用并与图的其余部分正确解析。解析器在*编译*板条箱时会再次运行,以根据命令行上选择的功能确定实际使用的功能。
依赖项的解析使用它们上启用的所有功能的并集。例如,如果一个包依赖于启用了 serde
依赖项 的 im
包,而另一个包依赖于启用了 rayon
依赖项 的 im
包,则 im
将使用这两个功能启用构建,并且 serde
和 rayon
板条箱将包含在解析图中。如果没有包依赖于启用了这些功能的 im
,则这些可选依赖项将被忽略,并且它们不会影响解析。
在工作区中构建多个包时(例如使用 --workspace
或多个 -p
标志),所有这些包的依赖项的功能将被统一。如果您希望避免为不同的工作区成员进行这种统一,则需要通过单独的 cargo
调用来构建它们。
解析器将跳过缺少所需功能的包版本。例如,如果一个包依赖于具有 perf
功能 的 regex
版本 ^1
,则它可以选择的最旧版本是 1.3.0
,因为该版本之前的版本不包含 perf
功能。类似地,如果从新版本中删除了一个功能,则需要该功能的包将停留在包含该功能的旧版本上。不建议在 SemVer 兼容版本中删除功能。请注意,可选依赖项也定义了一个隐式功能,因此删除可选依赖项或使其成为非可选依赖项可能会导致问题,请参阅 删除可选依赖项。
功能解析器版本 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
功能。 -
在 构建依赖项 或 proc-macros 上启用的功能在将这些相同的依赖项用作普通依赖项时不会被统一。例如
[dependencies] log = "0.4" [build-dependencies] log = {version = "0.4", features=['std']}
在构建构建脚本时,
log
板条箱将使用std
功能构建。在构建包的库时,它不会启用该功能。 -
在 开发依赖项 上启用的功能在将这些相同的依赖项用作普通依赖项时不会被统一,除非当前正在构建这些开发依赖项。例如
[dependencies] serde = {version = "1.0", default-features = false} [dev-dependencies] serde = {version = "1.0", features = ["std"]}
在此示例中,该库通常将链接到没有
std
功能的serde
。但是,当作为测试或示例构建时,它将包含std
功能。例如,cargo test
或cargo build --all-targets
将统一这些功能。请注意,依赖项中的开发依赖项始终被忽略,这仅与顶级包或工作区成员相关。
链接
links
字段 用于确保只有一个本机库副本链接到二进制文件中。解析器将尝试找到一个图,其中每个 links
名称只有一个实例。如果它无法找到满足该约束的图,它将返回错误。
例如,如果一个包依赖于 libgit2-sys
版本 0.11
,而另一个包依赖于 0.12
,则这是一个错误,因为 Cargo 无法统一它们,但它们都链接到 git2
本机库。由于此要求,如果您的库正在普遍使用,则在使用 links
字段进行 SemVer 不兼容的发布时,请务必格外小心。
已撤销的版本
已撤销的版本 是那些被标记为不应使用的版本。当解析器构建图时,它将忽略所有已撤销的版本,除非它们已经存在于 Cargo.lock
文件中或由 cargo update
的 --precise
标志(仅限 nightly 版本)明确请求。
依赖项更新
依赖项解析由所有需要了解依赖关系图的 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 有几种机制可以覆盖图中的依赖项。覆盖依赖项 一章详细介绍了如何使用覆盖。覆盖显示为注册表的覆盖,用新条目替换修补版本。否则,解析将像往常一样执行。
依赖项类型
包中存在三种类型的依赖项:普通依赖项、构建依赖项 和 开发依赖项。在大多数情况下,从解析器的角度来看,这些都被视为相同的。一个区别是非工作区成员的开发依赖项始终被忽略,并且不影响解析。
具有 [target]
表的 特定于平台的依赖项 的解析方式就好像所有平台都已启用一样。换句话说,解析器会忽略平台或 cfg
表达式。
开发依赖项循环
通常,解析器不允许图中存在循环,但它允许 开发依赖项 存在循环。例如,项目“foo”对“bar”具有开发依赖项,而“bar”对“foo”具有普通依赖项(通常作为“路径”依赖项)。这是允许的,因为从构建工件的角度来看,实际上没有循环。在此示例中,将构建“foo”库(它不需要“bar”,因为“bar”仅用于测试),然后可以构建依赖于“foo”的“bar”,然后可以构建链接到“bar”的“foo”测试。
请注意,这可能会导致令人困惑的错误。在构建库单元测试的情况下,最终测试二进制文件中实际上链接了两个库副本:一个与“bar”链接,另一个包含单元测试的库。与 版本不兼容性危害 部分中突出显示的问题类似,两者之间的类型不兼容。在这种情况下,从“bar”中公开“foo”的类型时要小心,因为“foo”单元测试不会将它们视为与本地类型相同。
如果可能,请尝试将您的包拆分为多个包并对其进行重组,使其严格保持非循环。
解析器版本
可以通过在 Cargo.toml
中指定解析器版本来使用不同的特征解析器算法,如下所示
[package]
name = "my-package"
version = "1.0.0"
resolver = "2"
版本 "1"
解析器是 Cargo 1.50 及之前版本附带的原始解析器。如果根包指定了 edition = "2021"
或更新的版本,则默认为 "2"
。否则默认为 "1"
。
版本 "2"
解析器引入了 特征统一 的变化。有关更多详细信息,请参阅 特征章节。
解析器是一个全局选项,会影响整个工作空间。依赖项中的 resolver
版本将被忽略,只会使用顶级包中的值。如果使用 虚拟工作空间,则应在 [workspace]
表中指定版本,例如
[workspace]
members = ["member1", "member2"]
resolver = "2"
建议
以下是一些关于在包中设置版本以及指定依赖项要求的建议。这些是一般准则,应该适用于常见情况,但当然有些情况可能需要指定特殊的要求。
-
在决定如何更新版本号以及是否需要进行语义化版本不兼容的版本更改时,请遵循 语义化版本指南。
-
在大多数情况下,对依赖项使用插入符号要求,例如
"1.2.3"
。这确保了解析器在选择版本时能够最大限度地灵活,同时保持构建兼容性。- 使用当前使用的版本指定所有三个组件。这有助于设置将使用的最低版本,并确保其他用户不会最终得到可能缺少您的包所需内容的旧版本依赖项。
- 避免使用
*
要求,因为 crates.io 上不允许使用它们,并且它们可能会在正常的cargo update
期间引入语义化版本破坏性更改。 - 避免过于宽泛的版本要求。例如,
>=2.0.0
可以引入任何语义化版本不兼容的版本,例如版本5.0.0
,这可能会导致将来构建失败。 - 尽可能避免过于狭窄的版本要求。例如,如果您指定了一个波浪号要求,例如
bar="~1.3"
,而另一个包指定了一个bar="1.4"
的要求,这将无法解析,即使次要版本应该兼容。
-
尝试使依赖项版本与您的库实际需要的最低版本保持最新。例如,如果您有一个
bar="1.0.12"
的要求,然后在未来的版本中,您开始使用“bar”的1.1.0
版本中添加的新功能,请将您的依赖项要求更新为bar="1.1.0"
。如果您没有这样做,可能不会立即显现出来,因为 Cargo 可以在您运行全局
cargo update
时有机会选择最新版本。但是,如果另一个用户依赖于您的库,并运行cargo update your-library
,则如果“bar”在其Cargo.lock
中被锁定,则它将*不会*自动更新“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
└── ...
为什么该依赖项上的该功能被启用了?
您可能会发现,是某个已激活的功能导致 rand
出现。**要找出哪个包激活了该功能,您可以添加 --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" # note: open requirements such as this are discouraged
在这个例子中,Cargo 可能会构建 rand
crate 的两个副本,即使版本 0.7.3
的单个副本可以满足所有要求。这是因为解析器算法倾向于为包 B 构建最新可用的 rand
版本,在撰写本文时为 0.8.5
,但这与包 A 的规范不兼容。解析器算法目前不会尝试在这种情况下“去重”。
不鼓励在 Cargo 中使用开放式版本要求,例如 >=0.6
。但是,如果您遇到这种情况,可以使用带有 --precise
标志的 cargo update
命令手动删除此类重复项。
为什么没有选择更新的版本?
假设您注意到在运行以下命令时没有选择最新版本的依赖项
$ cargo update
您可以启用一些额外的日志记录来查看为什么会发生这种情况
$ env CARGO_LOG=cargo::core::resolver=trace cargo update
**注意:**Cargo 日志目标和级别可能会随着时间的推移而改变。
破坏语义化版本的补丁版本破坏了构建
有时,项目可能会无意中发布一个带有破坏语义化版本更改的点版本。当用户使用 cargo update
进行更新时,他们将获取此新版本,然后他们的构建可能会中断。在这种情况下,建议项目应该 撤销 该版本,并删除破坏语义化版本的更改,或者将其发布为新的语义化主版本增加。
如果更改发生在第三方项目中,请尽可能(礼貌地!)与项目合作解决问题。
在等待版本被撤销的同时,一些解决方法取决于具体情况
- 如果您的项目是最终产品(例如二进制可执行文件),只需避免更新
Cargo.lock
中有问题的包。这可以使用cargo update
中的--precise
标志来完成。 - 如果您在 crates.io 上发布二进制文件,则可以临时添加
=
要求以强制依赖项使用特定的良好版本。- 二进制项目也可以建议用户在使用
cargo install
时使用--locked
标志来使用包含已知良好版本的原始Cargo.lock
。
- 二进制项目也可以建议用户在使用
- 库也可以考虑发布一个带有更严格要求的临时新版本,以避免麻烦的依赖项。您可能需要考虑使用范围要求(而不是
=
)来避免过于严格的要求,这些要求可能会与使用相同依赖项的其他包冲突。一旦问题得到解决,您可以发布另一个点版本,将依赖项放宽回插入符号要求。 - 如果看起来第三方项目无法或不愿意撤销该版本,则一种选择是更新您的代码以与更改兼容,并更新依赖项要求以将最低版本设置为新版本。您还需要考虑这是否是您自己库的语义化版本破坏性更改,例如,如果它公开了来自依赖项的类型。