Rust projects declare their dependencies, build configurations, and metadata in a TOML file called Cargo.toml.

Let’s set up a simple Rust project and see Cargo.toml in action.

cargo new my_rust_project
cd my_rust_project

Now, let’s look at the default Cargo.toml file:

[package]
name = "my_rust_project"
version = "0.1.0"
edition = "2021"

[dependencies]

This is the bare minimum. The [package] section defines your crate’s identity. name is how it’s known, version follows semantic versioning, and edition specifies the Rust language edition. The [dependencies] section is where the magic happens.

Let’s add the rand crate for random number generation. We’ll specify a version constraint.

[package]
name = "my_rust_project"
version = "0.1.0"
edition = "2021"

[dependencies]
rand = "0.8.5"

When you run cargo build, Cargo will fetch rand version 0.8.5 (or any compatible patch version) from crates.io and compile it. You can also use version ranges. rand = "0.8" would allow any 0.8.x version. rand = "~0.8.5" allows 0.8.5 and any later patch versions (0.8.6, 0.8.7, etc.) but not 0.9.0. rand = "^0.8.5" (caret) is the most common and allows 0.8.5 and any later minor or patch versions (0.8.6, 0.9.0, 0.9.1) but not 1.0.0.

Now, let’s add a dependency that’s only needed for development, like criterion for benchmarking. We’ll put this in a [dev-dependencies] section.

[package]
name = "my_rust_project"
version = "0.1.0"
edition = "2021"

[dependencies]
rand = "0.8.5"

[dev-dependencies]
criterion = "0.4.0"

Dependencies in [dev-dependencies] are only compiled and available when running cargo test or cargo bench.

Rust’s feature system allows for conditional compilation. A dependency might have optional features that you can enable. Let’s say serde (for serialization/deserialization) is a dependency, and it has a derive feature.

[package]
name = "my_rust_project"
version = "0.1.0"
edition = "2021"

[dependencies]
rand = "0.8.5"
serde = { version = "1.0.150", features = ["derive"] }

Here, we’re not just specifying the version for serde, but also enabling its derive feature. This means the #[derive(Serialize, Deserialize)] attributes will be available in your code. You can enable multiple features: features = ["serde_derive", "json"].

Cargo also has profiles for different build configurations: dev (for development, default), release (for production builds), and test. These are configured in sections like [profile.release].

[package]
name = "my_rust_project"
version = "0.1.0"
edition = "2021"

[dependencies]
rand = "0.8.5"

[profile.release]
opt-level = 3
lto = true
codegen-units = 1
panic = "abort"

In the [profile.release] section:

  • opt-level = 3: Enables aggressive optimizations.
  • lto = true: Enables Link-Time Optimization, which can significantly improve performance by optimizing across crate boundaries.
  • codegen-units = 1: Reduces the number of code generation units, allowing for more inter-procedural optimizations.
  • panic = "abort": Configures the behavior on panic. abort causes the program to exit immediately, which can lead to smaller binaries and faster startup in release builds. The default is unwind, which attempts to restore the stack.

The [profile.dev] section can be customized too, though it defaults to faster compile times over runtime performance. For instance, debug = true (the default) includes debug information. overflow-checks = true (the default) adds checks for integer overflows.

When you run cargo build, it uses the dev profile. For optimized builds, you use cargo build --release, which applies the settings from [profile.release].

The [workspace] section is crucial for managing multiple related crates (a workspace). It allows you to define a common Cargo.lock and share dependencies.

# In the root of your workspace
[workspace]
members = [
    "my_rust_project",
    "my_other_crate",
]

This tells Cargo that my_rust_project and my_other_crate are part of the same workspace.

Cargo.toml also supports other metadata like description, license, repository, and authors. These are important for publishing your crate to crates.io.

The [patch.crates-io] section allows you to override specific versions of dependencies for local development or testing. For example, if you’re working on a fork of a dependency:

[patch.crates-io]
some-dependency = { git = "https://github.com/my-username/some-dependency.git", branch = "my-feature-branch" }

This tells Cargo to use the some-dependency from your Git repository instead of fetching it from crates.io.

The most surprising thing about Cargo.toml is how its structure dictates not just what code gets built, but how it gets built, enabling fine-grained control over compilation speed, binary size, and runtime performance, often with a single configuration file.

When you add a dependency with a version like rand = "0.8.5", Cargo doesn’t just fetch that exact version. It resolves the entire dependency graph, including transitive dependencies, and picks a compatible set of versions for all of them. This resolution process is deterministic and recorded in Cargo.lock to ensure reproducible builds.

The next concept you’ll likely encounter is handling conditional compilation within your Rust code itself, often driven by these Cargo features.

Want structured learning?

Take the full Cargo course →