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

  1. Create a package, run it, then delete target and time the next build.
  2. Introduce a type error, run cargo check, and confirm that the binary in target/debug still runs.
  3. 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.

  1. Where does cargo build put the binary of a debug build?

    Answer

    In target/debug

    The target folder holds every artefact, and the profile names the subfolder.

  2. What does cargo check skip?

    Answer

    Code generation and the binary

    It runs the front end of the compiler and reports errors without writing an executable.

  3. Why does a second cargo build finish without compiling?

    Answer

    Cargo recorded that no input changed

    The fingerprints of the inputs decide whether a package is rebuilt.

  4. 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.

  5. 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.

  6. 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.

  7. 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.

  8. 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.

  9. 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.

  10. What is the first line of the compiler output for this program? fn main() { prnitln!("hello"); }

    Answer

    error: cannot find macro prnitln in this scope

    A 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