Installing with rustup

The recommended way to install Rust is rustup, the official installer and version manager. It downloads a complete toolchain: the compiler rustc, the standard library, the build tool Cargo and, by default, the formatter rustfmt, the linter Clippy and the offline documentation. Toolchains are named after their channel and target, such as stable-aarch64-unknown-linux-gnu: stable Rust for 64-bit ARM Linux. On Linux and macOS the installation is one command, the one the Rust book gives:

curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh

The options make curl refuse anything but HTTPS with TLS (Transport Layer Security) 1.2 or newer, and fail on HTTP errors instead of piping an error page into the shell. On Windows the installer is the program rustup-init.exe from rust-lang.org. rustup installs everything in your home folder: toolchains in ~/.rustup, and in ~/.cargo/bin small programs named rustc, cargo, rustfmt and so on. Those are not the tools themselves but proxies: each one asks rustup which toolchain is active and starts the real program from it. No administrator rights are needed.

For the commands to be found, ~/.cargo/bin must be on the PATH. The installer offers to add a line to your shell profile; in a running shell, load it yourself:

. "$HOME/.cargo/env"     # adds ~/.cargo/bin to PATH if it is missing
rustc --version
cargo --version
rustc 1.99.0 (b940084d7 2026-09-28)
cargo 1.99.0 (5f94df478 2026-08-27)

rustup show tells you what is installed and which toolchain is active. On the 64-bit Linux machine (ARM64) that runs the examples on these pages:

Default host: aarch64-unknown-linux-gnu
rustup home:  /home/amara/.rustup

installed toolchains
--------------------
stable-aarch64-unknown-linux-gnu (active, default)

active toolchain
----------------
name: stable-aarch64-unknown-linux-gnu
active because: it's the default toolchain
installed targets:
  aarch64-unknown-linux-gnu

Distribution packages and the linker

Linux distributions also package rustc and cargo, installed with the package manager. Those versions are tested with the rest of the distribution, but they are often older than the current stable release, and many projects need a recent compiler to build. Debian 13 and Ubuntu 26.04 ship recent Rust releases; older releases may be well behind. For development, rustup is the usual choice; for building a package in a distribution, its own rustc.

Either way, Rust needs a linker to produce an executable, and on Linux it uses the system's C compiler for that (cc, usually GCC). Without one, the first build fails like this:

error: linker `cc` not found
  |
  = note: No such file or directory (os error 2)

error: aborting due to 1 previous error

The fix is to install a C compiler, for example the build-essential package on Debian and Ubuntu.

Managing toolchains and components

rustup can keep several toolchains side by side and switch between them. The commands you need most:

Command What it does
rustup update Installs the newest release of every installed channel; run it every few weeks
rustup show Installed toolchains and the active one
rustup component add rust-docs Adds a component (here the offline documentation) to the active toolchain
rustup toolchain install nightly Installs the nightly channel next to stable (see the release train)
cargo +nightly build Uses another toolchain for one command
rustup default stable Sets the toolchain used everywhere else
rustup doc Opens the offline documentation, including the Book

rustup installs one of three profiles. minimal has only rustc, the standard library and Cargo; default adds the documentation, rustfmt and Clippy; complete adds every component, and the rustup documentation says it should never be used. The test machine for these pages has the minimal profile plus rustfmt and Clippy, so the offline documentation is missing, and rustup says how to fix that:

info: `rust-docs` not installed in toolchain `stable-aarch64-unknown-linux-gnu`
help: run `rustup component add --toolchain stable-aarch64-unknown-linux-gnu rust-docs` to install it
error: unable to view documentation which is not installed

A project can pin the toolchain it needs in a file named rust-toolchain.toml; rustup then switches to that version automatically inside the project folder. That matters for reproducible builds and is covered in The Crate Ecosystem and Supply-Chain Safety.

A first program with rustc

A Rust source file has the extension .rs. The compiler turns it into an executable with the same name, which you start like any other program. This example writes the file, compiles it and runs it:

cat > hello.rs <<'EOF'
fn main() {
    println!("Hello, Kestrel!");
}
EOF
rustc --edition 2024 hello.rs
ls
./hello
echo "exit status: $?"
hello
hello.rs
Hello, Kestrel!
exit status: 0
  • fn main() declares the function where the program starts. It takes no arguments and returns nothing; when it ends normally, the exit status is 0.
  • println! is a macro (the ! says so) that prints its text and a newline. Statements end with a semicolon; indentation is four spaces by convention, not by rule.
  • rustc printed nothing: no news is good news. The result, hello, is a native executable (on Windows, hello.exe) that runs without Rust installed, on machines with the same processor type and operating system (on Linux also a C library, glibc, at least as new as the one it was built against).

