掌握 Cargo.toml 的格式规则,避免挫败感
在 JavaScript 和其他语言中,我们称令人惊讶或不一致的行为为“Wat!”(即“什么!?”)。
例如,在 JavaScript 中,空数组加空数组会产生一个空字符串,[] + [] === ""。Wat!
在另一个极端,某种语言有时会表现出令人惊讶的一致性。我称之为“Wat Not”。
Rust 通常比 JavaScript 更加一致。然而, 一些与 Rust 相关的格式会带来惊喜。
具体来说,本文将介绍 Cargo.toml 中的九个 wats 和 wat nots。
回想一下,Cargo.toml 是定义 Rust 项目配置和依赖项的清单文件。
其格式 TOML(Tom's Obvious, Minimal Language)表示嵌套的键/值对和/或数组。
JSON 和 YAML 是类似的格式。与 JSON 不同的是,TOML 被设计为易于人类阅读和编写。
这九个 wats 和 wat nots 的旅程不会像 JavaScript 的怪癖那样有趣(谢天谢地)。
然而,如果你曾经对 Cargo.toml 的格式感到困惑,我希望本文能让你感觉更好。
最重要的是,当你了解了这九个 wats 和 wat nots 后,希望你能更轻松有效地编写 Cargo.toml。
本文不是关于“修复” Cargo.toml。该文件格式在其主要用途上非常出色:指定 Rust 项目的配置和依赖项。
相反,本文旨在理解其格式及其怪癖。
Wat 1:依赖项 vs. 配置文件部分名称
你可能知道如何在 Cargo.toml 中添加 [dependencies] 部分。这样的部分指定了发布依赖项,例如:
[dependencies]
serde = "1.0"
同样,你可以使用 [dev-dependencies] 部分指定开发依赖项,使用 [build-dependencies] 部分指定构建依赖项。
你可能还需要设置编译器选项,例如优化级别和是否包含调试信息。你可以通过发布、开发和构建的配置文件部分来设置这些选项。
你能猜出这三个部分的名称吗?是 [profile]、[dev-profile] 和 [build-profile] 吗?
不!它们是 [profile.release]、[profile.dev] 和 [profile.build]。Wat!
[dev-profile] 会比 [profile.dev] 更好吗?[dependencies.dev] 会比 [dev-dependencies] 更好吗?
我个人更喜欢带点的名称。(在“Wat Not 9”中,我们将看到点的强大之处。)然而,我愿意记住依赖项和配置文件的工作方式不同。
Wat 2:依赖项继承
你可能会认为点适用于配置文件,而连字符更适用于依赖项,因为 [dev-dependencies] 继承自 [dependencies]。换句话说,[dependencies] 中的依赖项在 [dev-dependencies] 中也可用。那么,这是否意味着 [build-dependencies] 也继承自 [dependencies]?
不![build-dependencies] 不继承自 [dependencies]。Wat!
我发现这种 Cargo.toml 的行为既方便又令人困惑。
Wat 3:默认键
你可能知道,可以这样写:
[dependencies]
serde = { version = "1.0" }
也可以这样写:
[dependencies]
serde = "1.0"
这里的原则是什么?一般的 TOML 中如何指定一个键为默认键?
你不能!一般的 TOML 没有默认键。Wat!
Cargo TOML 对 [dependencies] 部分中的 version 键进行了特殊处理。这是 Cargo 特有的功能,而不是一般的 TOML 功能。据我所知,Cargo TOML 没有其他默认键。
Wat 4:子功能
使用 Cargo.toml 的 [features],你可以创建依赖项不同的项目版本。这些依赖项本身的功能也可能不同,我们称之为子功能。
在这里,我们创建了两个项目版本。默认版本依赖于带有默认功能的 getrandom。wasm 版本依赖于带有 js 子功能的 getrandom:
[features]
default = []
wasm = ["getrandom-js"]
[dependencies]
rand = { version = "0.8" }
getrandom = { version = "0.2", optional = true }
[dependencies.getrandom-js]
package = "getrandom"
version = "0.2"
optional = true
features = ["js"]
在这个例子中,wasm 是我们项目的一个功能,依赖于依赖项别名 getrandom-rs,它代表带有 js 子功能的 getrandom crate 版本。
那么,如何在避免冗长的 [dependencies.getrandom-js] 部分的情况下给出相同的规范?
在 [features] 中,将 getrandom-js 替换为 "getrandom/js"。我们可以这样写:
[features]
default = []
wasm = ["getrandom/js"]
[dependencies]
rand = { version = "0.8" }
getrandom = { version = "0.2", optional = true }
一般来说,在 Cargo.toml 中,功能规范(如 wasm = ["getrandom/js"])可以列出:
- 其他功能
- 依赖项别名
- 依赖项
- 一个或多个依赖项“斜杠”一个子功能
这不是标准的 TOML,而是 Cargo.toml 特有的简写。
附加:你如何用简写表示你的 wasm 功能应包括带有两个子功能的 getrandom:js 和 test-in-browser?
答案:列出依赖项两次。
wasm = ["getrandom/js","getrandom/test-in-browser"]
Wat 5:目标的依赖项
我们已经看到如何指定发布、调试和构建的依赖项。
[dependencies]
#...
[dev-dependencies]
#...
[build-dependencies]
#...
我们已经看到如何指定各种功能的依赖项:
[features]
default = []
wasm = ["getrandom/js"]
你会怎么猜测我们如何为各种目标(例如某个版本的 Linux、Windows 等)指定依赖项?
我们在 [dependencies] 前加上 target.TARGET_EXPRESSION 前缀,例如:
[target.x86_64-pc-windows-msvc.dependencies]
winapi = { version = "0.3.9", features = ["winuser"] }
按照一般 TOML 的规则,我们也可以这样说:
[target]
x86_64-pc-windows-msvc.dependencies={winapi = { version = "0.3.9", features = ["winuser"] }}
我觉得这种前缀语法很奇怪,但我无法提出更好的替代方案。不过,我确实想知道为什么功能不能以相同的方式处理:
# 不允许
[feature.wasm.dependencies]
getrandom = { version = "0.2", features=["js"]}
Wat Not 6:目标 cfg 表达式
这是我们的第一个“Wat Not”,即它是一种让我感到惊讶的一致性。
除了具体目标(如 x86_64-pc-windows-msvc),你还可以在单引号中使用 cfg 表达式。例如:
[target.'cfg(all(windows, target_arch = "x86_64"))'.dependencies]
我不认为这是一个“wat!”。我认为这很棒。
回想一下,cfg 是“配置”的缩写,是 Rust 通常用于条件编译代码的机制。例如,在我们的 main.rs 中,我们可以这样说:
if cfg!(target_os = "linux") {
println!("This is Linux!");
}
在 Cargo.toml 中,在目标表达式中,几乎支持整个 cfg 迷你语言。
all()、any()、not()target_archtarget_featuretarget_ostarget_familytarget_envtarget_abitarget_endiantarget_pointer_widthtarget_vendortarget_has_atomicunixwindows
cfg 迷你语言唯一不支持的部分(我认为)是你不能使用 --cfg 命令行参数设置值。此外,一些 cfg 值(如 test)没有意义。