Self-hosting

Toolchain

How SlopOS gets a Rust compiler, a C and C++ compiler and tools like git that build for SlopOS and run on it.

With the SlopOS toolchain you can build the kernel and its programs on your Linux machine, and, once you have cross-built it, compile Rust, C and C++ inside SlopOS itself. On the host, just setup prepares everything the normal build needs: a copy of the Rust standard library that knows about SlopOS. With just toolchain (hours of CPU, once) you also get rustc, cargo, clang, git, cmake, ninja, bash and the libraries behind them, compiled to run inside SlopOS, which is what the development machine uses to build SlopOS from inside SlopOS.

None of the techniques here are new. Rust's own build system, bootstrap, already knows how to build a compiler on one OS that runs on another. LLVM and clang are designed to be ported to new operating systems. Other systems have taken the same road: Rust ships ready-made compilers for FreeBSD, and Redox has a target in Rust whose standard library is prebuilt. SlopOS is further back than either: Rust has never heard of it, so everything that teaches Rust, LLVM and C programs about SlopOS lives in this repository as a set of small, pinned patches.

Why SlopOS needs its own target

A compiler produces code for a processor and an operating system, because the way a program opens a file, starts a thread or finds its libraries is different on every OS. Rust names that pair a target: x86_64-unknown-linux-gnu is Linux on a 64-bit Intel or AMD processor. When you cargo build on Linux you are building for that target, with a standard library rustup downloaded ready-made.

Rust has no target for SlopOS, and no ready-made standard library. So SlopOS needs three things that rustup users never think about:

  1. A description of the target: how to call functions, how to link, what the OS is called.
  2. A standard library that knows how to do files, threads and sockets on SlopOS.
  3. For the guest to compile anything, a compiler that itself runs on SlopOS. That is a much bigger job, because the compiler depends on LLVM, which is C++ and expects a C++ standard library and a few dozen other things from the OS.

The two targets

SlopOS has two targets, because the kernel and ordinary programs need very different things from the compiler:

TargetUsed forWhat makes it different
x86_64-slosThe kernelNo operating system underneath (os: none), no floating-point registers, no std
x86_64-unknown-sloposEvery programA Unix-like OS with a C library (slibc), shared libraries and threads

The program target is described twice: as a JSON file the host build reads, and built into the forked compiler (see below), which is what lets a compiler run on SlopOS. A check (just check-rustc-target) keeps the two identical, because a disagreement between them compiles fine and quietly produces programs for a slightly different target.

Because neither target is one Rust ships, cargo builds the standard library from source for each build, with the unstable -Zbuild-std option.

The owned sysroot

A sysroot is the directory a compiler takes its standard library from. With rustup it lives inside the toolchain directory and you never touch it. -Zbuild-std reads the standard library's source from there.

just setup makes a sysroot of SlopOS's own in third_party/rust-slopos and registers it with rustup as the toolchain slopos, so every build runs as cargo +slopos. It is a copy of a pinned nightly (hardlinked, so it costs almost no disk), except for the standard library's sources, which are a real copy with two forks applied: Rust's std and the libc crate, both taught about SlopOS. That is why ordinary crates that use std::fs, std::thread, std::process or std::net compile for SlopOS unchanged.

Nothing is patched in place inside rustup's directories. The forks are kept as patch files, and the nightly version and a checksum of each patch are pinned in toolchain/PIN. A check fails the build if the sysroot on disk stops matching the pin, so a stale copy can't silently build the kernel. Running just setup again is cheap when nothing changed.

Older checkouts patched rustup's rust-src component in place. If that residue is still there, just setup refuses to continue and prints the commands that restore the component.

The compiler

A JSON target file is enough to build programs for SlopOS on Linux. Building a compiler that runs on SlopOS is different: Rust's bootstrap only builds a compiler for targets it has built in, so x86_64-unknown-slopos has to be added to rustc itself. SlopOS carries that as three small patches to rustc:

  • the target itself;
  • telling LLVM's build that SlopOS is a Unix (without that, LLVM's build treats it as an unknown platform and leaves out all the Unix code SlopOS needs);
  • linking the C++ standard library into the compiler statically, so the compiler doesn't depend on one at run time.

A handful of crates that rustc and cargo use (getrandom, errno, rustix, nix, libloading, socket2, stacker) also need to know about SlopOS, and get small patches of their own. Cargo itself needs none.

