Post

Rust Notes: Cargo

A practical guide to Rust Cargo.

Rust Notes: Cargo

Rust Cargo: Cargo.toml, Cargo.lock, Dependencies, Features, Workspaces, and More

Cargo is Rust’s package manager and build system.

It is responsible for much more than compiling Rust code. Cargo manages dependencies, resolves dependency versions, builds packages, runs tests, handles features, manages workspaces, creates distributable packages, and can publish packages to registries such as crates.io.

A useful mental model is:

1
2
3
4
5
6
7
8
9
10
11
12
                         Cargo
                           |
        +------------------+------------------+
        |                  |                  |
   Build system       Dependency manager   Package manager
        |                  |                  |
   rustc/rustdoc      Cargo.toml           crates.io
        |              Cargo.lock
        |                  |
        +----------+-------+
                   |
              Rust project

The two files that are most important to understand are:

1
2
Cargo.toml
Cargo.lock

They have very different purposes.


1. Cargo.toml vs Cargo.lock

The easiest way to remember the difference is:

1
2
3
4
5
6
7
Cargo.toml
    =
"What dependencies and configuration does my project want?"

Cargo.lock
    =
"What exact dependency versions did Cargo resolve?"

For example, Cargo.toml might say:

1
2
3
[dependencies]
serde = "1"
tokio = "1"

This specifies version requirements.

It does not necessarily mean:

1
2
serde = exactly version 1.0.x
tokio = exactly version 1.x.x

Instead, these are version requirements that Cargo resolves according to its dependency resolver.

The resulting exact versions are recorded in Cargo.lock.


2. What Is a Cargo Package?

A Cargo package normally contains:

1
2
3
4
my-project/
├── Cargo.toml
└── src/
    └── main.rs

For a library:

1
2
3
4
my-library/
├── Cargo.toml
└── src/
    └── lib.rs

The Cargo.toml file is called the manifest of the package.

Cargo uses this manifest to understand how the package should be built and what it depends on.


3. Creating a Package

Create a binary package:

1
cargo new my-app

This creates:

1
2
3
4
my-app/
├── Cargo.toml
└── src/
    └── main.rs

Create a library:

1
cargo new my-library --lib

This creates:

1
2
3
4
my-library/
├── Cargo.toml
└── src/
    └── lib.rs

Cargo provides commands such as new, build, check, test, run, add, remove, update, tree, metadata, package, and publish.


4. Basic Cargo.toml

A simple Cargo.toml might look like:

1
2
3
4
5
6
7
8
[package]
name = "my-app"
version = "0.1.0"
edition = "2024"

[dependencies]
serde = "1"
tokio = "1"

There are two major sections here:

1
[package]

and:

1
[dependencies]

The first describes the package.

The second describes dependencies.


5. The [package] Section

The [package] section describes the package itself.

A common example:

1
2
3
4
5
6
7
8
[package]
name = "my-app"
version = "0.1.0"
edition = "2024"
rust-version = "1.85"
description = "My Rust application"
license = "MIT"
repository = "https://github.com/example/my-app"

Important fields include:

1
2
3
4
5
6
7
8
9
10
11
name
version
edition
rust-version
description
license
repository
homepage
documentation
readme
authors

Cargo’s manifest reference defines these and many other package fields.


6. name

1
2
[package]
name = "my-app"

This is the package name.

It is also used when selecting the package:

1
cargo build -p my-app

For a library package, the crate name is generally derived from the package name, with hyphens converted to underscores.

For example:

1
name = "my-library"

can be imported as:

1
use my_library::SomeType;

7. version

1
version = "0.1.0"

This is the package version.

Cargo uses semantic versioning conventions for package versions.

For example:

1
2
3
4
0.1.0
0.2.0
1.0.0
1.2.3

When publishing a crate, the version is part of the crate’s identity.


8. edition

1
edition = "2024"

The Rust edition controls language behavior and compatibility rules.

Common editions include:

1
2
3
4
2015
2018
2021
2024

The edition is not the same thing as the compiler version.

For example:

1
edition = "2024"

