Skip to main content

九个 Rust `Cargo.toml` 的 Wats 和 Wat Nots

鱼雪

掌握 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],你可以创建依赖项不同的项目版本。这些依赖项本身的功能也可能不同,我们称之为子功能

在这里,我们创建了两个项目版本。默认版本依赖于带有默认功能的 getrandomwasm 版本依赖于带有 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 开发体验