Why this matters
rustc compiles one crate, and for learning that is all you need: every exercise in this course is a single main.rs built with rustc. Real Rust projects, though, are almost never built by calling rustc directly. They use cargo, Rust's build tool and package manager, and every Rust project you open will have a Cargo.toml at its root. You should know what cargo does, what it puts where, and how it relates to the plain rustc you use here, so that moving from these exercises to a real project feels like a small step.
rustc: one crate at a time
In Rust, the unit of compilation is the crate. A crate is a tree of source code that rustc compiles in one go into either an executable (a binary crate) or a library (a library crate) that other crates can use. Your main.rs with a fn main is a binary crate.
Calling rustc yourself means choosing the options yourself:
$ rustc --edition 2021 -g -o prog main.rs # debug build, as in the exercises
$ rustc --edition 2021 -O -o prog main.rs # optimized build
That works for one file and no dependencies. Once a project has several crates, external libraries, tests and different build settings, typing rustc commands by hand becomes impractical. That is the job cargo takes on.
cargo: the project tool
cargo creates projects, works out what needs compiling, calls rustc with the right options, downloads and builds dependencies, runs tests, and builds documentation. Creating a project:
$ cargo new hello
Creating binary (application) `hello` package
$ cd hello
$ tree -a -I .git
.
├── .gitignore
├── Cargo.toml
└── src
└── main.rs
cargo new also runs git init. src/main.rs contains the same hello-world program you wrote in the previous lesson. The interesting file is Cargo.toml, the manifest:
[package]
name = "hello"
version = "0.1.0"
edition = "2024"
[dependencies]
It is written in TOML, a simple key = value format with [sections]:
name: the package name, which is also the name of the executable.version: your package's version, in semantic versioning formMAJOR.MINOR.PATCH.edition: which edition of the language rules to use. Editions (2015, 2018, 2021, 2024) let Rust make small incompatible changes, such as new keywords, without breaking old code: each crate declares its edition, and crates of different editions work together. A new project gets the newest edition. This course uses 2021; nothing you learn here changes in 2024 except a few details we will point out.[dependencies]: external crates from crates.io, written likerand = "0.8". cargo downloads them, with their own dependencies, and records the exact versions it chose in a file calledCargo.lock.
Note: the sandbox has no network, so the exercises use only the standard library (
std), which is large and covers everything this course needs. The standard library ships with the compiler and needs no entry in[dependencies].
Building and running with cargo
$ cargo build
Compiling hello v0.1.0 (/tmp/hello)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.30s
$ ./target/debug/hello
Hello, world!
$ cargo run
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.00s
Running `target/debug/hello`
Hello, world!
cargo run builds if anything changed and then runs the program. The second time, nothing had changed, so the build took no time: cargo tracks which source files each output depends on and recompiles only what is out of date.
Arguments after -- go to your program, not to cargo: cargo run -- input.txt.
The other everyday commands:
| command | what it does |
|---|---|
cargo check | type-check and borrow-check only, no machine code; the fastest feedback |
cargo build --release | optimized build in target/release/ |
cargo test | compile the tests and run them (module 12) |
cargo doc --open | build HTML documentation for your crate and its dependencies |
cargo fmt | reformat the code in the standard style |
cargo clippy | extra lints that catch common mistakes and unidiomatic code |
(cargo fmt and cargo clippy are separate components that may not be installed everywhere.)
Profiles: debug and release
cargo builds with a profile, a named set of compiler options:
dev (cargo build) release (cargo build --release)
optimization none (like rustc -C opt-level=0) full (like rustc -O)
debug info yes (like rustc -g) no
debug assertions on off
integer overflow panics (crashes with a message) wraps around silently
output target/debug/ target/release/
Unoptimized code compiles faster and is easier to step through in a debugger. It is also often ten or more times slower to run. If you ever time a Rust program, time the release build.
Two rows in the table change behavior, not just speed. With debug assertions on, the program runs extra checks; the most visible is that arithmetic overflow (a number too big for its type) stops the program with an error instead of silently wrapping. You will see this in the next module. Your code can ask which kind of build it is part of with cfg!(debug_assertions), a macro the compiler replaces with true or false.
What cargo actually runs
cargo is a driver; the compiling is still done by rustc. Ask cargo to show its work:
$ cargo build -v
Compiling hello v0.1.0 (/tmp/hello)
Running `rustc --crate-name hello --edition=2024 src/main.rs ... --crate-type bin
-C debuginfo=2 ... --out-dir /tmp/hello/target/debug/deps ...`
The line is long, but you can recognize the pieces: the edition, the source file, debug info. That is the relationship to keep in mind: rustc compiles a crate; cargo decides what to compile, with which options, in which order. When this course says "compile with rustc", the same code would sit in src/main.rs of a cargo project and build with cargo run.
Common mistakes
Running cargo outside a project.
error: could not find `Cargo.toml` in `/home/learner` or any parent directory
cargo looks for Cargo.toml in the current directory and its parents. cd into the project.
Expecting the binary next to the source. After cargo build there is no ./hello; it is target/debug/hello. Use cargo run and you do not need to remember.
Benchmarking a debug build. "Rust is slow" reports very often turn out to be measurements of cargo run without --release.
Committing target/. It is large and regenerated on every machine. The .gitignore from cargo new already excludes it. (Do commit Cargo.lock for programs, so that everyone builds with the same dependency versions.)
Recap
- A crate is what rustc compiles at once: a binary or a library.
rustc file.rsis enough for single-file programs like the exercises.- cargo manages projects:
Cargo.tomldescribes the package,src/main.rsis the binary's root,target/holds the output. cargo check,cargo build,cargo run,cargo testand--releasecover daily work.- The dev profile is unoptimized with debug assertions; release is optimized without them.
- The edition picks language rules, not a compiler version.
Going further: in the sandbox shell, cd /tmp && cargo new demo && cd demo && cargo run works without a network. Then try cargo build -v and cargo build --release and compare the sizes of the two executables with ls -l target/*/demo. The Cargo Book (doc.rust-lang.org/cargo) documents every manifest key.