does not mean:

1
Rust compiler version 2024

It means:

Compile this package using the Rust 2024 edition rules.


9. rust-version

You can specify the minimum Rust version supported by your package:

1
rust-version = "1.85"

This tells Cargo:

This package requires at least Rust 1.85.

This is particularly useful for libraries because users may have different compiler versions.


10. Dependencies

Dependencies are normally declared in:

1
[dependencies]

For example:

1
2
3
4
[dependencies]
serde = "1"
tokio = "1"
anyhow = "1"

This means:

1
2
3
4
5
my application
    |
    +-- serde
    +-- tokio
    +-- anyhow

Cargo resolves the complete dependency graph.


11. Dependency Version Requirements

You will commonly see:

1
serde = "1"

or:

1
serde = "1.0"

or:

1
serde = "1.0.200"

These are version requirements, not necessarily exact versions.

For example:

1
serde = "1"

allows compatible versions within the 1.x line according to Cargo’s version requirement rules.

The exact selected version is recorded in Cargo.lock.


12. Exact Dependency Versions

You can request an exact version:

1
serde = "=1.0.219"

The = means:

1
exactly 1.0.219

This is much more restrictive than:

1
serde = "1"

In most projects, normal compatible version requirements are preferred.


13. Dependency Sources

Dependencies can come from different sources.

crates.io

1
2
[dependencies]
serde = "1"

Git repository

1
2
3
4
[dependencies]
some-library = {
    git = "https://github.com/example/some-library"
}

Git branch

1
2
3
4
5
[dependencies]
some-library = {
    git = "https://github.com/example/some-library",
    branch = "main"
}

Git tag

1
2
3
4
5
[dependencies]
some-library = {
    git = "https://github.com/example/some-library",
    tag = "v1.2.0"
}

Git revision

1
2
3
4
5
[dependencies]
some-library = {
    git = "https://github.com/example/some-library",
    rev = "abcdef123456"
}

Local path

1
2
3
4
[dependencies]
my-library = {
    path = "../my-library"
}

14. Dependency Aliasing

You can give a dependency a different name:

1
2
3
4
5
[dependencies]
my_serde = {
    package = "serde",
    version = "1"
}

Then:

1
use my_serde::Serialize;

The package field specifies the actual package name while the key specifies the dependency name used by your crate.


15. Dependency Features

Dependencies can expose Cargo features.

For example:

1
2
3
4
5
[dependencies]
tokio = {
    version = "1",
    features = ["rt", "macros", "net"]
}

This means:

1
2
3
4
5
tokio
 |
 +-- rt
 +-- macros
 +-- net

Cargo features are compile-time configuration options.


16. Optional Dependencies

A dependency can be optional:

1
2
3
4
5
[dependencies]
serde = {
    version = "1",
    optional = true
}

Then the dependency is not necessarily enabled for every build.

A feature can enable it:

1
2
[features]
json = ["dep:serde"]

Now:

1
cargo build --features json

enables serde.

The dependency graph becomes:

1
2
3
4
json feature
     |
     v
   serde

17. Cargo Features

Features are compile-time switches.

For example:

1
2
3
4
5
[features]
default = ["logging"]
logging = []
async = []
json = []

You can enable a feature:

1
cargo build --features async

Multiple features:

1
cargo build --features "async json"

All features:

1
cargo build --all-features

Disable default features:

1
cargo build --no-default-features

Features are commonly used for:

  • optional functionality
  • optional dependencies
  • platform support
  • no_std support
  • different backends
  • different implementations

18. default Features

A special feature named default can specify features enabled automatically.

1
2
3
4
[features]
default = ["std"]
std = []
no_std = []

Running:

1
cargo build

enables:

1
2
3
default
  |
  +-- std

To disable default features:

1
cargo build --no-default-features

You can then explicitly enable another configuration:

1
cargo build --no-default-features --features no_std

19. Conditional Compilation

Features become especially useful with Rust’s cfg attributes.

Cargo:

1
2
[features]
logging = []

Rust:

