The Cargo Guide

Manifest Fundamentals: Understanding Cargo.toml

What Cargo.toml Is

Every Cargo package is defined by a Cargo.toml file called the manifest. It is written in TOML and tells Cargo what the package is, how it should be built, and what metadata should travel with it when it is packaged or published.

A minimal manifest looks like this:

[package]
name = "hello_manifest"
version = "0.1.0"
edition = "2024"
 
[dependencies]

This already tells Cargo three important things:

  • the package name
  • the package version
  • the Rust edition to compile with

The rest of the guide explains how to read and shape that manifest intentionally.

TOML Basics That Matter for Cargo

Cargo manifests are written in TOML, but you only need a small part of TOML to work effectively with Cargo.

Strings:

name = "my_package"
license = "MIT OR Apache-2.0"

Integers and booleans:

publish = false

Arrays:

keywords = ["cli", "parser", "rust"]
exclude = ["notes/", "drafts/"]

Tables:

[package]
name = "my_package"
version = "0.1.0"
edition = "2024"
 
[dependencies]
serde = "1"

Nested tables:

[package.metadata.docs]
featured = true

A practical rule is that most Cargo work consists of editing named tables like [package] and [dependencies], then filling them with strings, arrays, and booleans.

The [package] Table

The [package] table is where the manifest describes the package itself.

Example:

[package]
name = "report_builder"
version = "0.1.0"
edition = "2024"
description = "Generate simple text reports"
readme = "README.md"
repository = "https://github.com/example/report_builder"
license = "MIT"

This table is the package identity card. Some fields are essential for Cargo to understand the package. Others matter for publication, discoverability, documentation, or external tooling.

Required vs Optional Package Metadata

At a practical beginner level, the most important distinction is between fields that are fundamentally required for a normal package definition and fields that are optional but often valuable.

A compact manifest with the core fields:

[package]
name = "slug_tools"
version = "0.1.0"
edition = "2024"
 
[dependencies]

Then a richer manifest:

[package]
name = "slug_tools"
version = "0.1.0"
edition = "2024"
rust-version = "1.85"
description = "Utilities for creating URL-friendly slugs"
readme = "README.md"
repository = "https://github.com/example/slug_tools"
homepage = "https://example.com/slug_tools"
documentation = "https://docs.rs/slug_tools"
license = "MIT OR Apache-2.0"
keywords = ["slug", "text", "url"]
categories = ["text-processing"]

The point is not to memorize a formal legal boundary. It is to understand that some fields define the package operationally, while others improve how the package is presented, constrained, or published.

Package Name

The package name is the identity Cargo uses for the package.

[package]
name = "weather_tools"
version = "0.1.0"
edition = "2024"

In a package with a library target, that name usually becomes the crate name used from Rust code:

pub fn c_to_f(c: f64) -> f64 {
    c * 9.0 / 5.0 + 32.0
}
fn main() {
    println!("{}", weather_tools::c_to_f(20.0));
}

That is why package naming is not just cosmetic. It affects how your code is referenced and how your package appears to users.

Version

The version field identifies the current package version.

[package]
name = "api_client"
version = "0.3.2"
edition = "2024"

Beginners can think of this as the package's public revision number. It becomes especially important once other packages depend on it or once it is published.

For a learning project, 0.1.0 is a normal starting point.

Edition

The edition field tells Cargo which Rust edition the package uses.

[package]
name = "notes_app"
version = "0.1.0"
edition = "2024"

A practical beginner rule is to use the current edition for new projects unless you are deliberately matching an older codebase.

The edition is package-wide in the sense that it affects the targets in the package, including binaries, libraries, tests, benches, and examples.

rust-version

The rust-version field expresses the minimum supported Rust toolchain version for the package.

[package]
name = "parser_core"
version = "0.1.0"
edition = "2024"
rust-version = "1.85"

This is useful because it communicates compatibility to both humans and tooling.

