Install Rust on Linux, macOS and Windows

A Rust installation is three things: the compiler rustc, the package manager cargo, and the standard library for the target you build for. There are two routes to them, a package from your distribution or the rustup installer, and the course works with whatever route the machine already took.

The container behind this lesson came from the distribution package: rustc and cargo 1.96.1, built for x86_64-alpine-linux-musl, and no rustup at all. That matters for the commands below, because a rustup installation would add toolchain switching and the clippy and rustfmt components, while this one has neither. Every command on this page was run here, and the pages linked below cover the systems this container does not run on.

What an installation contains

rustc --version --verbose reports more than the number: the release, the commit it was built from, the host triple and the LLVM version that does the actual code generation. The host triple is the important line. It says which operating system and which C library this compiler targets, and therefore which binaries it can produce without extra setup.

The program below asks the same question from inside a compiled binary. The constants are set by the compiler for its target, so this is also the quickest way to see what a machine is actually building.

fn main() { println!("{}-{}", std::env::consts::ARCH, std::env::consts::OS); println!("pointer width: {} bits", std::mem::size_of::<usize>() * 8); }

rustc --version --verbose
cargo --version
which rustc cargo
rustc --edition 2024 platform.rs
./platform

rustc 1.96.1 (31fca3adb 2026-06-26) (Alpine Linux Rust 1.96.1-r0)
binary: rustc
commit-hash: 31fca3adb283cc9dfd56b49cdee9a96eb9c96ffd
commit-date: 2026-06-26
host: x86_64-alpine-linux-musl
release: 1.96.1
LLVM version: 22.1.3
cargo 1.96.1 (356927216 2026-06-26) (Alpine Linux Rust 1.96.1-r0)
/usr/bin/rustc
/usr/bin/cargo
x86_64-linux
pointer width: 64 bits

Two names in /usr/bin, one version line for each tool, and a program that agrees with the host triple. On a machine with rustup the same commands would show ~/.cargo/bin/rustc instead, a shim that picks a toolchain before it runs the real compiler.

Linux

On Alpine the package is rust and the package manager is apk; on Debian and Ubuntu the packages are rustc and cargo. Distribution packages are older than the current release and are not updated between distribution releases, which is a feature for a server and a nuisance for a learner.

The other route is rustup, the installer from rustup.rs. It puts its shims in ~/.cargo/bin, installs the current stable toolchain, and can add components and targets later. This container has no rustup, so this course cannot show its output; the commands it adds are rustup update and rustup component add, and the distribution route needs neither.

Either way, the test is the same: a directory that has never seen a build, one file, and two commands. The binary lands beside the source, and it can be run by path.

fn main() { println!("Hello from a fresh directory"); }

mkdir -p fresh
cd fresh
rustc --edition 2024 hello.rs
./hello

Hello from a fresh directory

If the shell answers command not found instead, the directory that holds rustc is not on PATH. For a distribution package that directory is /usr/bin, which is always on the path; for rustup it is ~/.cargo/bin, and the installer asks you to add it to your shell profile.

Windows and macOS

The course has a page per system, because the details that go wrong are the system's, not Rust's: Windows 11, macOS, Ubuntu, Rocky Linux and FreeBSD.

On Windows the choice that matters is the C toolchain behind the linker: the MSVC build tools, which is the default and needs the Visual Studio Build Tools, or the GNU toolchain from MinGW-w64. A x86_64-pc-windows-msvc target links against the system libraries and produces an ordinary .exe. On macOS the compiler uses the system linker and the Xcode command line tools, which the installer checks for before it starts.

The program below is the same one on all of them. Compile-time predicates such as cfg!(windows) are resolved by rustc, so the source can ask about the target without a run-time check, and the same source gives different answers per system.

fn main() { println!("windows: {}", cfg!(windows)); println!("unix: {}", cfg!(unix)); println!("target_os: {}", std::env::consts::OS); }

rustc --edition 2024 cfg.rs
./cfg

windows: false
unix: true
target_os: linux

A program that prints windows: true was compiled on Windows, and no if at run time decided that. The same mechanism selects whole modules and dependencies, which the lessons on features and no_std use later.

Docker and musl