1
2
3
4
#[cfg(feature = "logging")]
fn log_message() {
    println!("logging enabled");
}

When the feature is enabled:

1
cargo build --features logging

the function is compiled.

Without the feature, it is not compiled.

This is compile-time configuration:

1
2
3
4
5
6
7
8
9
10
Cargo feature
      |
      v
rustc --cfg
      |
      v
#[cfg(...)]
      |
      v
selected code

20. Development Dependencies

Dependencies used only for development go under:

1
[dev-dependencies]

For example:

1
2
3
[dev-dependencies]
pretty_assertions = "1"
criterion = "0.5"

These dependencies are typically used by:

  • tests
  • benchmarks
  • examples

They are not normal runtime dependencies of the package.


21. Build Dependencies

Dependencies required by a build script go under:

1
[build-dependencies]

For example:

1
2
3
[build-dependencies]
cc = "1"
bindgen = "0.71"

If you have:

1
build.rs

and it needs cc, then:

1
2
[build-dependencies]
cc = "1"

is appropriate.

This is especially common when building native code or generating Rust bindings.


22. Target-Specific Dependencies

Dependencies can be conditional on the target platform.

For example:

1
2
[target.'cfg(windows)'.dependencies]
windows = "0.60"

For Linux:

1
2
[target.'cfg(target_os = "linux")'.dependencies]
libc = "0.2"

You can also target architectures:

1
2
[target.'cfg(target_arch = "x86_64")'.dependencies]
some-x86-library = "1"

This is very useful for systems programming.


23. Environment-Specific Configuration

Cargo also supports configuration outside Cargo.toml.

For example:

1
2
.cargo/
└── config.toml

A project might contain:

1
2
[build]
jobs = 4

Cargo configuration is different from package manifest configuration.

A useful distinction is:

1
2
3
4
5
6
7
Cargo.toml
    |
    +-- describes the package

.cargo/config.toml
    |
    +-- configures Cargo's behavior

Cargo searches for configuration files hierarchically from the current directory and its parents, up to the user’s Cargo home directory.


24. Cargo Aliases

You can define command aliases in .cargo/config.toml:

1
2
3
4
[alias]
c = "check"
b = "build"
t = "test"

Then:

1
cargo c

means:

1
cargo check

and:

1
cargo t

means:

1
cargo test

This can be useful for large projects with frequently used command combinations.


25. Cargo Profiles

Cargo profiles control compiler settings.

The most common profiles are:

1
2
3
4
dev
release
test
bench

You can configure release builds:

1
2
3
4
[profile.release]
opt-level = 3
lto = true
codegen-units = 1

Then:

1
cargo build --release

uses these settings.


26. Debug vs Release

Normal:

1
cargo build

uses the development profile.

Release:

1
cargo build --release

uses the release profile.

Conceptually:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
cargo build
     |
     v
dev profile
     |
     +-- faster compilation
     +-- debugging information
     +-- less optimization


cargo build --release
     |
     v
release profile
     |
     +-- more optimization
     +-- slower compilation
     +-- optimized binary

27. Important Release Options

A common release configuration is:

1
2
3
4
5
[profile.release]
opt-level = 3
lto = true
codegen-units = 1
strip = true

opt-level

Controls optimization.

Common values include:

1
2
3
4
5
6
0
1
2
3
"s"
"z"

For example:

1
opt-level = 3

means aggressive optimization.


28. Link-Time Optimization

You can enable LTO:

1
2
[profile.release]
lto = true

LTO stands for:

Link-Time Optimization

It allows optimization across crate boundaries during linking.

This can improve runtime performance or binary size, at the cost of longer build times.


29. codegen-units

You can configure:

1
2
[profile.release]
codegen-units = 1

Fewer codegen units can allow better optimization but can increase compilation time.

A typical performance-oriented configuration might therefore be:

1
2
3
4
[profile.release]
opt-level = 3
lto = true
codegen-units = 1

30. Cargo.lock

Now we reach one of the most important Cargo concepts.

Cargo.toml specifies requirements.

Cargo.lock records the resolved dependency graph.

For example:

