cargo build, run and check
rustc compiles one file, and cargo compiles a package: it reads the manifest, finds the source files, passes the right flags and puts the result in a folder named target. The three commands of this lesson are the loop of every Rust project.
Coming from C++? Build tools puts a Makefile and a Cargo manifest next to each other, builds the same program with both, and shows what cargo decides without being asked.
There is one package in the lesson, built, run, checked, changed and broken. Every output below is the real output of this machine; the times differ on every run and are the least interesting part of the lines.
The first build
cargo new writes the manifest and one source file. The manifest names the package and its edition, and the source is a main that prints one line.
cargo build compiles the package and the standard library pieces it needs, then writes the binary to target/debug/hello, named after the package.
cargo new --vcs none hello
cd hello
cat Cargo.toml
cargo build
[package]
name = "hello"
version = "0.1.0"
edition = "2024"
[dependencies]
Creating binary (application) `hello` package
note: see more `Cargo.toml` keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
Compiling hello v0.1.0 (/tmp/rw7/L079/hello)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.76s
The two words to read are Compiling and Finished. The profile is dev, which means no optimisation and debug information, and the folder is target/debug. The first program lesson builds the same file with rustc directly.
fn main() {
println!("Hello, world!");
}
ls target/debug
build
deps
examples
hello
hello.d
incremental
The binary is hello, the file hello.d lists the source files it was built from, and deps holds the compiled dependencies. Cargo owns the whole folder, and deleting it costs one rebuild.
cargo run
cargo run builds when the source changed and then starts the binary, so it is the command of the edit loop. The last line of its output is the program's own, and the exit status of cargo is the exit status of the program.
cargo run
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.02s
Running `target/debug/hello`
Hello, world!
Nothing was compiled because nothing changed, which is why the line is Finished without a Compiling in front of it. Arguments after -- go to the program and are what a later lesson on the command line reads.
cargo check
cargo check runs the compiler without code generation, so it reports every error and warning of a build and writes no binary. In a large project it is several times faster, which is why it is bound to a key in most editors.
cargo check
Checking hello v0.1.0 (/tmp/rw7/L079/hello)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.38s
The verb is Checking, and the binary in target/debug is not updated. A check after a successful build still runs, because the metadata it produces is a different artefact from the binary. The editor lesson puts the three commands into the loop.
What a rebuild repeats
Cargo compares the timestamps and the flags of every input with what it recorded in the last build. A changed source file recompiles the package and nothing else; an unchanged tree prints Finished at once.
The program now calls a function instead of printing directly, and the run after the change shows one compilation.
fn greet() {
println!("hello");
}
fn main() {
greet()
}
touch src/main.rs
cargo build
cargo build
Compiling hello v0.1.0 (/tmp/rw7/L079/hello)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.56s
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.01s
The second command compiled nothing, and the first one compiled the package because touch changed the timestamp. That check is the reason a build after a git checkout can be slower than expected: every file it touched looks new.
A build that fails
A failed build writes no binary and leaves the previous one in place, so an old executable can still run after a broken edit. The output is the compiler's own, with the file path relative to the package root, and cargo adds a final line naming the crate that failed.
The first program assigns a string to an integer. The second calls a macro whose name is misspelled, and the compiler offers the name it thinks was meant.
fn main() {
let total: i32 = "seven";
println!("{total}");
}
cargo build
Compiling hello v0.1.0 (/tmp/rw7/L079/hello)
error[E0308]: mismatched types
--> src/main.rs:2:22
|
2 | let total: i32 = "seven";
| --- ^^^^^^^ expected `i32`, found `&str`
| |
| expected due to this
For more information about this error, try `rustc --explain E0308`.
error: could not compile `hello` (bin "hello") due to 1 previous error
fn main() {
prnitln!("hello");
}
cargo build
Compiling hello v0.1.0 (/tmp/rw7/L079/hello)
error: cannot find macro `prnitln` in this scope
--> src/main.rs:2:5
|
2 | prnitln!("hello");
| ^^^^^^^
|
--> /rustc/31fca3adb283cc9dfd56b49cdee9a96eb9c96ffd/library/std/src/macros.rs:141:0
|
= note: similarly named macro `println` defined here
help: a macro with a similar name exists
|
2 | println!("hello");
| ~~~~~~~
error: could not compile `hello` (bin "hello") due to 1 previous error
The final line is the summary a build server greps for, and the exit status is 101, which is how a pipeline knows the build failed without parsing the text. The next lesson adds a dependency to the same manifest, and debug and release changes the profile the last line names.
Exercises
- Create a package, run it, then delete target and time the next build.
- Introduce a type error, run cargo check, and confirm that the binary in target/debug still runs.
- Change a comment in the source and watch whether cargo recompiles the package.
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
Where does
cargo buildput the binary of a debug build?Answer
In target/debug
The target folder holds every artefact, and the profile names the subfolder.
-
What does
cargo checkskip?Answer
Code generation and the binary
It runs the front end of the compiler and reports errors without writing an executable.
-
Why does a second
cargo buildfinish without compiling?Answer
Cargo recorded that no input changed
The fingerprints of the inputs decide whether a package is rebuilt.
-
What happens to the binary when a build fails?
Answer
The previous binary stays in place
Cargo writes the new binary only after a successful build, so the old one can still run.
-
What does this program print?
fn greet(name: &str) -> String { format!("hello {name}") } fn main() { println!("{}", greet("rust")); }Answer
hello rust
The argument is formatted into the placeholder and the string is printed.
-
What does this program print?
fn total(a: i32, b: i32) -> i32 { a + b } fn main() { let value = total(40, 2); println!("{value}"); }Answer
42
The function returns the sum and main prints it.
-
What does this program print?
fn main() { let message = String::from("build"); let length = message.len(); println!("{message} {length}"); }Answer
build 5
The five bytes of the word are counted, and the string is still usable after the borrow.
-
What does this program print?
fn main() { let values = [1, 2, 3]; let sum: i32 = values.iter().sum(); println!("{sum}"); }Answer
6
The iterator walks the three elements and sums them.
-
What is the first line of the compiler output for this program?
fn main() { let total: i32 = "seven"; println!("{total}"); }Answer
error[E0308]: mismatched types
A string literal is not an integer, and the compiler names the two types.
-
What is the first line of the compiler output for this program?
fn main() { prnitln!("hello"); }Answer
error: cannot find macro
prnitlnin this scopeA macro call with an unknown name is reported without an error code, with a suggestion.
The code on this page was built with rustc 1.96.1 and cargo 1.96.1 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container.
Article author: Arthur Isaev