Cargo.toml vs Cargo.lock

Cargo.tomlCargo.lockCargo 的两个元配置文件,但是它们拥有不同的目的:

  • 前者从用户的角度出发来描述项目信息和依赖管理,因此它是由用户来编写
  • 后者包含了依赖的精确描述信息,它是由 Cargo 自行维护,因此不要去手动修改

它们的关系跟 package.jsonpackage-lock.json 非常相似,从 JavaScript 过来的同学应该会比较好理解。

是否上传本地的 Cargo.lock

当本地开发时,Cargo.lock 自然是非常重要的,但是当你要把项目上传到 Git 时,例如 GitHub,那是否上传 Cargo.lock 就成了一个问题。

关于是否上传,有如下经验准则:

  • 从实践角度出发,如果你构建的是三方库类型的服务,请把 Cargo.lock 加入到 .gitignore 中。
  • 若构建的是一个面向用户终端的产品,例如可以像命令行工具、应用程序一样执行,那就把 Cargo.lock 上传到源代码目录中。

例如 axum 是 web 开发框架,它属于三方库类型的服务,因此源码目录中不应该出现 Cargo.lock 的身影,它的归宿是 .gitignore。而 ripgrep 则恰恰相反,因为它是一个面向终端的产品,可以直接运行提供服务。

那么问题来了,为何会有这种选择?

原因是 Cargo.lock 会详尽描述上一次成功构建的各种信息:环境状态、依赖、版本等等,Cargo 可以使用它提供确定性的构建环境和流程,无论何时何地。这种特性对于终端服务是非常重要的:能确定、稳定的在用户环境中运行起来是终端服务最重要的特性之一。

而对于三方库来说,情况就有些不同。它不仅仅被库的开发者所使用,还会间接影响依赖链下游的使用者。用户引入了三方库是不会去看它的 Cargo.lock 信息的,也不应该受这个库的确定性运行条件所限制。

还有个原因,在项目中,可能会有几个依赖库引用同一个三方库的同一个版本,那如果该三方库使用了 Cargo.lock 文件,那可能三方库的多个版本会被引入使用,这时就会造成版本冲突。换句话说,通过指定版本的方式引用一个依赖库是无法看到该依赖库的完整情况的,而只有终端的产品才会看到这些完整的情况。

假设没有 Cargo.lock

Cargo.toml 是一个清单文件( manifest )包含了我们 package 的描述元数据。例如,通过以下内容可以说明对另一个 package 的依赖 :

#![allow(unused)]
fn main() {
[package]
name = "hello_world"
version = "0.1.0"

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

可以看到,只有一个依赖,且该依赖的来源是 GitHub 上一个特定的仓库。由于我们没有指定任何版本信息,Cargo 会自动拉取该依赖库的最新版本( mastermain 分支上的最新 commit )。

这种使用方式,其实就错失了包管理工具的最大的优点:版本管理。例如你在今天构建使用了版本 A,然后过了一段时间后,由于依赖包的升级,新的构建却使用了大更新版本 B,结果因为版本不兼容,导致了构建失败。

可以看出,确保依赖版本的确定性是非常重要的:

#![allow(unused)]
fn main() {
[dependencies]
regex = { git = "https://github.com/rust-lang/regex.git", rev = "9f9f693" }
}

这次,我们使用了指定 rev ( revision ) 的方式来构建,那么不管未来何时再次构建,使用的依赖库都会是该 rev ,而不是最新的 commit

但是,这里还有一个问题:rev 需要手动的管理,你需要在每次更新包的时候都思考下 SHA-1,这显然非常麻烦。

当有了 Cargo.lock

当有了 Cargo.lock 后,我们无需手动追踪依赖库的 revCargo 会自动帮我们完成,还是之前的清单:

#![allow(unused)]
fn main() {
[package]
name = "hello_world"
version = "0.1.0"

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

第一次构建时,Cargo 依然会拉取最新的 master commit,然后将以下信息写到 Cargo.lock 文件中:

#![allow(unused)]
fn main() {
[[package]]
name = "hello_world"
version = "0.1.0"
dependencies = [
 "regex 1.5.0 (git+https://github.com/rust-lang/regex.git#9f9f693768c584971a4d53bc3c586c33ed3a6831)",
]

[[package]]
name = "regex"
version = "1.5.0"
source = "git+https://github.com/rust-lang/regex.git#9f9f693768c584971a4d53bc3c586c33ed3a6831"
}

可以看出,其中包含了依赖库的准确 rev 信息。当未来再次构建时,只要项目中还有该 Cargo.lock 文件,那构建依然会拉取同一个版本的依赖库,并且再也无需我们手动去管理 revSHA 信息!

更新依赖

由于 Cargo.lock 会锁住依赖的版本,你需要通过手动的方式将依赖更新到新的版本:

#![allow(unused)]
fn main() {
$ cargo update            # 更新所有依赖
$ cargo update -p regex   # 只更新 “regex”
}

以上命令将使用新的版本信息重新生成 Cargo.lock ,需要注意的是 cargo update -p regex 传递的参数实际上是一个 Package IDregex 只是一个简写形式。