1
2
3
[dependencies]
tokio = "1"
serde = "1"

Cargo might resolve:

1
2
tokio 1.48.0
serde 1.0.228

and record the exact versions and source information in:

1
Cargo.lock

31. Why Cargo.lock Exists

Imagine your project says:

1
serde = "1"

Today:

1
serde 1.0.220

is selected.

Six months later:

1
serde 1.0.230

is available.

Without a lockfile, different dependency resolution could potentially result in different versions.

With:

1
Cargo.lock

Cargo can reproduce the resolved dependency graph.

Think:

1
2
3
4
5
6
7
8
9
10
11
12
Cargo.toml
    |
    | version requirements
    v
Dependency resolver
    |
    v
Cargo.lock
    |
    | exact resolved graph
    v
Build

32. Should Cargo.lock Be Committed?

For applications and binaries, generally yes.

For libraries, Cargo has historically treated lockfiles differently because downstream users resolve library dependencies as part of their own dependency graph.

A useful rule is:

1
2
3
4
5
6
Application / binary
    -> commit Cargo.lock

Published library
    -> usually don't commit Cargo.lock as part of the library repository
       unless there is a project-specific reason

The exact behavior and packaging rules can differ depending on the package and publishing workflow, so the important point is to understand the distinction between an application’s reproducible dependency set and a library’s role in another project’s dependency graph.


33. Updating Dependencies

You can run:

1
cargo update

This asks Cargo to update dependencies according to the requirements in Cargo.toml.

For example:

1
serde = "1"

may allow Cargo to move from one compatible 1.x release to another.

The lockfile changes accordingly.


34. Updating One Dependency

You can update a particular package:

1
cargo update -p serde

This is useful when you don’t want to update the entire dependency graph.


35. cargo tree

One of the most useful Cargo commands for understanding dependencies is:

1
cargo tree

For example:

1
2
3
4
5
my-app
├── serde
│   └── serde_derive
└── tokio
    └── ...

This shows the dependency graph.

When debugging dependency problems, cargo tree is extremely useful.


36. Duplicate Dependencies

Suppose:

1
2
3
4
5
my-app
├── library-a
│   └── foo 1.0
└── library-b
    └── foo 2.0

Cargo may need to compile two versions:

1
2
foo 1.x
foo 2.x

You can investigate this with:

1
cargo tree

or:

1
cargo tree -d

The -d option is useful for identifying duplicate package versions.


37. cargo metadata

For tools and scripts, another important command is:

1
cargo metadata

For machine-readable output:

1
cargo metadata --format-version 1

It provides information about:

  • packages
  • dependencies
  • workspace members
  • workspace root
  • target directory
  • package IDs

This is useful when building tooling around Cargo projects.


38. cargo check

During development, prefer:

1
cargo check

when you only want to know whether the code compiles.

It usually avoids generating the final executable.

For a workspace:

1
cargo check --workspace

A common development loop is:

1
2
3
cargo check
cargo test
cargo clippy

39. cargo build

Build the project:

1
cargo build

Release build:

1
cargo build --release

Build a particular package:

1
cargo build -p my-crate

Build an entire workspace:

1
cargo build --workspace

40. cargo run

For binary crates:

1
cargo run

Pass arguments to the application after --:

1
cargo run -- --config config.toml

For a workspace:

1
cargo run -p my-cli

41. cargo test

Run tests:

1
cargo test

Run workspace tests:

1
cargo test --workspace

Run a particular test:

1
cargo test test_name

Run tests for one package:

1
cargo test -p my-library

42. cargo clippy

Clippy is Rust’s linting tool.

Run:

1
cargo clippy

For a workspace:

1
cargo clippy --workspace

In CI, you might use:

1
cargo clippy --workspace --all-targets --all-features -- -D warnings

This treats warnings as errors.


43. cargo fmt

Formatting is handled by rustfmt:

1
cargo fmt

Check formatting without modifying files:

1
cargo fmt -- --check

A common CI pipeline is:

1
2
3
4
cargo fmt -- --check
cargo check --workspace
cargo clippy --workspace --all-targets --all-features -- -D warnings
cargo test --workspace

