Building Rust Tier-3 on stable

📅 2024-03-08⏳ 5 min read

If you’re developing for Tier 3 targets in Rust, you’re most likely using the nightly compiler. For me, this was not ideal, so here’s a sinful guide on how to switch to the stable version of the compiler.

TL;DR: set RUSTC_BOOTSTRAP=1 env variable. But if you want to know why, then tag along :)

🔗Targets? Tiers? Nightly?

If you’re not familiar with Rust and the short summary above confuses you, let me explain.

The official recomended way of installing Rust is via rustup. The majority of users will ever need to run rustup install stable and once in 6 weeks do rustup update. But here we’ll dive into more niche use cases.

🔗Versions and Channels

When installing Rust with rustup, first we need to pick a toolchain version. You can either choose a fixed one (e.g. 1.76, 1.76.1) or use a channel: stable, beta or nightly.

Additionaly, a channel can be pinned with a particular date, which is especially handy on nightly: nightly-2024-03-08 will installed a fixed unstable verstion and will not update it. Otherwise, you get a new version every day and will have to rebuild all your projects, which use it.

The nightly version is a bit special, since it can use “nightly” features. In early days of Rust that was a huge problem, because too many nice things were feature-gated, so nightly was very tempting. Many crates required it, and if you have any of those in your dependencies, you had to switch to the dark side. Luckily, the dawn has come, and now I haven’t seen this problem for many years already (except in the topic of this post ofc).

🔗Toolchains

Rust compiler usually doesn’t come alone. There’re few notable companions:

  • cargo - the package manager
  • clippy - extra linter
  • rust-analyzer - language server
  • rust-docs - documentation
  • rust-src - compiler sources to make “go to definition” work
  • rustfmt - code formatter

A compiler and these additional components form a toolchain, since they are versioned together. Newer compeler will require newer components, so it makes sense to treat them as a bundle.

Some of these compenents are installed automatically, some can be added with rustup component add.

Once added, they will be installed for every toolchain that you have.

🔗Targets and Platforms

Rust compiler can target many different platforms or targets (the official sources seem to use both terms interchangeably). The full list of what your compiler supports can be found via rustc --print target-list (gives me 227 entries!).

A target name (e.g. x86_64-unknown-linux-gnu) is called “target triple”. Historically it had three fields (thus the name), though more field were added over time (thus confusion). There are1:

  1. architecture (and sometimes a subarchitecture) aarch64, i686, x86_64
  2. vendor (whatever that means) pc, apple, unknown
  3. operating_system (sometimes also the environment) linux, windows, darwin
  4. environment (often omitted for operating systems with a single predominant environment) gnu, msvc, musl
  5. binary_format (rarely used) elf

At least three fields are required, so don’t be surprized when you see wasm64-unknown-unknown. Just ignore the unknown, it should rather be read as any in most cases.

All platforms are divided into three tiers:

  • Tier 1 – “guaranteed to work” (tested against all Rust crates from crates.io)
  • Tier 2 – “guaranteed to build” (built by CI and run tests)
  • Tier 3 – “no guarantees” (has support in the codebase, but not tested regularly)

🔗Putting it all together

A full toolchain version can look something like this:

nightly-2024-03-08-x86_64-unknown-linux-gnu

Hopefully it makes a bit more sense now :)

🔗The Problem

When you install a “stable” Rust toolchain with rustup, you select a target to install. By default, it uses your current one (e.g. x86_64-unknown-linux-gnu), but you are free to install another to cross-compile your program.

Rustup simply downloads and unpacks the compiler binaries for the selected platform. However, for Tier 3 there are no pre-builds, since building is not guaranteed. Bad luck!

The only way is to compile the rust compiler for the target platform with the installed rust compiler. This process is called bootstraping and is very well illustrated here2:

blue=downloaded<br/>yellow=build with stage0 compiler<br/>green=build with stage1 compiler

blue=downloaded
yellow=build with stage0 compiler
green=build with stage1 compiler

And there is one issues: the compiler uses nightly features, so normally we’d have to use the nightly compiler for bootstrapping. This is exactly why all guides on embedded development suggest using it3.

🔗The solution

If you look close enough on the diagram above, you’ll notice that the next version is compiled with the current stable one! This is achieved with a hack: there’s a special environment variable, which allows using nightly features for bootstrapping RUSTC_BOOTSTRAP=1.

We can use it for solving the aforementioned issue and allow compiler to build itself for our target.

Note: setting this variables is required only once for the first build of you project and after Rust updates. I don’t recommend keeping it permanently set.

RUSTC_BOOTSTRAP=1 cargo build

And now you’re can use stable Rust on any target platform 😎