A helpful mental model is:

  • edition says which language edition the package is written against
  • rust-version says how old the compiler is allowed to be

Those are related, but not the same thing.

Description and Discovery Metadata

Several manifest fields help people discover and understand a package.

[package]
name = "markdown_snips"
version = "0.1.0"
edition = "2024"
description = "Extract fenced code blocks from Markdown files"
keywords = ["markdown", "parser", "codeblock"]
categories = ["parsing", "text-processing"]

These fields do not change how the package compiles, but they improve how it is presented in package indexes and documentation sites.

The core lesson is that a manifest is not only a build file. It is also package-facing metadata.

readme, repository, homepage, and documentation

These fields point users to information about the package.

[package]
name = "task_clock"
version = "0.1.0"
edition = "2024"
readme = "README.md"
repository = "https://github.com/example/task_clock"
homepage = "https://example.com/task_clock"
documentation = "https://docs.rs/task_clock"

A practical way to think about them:

  • readme: the main introductory file shipped with the package
  • repository: where the source code lives
  • homepage: a general landing page for the project
  • documentation: the canonical docs URL

You should usually provide these intentionally rather than treating them as afterthoughts.

It is also useful to know that Cargo can infer a README in common cases unless you explicitly disable it.

[package]
name = "no_readme_demo"
version = "0.1.0"
edition = "2024"
readme = false

License and license-file

Licensing metadata tells users and registries how the package is licensed.

Using an SPDX expression:

[package]
name = "config_loader"
version = "0.1.0"
edition = "2024"
license = "MIT OR Apache-2.0"

Using a license file:

[package]
name = "config_loader"
version = "0.1.0"
edition = "2024"
license-file = "LICENSE.txt"

These are not the same thing:

  • license is a machine-readable license expression
  • license-file points to the actual license text file

A package may use one or the other depending on the situation. In practice, many Rust packages use a standard SPDX expression in license, and some also maintain license text files in the repository.

include and exclude

The include and exclude fields control which files are packaged when the crate is prepared for publication.

Example using exclude:

[package]
name = "templater"
version = "0.1.0"
edition = "2024"
exclude = [
  "drafts/",
  "design-notes/",
  "*.psd"
]

Example using include:

[package]
name = "templater"
version = "0.1.0"
edition = "2024"
include = [
  "src/**",
  "Cargo.toml",
  "README.md",
  "LICENSE*"
]

These fields are especially important when a repository contains extra assets, experiments, or internal notes that should not be part of the published package.

A very useful inspection command is:

cargo package --list

That lets you verify what would actually be included.

publish Controls

The publish field lets you control whether and where a package may be published.

To prevent publication entirely:

[package]
name = "internal_tool"
version = "0.1.0"
edition = "2024"
publish = false

This is especially useful for internal packages, experiments, or workspace members that should never be sent to a registry.

You may also see publication controls used in more specialized registry setups, but the most important beginner case is simply understanding that publication can be intentionally disabled.

A Realistic Package Metadata Example

Here is a fuller manifest that combines many common metadata fields in one place:

[package]
name = "journal_parser"
version = "0.2.0"
edition = "2024"
rust-version = "1.85"
description = "Parse structured journal entries into Rust data types"
readme = "README.md"
repository = "https://github.com/example/journal_parser"
homepage = "https://example.com/journal_parser"
documentation = "https://docs.rs/journal_parser"
license = "MIT OR Apache-2.0"
keywords = ["parser", "journal", "text"]
categories = ["parsing", "command-line-utilities"]
include = [
  "src/**",
  "Cargo.toml",
  "README.md",
  "LICENSE*"
]
 
[dependencies]
serde = { version = "1", features = ["derive"] }

This is the kind of manifest that communicates not just how to build the package, but also what the package is, where it lives, what it supports, and what gets shipped.

package.metadata for Tool-Specific Data