just rustc-src downloads the pinned nightly's source (a few hundred megabytes) and applies the patches.

LLVM and clang

rustc generates machine code through LLVM, which is written in C++. So a rustc that runs on SlopOS needs LLVM to run on SlopOS too, and since we were porting LLVM anyway, clang (LLVM's C and C++ compiler) comes with it. The port is small, because LLVM has very little OS-specific code:

  • the two places where LLVM picks code by OS and has no default for a new one;
  • teaching LLVM and clang the name SlopOS, so C code sees __slopos__ defined;
  • a clang driver for SlopOS, which turns cc a.o -o a into the full command that links a SlopOS program (startup code, the dynamic loader, the C library, compiler runtime);
  • one line in config.guess, so ./configure scripts recognise a SlopOS machine.

just llvm-src prepares the patched LLVM source (over a gigabyte on disk).

The C++ runtime

LLVM needs a C++ standard library, and SlopOS has one: LLVM's own libc++, built from the same pinned LLVM release. It ships as a single shared library, libc++.so, that also contains the part that handles exceptions. Keeping them in one file matters because a C++ exception is matched by comparing type information, and two copies of that machinery in one process can leave an exception that nobody can catch.

The runtime is built with everything LLVM's sources use (locales, wide characters, <filesystem>, random devices). No program in the shipped system uses C++, so the runtime only goes onto the test image, where probes exercise exceptions across libraries, iostreams and locales. The cross-built compiler links its own copy in statically.

Tools on the host

Building the C++ runtime and the LLVM port is the one place SlopOS needs more on your Linux machine than Rust and QEMU:

ToolWhy
clang, clang++, ld.lld, llvm-ar, all from the same LLVM major versionBuilding the C++ runtime and the LLVM port. The version must be at least the floor in toolchain/cxx/PIN
cmake, ninjaThe same builds
python3Rust's bootstrap (only for just toolchain)
pkg-config, meson, make, perl, gitThe third-party programs below

The LLVM version is a minimum, not an exact match, because distributions ship different versions, and the pinned LLVM sources already decide what the runtime contains. The host compiler only turns them into code. If your default clang is the wrong one, set CLANG, CLANGXX, LD_LLD and LLVM_AR.

Third-party programs

The guest needs more than a compiler. Cargo fetches crates over HTTPS and git dependencies; the build scripts want bash, CMake and Ninja; and you want git. Each of these, and each library behind them, is a recipe: a small file in toolchain/recipes/<name>/ that names an upstream release tarball, its SHA-256 checksum, its licence, how to build it and what it depends on. This is the same shape as Redox's cookbook.

RecipeWhat it provides
zlibCompression, for most of the below
nghttp2HTTP/2 for curl
Mbed TLSTLS for curl
OpenSSLTLS for libssh2 and libgit2
curllibcurl: git's HTTPS transport and cargo's registry client
libssh2, libgit2Cargo's git dependencies
gitgit
CMake, NinjaC and C++ builds in the guest
bashScripts that need bash

just recipes builds them on the host into builddir/slopos-recipes/prefix, and just toolchain builds them too and installs them with the compiler. Tarballs are cached under third_party/recipes/.

A patch may only teach a project about SlopOS

A recipe may carry a patch, but only one that tells the project SlopOS exists. If a program needs a function SlopOS doesn't have, the function goes into the C library or the kernel, once, behaving as POSIX says, so every other program that calls it works too. Porting git, for example, is what added <utime.h>, <grp.h>, mkstemp, freopen and execl to slibc; bash, CMake and Ninja brought ppoll, pselect, getloadavg, getopt_long and ttyname.

A check (scripts/check_recipes.sh) holds patches to that rule mechanically: a patch may only add lines, every change must mention SlopOS, and configure arguments may not contain code. CMake learns about SlopOS the way it knows other systems, from a platform description (toolchain/cmake/Platform) that CMake's own recipe carries, so CMake running in the guest knows the platform too.

Why git doesn't use OpenSSL

Git is licensed GPL-2.0-only, and OpenSSL 3 is Apache-2.0, a licence that GPL version 2 code can't be combined with. So the curl that git uses gets its TLS from Mbed TLS, which may be used under the GPL-2.0-or-later instead, and OpenSSL serves only libgit2 and libssh2, which only cargo loads. The recipe check enforces this on the built files: it follows every library a GPL-2.0-only program loads and fails if one has an incompatible licence. For the same reason slibc, SlopOS's C library, is licensed MIT OR Apache-2.0.

Building the toolchain

just rustc-src     # the patched compiler sources
just test          # any test build: the toolchain reuses its libraries
just toolchain     # the cross-build: hours of CPU
just boot          # installs the result at /usr/local in the guest

just toolchain runs Rust's bootstrap with Linux as the machine doing the build and SlopOS as the machine the compiler will run on. Around that it assembles what the bootstrap doesn't provide: a SlopOS C library and headers to link against, the C++ runtime, the recipes, clang and its support files, cc, c++ and ld.lld links, and every project's licence text. The result goes into builddir/slopos-toolchain/install, and only once the build has finished, so a failed run never leaves a half toolchain that just boot would pick up. Re-running after a patch reuses every compiler stage already built.

It is expensive: hours of CPU, many gigabytes of disk, and a download of the LLVM sources. just check-bootstrap-config is the cheap half: it checks the build plan and a test compile and link without building the compiler.

just distclean removes builddir/, and the cross-built toolchain with it.

Release settings: --pgo

just toolchain --pgo builds the compiler the way the Rust project builds its releases: whole-program link-time optimization and profile-guided optimization, where the compiler is first built with instrumentation, run on a real workload, and then rebuilt using what was measured. Here the workload is this repository's kernel build, gathered by just toolchain-profile on a Linux-hosted twin of the same compiler. The result is a faster compiler at the cost of a much longer build.

It is off by default, and no compiler built this way has passed the toolchain tests in the guest yet.

Building without the network

Every dependency is pinned by Cargo.lock, which is checked in. just vendor copies every crate the build needs into third_party/vendor, including the ones the standard library pulls in, which cargo can't see from the workspace. Builds can then point at that directory instead of crates.io and run fully offline. CI checks that the kernel and programs build this way from an empty cargo cache, and the guest's test root is seeded with the vendored crates and the LLVM tarball, so its builds never read the network.

Adding a dependency therefore means a Cargo.lock change, a just vendor, and an entry in NOTICE.md.

What isn't done

  • The toolchain doesn't rebuild itself inside SlopOS. That would need Python in the guest, tens of gigabytes of disk and hours of CPU, and isn't planned.
  • The --pgo build hasn't yet produced a compiler that passed just test-toolchain.

For contributors

The sysroot comes from scripts/make_slopos_sysroot.sh, and scripts/check_toolchain_pin.sh fails the build if it has drifted: a fork cut against another nightly, a patch edited without updating its checksum, or a leftover from an older layout. Builds use trim-paths, so panic locations don't depend on where the checkout is.

The program target links through cc, using clang's SlopOS driver, and supports unwinding, so build scripts and procedural macros work in the guest. The system's own programs instead link directly with rust-lld and abort on panic; scripts/build_userland.sh passes those flags explicitly. just check-clang-driver holds the driver's link line to the one that script writes.

Until a clang built from the port runs the cross-build, just toolchain uses a wrapper around the host's clang that compiles as SlopOS and links with the SlopOS sysroot. Every SlopOS object in the toolchain links with -z pack-relative-relocs, which slibc's loader supports. libc++ is linked statically into libLLVM, which exports it to clang and the other LLVM programs so they share one copy (libc++ compares error categories by address), and into librustc_driver.

.cargo/vendor.toml redirects crates.io to the vendored directory and is passed with --config rather than living in .cargo/config.toml, so an ordinary host build still uses crates.io. scripts/check_offline_build.sh --pins-only, part of the framekernel gates, checks every locked package against its vendored checksum; just check-offline-build is the CI check.

Further reading

In the source

WhereWhat
toolchain/PIN, toolchain/rust/, toolchain/libc/The pinned nightly and the standard library forks
targets/The two target descriptions
toolchain/compiler/, toolchain/crates/The rustc patches and the crate ports
toolchain/llvm/, toolchain/cxx/The LLVM and clang port; the C++ runtime's pin
toolchain/recipes/, scripts/build_recipes.sh, scripts/check_recipes.shThird-party programs and the patch and licence checks
scripts/bootstrap_slopos_toolchain.shjust toolchain
scripts/make_vendor.sh, .cargo/vendor.tomlOffline builds

On this page