In a Docker image the distribution package is usually the right choice, because the image already carries the C library the binary will link against. This container is Alpine, so the C library is musl rather than glibc, and the compiler's host triple says so. The Rust in Docker page has the image and the commands.

The difference is visible at compile time. A binary built here links /lib/ld-musl-x86_64.so.1, while the same source built on a glibc system links the glibc loader, and the two binaries are not interchangeable. The program below prints the C library the compiler was built for, which is the fact to check before copying a binary into another image.

fn main() { let env = if cfg!(target_env = "musl") { "musl" } else { "gnu" }; println!("target env: {}", env); println!("debug assertions: {}", cfg!(debug_assertions)); }

rustc --edition 2024 musl.rs
./musl

target env: musl
debug assertions: true

The second line is a reminder that this is a debug build: assertions are on, overflow checks are on, and the binary is larger than the release build the lesson on build variants produces.

Verifying an installation

A compiler that reports a version is not yet a compiler that works. The verification is a program with a few moving parts, built from a clean directory, whose output and exit status are both checked by hand. The one below sums an array, prints the total and ends at the end of main.

The build prints nothing when it succeeds, which is the normal case for rustc. The shell reports the exit status separately with echo $?: zero for a program that finished normally, and the number passed to std::process::exit when it did not, as the lesson on the first program shows.

fn main() { let numbers = [3, 5, 7]; let total: i32 = numbers.iter().sum(); println!("total: {}", total); }

rustc --edition 2024 sum.rs
./sum
echo "status: $?"

total: 15

A warning here instead of silence would also be information: rustc prints warnings to standard error and still writes the binary, and the lesson on warnings and errors reads them.

Exercises

  1. Run rustc --version --verbose on your machine and write down the host triple. Which part of it names the C library?
  2. Compile sum.rs with -o /tmp/sum instead. Where does the binary go, and what does ls sum print afterwards?
  3. On Windows, which target triple would a MinGW-w64 installation use, and how would you ask rustc for the list of targets it knows?

Questions

A new set of questions in a new order every time. Choose one answer for each, then check.

  1. What does cargo add to a build that plain rustc does not do?

    Answer

    It downloads dependencies, keeps a lock file and knows the project layout

    rustc compiles one crate; cargo is the front end that resolves crates, pins versions and calls rustc.

  2. What does rustup install?

    Answer

    The Rust toolchains and their components

    rustup manages toolchains and components such as clippy, rustfmt and extra targets.

  3. What does a target triple such as x86_64-alpine-linux-musl name?

    Answer

    The architecture, the vendor, the operating system and the C library

    The triple is the target the compiler builds for, and the last part names the C library.

  4. What is PATH used for when you type rustc?

    Answer

    It names the directories the shell searches for the command

    A command without a slash is looked up in the directories listed in PATH, in order.

  5. What does this program print? fn main() { println!("windows: {}", cfg!(windows)); println!("unix: {}", cfg!(unix)); }

    Answer

    windows: false / unix: true

    cfg! is resolved at compile time, and a Linux or macOS target is unix and not windows.

  6. What does this program print? fn main() { let numbers = [3, 5, 7]; let total: i32 = numbers.iter().sum(); println!("total: {total}"); }

    Answer

    total: 15

    sum adds the three integers, and the annotation says the result is an i32.

  7. What does this program print on the machine of this course? fn main() { let env = if cfg!(target_env = "musl") { "musl" } else { "gnu" }; println!("target env: {}", env); println!("debug assertions: {}", cfg!(debug_assertions)); }

    Answer

    target env: musl / debug assertions: true

    The container is Alpine with musl, and the build is a debug build, which keeps assertions on.

  8. What does this program print? fn main() { println!("Hello from a fresh directory"); }

    Answer

    Hello from a fresh directory

    The program prints its one literal, with no cargo template involved.

  9. What does rustc --version --verbose add to the short version line?

    Answer

    The commit hash, the host triple and the LLVM version

    The verbose form names the commit, the host triple that decides the target, and the LLVM version.

  10. Which checks does a build made without -O keep?

    Answer

    Debug assertions and integer overflow checks

    Without optimisation rustc keeps the debug profile, and the lesson names both checks when it runs musl.rs.

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