Master Cargo.toml formatting rules and avoid frustration
In JavaScript and other languages, we call a surprising or inconsistent behavior a “Wat!” [that is, a “What!?”]. For example, in JavaScript, an empty array plus an empty array produces an empty string, [] + [] === ""
. Wat!
At the other extreme, a language sometimes behaves with surprising consistency. I’m calling that a “Wat Not”.
Rust is generally (much) more consistent than JavaScript. Some Rust-related formats, however, offer surprises. Specifically, this article looks at nine wats and wat nots in Cargo.toml
.
Recall that Cargo.toml
is the manifest file that defines your Rust project’s configuration and dependencies. Its format, TOML (Tom’s Obvious, Minimal Language), represents nested key/value pairs and/or arrays. JSON and YAML are similar formats. Like YAML, but unlike JSON, Tom designed TOML for easy reading and writing by humans.
This journey of nine wats and wat nots will not be as entertaining as JavaScript’s quirks (thank goodness). However, if you’ve ever found Cargo.toml
‘s format confusing, I hope this article will help you feel better about yourself. Also, and most importantly, when you learn the nine wats and wat nots, I hope you will be able to write your Cargo.toml
more easily and effectively.
This article is not about “fixing” Cargo.toml
. The file format is great at its main purpose: specifying the configuration and dependencies of a Rust project. Instead, the article is about understanding the format and its quirks.
Wat 1: Dependencies vs. Profile Section Names
You probably know how to add a [dependencies]
section to your Cargo.toml
. Such a section specifies release dependencies, for example:
[dependencies]
serde = "1.0"
Along the same lines, you can specify development dependencies with a [dev-dependencies]
section and build dependencies with a [build-dependencies]
section.
You may also need to set compiler options, for example, an optimization level and whether to include debugging information. You do that with profile sections for release, development, and build. Can you guess the names of these three sections? Is it [profile]
, [dev-profile]
and [build-profile]
?
No! It’s [profile.release]
, [profile.dev]
, and [profile.build]
. Wat?
Would [dev-profile]
be better than [profile.dev]
? Would [dependencies.dev]
be better than [dev-dependencies]
?
I personally prefer the names with dots. (In “Wat Not 9”, we’ll see the power of dots.) I am, however, willing to just remember the dependences work one way and profiles work another.
Wat 2: Dependency Inheritance
You might argue that dots are fine for profiles, but hyphens are better for dependencies because [dev-dependencies]
inherits from [dependencies]
. In other words, the dependencies in [dependencies]
are also available in [dev-dependencies]
. So, does this mean that [build-dependencies]
inherits from [dependencies]
?
No! [build-dependencies]
does not inherit from [dependencies]
. Wat?
I find this Cargo.toml
behavior convenient but confusing.
Wat 3: Default Keys
You likely know that instead of this:
[dependencies]
serde = { version = "1.0" }
you can write this:
[dependencies]
serde = "1.0"
What’s the principle here? How in general TOML do you designate one key as the default key?
You can’t! General TOML has no default keys. Wat?
Cargo TOML does special processing on the version
key in the [dependencies]
section. This is a Cargo-specific feature, not a general TOML feature. As far as I know, Cargo TOML offers no other default keys.
Wat 4: Sub-Features
With Cargo.toml
[features]
you can create versions of your project that differ in their dependences. Those dependences may themselves differ in their features, which we’ll call sub-features.
Here we create two versions of our project. The default version depends on getrandom
with default features. The wasm
version depends on getrandom
with the js
sub-feature:
[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"]
In this example, wasm
is a feature of our project that depends on dependency alias getrandom-rs
which represents the version of the getrandom
crate with the js
sub-feature.
So, how can we give this same specification while avoiding the wordy [dependencies.getrandom-js]
section?
In [features],
replace getrandom-js"
with "getrandom/js"
. We can just write:
[features]
default = []
wasm = ["getrandom/js"][dependencies]
rand = { version = "0.8" }
getrandom = { version = "0.2", optional = true }
Wat!
In general, in Cargo.toml
, a feature specification such as wasm = ["getrandom/js"]
can list
- other features
- dependency aliases
- dependencies
- one or more dependency “slash” a sub-feature
This is not standard TOML. Rather, it is a Cargo.toml
-specific shorthand.
Bonus: Guess how you’d use the shorthand to say that your wasm
feature should include getrandom
with two sub-features: js
and test-in-browser
?
Answer: List the dependency twice.
wasm = ["getrandom/js","getrandom/test-in-browser"]
Wat 5: Dependencies for Targets
We’ve seen how to specify dependencies for release, debug, and build.
[dependencies]
#...
[dev-dependencies]
#...
[build-dependencies]
#...
We’ve seen how to specify dependencies for various features:
[features]
default = []
wasm = ["getrandom/js"]
How would you guess we specify dependences for various targets (e.g. a version of Linux, Windows, etc.)?
We prefix [dependences]
with target.TARGET_EXPRESSION
, for example:
[target.x86_64-pc-windows-msvc.dependencies]
winapi = { version = "0.3.9", features = ["winuser"] }
Which, by the rules of general TOML means we can also say:
[target]
x86_64-pc-windows-msvc.dependencies={winapi = { version = "0.3.9", features = ["winuser"] }}
Wat!
I find this prefix syntax strange, but I can’t suggest a better alternative. I do, however, wonder why features couldn’t have been handle the same way:
# not allowed
[feature.wasm.dependencies]
getrandom = { version = "0.2", features=["js"]}
Wat Not 6: Target cfg Expressions
This is our first “Wat Not”, that is, it is something that surprised me with its consistency.
Instead of a concrete target such as x86_64-pc-windows-msvc
, you may instead use a cfg
expression in single quotes. For example,
[target.'cfg(all(windows, target_arch = "x86_64"))'.dependencies]
I don’t consider this a “wat!”. I think it is great.
Recall that cfg
, short for “configuration”, is the Rust mechanism usually used to conditionally compile code. For example, in our main.rs
, we can say:
if cfg!(target_os = "linux") {
println!("This is Linux!");
}
In Cargo.toml
, in target expressions, pretty much the whole cfg
mini-language is supported.
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
The only parts of the cfg
mini-language not supported are (I think) that you can’t set a value with the --cfg
command line argument. Also, some cfg values such as test
don’t make sense.
Wat 7: Profiles for Targets
Recall from Wat 1 that you set compiler options with [profile.release]
, [profile.dev]
, and [profile.build]
. For example:
[profile.dev]
opt-level = 0
Guess how you set compiler options for a specific target, such as Windows? Is it this?
[target.'cfg(windows)'.profile.dev]
opt-level = 0
No. Instead, you create a new file named .cargo/config.toml
and add this:
[target.'cfg(windows)']
rustflags = ["-C", "opt-level=0"]
Wat!
In general, Cargo.toml
only supports target.TARGET_EXPRESSION
as the prefix of dependency section. You may not prefix a profile section. In .cargo/config.toml
, however, you may have [target.TARGET_EXPRESSION]
sections. In those sections, you may set environment variables that set compiler options.
Wat Not 8: TOML Lists
Cargo.toml
supports two syntaxes for lists:
- Inline Array
- Table Array
This example uses both:
[package]
name = "cargo-wat"
version = "0.1.0"
edition = "2021"[dependencies]
rand = { version = "0.8" }
# Inline array 'features'
getrandom = { version = "0.2", features = ["std", "test-in-browser"] }
# Table array 'bin'
[[bin]]
name = "example"
path = "src/bin/example.rs"
[[bin]]
name = "another"
path = "src/bin/another.rs"
Can we change the table array to an inline array? Yes!
# Inline array '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" }
# Inline array 'features'
getrandom = { version = "0.2", features = ["std", "test-in-browser"] }
Can we change the inline array of features into a table array?
No. Inline arrays of simple values (here, strings) cannot be represented as table arrays. However, I consider this a “wat not”, not a “wat!” because this is a limitation of general TOML, not just of Cargo.toml
.
Aside: YAML format, like TOML format, offers two list syntaxes. However, both of YAMLs two syntaxes work with simple values.
Wat Not 9: TOML Inlining, Sections, and Dots
Here is a typical Cargo.toml
. It mixes section syntax, such as [dependences]
with inline syntax such as 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"
Can we re-write it to be 100% inline? Yes.
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" },
]
We can also re-write it with maximum sections:
[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"
Finally, let’s talk about dots. In TOML, dots are used to separate keys in nested tables. For example, a.b.c
is a key c
in a table b
in a table a
. Can we re-write our example with “lots of dots”? Yes:
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" },
]
I appreciate TOML’s flexibility with respect to sections, inlining, and dots. I count that flexibility as a “wat not”. You may find all the choices it offers confusing. I, however, like that Cargo.toml
lets us use TOML’s full power.
Conclusion
Cargo.toml
is an essential tool in the Rust ecosystem, offering a balance of simplicity and flexibility that caters to both beginners and seasoned developers. Through the nine wats and wat nots we’ve explored, we’ve seen how this configuration file can sometimes surprise with its idiosyncrasies and yet impress with its consistency and power.
Understanding these quirks can save you from potential frustrations and enable you to leverage Cargo.toml
to its fullest. From managing dependencies and profiles to handling target-specific configurations and features, the insights gained here will help you write more efficient and effective Cargo.toml
files.
In essence, while Cargo.toml
may have its peculiarities, these characteristics are often rooted in practical design choices that prioritize functionality and readability. Embrace these quirks, and you’ll find that Cargo.toml
not only meets your project’s needs but also enhances your Rust development experience.
Please follow Carl on Medium. I write on scientific programming in Rust and Python, machine learning, and statistics. I tend to write about one article per month.