Installing Rust and Using Cargo
Installing the toolchain with rustup, compiling a first program with rustc, and working with Cargo: new projects, Cargo.toml, build, run and check, and the difference between debug and release builds.
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. -
rustcprinted 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 builddid 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.
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.