Rust Notes: Cargo
A practical guide to Rust 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_stdsupport- 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:
| File | Purpose |
|---|---|
Cargo.toml | Defines the package and desired dependency/configuration requirements |
Cargo.lock | Records the resolved dependency versions and graph |
.cargo/config.toml | Configures Cargo’s behavior |
build.rs | Performs 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.