44. cargo add

Instead of manually editing:

1
[dependencies]

you can use:

1
cargo add serde

You can specify a version:

1
cargo add serde@1

Features:

1
cargo add tokio -F rt -F macros

Development dependency:

1
cargo add --dev criterion

Optional dependency:

1
cargo add serde --optional

Git dependency:

1
cargo add some-library --git https://github.com/example/some-library

Path dependency:

1
cargo add my-library --path ../my-library

Cargo’s cargo add command supports these dependency source and feature options.


45. cargo remove

Remove a dependency:

1
cargo remove serde

Development dependency:

1
cargo remove --dev criterion

46. cargo clean

Remove build artifacts:

1
cargo clean

This removes the generated build output, typically under:

1
target/

It does not mean:

1
delete Cargo.lock

Cargo’s dependency resolution information is separate from build artifacts.


47. target/

Cargo normally puts build artifacts into:

1
target/

For example:

1
2
3
4
5
6
7
8
9
target/
├── debug/
│   ├── my-app
│   ├── deps/
│   └── incremental/
│
└── release/
    ├── my-app
    └── deps/

You generally do not commit target/ to Git.

A typical .gitignore contains:

/target

48. Cargo Workspace

A workspace is a collection of packages managed together.

For example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
project/
├── Cargo.toml
├── Cargo.lock
│
├── core/
│   ├── Cargo.toml
│   └── src/
│
├── network/
│   ├── Cargo.toml
│   └── src/
│
└── cli/
    ├── Cargo.toml
    └── src/

Root Cargo.toml:

1
2
3
4
5
6
7
8
[workspace]
resolver = "3"

members = [
    "core",
    "network",
    "cli",
]

Cargo workspaces provide:

  • shared Cargo.lock
  • shared target
  • workspace-wide commands
  • shared dependency declarations
  • shared package metadata
  • workspace-level profiles

49. Workspace Dependencies

A workspace can centralize dependency versions:

1
2
3
4
5
[workspace.dependencies]
serde = "1"
tokio = "1"
anyhow = "1"
thiserror = "2"

A member can inherit them:

1
2
3
[dependencies]
serde = { workspace = true }
tokio = { workspace = true }

This avoids repeating versions across many crates.

Important:

1
2
[workspace.dependencies]
serde = "1"

does not automatically add serde to every crate.

The member still needs:

1
serde = { workspace = true }

50. Workspace Package Metadata

The workspace can also centralize package metadata:

1
2
3
4
5
[workspace.package]
version = "0.1.0"
edition = "2024"
license = "MIT"
repository = "https://github.com/example/project"

A member can inherit:

1
2
3
4
5
6
7
[package]
name = "core"

version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true

This becomes very useful in large repositories.


51. Virtual Workspace

A workspace does not necessarily need a root package.

For example:

1
2
3
4
5
6
7
8
[workspace]
resolver = "3"

members = [
    "crates/core",
    "crates/network",
    "crates/cli",
]

This is called a virtual workspace.

The root is simply the workspace.


52. Cargo Workspace Commands

Build everything:

1
cargo build --workspace

Check everything:

1
cargo check --workspace

Test everything:

1
cargo test --workspace

Run Clippy:

1
cargo clippy --workspace

Build one package:

1
cargo build -p core

Run one binary:

1
cargo run -p cli

53. Build Scripts

Cargo supports build scripts through:

1
build.rs

Example:

1
2
3
4
5
my-project/
├── Cargo.toml
├── build.rs
└── src/
    └── main.rs

Cargo runs the build script before compiling the package.

A build script can be useful for:

  • compiling C/C++ code
  • generating Rust source
  • finding system libraries
  • generating bindings
  • communicating build-time information to rustc

For example:

1
2
[build-dependencies]
cc = "1"

and:

1
2
3
4
5
fn main() {
    cc::Build::new()
        .file("src/native.c")
        .compile("native");
}

This is particularly relevant in systems programming.


54. Cargo and Native Libraries

When working with Rust systems software, you may encounter:

1
2
3
4
5
6
build.rs
cc
bindgen
pkg-config
system libraries
linker configuration

Cargo can coordinate these build steps.

For example:

1
2
3
4
5
6
7
8
9
10
11
Cargo
 |
 +-- build.rs
 |      |
 |      +-- find libfoo
 |      +-- generate bindings
 |      +-- compile C code
 |
 +-- rustc
 |
 +-- linker

This is one reason Cargo is more than a simple package downloader.


55. Cargo Registries

The default public Rust package registry is:

1
crates.io

You can declare:

1
2
[dependencies]
serde = "1"

and Cargo normally obtains the package from the configured registry.

Cargo also supports alternative registries, which are useful for organizations with private packages.

This is especially relevant for companies with internal Rust infrastructure.


56. Publishing a Crate

Before publishing, you can inspect the package:

1
cargo package

You can see what files would be included:

1
cargo package --list

Then publish:

1
cargo publish

Publishing creates a distributable .crate archive and uploads it to the configured registry, normally crates.io.


57. include and exclude

Cargo lets you control which files are included in a published package.

For example:

1
2
3
4
5
6
7
8
[package]
name = "my-library"
version = "0.1.0"
include = [
    "src/**",
    "Cargo.toml",
    "README.md",
]

You can also use:

1
2
3
exclude = [
    "tests/data/**",
]

This matters when publishing libraries.


58. Cargo Configuration vs Cargo.toml

This distinction is easy to miss.

Cargo.toml

Describes the package:

1
2
3
4
5
6
package
dependencies
features
targets
workspace
profiles

.cargo/config.toml

Configures Cargo itself:

1
2
3
4
5
6
7
build settings
target
linker
aliases
registries
network settings
rustflags

Think:

1
2
3
4
5
6
7
Cargo.toml
    =
"What is this project?"

.cargo/config.toml
    =
"How should Cargo operate?"

59. Cross Compilation

Cargo can build for another target:

1
cargo build --target x86_64-unknown-linux-gnu

or:

1
cargo build --target aarch64-unknown-linux-gnu

The target can also be configured:

1
2
[build]
target = "aarch64-unknown-linux-gnu"

in .cargo/config.toml.

For systems programming, understanding Cargo’s target configuration becomes very important.


60. Cargo and rustc

Cargo is not the Rust compiler.

The compiler is:

1
rustc

Cargo orchestrates the compilation process.

Conceptually:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Cargo
 |
 +-- reads Cargo.toml
 |
 +-- resolves dependencies
 |
 +-- reads Cargo.lock
 |
 +-- prepares build
 |
 +-- invokes rustc
 |
 +-- invokes rustdoc when needed
 |
 +-- invokes linker
 |
 +-- produces artifacts

This distinction is important.

You can invoke:

1
rustc main.rs

without Cargo.

But Cargo gives you the complete package/build/dependency ecosystem.


61. Cargo as a Build Graph Manager

A useful way to think about Cargo is as a build graph manager.

Suppose:

1
2
3
4
5
6
7
8
9
my-app
 |
 +-- library-a
 |      |
 |      +-- serde
 |
 +-- library-b
        |
        +-- tokio

Cargo determines:

1
2
3
4
5
6
what must be compiled
what depends on what
which versions are required
which features are enabled
which build scripts must run
which artifacts can be reused

Then it executes the required build steps.


62. Dependency Resolution

Suppose:

1
2
3
4
5
6
7
8
9
my-app
 |
 +-- library-a
 |      |
 |      +-- foo ^1.0
 |
 +-- library-b
        |
        +-- foo ^1.2

Cargo tries to find a compatible dependency graph.

It might resolve:

1
foo 1.5

for both.

But if constraints are incompatible:

1
2
library-a -> foo 1.x
library-b -> foo 2.x

Cargo may need:

1
2
foo 1.x
foo 2.x

in the same dependency graph.

This is why cargo tree is so useful.


63. Reproducible Builds

For applications, a committed:

1
Cargo.lock

combined with:

1
cargo build --locked

can be useful in CI.