Cargo also allows arbitrary external-tool metadata under package.metadata.

[package]
name = "site_builder"
version = "0.1.0"
edition = "2024"
 
[package.metadata.docs]
default-theme = "dark"
show-private-api = false
 
[package.metadata.release]
pre-release-checklist = "RELEASE_NOTES.md"

A good mental model is that package.metadata is not for Cargo's own core behavior. It is a reserved space where external tools can store package-related configuration without colliding with standard Cargo keys.

Badges and Older Metadata History

If you read older manifests or blog posts, you may encounter badge-related metadata.

Example of an older style you may still see in historical material:

[badges]
maintenance = { status = "experimental" }

The main lesson is not to build your manifest design around old badge-oriented examples. They are better understood as historical context than as the center of modern Cargo manifest design.

For modern learning, it is more useful to focus on clear package metadata, publication controls, and package.metadata for tool-specific extensions.

How Manifest Metadata Connects to Real Code

Manifest fields feel abstract until they are tied to a package with actual code.

Example package:

[package]
name = "titlecase_utils"
version = "0.1.0"
edition = "2024"
rust-version = "1.85"
description = "Small helpers for title-casing strings"
readme = "README.md"
repository = "https://github.com/example/titlecase_utils"
license = "MIT"
 
[dependencies]
// src/lib.rs
pub fn titlecase(s: &str) -> String {
    s.split(' ')
        .filter(|part| !part.is_empty())
        .map(|part| {
            let mut chars = part.chars();
            match chars.next() {
                Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
                None => String::new(),
            }
        })
        .collect::<Vec<_>>()
        .join(" ")
}
// src/main.rs
fn main() {
    println!("{}", titlecase_utils::titlecase("hello cargo manifest"));
}
cargo run

The code is the package's behavior. The manifest is the package's contract, identity, compatibility signal, and packaging boundary.

Common Beginner Mistakes

A few mistakes show up often.

Mistake 1: treating Cargo.toml as a random config dump.

A better approach is to treat each field as part of a clean package contract.

Mistake 2: confusing edition with rust-version.

They answer different questions.

Mistake 3: forgetting that packaging metadata matters.

A crate with code but no useful metadata is harder for others to discover, trust, and use.

Mistake 4: using include or exclude carelessly.

It is easy to accidentally omit important files or ship files you did not intend to publish.

Mistake 5: putting custom tool settings in standard Cargo fields.

Use package.metadata for tool-specific extensions.

A Hands-On Exercise

Create a small package and improve its manifest step by step.

Start with:

cargo new manifest_lab --lib
cd manifest_lab

Initial manifest:

[package]
name = "manifest_lab"
version = "0.1.0"
edition = "2024"
 
[dependencies]

Then enrich it:

[package]
name = "manifest_lab"
version = "0.1.0"
edition = "2024"
rust-version = "1.85"
description = "Practice package metadata in Cargo manifests"
readme = "README.md"
repository = "https://github.com/example/manifest_lab"
homepage = "https://example.com/manifest_lab"
documentation = "https://docs.rs/manifest_lab"
license = "MIT"
exclude = ["notes/"]
 
[dependencies]

Add a simple library:

pub fn normalize(s: &str) -> String {
    s.trim().to_lowercase()
}

Then inspect what Cargo would package:

cargo package --list

This exercise helps connect manifest fields to a real package rather than leaving them as isolated definitions.

Mental Model Summary

A strong mental model for manifest fundamentals is:

  • Cargo.toml is the package manifest
  • TOML gives the syntax shape
  • [package] defines package identity and metadata
  • edition and rust-version describe compatibility in different ways
  • fields like readme, repository, homepage, and documentation help users find and understand the package
  • license and license-file communicate legal usage
  • include, exclude, and publish shape what gets packaged and whether publication is allowed
  • package.metadata gives external tools a safe extension space

Once that model is stable, Cargo manifests become much easier to read and design intentionally.