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:
- A description of the target: how to call functions, how to link, what the OS is called.
- A standard library that knows how to do files, threads and sockets on SlopOS.
- 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:
| Target | Used for | What makes it different |
|---|---|---|
x86_64-slos | The kernel | No operating system underneath (os: none), no floating-point registers, no std |
x86_64-unknown-slopos | Every program | A 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 ainto the full command that links a SlopOS program (startup code, the dynamic loader, the C library, compiler runtime); - one line in
config.guess, so./configurescripts 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:
| Tool | Why |
|---|---|
clang, clang++, ld.lld, llvm-ar, all from the same LLVM major version | Building the C++ runtime and the LLVM port. The version must be at least the floor in toolchain/cxx/PIN |
cmake, ninja | The same builds |
python3 | Rust's bootstrap (only for just toolchain) |
pkg-config, meson, make, perl, git | The 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.
| Recipe | What it provides |
|---|---|
| zlib | Compression, for most of the below |
| nghttp2 | HTTP/2 for curl |
| Mbed TLS | TLS for curl |
| OpenSSL | TLS for libssh2 and libgit2 |
| curl | libcurl: git's HTTPS transport and cargo's registry client |
| libssh2, libgit2 | Cargo's git dependencies |
| git | git |
| CMake, Ninja | C and C++ builds in the guest |
| bash | Scripts 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 guestjust 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
--pgobuild hasn't yet produced a compiler that passedjust 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
- Bootstrapping the compiler and What bootstrapping does, rustc dev guide. How rustc is built in stages, and what "build", "host" and "target" mean. Start here.
- Platform support and the target tier policy. What Rust ships for each OS. FreeBSD is "tier 2 with host tools" (a downloadable compiler), Redox "tier 2 without host tools" (a prebuilt standard library). SlopOS isn't listed.
- Custom targets
and
-Zbuild-std. The two Rust features that let you build for a target Rust doesn't know. - How to cross-compile LLVM. The LLVM side of building a compiler on one machine for another.
- Application porting, the Redox book. Another Rust OS's recipe system, the model for ours.
- Apache License v2.0 and GPL compatibility, Apache Software Foundation. Why GPL-2.0-only code can't link Apache-2.0 code, the reason behind git's TLS library.
In the source
| Where | What |
|---|---|
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.sh | Third-party programs and the patch and licence checks |
scripts/bootstrap_slopos_toolchain.sh | just toolchain |
scripts/make_vendor.sh, .cargo/vendor.toml | Offline builds |