Creating a project with Cargo

Cargo is Rust's build tool and package manager in one: it creates projects, downloads dependencies, calls rustc with the right options, runs tests and builds documentation. Where C needs make, a package manager and a test framework, Rust has one command. Cargo works with packages: a folder with a Cargo.toml file that describes it and the source code of one or more crates, usually a program, a library, or both.

cargo new creates a package. Normally it also creates a Git repository and a .gitignore that excludes the build folder; the examples here switch that off with --vcs none:

cargo new --vcs none meter-report
cd meter-report
find . -type f | sort
cat src/main.rs
    Creating binary (application) `meter-report` package
note: see more `Cargo.toml` keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
./Cargo.toml
./src/main.rs
fn main() {
    println!("Hello, world!");
}

"Binary" means the package builds an executable; cargo new --lib creates a library instead, with src/lib.rs (see Functions, Modules and Crates). The source code lives in src/, and the description of the package in Cargo.toml, its manifest:

[package]
name = "meter-report"
version = "0.1.0"
edition = "2024"

[dependencies]

The file is written in TOML (Tom's Obvious, Minimal Language), a simple configuration format of [sections] and key = value lines. name and version identify the package (versions follow semantic versioning), and edition tells Cargo which edition to pass to the compiler. Libraries the program uses go under [dependencies]; for now there are none. Other sections follow later, such as [profile.release] below.

Build, run and check

Three commands cover daily work. cargo build compiles the package, cargo run builds it if needed and then starts it, and cargo check only checks the code:

cargo new -q --vcs none meter-report
cd meter-report
cargo build
cargo build                  # nothing changed: nothing to do
cargo run
cargo check
cat Cargo.lock
   Compiling meter-report v0.1.0 (/home/amara/meter-report)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.25s
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.01s
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.01s
     Running `target/debug/meter-report`
Hello, world!
    Checking meter-report v0.1.0 (/home/amara/meter-report)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.03s
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4

[[package]]
name = "meter-report"
version = "0.1.0"
  • The indented status lines (Compiling, Finished, Running) are Cargo's; they go to standard error, so they do not mix with the program's own output when you redirect it. -q (quiet) hides them.
  • The second cargo build did nothing: Cargo rebuilds only when a source file or the configuration changed.
  • cargo check runs the compiler's checks (types, borrowing, every error and warning) but produces no executable, which makes it faster than a build. It is what you run, or your editor runs for you, while writing code.
  • The first build created Cargo.lock, which records the exact version of every package in the build. With no dependencies it lists only the package itself; with dependencies, it is what makes the next build use the same versions. Commit it to version control; see The Crate Ecosystem and Supply-Chain Safety.
The Cargo build flow. Cargo.toml and src/main.rs go into cargo build. A plain cargo build writes the executable to target/debug using the dev profile; with --release it writes to target/release using the release profile. On the first build, cargo also writes Cargo.lock next to Cargo.toml. Cargo.toml src/main.rs Cargo.lock cargo build target/debug/ dev profile target/release/ release profile --release first build
cargo build reads the manifest and the sources and writes the executable to target/debug, or to target/release with --release. The lock file is written on the first build.

Everything Cargo builds lands in target/, which can grow to hundreds of megabytes in a real project. It is safe to delete (cargo clean does that); it is rebuilt when needed, and it never belongs in version control.

Arguments for the program itself come after --, so Cargo does not read them as its own options. The executable in target/debug can also be started directly:

cargo new -q --vcs none meter-report
cd meter-report
cat > src/main.rs <<'EOF'
fn main() {
    let homes: Vec<String> = std::env::args().skip(1).collect();
    println!("report for {} homes: {}", homes.len(), homes.join(", "));
}
EOF
cargo run -- h0017 h0042
cargo run -q -- h0017
./target/debug/meter-report h0007 h0017 h0042
   Compiling meter-report v0.1.0 (/home/amara/meter-report)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.42s
     Running `target/debug/meter-report h0017 h0042`
report for 2 homes: h0017, h0042
report for 1 homes: h0017
report for 3 homes: h0007, h0017, h0042

Compiler errors in Cargo

When the code has an error, Cargo shows the compiler's full message, with the file path inside the package, and stops with exit status 101:

cargo new -q --vcs none meter-report
cd meter-report
cat > src/main.rs <<'EOF'
fn main() {
    let kwh: u32 = "350";
    println!("{kwh} kWh");
}
EOF
cargo run
   Compiling meter-report v0.1.0 (/home/amara/meter-report)
error[E0308]: mismatched types
 --> src/main.rs:2:20
  |
2 |     let kwh: u32 = "350";
  |              ---   ^^^^^ expected `u32`, found `&str`
  |              |
  |              expected due to this

For more information about this error, try `rustc --explain E0308`.
error: could not compile `meter-report` (bin "meter-report") due to 1 previous error

Read it from the top: the error code and a one-line summary, the location (src/main.rs:2:20: line 2, column 20), the source line with markers under the parts involved, and a hint. Here the variable was declared as a number (u32) but given text (&str, a string). The fix is to write 350, or to convert the text with "350".parse(). The suggested command prints a longer explanation, offline:

rustc --explain E0308 | head -n 1
Expected type did not match the received type.

Debug and release builds

Cargo builds with a profile: a named set of compiler settings. Plain cargo build and cargo run use the dev profile, made for development: fast to compile, unoptimized, with debug information and with checks that cost speed. --release uses the release profile, made for the program you ship: slower to compile, fully optimized, without debug information and without those checks.

Setting dev (default) release (--release)
Output folder target/debug/ target/release/
Optimization (opt-level) 0 (none) 3 (full)
Debug information (debug) yes no
Integer overflow checks on: overflow panics off: overflow wraps around
debug_assert! checks on off
Incremental compilation on off

The overflow line is the one to remember. This program multiplies Kestrel's roughly 400,000 meters by the 8,760 hours in a year, which does not fit in an i32 (at most 2,147,483,647). The value comes from the command line so the compiler cannot compute it in advance:

cargo new -q --vcs none overflow-demo
cd overflow-demo
cat > src/main.rs <<'EOF'
fn main() {
    // One meter more per command-line argument: the compiler cannot
    // know the value in advance.
    let meters: i32 = 400_000 + std::env::args().count() as i32 - 1;
    let hours_per_year = 8_760;
    println!("meter-hours per year: {}", meters * hours_per_year);
}
EOF
cargo run -q
echo "exit status: $?"
cargo run -q --release
echo "exit status: $?"
thread 'main' (48213) panicked at src/main.rs:6:42:
attempt to multiply with overflow
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
exit status: 101
meter-hours per year: -790967296
exit status: 0

The debug build stops with a panic; the release build prints a negative number and reports success. Neither is undefined behavior, as it would be in C: Rust allows only a panic or a two's complement wrap-around, and with overflow checks off, as in a release build, the compiler wraps. But a silently wrong number is worse than a crash, so test with debug builds, and use a type that is big enough (u64 here) or the explicit methods on Variables, Types and Mutability.

Profiles can be changed in Cargo.toml. A program that would rather crash than compute a wrong bill can keep overflow checks in its release build:

cargo new -q --vcs none overflow-demo
cd overflow-demo
cat > src/main.rs <<'EOF'
fn main() {
    let meters: i32 = 400_000 + std::env::args().count() as i32 - 1;
    println!("meter-hours per year: {}", meters * 8_760);
}
EOF
cat >> Cargo.toml <<'EOF'

[profile.release]
overflow-checks = true
EOF
cat Cargo.toml
cargo run -q --release
[package]
name = "overflow-demo"
version = "0.1.0"
edition = "2024"

[dependencies]

[profile.release]
overflow-checks = true

thread 'main' (48213) panicked at src/main.rs:3:42:
attempt to multiply with overflow
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Release builds are also much smaller, because Cargo strips debug information from them by default (since Rust 1.77). The same "Hello, world!" package, built both ways on the test machine:

4.5M target/debug/meter-report
458K target/release/meter-report

More Cargo commands

Cargo has a command for most tasks around the code. Later pages cover them in detail:

Command What it does See
cargo test Builds and runs the tests, including examples in docs Testing, Docs and Tooling
cargo fmt Formats the code in the standard style (rustfmt) Testing, Docs and Tooling
cargo clippy Runs Clippy, which suggests better and more idiomatic code Testing, Docs and Tooling
cargo doc Builds HTML documentation from the code's doc comments Testing, Docs and Tooling
cargo add, cargo update Adds a dependency to Cargo.toml; updates the versions in Cargo.lock The Crate Ecosystem and Supply-Chain Safety
cargo tree Shows the dependency tree The Crate Ecosystem and Supply-Chain Safety
cargo clean Deletes the target/ folder This page

Every command explains itself with --help, for example cargo build --help, and cargo --list lists them all.