--locked tells Cargo that it must not change the lockfile.

If the lockfile is missing or dependency resolution would change it, Cargo fails.

This is useful when you want CI to build using exactly the dependency resolution committed to the repository.


64. --offline

You can tell Cargo not to access the network:

1
cargo build --offline

Cargo then tries to use locally available dependency information and cached packages.

This can be useful for:

  • offline development
  • restricted build environments
  • reproducibility testing
  • isolated CI/build systems

But it can fail if the required dependencies are not already available locally.


65. --frozen

You can use:

1
cargo build --frozen

This is effectively:

1
2
3
--locked
+
--offline

So Cargo must:

1
2
3
not modify Cargo.lock
+
not access the network

This is useful in controlled build environments.


66. Important CI Commands

A Rust project’s CI might contain:

1
2
3
4
5
cargo fmt -- --check
cargo check --workspace
cargo clippy --workspace --all-targets --all-features -- -D warnings
cargo test --workspace
cargo build --workspace --release

For stricter reproducibility:

1
cargo test --workspace --locked

or:

1
cargo build --workspace --locked

67. Useful Cargo Command Categories

Instead of memorizing every command, group them mentally.

Development

1
2
3
4
5
6
cargo check
cargo build
cargo run
cargo test
cargo fmt
cargo clippy

Dependencies

1
2
3
4
5
cargo add
cargo remove
cargo update
cargo tree
cargo metadata

Workspace

1
2
3
4
cargo build --workspace
cargo test --workspace
cargo check --workspace
cargo build -p package

Packaging

1
2
cargo package
cargo publish

Build management

1
2
3
cargo clean
cargo build --release
cargo build --target ...

68. A Complete Cargo.toml Example

Here is an example combining many of the concepts:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
[package]
name = "my-vmm"
version = "0.1.0"
edition = "2024"
rust-version = "1.85"
license = "Apache-2.0"
description = "A small experimental VMM"

[dependencies]
anyhow = "1"
thiserror = "2"

tokio = {
    version = "1",
    optional = true,
    features = ["rt", "macros"]
}

serde = {
    version = "1",
    optional = true,
    features = ["derive"]
}

[dev-dependencies]
pretty_assertions = "1"

[build-dependencies]
cc = "1"

[features]
default = ["std"]

std = []

async = [
    "dep:tokio",
]

json = [
    "dep:serde",
]

[target.'cfg(unix)'.dependencies]
libc = "0.2"

[profile.release]
opt-level = 3
lto = true
codegen-units = 1

This single manifest demonstrates:

1
2
3
4
5
6
7
[package]
[dependencies]
[dev-dependencies]
[build-dependencies]
[features]
[target.*.dependencies]
[profile.release]

69. Complete Project Structure

A larger project might look like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
my-vmm/
├── Cargo.toml
├── Cargo.lock
├── README.md
├── LICENSE
├── .gitignore
│
├── .cargo/
│   └── config.toml
│
├── crates/
│   ├── vmm-core/
│   │   ├── Cargo.toml
│   │   └── src/
│   │
│   ├── vmm-memory/
│   │   ├── Cargo.toml
│   │   └── src/
│   │
│   ├── vmm-device/
│   │   ├── Cargo.toml
│   │   └── src/
│   │
│   └── vmm-net/
│       ├── Cargo.toml
│       └── src/
│
├── tools/
│   └── cli/
│       ├── Cargo.toml
│       └── src/
│
└── target/

The important files have different responsibilities:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Cargo.toml
    ↓
project/package/workspace definition

Cargo.lock
    ↓
resolved dependency graph

.cargo/config.toml
    ↓
Cargo configuration

build.rs
    ↓
build-time code

target/
    ↓
generated build artifacts

70. The Most Important Mental Model

When working with Cargo, think in layers:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
                         Cargo
                           |
            +--------------+--------------+
            |              |              |
         Manifest       Lockfile       Config
       Cargo.toml      Cargo.lock    .cargo/config.toml
            |              |              |
            v              v              v
      desired setup    exact graph    Cargo behavior
            |
            v
       Dependency
         graph
            |
            v
       Feature graph
            |
            v
        Build graph
            |
            v
          rustc
            |
            v
       linker / artifacts
            |
            v
         target/

