Skip to content
← Back to explainers

Rust explainer

Cargo.toml essentials

About 5 min to read. Allow extra time to work through examples.

Choose package metadata, place dependencies correctly, control packaged files, and configure lints and lockfiles.

This article uses Rust. You can make it your practice mode to browse related content across the site.

Cargo.toml describes a Cargo package: its name, Rust edition, dependencies and build settings. The Rust exercises here use a direct compiler invocation, so try this article's examples in a local Cargo project.

Start with package metadata

This manifest works with a source file at src/lib.rs or src/main.rs:

[package]
name = "image-proc"
version = "0.3.1"
edition = "2024"
rust-version = "1.85"
description = "Image preprocessing utilities"
license = "MIT OR Apache-2.0"
repository = "https://github.com/you/image-proc"

Replace the example's name, description, license and repository with your own. The license expression means users may choose either license; include the corresponding license files when distributing the package.

Set edition explicitly. If omitted, Cargo assumes edition 2015 for backward compatibility. cargo new writes an edition field for you. An edition selects language rules; it does not pin the compiler version.

rust-version declares the minimum supported Rust version, often called the MSRV. Cargo normally rejects an older compiler before building the package. The edition-2024 resolver prefers compatible dependency versions when possible, but that preference cannot guarantee that every dependency has a compatible release. Test on your declared minimum version in CI to check the promise.

Publishing also needs suitable metadata. crates.io requires a description and license information, and a package version is required for publishing. A repository link and README help users understand and evaluate the crate. For a package you do not intend to publish, set publish = false under [package].

Control what goes into the package

Large test fixtures may belong in your repository without belonging in the downloadable crate. Add packaging patterns under [package], for example:

exclude = ["fixtures/**", "bench-data/**"]

Alternatively, use an explicit list of files to include:

include = ["src/**", "Cargo.toml", "README.md", "LICENSE-*"]

include takes precedence if both fields are present; it does not combine with exclude to subtract files. Prefer one approach so the intent is clear. An allowlist must also cover any build script, generated-code inputs or other assets the package needs.

Without these fields, Cargo uses its default selection rules, including Git ignore rules when applicable and special treatment for files such as the manifest, lockfile, nested packages and build output. Do not guess the archive's contents from the repository listing. Inspect them with:

cargo package --list

This lists package contents without publishing. Review the list before a release, especially after adding fixtures or changing packaging patterns.

Put dependencies in the right table

These are example version requirements, not a list of the newest releases:

[dependencies]
serde = { version = "1", features = ["derive"] }
rand = "0.8"

[dev-dependencies]
criterion = "0.5"

[build-dependencies]
cc = "1"

[target.'cfg(unix)'.dependencies]
nix = "0.29"
Table Where the dependency is available
dependencies Ordinary package code, including library and binary targets
dev-dependencies Tests, examples and benchmarks, including unit tests inside src/lib.rs
build-dependencies The build script, normally build.rs, compiled for the host machine
target.'cfg(...)'.dependencies Ordinary code when the target condition matches

A dev dependency cannot be used by ordinary library code just because the dependency exists in your manifest. Put test-only uses in a #[cfg(test)] module. Consumers of your library do not inherit its dev dependencies as requirements for building the library.

Target predicates such as cfg(unix) and cfg(target_os = "windows") describe the compilation target. These tables do not support arbitrary Rust conditions: for example, use [features] and optional dependencies instead of cfg(feature = "...") to select dependencies by feature. cfg(test) is not supported for selecting dependencies either.

To use a dependency under a different name in your code, set package:

[dependencies]
rand_old = { package = "rand", version = "0.7" }
rand = "0.8"

Here, rand_old and rand refer to two incompatible release lines of the same package. They both have major version zero; Cargo treats these different minor versions as incompatible. Types from the two versions are distinct, even when they share a name. Keep both only when your project needs them.

Configure lints

Cargo supports manifest lint settings from Rust 1.74 onward. Source attributes such as #![deny(unsafe_code)] remain valid too. Append these tables to the package manifest:

[lints.rust]
unsafe_code = "forbid"

[lints.clippy]
all = { level = "deny", priority = -1 }
needless_range_loop = "allow"

The all group applies first because its priority is lower. The individual lint has the default priority, zero, so it can override the group's deny. Give groups and their overrides different priorities instead of relying on their order in the TOML file. forbid is stronger than deny: it cannot be relaxed by a source-level allow.

These settings apply to this package, not as a blanket policy for all its dependencies. cargo clippy checks Clippy lints; a normal cargo build does not run those checks.

Keep a lockfile for repeatable development

Commit Cargo.lock for applications and libraries. It records the resolved dependency versions used to develop and test the project. A library consumer resolves dependencies using their own project; your library's lockfile does not pin their build.

In CI, cargo test --locked requires the existing lockfile to remain unchanged. This helps prevent unnoticed dependency resolution changes. It does not pin the Rust toolchain, system libraries or build environment, and it may still need to download dependencies. Check updates deliberately with your test suite.

Optional checks for manifest quality

Clippy's cargo lint group is opt-in:

cargo clippy -- -W clippy::cargo

It includes checks for missing package metadata, wildcard dependency requirements and multiple versions of a dependency. These are prompts for review: a lint cannot tell whether two dependency versions are intentional.

Clippy also has feature-name checks, such as negative_feature_names and redundant_feature_names. They flag naming patterns, not proof that a feature removes functionality or breaks a build. See the Cargo feature-resolution exercise for why compatible, additive features are easier to combine.

Use the Cargo manifest reference when you need the full rules for a field.

step 14 of 22

Modules, Visibility, Testing and Docs

Up next Version requirements: caret, tilde, wildcard, pre-releases