掌握 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_arch
target_feature
target_os
target_family
target_env
target_abi
target_endian
target_pointer_width
target_vendor
target_has_atomic
unix
windows
cfg
迷你语言唯一不支持的部分(我认为)是你不能使用 --cfg
命令行参数设置值。此外,一些 cfg
值(如 test
)没有意义。
Wat 7:目标的配置文件
回想一下 Wat 1 中,你可以通过 [profile.release]
、[profile.dev]
和 [profile.build]
设置编译器选项。例如:
[profile.dev]
opt-level = 0
你如何为特定目标(如 Windows)设置编译器选项?是这样吗?
[target.'cfg(windows)'.profile.dev]
opt-level = 0
不。相反,你需要创建一个名为 .cargo/config.toml
的新文件,并添加以下内容:
[target.'cfg(windows)']
rustflags = ["-C", "opt-level=0"]
Wat!
一般来说,Cargo.toml
只支持 target.TARGET_EXPRESSION
作为依赖项部分的前缀。你不能为配置文件部分加前缀。然而,在 .cargo/config.toml
中,你可以有 [target.TARGET_EXPRESSION]
部分。在这些部分中,你可以设置环境变量来设置编译器选项。
Wat Not 8:TOML 列表
Cargo.toml
支持两种列表语法:
- 内联数组
- 表数组
这个例子使用了两者:
[package]
name = "cargo-wat"
version = "0.1.0"
edition = "2021"
[dependencies]
rand = { version = "0.8" }
# 内联数组 'features'
getrandom = { version = "0.2", features = ["std", "test-in-browser"] }
# 表数组 'bin'
[[bin]]
name = "example"
path = "src/bin/example.rs"
[[bin]]
name = "another"
path = "src/bin/another.rs"
我们可以将表数组更改为内联数组吗?可以!
# 内联数组 'bin'
bins = [
{ name = "example", path = "src/bin/example.rs" },
{ name = "another", path = "src/bin/another.rs" },
]
[package]
name = "cargo-wat"
version = "0.1.0"
edition = "2021"
[dependencies]
rand = { version = "0.8" }
# 内联数组 'features'
getrandom = { version = "0.2", features = ["std", "test-in-browser"] }
我们可以将功能的内联数组更改为表数组吗?
不可以。简单值(此处为字符串)的内联数组不能表示为表数组。然而,我认为这是一个“wat not”,而不是“wat!”,因为这是一般 TOML 的限制,而不仅仅是 Cargo.toml 的限制。
附带说明:YAML 格式与 TOML 格式一样,提供两种列表语法。然而,YAML 的两种语法都适用于简单值。
Wat Not 9:TOML 内联、部分和点
这是一个典型的 Cargo.toml
。它混合了部分语法(如 [dependencies]
)和内联语法(如 getrandom = {version = "0.2", features = ["std", "test-in-browser"]}
)。
[package]
name = "cargo-wat"
version = "0.1.0"
edition = "2021"
[dependencies]
rand = "0.8"
getrandom = { version = "0.2", features = ["std", "test-in-browser"] }
[target.x86_64-pc-windows-msvc.dependencies]
winapi = { version = "0.3.9", features = ["winuser"] }
[[bin]]
name = "example"
path = "src/bin/example.rs"
[[bin]]
name = "another"
path = "src/bin/another.rs"
我们可以将其完全重写为 100% 内联吗?可以。
package = { name = "cargo-wat", version = "0.1.0", edition = "2021" }
dependencies = { rand = "0.8", getrandom = { version = "0.2", features = [
"std",
"test-in-browser",
] } }
target = { 'cfg(target_os = "windows")'.dependencies = { winapi = { version = "0.3.9", features = [
"winuser",
] } } }
bins = [
{ name = "example", path = "src/bin/example.rs" },
{ name = "another", path = "src/bin/another.rs" },
]
我们也可以将其重写为最大部分:
[package]
name = "cargo-wat"
version = "0.1.0"
edition = "2021"
[dependencies.rand]
version = "0.8"
[dependencies.getrandom]
version = "0.2"
features = ["std", "test-in-browser"]
[target.x86_64-pc-windows-msvc.dependencies.winapi]
version = "0.3.9"
features = ["winuser"]
[[bin]]
name = "example"
path = "src/bin/example.rs"
[[bin]]
name = "another"
path = "src/bin/another.rs"
最后,让我们谈谈点。
在 TOML 中,点用于分隔嵌套表中的键。例如,a.b.c 是表 a 中表 b 中的键 c。我们可以用“很多点”重写我们的例子吗?可以:
package.name = "cargo-wat"
package.version = "0.1.0"
package.edition = "2021"
dependencies.rand = "0.8"
dependencies.getrandom.version = "0.2"
dependencies.getrandom.features = ["std", "test-in-browser"]
target.x86_64-pc-windows-msvc.dependencies.winapi.version = "0.3.9"
target.x86_64-pc-windows-msvc.dependencies.winapi.features = ["winuser"]
bins = [
{ name = "example", path = "src/bin/example.rs" },
{ name = "another", path = "src/bin/another.rs" },
]
我欣赏 TOML 在部分、内联和点方面的灵活性。我认为这种灵活性是一个“wat not”。你可能会发现所有这些选择令人困惑。然而,我喜欢 Cargo.toml
让我们使用 TOML 的全部功能。
结论
Cargo.toml
是 Rust 生态系统中的一个重要工具,提供了简单性和灵活性的平衡,既适合初学者也适合经验丰富的开发人员。通过我们探讨的九个 wats 和 wat nots,我们看到了这个配置文件有时会因其特性而令人惊讶,但同时也因其一致性和强大而令人印象深刻。
理解这些怪癖可以让你避免潜在的挫败感,并使你能够充分利用 Cargo.toml
。从管理依赖项和配置文件到处理特定目标的配置和功能,这些见解将帮助你编写更高效和有效的 Cargo.toml
文件。
总之,虽然 Cargo.toml
可能有其独特之处,但这些特性往往源于实用的设计选择,优先考虑功能性和可读性。
接受这些怪癖,你会发现 Cargo.toml 不仅能满足你的项目需求,还能提升你的 Rust 开发体验。