Getting Started: Programs and the Compiler · about 30 min

rustc and cargo

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]:

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:

commandwhat it does
cargo checktype-check and borrow-check only, no machine code; the fastest feedback
cargo build --releaseoptimized build in target/release/
cargo testcompile the tests and run them (module 12)
cargo doc --openbuild HTML documentation for your crate and its dependencies
cargo fmtreformat the code in the standard style
cargo clippyextra 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

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.

Exercises

Exercise 1 · quiz

The fastest feedback

You are in the middle of a change and only want to know whether the project compiles. Which cargo command type-checks and borrow-checks the code but skips generating machine code, so it finishes fastest?

Type the full command.

Answer

cargo check runs the compiler through parsing, type checking and borrow checking, then stops before code generation and linking, which are the slow parts. Many Rust programmers run it (or let their editor run it) constantly while writing code, and only cargo build or cargo run when they want to execute something.

Exercise 2 · quiz

Where did it go?

In a cargo project whose Cargo.toml says name = "wordcount", you run cargo build --release. Where is the executable?

Answer
  • target/release/wordcount

cargo keeps everything it generates under target/, one directory per profile. A plain cargo build uses the dev profile and writes target/debug/wordcount; --release uses the optimized release profile and writes target/release/wordcount. Because all output lives in target/, the generated .gitignore contains just /target.

Exercise 3 · quiz

Reading a manifest

Which statements about this Cargo.toml are true? Select all that apply.

[package]
name = "notes"
version = "0.3.1"
edition = "2021"

[dependencies]
Answer
  • The package is called notes and is at version 0.3.1

  • The code is compiled with the 2021 edition of the language rules

  • The package has no external dependencies

The edition is not a compiler version. It selects a set of language rules (a few keywords and defaults changed in 2018, 2021 and 2024), and every modern rustc understands all editions. Crates of different editions can depend on each other freely. An empty [dependencies] table means the package uses only the standard library.

Exercise 4 · write code

Which profile am I?

The built-in macro cfg!(debug_assertions) evaluates, at compile time, to true in an unoptimized debug build and false in a release build. The exercises here are built like cargo's dev profile.

Write a program that prints these two lines, using cfg!(debug_assertions) to produce the value on the second line (do not type true yourself):

built by rustc
debug assertions: true

println!("label: {}", value) prints the label with the value in place of {}.

File: main.rs

Starting point

fn main() {
    println!("built by rustc");
}

Example

expected output

built by rustc
debug assertions: true
Hint 1

The second line is println!("debug assertions: {}", ...); with the macro call in place of ....

Reference solution
fn main() {
    println!("built by rustc");
    println!("debug assertions: {}", cfg!(debug_assertions));
}