23.4 The Manifest: Cargo.toml
The Cargo.toml
file is the heart of a Rust package. It uses the TOML (Tom’s Obvious, Minimal Language) format to define metadata and dependencies.
23.4.1 Common Sections
A typical Cargo.toml
includes several sections:
[package]
name = "my_package" # Name of the package (often matches the main crate name)
version = "0.1.0"
edition = "2024" # Specifies the Rust edition
authors = ["Your Name <you@example.com>"]
description = "A short description of what my_package does."
license = "MIT OR Apache-2.0" # SPDX license expression
repository = "https://github.com/your_username/my_package" #Optional: URL to source
readme = "README.md" # Optional: Path to README file
keywords = ["cli", "utility"] # Optional: Keywords for Crates.io search
[dependencies]
# Lists packages needed to compile and run the package's code
serde = { version = "1.0", features = ["derive"] } # Example with version and features
rand = "0.8"
log = "0.4"
[dev-dependencies]
# Lists packages needed only for tests, examples, and benchmarks
assert_cmd = "2.0"
criterion = "0.4"
[build-dependencies]
# Lists packages needed by build scripts (build.rs)
# Example: cc = "1.0"
[features]
# Defines optional features for conditional compilation
default = ["std_feature"] # Default features enabled if none specified
std_feature = []
serde_support = ["dep:serde"] # Feature enabling an optional dependency package
[profile.release]
# Customizes the 'release' build profile (e.g., for optimizations)
opt-level = 3 # Optimization level (0-3, 's', 'z')
lto = true # Enable Link-Time Optimization
codegen-units = 1 # Fewer codegen units for potentially better optimization
# See also: [profile.dev], [profile.test], [profile.bench]
[[bin]]
name = "my_cli" # Define an additional binary crate within this package
path = "src/bin/cli.rs"
-
[package]
: Core metadata about the package.name
,version
,authors
,description
,license
: These fields are essential, especially if publishing to Crates.io. Therepository
field is highly recommended to point to the source code, e.g., on GitHub.edition
: Specifies the Rust edition the package’s crates are written against (e.g.,"2015"
,"2018"
,"2021"
, or"2024"
). As of May 2025, the latest stable edition is 2024, released in February 2025. Rust editions are a powerful mechanism that allows the language to evolve by introducing changes (like new keywords or different interpretations of syntax) that might otherwise be breaking, without invalidating older code. New editions are typically released every three years.- How it works: The edition tells the Rust compiler which set of language rules and idioms to apply when compiling the crates within that specific package. Older code continues to compile correctly under its declared edition, even as newer editions introduce changes.
- Interoperability: Crates compiled with different editions can seamlessly depend on each other and be linked into the same final binary. For example, your package containing a crate using
edition = "2024"
can depend on a library package whose crate was written usingedition = "2021"
(or even"2018"
), and all can be compiled correctly by the latest Rust compiler. - Forward Compatibility: The Rust compiler maintains support for all past stable editions. This means a future compiler (e.g., one supporting a hypothetical
edition = "2027"
) will still correctly compile youredition = "2024"
package, as well as packages written for the 2021, 2018, and 2015 editions. Your code doesn’t break as the language and compiler evolve over time. - Opt-In Evolution: Migrating a package to a newer edition is an explicit, opt-in process (often assisted by
cargo fix --edition
). This gives package authors control over when to adopt new idioms or potentially breaking syntax changes introduced in a new edition. - Not for Nightly Features: Editions define a coherent, stable set of language semantics for a particular era of Rust. They are distinct from enabling experimental, unstable features typically found only on the nightly compiler.
-
[dependencies]
: Lists the packages your package depends on to run. Cargo downloads these from Crates.io by default. (See Section 23.4.2 for details on versioning). -
[dev-dependencies]
: Packages needed only for development tasks like running tests, benchmarks, or examples for your package’s crates. They are not included when someone uses your package as a dependency. -
[build-dependencies]
: Packages required by abuild.rs
script (a script Cargo runs before compiling your package’s crates, often used for code generation or compiling C code). -
[features]
: Allows defining optional features that enable conditional compilation within your crates, often used to toggle functionality or optional dependencies on other packages. -
[profile.*]
: Sections for customizing build profiles (dev
,release
,test
,bench
). (See Section 23.6). -
[[bin]]
: Allows defining additional binary crates within the same package beyond the defaultsrc/main.rs
. Similarly,[[lib]]
can define multiple library crates within a package, although this is less common.
23.4.2 Specifying Dependencies
Dependencies are listed under the [dependencies]
(or [dev-dependencies]
, [build-dependencies]
) section. Each dependency specifies the package name and a version requirement.
Cargo and the broader Rust ecosystem adhere to Semantic Versioning (SemVer), as defined at semver.org. SemVer versions are typically in the format MAJOR.MINOR.PATCH
:
MAJOR
version (e.g.,1.0.0
->2.0.0
): Incremented when you make incompatible API changes (breaking changes).MINOR
version (e.g.,1.1.0
->1.2.0
): Incremented when you add functionality in a backward-compatible manner.PATCH
version (e.g.,1.1.1
->1.1.2
): Incremented when you make backward-compatible bug fixes.
A key point in the SemVer specification (specifically rule #4) is that “Major version zero (0.y.z) is for initial development. Anything MAY change at any time. The public API SHOULD NOT be considered stable.” This means, strictly by SemVer, a 0.1.0
version can have breaking changes introduced in 0.2.0
or even 0.1.1
.
However, the Rust community has adopted a common convention for 0.y.z
versions that offers more practical stability:
- Many Rust packages, even widely used ones, remain at version
0.y.z
for extended periods, indicating a generally stable API but acknowledging that some churn is still possible before a1.0.0
release. - For a
0.y.z
package, a change iny
(e.g., from0.1.5
to0.2.0
) is conventionally treated as a breaking change. - A change in
z
(e.g., from0.1.5
to0.1.6
) is expected to be backward-compatible (bug fixes or minor additions).
This convention allows Cargo to provide sensible default dependency update behavior. When you specify a version for a dependency package:
[dependencies]
regex = "1.5" # For versions >= 1.0.0
serde = "0.8.2" # For versions < 1.0.0
Cargo interprets these version strings using a caret requirement (^
) by default:
"1.5"
is shorthand for"^1.5.0"
, which means Cargo will accept any versionv
of theregex
package where1.5.0 <= v < 2.0.0
. It allows compatible MINOR and PATCH updates but not MAJOR version2.0.0
or higher, which would imply breaking changes in the crate’s API."0.8.2"
(or just"0.8"
) is shorthand for"^0.8.2"
, which means Cargo will accept any versionv
of theserde
package where0.8.2 <= v < 0.9.0
. It allows compatible PATCH updates (and minor additions if the package author follows the spirit of non-breaking changes for the PATCH digit) but not version0.9.0
or higher, respecting the convention that a0.y.z
to0.(y+1).0
change is breaking for the crate’s API.
This default behavior allows you to receive compatible updates automatically while guarding against breaking changes. Other common version specifiers offer more control:
- Tilde requirement:
"~1.5.2"
allows only PATCH updates if a MINOR version is specified (>=1.5.2, <1.6.0
). If only MAJOR and MINOR are specified, like"~1.5"
, it behaves like^1.5
. - Exact version:
"=1.5.2"
requires exactly version1.5.2
. - Explicit range:
">=1.5.0, <1.6.0"
specifies an explicit range. - Wildcard:
"1.*"
is equivalent to">=1.0.0, <2.0.0"
."*"
accepts any version (use with caution, often only for examples or very unstable dependencies).
You can also specify dependencies from other sources:
[dependencies]
# From a Git repository containing the package
some_lib = { git = "https://github.com/user/some_lib.git", branch = "main" }
# From a local path (useful during development or in workspaces)
local_util = { path = "../local_util" }
# With optional features enabled
# Here, "1.0" implies "^1.0.0"
serde_json = { version = "1.0", features = ["raw_value"] }
# Marked as optional (only included if a feature in your crate enables it)
# In [dependencies]:
# mio = { version = "0.8", optional = true } # "0.8" implies "^0.8.0"
# In [features]:
# network = ["dep:mio"]
Understanding these conventions is crucial for managing dependency packages effectively and ensuring your project remains buildable and stable as your dependencies evolve.
23.4.3 The Cargo.lock
File
When you build your package for the first time, or after modifying dependencies in Cargo.toml
, Cargo resolves all dependency packages (including transitive ones) and records the exact versions used in the Cargo.lock
file.
- Purpose: Ensures reproducible builds. Anyone building the package with the same
Cargo.lock
file will use the exact same dependency package versions, preventing unexpected changes due to automatic updates. - Management:
Cargo.lock
is automatically generated and updated by Cargo commands likebuild
,check
,add
,remove
, orupdate
. You should not edit it manually. - Version Control:
- For binary applications (packages): Always commit
Cargo.lock
to version control (e.g., Git). This guarantees that every developer, CI system, and deployment uses the same dependency set. - For libraries (packages): Committing
Cargo.lock
is optional and debated.- Pro-Commit: Ensures the library package’s own tests run with a consistent set of dependencies in CI.
- Anti-Commit: Libraries are typically used as dependencies themselves. The downstream application package’s
Cargo.lock
will ultimately determine the versions used. Committing the library package’sCargo.lock
doesn’t affect consumers and might cause merge conflicts. Many library package authors choose not to commitCargo.lock
.
- For binary applications (packages): Always commit
23.4.4 Updating Dependencies
cargo update
: ReadsCargo.toml
and updates dependencies listed inCargo.lock
to the latest compatible versions allowed by the version specifications inCargo.toml
. It does not changeCargo.toml
itself.cargo update -p <package_name>
: Updates only a specific dependency package and its dependents.
- Upgrading Dependencies (Major Versions): To use a new major version of a dependency package (e.g., moving from
serde
“1.0” to “2.0”), you must manually edit the version requirement inCargo.toml
. Tools likecargo-edit
(cargo upgrade
) can assist with this. - Checking for Outdated Dependencies: Use
cargo outdated
(from thecargo-outdated
tool) to see which dependency packages have newer versions available than what’s currently inCargo.lock
.