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

  1. Add itoa with an exact requirement and check that cargo tree reports the same version.
  2. Change the requirement to "1", run cargo update and compare the lock file.
  3. 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.

  1. 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.

  2. What records the exact version a build used?

    Answer

    Cargo.lock

    The manifest states a requirement and the lock file states the resolved version.

  3. What does cargo add itoa@1.0.19 write 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.

  4. 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.

  5. 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.

  6. 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.

  7. 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.

  8. 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.

  9. 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.

  10. 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 len on 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