Cargo config 使用备注

本文备注下如何配置 Cargo dependencies 为指定仓库。

本文旨在回答如下问题:

  1. Rust 项目中,如何配置 dependencies 的路径为指定仓库。
  2. 在不指定 branch 或 version 的情况下,dependencies 会拉取什么版本/仓库里的数据。
  3. Rust 项目中,配置 dependencies 的最佳实践。

以下为正文。

# 如何配置 dependencies 的路径?

出于稳定性考虑,工作中使用的项目,尤其是一些开源的项目,往往无法和社区时刻保持一致。甚至,在选定了一个基线版本后,以周或者年计,主干会持续保持这个版本。有一些效果较好的“新特性”需要合入时,就需要以 patch 的形式合入。同样出于稳定性考虑, 或者兼容性考虑,服务的依赖,甚至依赖的依赖都是无法进行升级的。如果项目的新特性需要依赖库进行变更,就只能将依赖库的基线版本单独维护,按需合入依赖库的 patch。这样的维护方式看起来较为繁琐,但是能够有效避免项目因依赖库的升级而引入新的问 题。
有了这样的需求,对应到 Rust 项目中,就需要对 Cargo.toml 里的 dependencies 配置进行调整,将部分依赖调整为自有库。 参照reference/specifying-dependencies,可以通过如下方式来指定依赖库:

1
2
[dependencies]
regex = { version = "1.10.3", git = "https://github.com/rust-lang/regex.git", branch = "next", rev = "0c0990399270277832fbb5b91a1fa118e6f63dba", tag = "11.10.4" }

除了 git,version、branch、rev 以及 tag 都是可选的,补充四者中的一个参数即可实现分支提交控制的目的。在实践过程中,考虑到分支上的提交是时刻进行的,在确认了基础的功能后,可以考虑发布一个版本。一来可以通过 tag 来进行版本/功能的 发布控制,同时也便于后续的功能梳理以及项目整理关键里程碑可视化。通过 branch 来指定依赖库会比较方便,这种方式对仓库的开发流程管理较为依赖:如果分支上提交了功能异常的代码,项目的编译就会异常。rev 方案也可以唯一指定代码版本,只是看起来 不像 tag 那样直观。

# dependences 默认拉取什么版本?

笔者在近期的工作中,遇到了如下的配置方式:

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

这里并没有指定 version, branch 或者 rev。但是在编译项目时也会进行拉取的动作。默认会拉取目标仓库的最新提交(效果和直接 git clone 一致)。
在复杂的项目中,通常会出现依赖库和依赖库的依赖有重合的情况。而Rust 工程在编译时,会通过校验目标库的 source 信息(Cargo.lock 中生成)来识别两个库是相同。当项目的依赖库的 dependence 配置和项目依赖库的依赖的 dependence 配置不同时, 就会出现种种类型不兼容的情况。比如,目前有这样的一个项目dep-check,其依赖 trait-libmiddle-lib两个库。同时,middle-lib又对 trait-lib存在依赖。当dep-check使用了trait_lib::Check,并且将该trait通过引用 middle-lib里的函数进行处理时,就涉及到dep-checkmiddle-libtrait-lib的引用校验问题:两个库里涉及的Check是否是同一个Trait?就笔者目前的理解来看,由于 rust 里的 trait 采用的不是 duck-typing,因此就需要编译器在编译时对类型进行强校验。在这个小示例中,如果 middle-lib 的 dependencies 设置如下:

1
2
[dependencies]
trait-lib = { git = "https://github.com/liyan-ah/trait-lib.git", tag = "1.0.0" }

而 dep-check 的 dependencies 设置如下:

1
2
3
[dependencies]
trait-lib = { git = "https://github.com/liyan-ah/trait-lib.git", tag = "1.0.0" }
middle-lib = { git = "https://github.com/liyan-ah/middle-lib.git" } # 注意,这种配置方式在 Cargo.lock 生成后,除非使用 cargo update 触发更新,否则依赖版本不会随着代码提交而更新。

dep-check 编译时,其引用的trait_lib::Check和 middle-lib 里使用的trait_lib::Check就无法认为是同一个(即使实际上代码的提交是同一个)。依赖库的设置可以通过 Cargo.lock 里的 source 来确认。当 dep-check 和 middle-lib 的 Cargo.lock 对 trait-lib 的 source 配置相同时,就不会出现类型不一致的问题。
由于使用了 tag / rev / branch 来作为 dependencies 的配置,一个问题是当 trait-lib 发生更新时,需要同时升级 middle-lib 和 dep-check 这两个仓库。否则就会出现版本不一致而编译失败的情况。如果使用 version 控制,由于 version 实际 上表示的是一个范围,只要 update 后的依赖库是同一个版本即可(准确来说,x.y.z 中的 x.y 保持一致即可),可 以在 cargo update 后检查 dep-check 的 Cargo.lock 中存在几个 trait-lib。在仅存在一个 trait-lib 时,说明依赖库不存在版本冲突,此时不需要更新 middle-lib 中的 Cargo.toml。

配置的问题在这里有描述:The dependency resolution is confused when using git dependency and there’s a lockfile

duck-typing 的接口在校验时还比较简单,只需要检查是否实现了目标类型/接口定义的函数即可。但是对于非 duck-typing,情况会复杂些:不同的库里是允许出现同名的 trait 的。如何确定项目中实现的 trait 和引用库中需要的 trait 是同一个 trait?这就需要确保项目中 trait 的来源库和引用库中所需要的 trait 来源库是相同的。而看起来,库是否相同,又是通过 source 来确定的。Cargo.toml 中的 source 毕竟是一个工程里的概念,是如何影响编译的呢?是否是编译过程中,函数签名里带着一些source 信息?还需要进一步的探索,期望能够整理成文档。

# dependences 配置的最佳实践?

golang 和 rust 都支持通过配置 git 仓库的地址来直接引用,个人还是比较喜欢 rust 的配置方式:通过 Cargo.toml 能够简洁、明了的声明各种依赖的信息,在工程里可以直接使用库名(而非 golang 里的项目地址)。此外,rust 里的 workspace 机制 对仓库里存在多个 sub-lib 时也能较好的处理依赖的管理。
下面是一个实践示例dep-check

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
[workspace]
members = [
    "dep-check",
    "dep-run",
]

default-members = ["dep-check"]
resolver = "2"

[workspacepackage]
name = "dep-check"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[workspace.dependencies]
trait-lib = { git = "https://github.com/liyan-ah/trait-lib.git", tag = "1.0.0" }
middle-lib = { git = "https://github.com/liyan-ah/middle-lib.git", tag = "1.0.0" }

对于其中的一个 member:dep-check,其配置为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
[package]
name = "dep-check"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
trait-lib = { workspace = true }
middle-lib = { workspace = true }

这样,就能很便捷的对项目里的依赖进行管理了。

以上。

Hello, World!
使用 Hugo 构建
主题 StackJimmy 设计