Skip to content
rusthacks.dev
Tooling

Rustup Explained Toolchains Targets and Components

rustup installs and switches Rust toolchains, targets and components. A guide to channels, overrides, rust-toolchain.toml, cross targets, offline use and clean upgrades.

13 min read
Summarize in AI
Flat illustration for the post “Rustup Explained Toolchains Targets and Components”, composed around threads.

Most Rust developers meet rustup once, paste the install command, and never think about it again until something breaks: a project that needs nightly, a CI job that picks up a newer compiler than your laptop, a missing target when cross-compiling, an editor that cannot find rust-analyzer. Each of those problems has a short answer once you hold the right mental model of what rustup is and what it manages. This guide builds that model from commands you can run, in the order of the questions people actually ask.

Every command output below was produced on a real machine with rustup 1.29.3, rustc 1.99.0 stable and a nightly of 1.101.0, on Apple silicon macOS. Paths and versions will differ on yours, but the structure is the same on Linux and Windows.

What rustup actually is

rustup is a toolchain multiplexer. It installs several complete Rust toolchains side by side and decides, every time you run cargo or rustc, which one to run. The cargo and rustc on your PATH are not the real tools. They are tiny proxies:

Text
$ ls -l ~/.cargo/bin/cargo ~/.cargo/bin/rustc
cargo -> rustup
rustc -> rustup

Both are links to the rustup binary itself. When you type cargo build, rustup starts, works out which toolchain applies to the current directory, and runs that toolchain's real cargo. This design is why one machine can build a project on stable and another on nightly with no switching ceremony, and it explains most surprises: if a command behaves differently in two directories, a different toolchain was selected.

The data lives in two directories. ~/.rustup holds toolchains and settings; ~/.cargo holds the proxies and anything installed with cargo install. Both can be relocated with the RUSTUP_HOME and CARGO_HOME environment variables, which is how CI systems and containers keep them on a cached volume.

Toolchains, channels and versions

A toolchain is a compiler, a standard library, Cargo and the tools bundled with them. It has a name made of a channel, an optional date or version, and the host platform:

Text
$ rustup toolchain list
stable-aarch64-apple-darwin (active, default)
nightly-aarch64-apple-darwin

The channels are the release trains. stable is the release most people use, produced every six weeks. beta is the next stable, useful for testing your project against what is about to ship. nightly is built every day and is the only channel that allows unstable features such as #![feature(...)], and tools like Miri and -Z flags. You can also name an exact version, 1.90.0, which never changes and is the right choice for reproducible builds.

Install, update and remove them with three commands:

Shell
rustup toolchain install stable
rustup toolchain install 1.90.0
rustup toolchain uninstall nightly

rustup update refreshes every installed channel, and rustup check reports what is out of date without changing anything. On the machine used here it said:

Text
$ rustup check
stable-aarch64-apple-darwin - update available: 1.99.0 (b940084d7 2026-09-28) -> 1.99.1 (58cf8f9c3 2026-10-13)
nightly-aarch64-apple-darwin - up to date: 1.101.0-nightly (249c1e4e3 2026-11-05)
rustup - up to date : 1.29.3

The last line is the manager updating itself, which rustup self update handles separately from the toolchains.

Toolchains are not small. On this machine the stable toolchain with several extra targets is 1.4 GB on disk and the nightly 538 MB. If a disk fills up on a build machine, rustup toolchain list followed by uninstalling old pinned versions is the first thing to check.

Which toolchain runs: the selection order

Because the proxy chooses a toolchain on every invocation, the order of precedence is worth memorizing. From highest to lowest priority:

  1. A +toolchain argument: cargo +nightly build.
  2. The RUSTUP_TOOLCHAIN environment variable.
  3. A directory override set with rustup override set.
  4. A rust-toolchain.toml file in the current directory or a parent.
  5. The default toolchain from rustup default.

Each rung is demonstrable. With a rust-toolchain.toml that names nightly in the current directory, the bare command uses it, and the two higher-priority mechanisms override it:

Text
$ cat rust-toolchain.toml
[toolchain]
channel = "nightly"

$ rustc --version
rustc 1.101.0-nightly (249c1e4e3 2026-11-05)

$ RUSTUP_TOOLCHAIN=stable rustc --version
rustc 1.99.0 (b940084d7 2026-09-28)

$ rustc +stable --version
rustc 1.99.0 (b940084d7 2026-09-28)

With the file removed, rustc --version goes back to the default, stable 1.99.0. A directory override stores the choice in rustup's own settings instead of in your repository, which is useful when you cannot change the project:

Text
$ rustup override set nightly
info: override toolchain for /private/tmp/rtdemo set to nightly-aarch64-apple-darwin
$ rustc --version
rustc 1.101.0-nightly (249c1e4e3 2026-11-05)
$ rustup override list
/private/tmp/rtdemo                     	nightly-aarch64-apple-darwin
$ rustup override unset
info: override toolchain for '/private/tmp/rtdemo' removed

To see which rule is currently winning, ask:

Shell
rustup show active-toolchain

Its output ends with the reason, for example nightly-aarch64-apple-darwin (overridden by '/private/tmp/rtdemo/rust-toolchain.toml'). When a teammate's build differs from yours, run this in the same directory first.

Pin the toolchain in the repository

The rust-toolchain.toml file is the answer to the phrase "it works on my machine". Committed at the repository root, it makes every developer and every CI job use the same compiler, and it installs what is missing automatically on first use:

rust-toolchain.tomlTOML
[toolchain]
channel = "1.90.0"
components = ["rustfmt", "clippy"]
targets = ["wasm32-unknown-unknown"]
profile = "minimal"

The first cargo command in that directory downloads the toolchain, the two components and the extra target if they are not installed. This is also what happened when the nightly file above was tried: rustup printed syncing channel updates for nightly, noticed clippy and rustfmt were current, and downloaded the standard library for the wasm32 target because the file asked for it. That is convenient, and it means a first build in a new directory can take a minute and need network access, which surprises people in offline CI.

Pin an exact version for applications and services, where reproducibility matters, and bump it deliberately in a pull request. For libraries, test against stable and the minimum version you support instead of pinning, since pinning your own checkout does not constrain your users. A library should declare rust-version in Cargo.toml to state its minimum.

Profiles and components

A toolchain is assembled from components, and a profile is a preset list of them.

ProfileContainsUse when
minimalrustc, rust-std, cargoCI jobs that only build and test
defaultminimal plus rust-docs, rustfmt, clippyA developer machine
completeEverything availableAlmost never

Set the default for future installs with rustup set profile minimal, or pass --profile minimal for one install. Smaller profiles download less and install faster, which matters when a CI job starts from nothing on every run: a minimal install is a fraction of the size, and you add exactly what the job needs:

Shell
rustup toolchain install stable --profile minimal --component clippy --component rustfmt

Components are added and listed per toolchain. On this machine:

Text
$ rustup component list --installed
cargo-aarch64-apple-darwin
clippy-aarch64-apple-darwin
llvm-tools-aarch64-apple-darwin
rust-std-aarch64-apple-darwin
rust-std-wasm32-unknown-unknown
rust-std-wasm32-wasip1
rustc-aarch64-apple-darwin
rustfmt-aarch64-apple-darwin

The ones you will add most often: rust-analyzer for editors, rust-src for tools that need the standard library's source (the editor uses it to jump into std, and -Z build-std needs it), llvm-tools for coverage and PGO work, and miri, which exists only on nightly. Add one with rustup component add rust-analyzer, optionally scoped with --toolchain nightly. The rust-docs component, part of the default profile but not installed on this machine, is what rustup doc opens, and the command says so plainly when it is missing:

Text
info: `rust-docs` not installed in toolchain `stable-aarch64-apple-darwin`
help: run `rustup component add --toolchain stable-aarch64-apple-darwin rust-docs` to install it

