Chapter 5: Cargo and Project Structure
Prerequisites
You will understand
- Cargo.toml anatomy and project layout
- Dependencies, features, and workspaces
cargo build,test,run,check
Reading time
Cargo.toml as Build Contract
One Repository, Several Crates, One Resolver
Step 1 - The Problem
Once a Rust project exists, you need to answer:
- how are crates and targets organized?
- how are dependencies declared?
- which versions are compatible?
- what belongs in the lockfile?
- when should multiple crates live in one workspace?
Without discipline here, project shape degrades quickly.
Step 2 - Rust’s Design Decision
Cargo manifests are explicit, structured, and central. They describe:
- package metadata
- targets
- dependencies
- features
- build scripts
- workspace relationships
Rust accepted:
- writing manifests directly
- exposing version and feature constraints explicitly
Rust refused:
- implicit dependency resolution stories hidden in editor state or build hacks
Step 3 - The Mental Model
Plain English rule: Cargo.toml is the build, dependency, and public-shape declaration for the package.
Step 4 - Minimal Code Example
[package]
name = "demo"
version = "0.1.0"
edition = "2024"
[dependencies]
serde = { version = "1.0", features = ["derive"] }
Step 5 - Walkthrough
This manifest says:
- the package is named
demo - it uses the 2024 edition syntax and semantics
- it depends on
serdeversion-compatible with1.0 - it enables the
derivefeature for that dependency
That is enough for Cargo to build a dependency graph and fetch compatible crate versions.
Step 6 - Three-Level Explanation
Cargo.toml describes what the project is and what it depends on.
The most important parts to read or write well are:
[package][dependencies][dev-dependencies][features][workspace]
Dependency entries are not just install instructions. They are compatibility and capability declarations.
Cargo manifests are architecture signals. Feature flags shape compilation surface. Workspaces shape crate boundaries. Lockfiles shape reproducibility. build.rs can extend the build graph into generated code or foreign compilation. All of that changes how a repo should be read and maintained.
Dependency Versioning
Common version forms:
"1.2.3"or"1.2": caret-compatible semver range by default"~1.2.3": patch-level compatibility"=1.2.3": exact version
You do not need to memorize every semver operator on day one, but you do need to know that dependency version strings are constraints, not simple installation requests.
Cargo.lock
The practical rule:
- commit
Cargo.lockfor binaries and applications - do not usually commit it for reusable libraries
Why?
- applications want reproducible deployments and CI
- libraries want downstream consumers to resolve against compatible versions in their own graph
The underlying invariant is reproducibility for shipped artifacts versus flexibility for reusable dependency crates.
Workspaces
Workspaces group related crates:
[workspace]
members = ["crates/core", "crates/cli"]
[workspace.dependencies]
serde = { version = "1.0", features = ["derive"] }
Use a workspace when:
- crates evolve together
- you want shared dependency declarations
- there is a real architectural split, not just aesthetic file organization
cargo add, cargo tree, cargo audit
Useful commands:
cargo add tracing
cargo tree
cargo audit
These help you:
- add dependencies consistently
- inspect the dependency graph
- check for known vulnerability advisories
build.rs
build.rs is a build script executed before the main crate compiles.
Use it when the build needs:
- generated bindings
- bundled C compilation
- target probing
- code generation based on environment or external inputs
If you see build.rs, treat it as part of the compilation contract, not as an afterthought.
Step 7 - Common Misconceptions
Wrong model 1: “Cargo.toml is mostly package metadata.”
Correction: it is also architecture, dependency, and feature contract.
Wrong model 2: “Feature flags are runtime toggles.”
Correction: Cargo features are compile-time graph and code-shape controls.
Wrong model 3: “Workspaces are just for very large projects.”
Correction: workspaces are useful whenever crate boundaries are real and shared management helps.
Wrong model 4: “Cargo.lock should always be committed or never be committed.”
Correction: the right choice depends on whether the package is an application or a library.
Step 8 - Real-World Pattern
Serious Rust repos are often legible from the manifest alone:
- async/network stack
- CLI surface
- proc-macro support
- serialization strategy
- workspace organization
That is why experienced contributors read Cargo.toml so early.
Step 9 - Practice Block
Code Exercise
Create a tiny workspace with one library crate and one CLI crate. Use [workspace.dependencies] for one shared dependency.
Code Reading Drill
Explain what this dependency line means:
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
Spot the Bug
Why is this misleading contributor guidance?
"Delete Cargo.lock whenever you have a dependency problem."
Refactoring Drill
Take a monolithic package with a growing CLI and reusable library logic and sketch when splitting into a workspace starts making sense.
Compiler Error Interpretation
If a crate compiles only with one feature combination and breaks badly with another, translate that as: “the manifest’s feature design may be failing to preserve additive compatibility.”
Step 10 - Contribution Connection
After this chapter, you can:
- read manifests as repo architecture
- reason about feature flags and lockfiles
- navigate workspaces with less confusion
Good first PRs include:
- clarifying manifest comments or docs
- simplifying feature organization
- improving workspace dependency sharing
In Plain English
Cargo.toml tells Rust what the project is, what it depends on, and how its pieces fit together. That matters because project structure is not separate from engineering; it shapes how the code is built, shared, and changed.
What Invariant Is Rust Protecting Here?
The build graph, version constraints, feature surface, and package boundaries should remain explicit and reproducible enough for both humans and tools to reason about safely.
If You Remember Only 3 Things
Cargo.tomlis architecture, not just metadata.Cargo.lockexists for reproducibility, but libraries and binaries use it differently.- Workspaces are about real crate boundaries, not decorative complexity.
Memory Hook
If source files are the rooms of a house, Cargo.toml is the blueprint and permit packet that says which rooms exist and how utilities reach them.
Flashcard Deck
| Question | Answer |
|---|---|
What does Cargo.toml primarily describe? | Package metadata, dependencies, features, targets, and workspace/build structure. |
| What does a dependency feature entry do? | Opts into compile-time capabilities of that dependency. |
When is Cargo.lock usually committed? | For applications and binaries, not usually for reusable libraries. |
| What is a Cargo workspace for? | Managing related crates that evolve together under one root. |
What does cargo tree show? | The resolved dependency graph. |
What does cargo add help with? | Adding dependencies consistently to the manifest. |
When do you need build.rs? | When compilation requires code generation, probing, or foreign build steps. |
| Are Cargo features runtime flags? | No. They are compile-time configuration of code and dependency graph. |
Chapter Cheat Sheet
| Need | Cargo feature | Why |
|---|---|---|
| declare package | [package] | metadata and edition |
| declare dependencies | [dependencies] | graph inputs |
| test-only deps | [dev-dependencies] | keep main graph cleaner |
| compile-time options | [features] | optional capabilities |
| multi-crate repo | [workspace] | grouped crate management |
| generated build step | build.rs | pre-compilation logic |