This mental model makes Cargo much easier to understand.


71. Cargo.toml vs Cargo.lock vs .cargo/config.toml

A concise comparison:

FilePurpose
Cargo.tomlDefines the package and desired dependency/configuration requirements
Cargo.lockRecords the resolved dependency versions and graph
.cargo/config.tomlConfigures Cargo’s behavior
build.rsPerforms package-specific build-time actions
target/Contains generated build artifacts

The distinction between these files is one of the most important things to understand about Cargo.


72. What You Should Usually Commit

A typical Rust application repository contains:

1
2
3
4
5
6
Cargo.toml
Cargo.lock
src/
README.md
LICENSE
.cargo/config.toml   # if project-specific

and normally does not commit:

1
target/

A .gitignore commonly contains:

/target

73. What Cargo Actually Does When You Run cargo build

Suppose you run:

1
cargo build

Conceptually, Cargo performs something like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
1. Find Cargo.toml
        |
        v
2. Read package configuration
        |
        v
3. Read dependency requirements
        |
        v
4. Resolve dependency graph
        |
        v
5. Read/update Cargo.lock
        |
        v
6. Determine enabled features
        |
        v
7. Determine build targets
        |
        v
8. Run build scripts
        |
        v
9. Compile dependencies
        |
        v
10. Compile your crate
        |
        v
11. Link binaries/libraries
        |
        v
12. Store artifacts in target/

This is why Cargo is much more than:

1
"download some libraries"

It is coordinating the entire Rust build process.


74. Practical Cargo Workflow

For everyday development, a useful workflow is:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# Create project
cargo new my-project

# Enter project
cd my-project

# Check code quickly
cargo check

# Build
cargo build

# Run
cargo run

# Test
cargo test

# Format
cargo fmt

# Lint
cargo clippy

# Inspect dependencies
cargo tree

# Add dependency
cargo add serde

# Update dependencies
cargo update

# Release build
cargo build --release

For CI:

1
2
3
4
5
cargo fmt -- --check
cargo check --workspace --locked
cargo clippy --workspace --all-targets --all-features -- -D warnings
cargo test --workspace --locked
cargo build --workspace --release --locked

75. Key Takeaways

Cargo is Rust’s package manager and build system.

The most important files are:

1
2
3
Cargo.toml
Cargo.lock
.cargo/config.toml

Remember their roles:

1
2
3
4
5
6
7
8
Cargo.toml
    "What does my project require?"

Cargo.lock
    "What exact dependency graph was resolved?"

.cargo/config.toml
    "How should Cargo behave?"

The most important sections of Cargo.toml are:

1
2
3
4
5
6
7
8
9
10
11
12
[package]
[dependencies]
[dev-dependencies]
[build-dependencies]
[features]
[target.'cfg(...)'.dependencies]
[workspace]
[workspace.dependencies]
[workspace.package]
[profile.dev]
[profile.release]
[patch.crates-io]

The most useful commands to know are:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
cargo new
cargo init
cargo check
cargo build
cargo run
cargo test
cargo fmt
cargo clippy
cargo add
cargo remove
cargo update
cargo tree
cargo metadata
cargo clean
cargo package
cargo publish

And for workspaces:

1
2
3
4
5
cargo check --workspace
cargo build --workspace
cargo test --workspace
cargo build -p <package>
cargo test -p <package>

The most important concepts to connect together are:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Cargo.toml
     |
     +-- dependencies
     +-- features
     +-- targets
     +-- workspace
     +-- profiles
            |
            v
     dependency resolver
            |
            v
       Cargo.lock
            |
            v
        build graph
            |
            v
          rustc
            |
            v
         target/

Once you understand this flow, Cargo becomes much easier to reason about, especially when working on large Rust systems projects with multiple crates, optional backends, platform-specific code, and workspace-wide CI.

This post is licensed under CC BY 4.0 by the author.