Targets: compiling for another platform

A target is a platform you can compile for. The standard library is precompiled per target, which is why adding one is a download and not a build. The installed set here:

Text
$ rustup target list --installed
aarch64-apple-darwin
aarch64-unknown-linux-musl
thumbv7em-none-eabihf
wasm32-unknown-unknown
wasm32-wasip1
x86_64-unknown-linux-gnu
x86_64-unknown-linux-musl

Add one with rustup target add wasm32-unknown-unknown, then build with cargo build --target wasm32-unknown-unknown. That is the whole story for targets that need no external linker, such as WebAssembly and bare-metal ARM. Targets that need a foreign linker and C library, such as building a Linux binary on macOS, also need a cross linker configured in .cargo/config.toml; adding the target gives Cargo the Rust half and not the C half. The error you get from missing it is a linker failure naming cc, and the usual fixes are a cross toolchain, cargo-zigbuild, or building inside a container for the target.

If you see can't find crate for 'core' or can't find crate for 'std' while building for an unusual target, the target is not installed for the toolchain that is running. Check the toolchain with rustup show, then add the target to that one. The mistake is easy to make with +nightly, because targets are installed per toolchain: adding wasm32-unknown-unknown to stable does nothing for nightly.

Running a command with another toolchain

You do not have to change your default to use another toolchain once. cargo +nightly miri test runs one command on nightly. rustup run nightly rustc --version is the explicit spelling, and it works for any program, not just Rust's:

Text
$ cargo +nightly --version
cargo 1.101.0-nightly (3c747ea3e 2026-11-03)
$ rustup run nightly rustc --version
rustc 1.101.0-nightly (249c1e4e3 2026-11-05)
$ rustup which rustc
/Users/you/.rustup/toolchains/stable-aarch64-apple-darwin/bin/rustc

rustup which prints the real binary behind a proxy, which settles questions about which compiler a script is using. Inside a script or CI step, prefer the +toolchain form over exporting RUSTUP_TOOLCHAIN globally, so the intent is visible on the line that needs it.

The minimum supported version is a separate promise

A pinned toolchain says which compiler you build with. The rust-version field in Cargo.toml says the oldest compiler your crate promises to work with, and Cargo enforces it with a clear message. Setting rust-version = "1.120" in a crate and building with 1.99.0 produced:

Text
error: rustc 1.99.0 is not supported by the following package:
  [email protected] requires rustc 1.120

Cargo refuses before compiling anything, which is the right behavior: a user on an old compiler learns the reason in one line and not from a wall of errors about syntax that compiler cannot parse. For a library, keep the two settings distinct. rust-version is your public promise and should trail stable by a margin your users can live with; a CI job that builds with exactly that version is how you keep the promise honest. The toolchain file, if you commit one, is for your own development and should follow stable more closely. Without the CI job, a rust-version of 1.70 is a claim nobody has checked, and the first user on 1.70 finds out.

Custom toolchains, mirrors and offline use

Two features cover the cases where the official servers are not the answer. rustup toolchain link <name> <path> registers a compiler you built yourself, for example a development build of rustc, under a name you choose; after that cargo +mydev build uses it like any other. And when a corporate network blocks static.rust-lang.org, setting RUSTUP_DIST_SERVER and RUSTUP_UPDATE_ROOT to an internal mirror redirects every download. Teams that build in isolated environments combine a mirror with a baked image: install the pinned toolchain once on a machine with network access, copy ~/.rustup into the image, and the first cargo command finds everything present and downloads nothing.

For a quick look at what a toolchain offers beyond what is installed, rustup component list prints every component with (installed) beside the ones you have. The unmarked lines on this machine included rust-analyzer and rust-src, two of the most commonly wanted additions, installable with rustup component add rust-analyzer rust-src. Check the list on the toolchain you plan to use, because availability differs by channel; miri, for instance, is a nightly component.

The six-week cycle and what it means for your pin

