Cargo.toml and dependencies
Cargo.toml is the file that says what the package is, what it is built from and what it depends on. The first two fields are a name and an edition; a dependency line adds a crate from crates.io, and a second file, Cargo.lock, records the exact version that was chosen.
The lesson starts from a package with no dependencies and adds one, then a second. The two crates are itoa 1.0.19 and ryu 1.0.23, both fetched from crates.io on this machine while the page was written, and every command below is the real output.
The manifest
The table [package] holds the metadata: the name that becomes the crate name, the version, the edition that decides which rules apply, and optional fields such as a description, a licence and a repository. The table [dependencies] starts empty.
The program is the smallest one a package can hold. The build names the package and the profile, and the run prints the line.
[package]
name = "digits"
version = "0.1.0"
edition = "2024"
description = "Format numbers into a buffer"
license = "MIT"
[dependencies]
fn main() {
println!("digits");
}
cargo new --vcs none digits
vi Cargo.toml
cargo run
Compiling digits v0.1.0 (/tmp/rw7/L080/digits)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.42s
Running `target/debug/digits`
digits
The name in the manifest is the name cargo uses for the binary, the tests and the documentation, so it is worth fixing before the first commit. The previous lesson covers the three commands and the target folder.
A dependency from crates.io
cargo add writes the line and picks the newest version, which is why the page names the exact one: the next release of itoa would compile the same program and might print the same output, but a lesson that a reader can reproduce needs the number.
The program formats an integer into a reusable buffer. itoa writes the digits without allocating, and the length is the number of characters.
cargo add itoa@1.0.19
cat Cargo.toml
Updating crates.io index
Adding itoa v1.0.19 to dependencies
Features:
- no-panic
Updating crates.io index
Locking 1 package to latest Rust 1.96.1 compatible version
[package]
name = "digits"
version = "0.1.0"
edition = "2024"
[dependencies]
itoa = "1.0.19"
use itoa::Buffer;
fn main() {
let mut buffer = Buffer::new();
let text = buffer.format(1234);
println!("{text} {}", text.len());
}
cargo run
Downloaded itoa v1.0.19
Compiling itoa v1.0.19
Compiling digits v0.1.0 (/tmp/rw7/L080/digits)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 2.32s
Running `target/debug/digits`
1234 4
The build downloaded the source, compiled the dependency once and then compiled the package against it. The second build compiles only the package, because the dependency has not changed. The lesson on crates explains the unit a package is made of.
Version requirements and Cargo.lock
A version string is a requirement, not a pin. "1.0.19" means "1.0.19 or newer within 1.0", which is the caret rule with the caret left out; "~1.0.19" allows only patch releases, and "=1.0.19" allows nothing else. The exact version that was chosen is written to Cargo.lock, and every build uses that file until cargo update changes it.
A second dependency shows the file with two entries. The program formats one integer and one float, and the two crates do the work.
cargo add ryu@1.0
cat Cargo.toml
Updating crates.io index
Locking 1 package to latest Rust 1.96.1 compatible version
Adding ryu v1.0.23
[package]
name = "digits"
version = "0.1.0"
edition = "2024"
[dependencies]
itoa = "1.0.19"
ryu = "1.0"
use itoa::Buffer;
use ryu::Buffer as FloatBuffer;
fn main() {
let mut integers = Buffer::new();
let mut floats = FloatBuffer::new();
println!("{} {}", integers.format(42), floats.format(1.5));
}
cargo run
cargo tree
Compiling ryu v1.0.23
Compiling digits v0.1.0 (/tmp/rw7/L080/digits)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.82s
Running `target/debug/digits`
42 1.5
digits v0.1.0 (/tmp/rw7/L080/digits)
├── itoa v1.0.19
└── ryu v1.0.23
The lock file records the same two versions with a checksum and the registry they came from, which is what makes a build reproducible on another machine:
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "itoa"
version = "1.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3bd1574e617a9a6c1f8f8181bd7e841e1f20e4277607ef28945765188d6284d4"
[[package]]
name = "ryu"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
A binary project commits the lock file, because the build that shipped has to be the build that is reproduced. A library usually leaves it out, because the applications that depend on the library decide the versions. The lesson on workspaces shows one lock file for several packages.
A dependency that does not exist
Cargo resolves every requirement against the index before it compiles anything, so a misspelt name fails at once and names the requirement that could not be met. Nothing is downloaded and no binary is written.
The manifest below adds itoaa beside the real crate. The build stops while it updates the index.
[dependencies]
itoa = "1.0.19"
itoaa = "1.0"
cargo build
Updating crates.io index
error: no matching package named `itoaa` found
location searched: crates.io index
required by package `digits v0.1.0 (/tmp/rw7/L080/digits)`
The three lines are the whole diagnosis: the name, the place it was searched and the package that asked for it. A dependency that exists but has no version matching the requirement fails with a different message that lists the versions the index holds, which is the case a caret rule usually prevents. The next lesson turns a dependency into an optional one.
Exercises
- Add itoa with an exact requirement and check that cargo tree reports the same version.
- Change the requirement to "1", run cargo update and compare the lock file.
- Remove a dependency from the manifest and find the lines in the lock file that disappear.
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
What does the version
"1.0.19"in a dependency line mean?Answer
1.0.19 or a newer compatible version
A bare version is a caret requirement, so patch and minor releases are allowed.
-
What records the exact version a build used?
Answer
Cargo.lock
The manifest states a requirement and the lock file states the resolved version.
-
What does
cargo add itoa@1.0.19write into the manifest?Answer
A dependency line with that version
The command edits the dependencies table and leaves the lock file to the next build.
-
When does cargo notice a package name that does not exist?
Answer
While it resolves the requirements against the index
Resolution happens before any compilation, so the error comes first.
-
What does this program print?
fn main() { let name = "digits"; let version = 1; println!("{name} {version}"); }Answer
digits 1
Both names are replaced by their values in the format string.
-
What does this program print?
use std::collections::BTreeMap; fn main() { let mut versions = BTreeMap::new(); versions.insert("itoa", "1.0.19"); versions.insert("ryu", "1.0.23"); for (name, version) in &versions { println!("{name} {version}"); } }Answer
itoa 1.0.19 / ryu 1.0.23
A BTreeMap iterates in key order, and itoa comes before ryu.
-
What does this program print?
fn parse(text: &str) -> Result<u32, std::num::ParseIntError> { text.parse() } fn main() { println!("{:?}", parse("1.0.19")); }Answer
Err(ParseIntError { kind: InvalidDigit })
The dot is not a digit, so the whole string is rejected.
-
What does this program print?
fn main() { let text = String::from("1.0.19"); let parts: Vec<&str> = text.split('.').collect(); println!("{} {}", parts.len(), parts[1]); }Answer
3 0
The two dots divide the text into three parts, and the second is the minor number.
-
What is the first line of the compiler output for this program?
fn main() { let text = String::from("1.0.19").as_str(); println!("{text}"); }Answer
error[E0716]: temporary value dropped while borrowed
The String is a temporary and the slice borrows it, so the borrow outlives the value.
-
What is the first line of the compiler output for this program?
fn main() { let digits = 1234; println!("{}", digits.len()); }Answer
error[E0689]: can't call method
lenon ambiguous numeric type{integer}The number is not a string or a collection, so it has no length method.
The code on this page was built with rustc 1.96.1 and cargo 1.96.1, with itoa 1.0.19 and ryu 1.0.23 from crates.io, and run on Alpine Linux 3.24.3 (x86_64) in a Docker container.
Article author: Arthur Isaev