Everything you have written on this site compiles without a manifest, because it is one file with no dependencies. Every real project has one, and it is the first thing a reviewer reads.
[package]
[package]
name = "image-proc"
version = "0.3.1"
edition = "2024"
rust-version = "1.85"
description = "Fast image preprocessing"
license = "MIT OR Apache-2.0"
repository = "https://github.com/you/image-proc"
name, version and edition are the load-bearing three.
editiondefaults to 2015 if you omit it. This is a hostile default, kept for compatibility with manifests written before editions existed. A 2015 crate needsextern crate, has different closure-capture anddyn-syntax rules, and lacksTryFromin the prelude.cargo newalways writes it; hand-written manifests sometimes do not.
rust-version — your MSRV, the minimum supported Rust version — used to
be documentation. It is now load-bearing: the edition-2024 resolver is
MSRV-aware, and given a choice it will pick an older version of a dependency
rather than one whose rust-version your toolchain cannot meet. Set it
honestly; setting it too low is a lie your CI will not catch.
crates.io additionally requires description and license (or
license-file) before it will accept a publish. clippy::cargo_common_metadata
nags about the rest — repository, keywords, categories, readme — and
it is a fair nag: those fields are the difference between a crate people can
evaluate and one they scroll past.
💡A crate you maintain has been published for a year. You want to stop shipping the 40 MB of test fixtures that are in the repository. What are your options and what is the trap? click to reveal
Two fields, and they are mutually exclusive: include lists what goes in
the package, exclude lists what stays out. You may set one or the other,
never both.
include = ["src/**/*.rs", "Cargo.toml", "README.md", "LICENSE-*"]
The trap is that neither is set by default, so the default behaviour is
“everything that is not gitignored”, and that is how crates end up shipping
fixtures, benchmark data and a .png of the logo at four resolutions. Every
cargo add of your crate then downloads all of it.
cargo package --list shows you exactly what would be included. Run it once
before your first publish, and again the day you add a fixtures directory.
The dependency tables
[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"
-
[dependencies]— available tosrc/, and propagated to your consumers as part of your dependency graph. -
[dev-dependencies]— available to tests, examples and benchmarks only. Not usable fromsrc/, and not propagated to anyone who depends on you. This is why a test helper crate does not become your users’ problem. -
[build-dependencies]— available tobuild.rsonly, and compiled for the host, not the target. -
Target-conditional tables use the same
cfgpredicate language from 10.12, which is a nice payoff:cfg(unix),cfg(target_os = "windows"),cfg(any(…))all work exactly as you implemented them.
Renaming is done with package:
[dependencies]
rand_old = { package = "rand", version = "0.7" }
rand = "0.8"
Two major versions of the same crate, side by side, under two names. This is
legal and occasionally necessary — and clippy::multiple_crate_versions will
point out when it happened by accident rather than on purpose, which is far
more common.
[lints], and the priority field
Since Rust 1.74 lint configuration belongs in the manifest rather than in
#![deny(...)] attributes at the top of lib.rs:
[lints.rust]
unsafe_code = "forbid"
[lints.clippy]
all = { level = "deny", priority = -1 }
needless_range_loop = "allow"
That priority = -1 is the part nobody explains. Within a table, entries are
applied in priority order, low to high, and equal priorities are ambiguous. So
whenever you enable a group and then override a member of it, the
group needs a lower priority than the override — otherwise Cargo cannot tell
which you meant, and the group may win.
Cargo.lock
Commit it. Yes, for libraries too.
The old advice was “binaries commit the lockfile, libraries do not”, on the grounds that a library’s lockfile is ignored by its consumers. That is still true — but the lockfile is also what makes your own CI reproducible, and the current official guidance is to commit it for everything. A library’s users are unaffected; a library’s maintainers get a build that does not spontaneously change on a Tuesday.
Lints about the manifest itself
Clippy has a whole cargo group, off by default and irrelevant on this site
because there is no manifest, but worth knowing exists:
| lint | catches |
|---|---|
cargo_common_metadata |
missing description, license, repository, … |
wildcard_dependencies |
foo = "*" — a requirement with no bound |
multiple_crate_versions |
two majors of one crate in the graph |
negative_feature_names |
no-std, disable-logging — features that subtract |
redundant_feature_names |
use-serde, with-rayon — noise in every name |
negative_feature_names is the one that matters most, and item 10.16 explains
why: features unify by union across the whole graph, so a feature that
removes behaviour is a build failure waiting for a third party to trigger it.