Stable moves on a fixed schedule: a new minor version every six weeks, and point releases like the 1.99.1 that rustup check reported above for urgent fixes. Beta is the next stable for six weeks before it ships, and nightly is the version after that. The practical consequences are concrete. A CI job that tracks stable upgrades itself every six weeks, so a new lint in clippy can turn a green pipeline red with no code change; pinning avoids it, and the scheduled beta job warns you three weeks earlier. Editions are separate from compiler versions: edition 2024 code compiles on any compiler new enough to know the edition, and a new compiler continues to compile old editions, so upgrading the toolchain never forces you to migrate. Rust's compatibility promise covers stable features across versions, which is why upgrading the toolchain on a project is usually a non-event and why pinning is about repeatability and not about fear.

Removing what you no longer need

Toolchains accumulate. A tidy-up is three commands: list what you have with rustup toolchain list, remove old pinned versions with rustup toolchain uninstall 1.85.0, and drop targets you no longer cross-compile for with rustup target remove x86_64-unknown-linux-musl. Each target is a few hundred megabytes of precompiled standard library, so the savings are real on a laptop with a small disk. To remove rustup and everything it installed, rustup self uninstall deletes ~/.rustup and ~/.cargo, so run it only when you mean it; anything installed with cargo install lives in ~/.cargo/bin and goes with it.

Using rustup in CI

A reliable CI install is four commands: a minimal profile, the toolchain named by rust-toolchain.toml, the components the job needs, and a cache of ~/.rustup and ~/.cargo keyed on the toolchain file's hash. The steps shown earlier are the whole script, and the details that matter are the ones that save minutes.

Use --profile minimal. A default install pulls documentation that no CI job reads. Pin the version so a new stable release on a Tuesday does not change your build, and bump it in a pull request. Run rustup show at the start of the job; the output records exactly which compiler built the artifact, and it is the first line to check when a build diverges from a developer's. And keep a separate scheduled job that builds with beta or nightly, so you learn about breakage from new compiler releases weeks before they become stable and urgent.

Troubleshooting the common failures

Most rustup problems are one of five, and each has a signature.

A message beginning rustup could not choose a version of cargo to run, because one wasn't specified explicitly, and no default is configured means the proxy found no rule to apply, typically on a fresh container where only the minimal installer ran. Run rustup default stable.

A pinned toolchain that is not installed is normally downloaded on first use. On a machine with no network, or a locked-down CI runner, that download fails, and the fix is to install the pinned toolchain in an earlier step that has network access, or bake it into the image.

A message saying a component is unavailable for download for channel nightly happens when a given nightly lacks a component, which occasionally occurs for a day or two. Move the pin back a date or use the previous nightly.

command not found: cargo after install means ~/.cargo/bin is not on PATH in that shell. The installer edits your shell profile, and a new shell picks it up.

A tool that works in your terminal and fails in your editor usually has a different PATH, since graphical applications launched from a dock do not read your shell profile. Start the editor from a terminal to confirm, then fix the environment the editor inherits.

When rustup is the wrong tool

On Linux distributions that package Rust, a system-installed compiler is fine for building the distribution's own software, and mixing it with rustup on the same PATH confuses everyone; pick one per machine. In a container image, the official rust:1.90 image already contains a pinned toolchain, and installing rustup on top adds nothing. For hermetic builds under Bazel or Nix, the build system supplies its own toolchain and ignores rustup completely. For the day-to-day case of one developer and one project, the default install plus a committed rust-toolchain.toml is all you need. For what to do with the tools the toolchain contains, see Cargo release profile settings and continuous integration for Rust.

If you remember one thing from this guide, make it the selection order: a +toolchain argument beats the environment variable, which beats a directory override, which beats rust-toolchain.toml, which beats the default. Every "why is it using that compiler" question is answered by walking down that list, and rustup show active-toolchain tells you where it stopped.

Also inRust
Taggedrustup
Share

Keep reading

Get new posts by email

We send one email when a new post is published. No spam.

Unsubscribe in one click. No spam, ever.