Skip to content

The orchestrator answers the bench's questions on ToyOS, and designs … #2235

The orchestrator answers the bench's questions on ToyOS, and designs …

The orchestrator answers the bench's questions on ToyOS, and designs … #2235

Workflow file for this run

name: toolchain
# A toolchain is a function of the rust fork, the three sysroot trees, the
# linker and this workflow — `toolchain::witness`'s own question, asked of git
# so a shell can ask it too. Nearly every branch changes none of them and this
# workflow does nothing.
#
# Its own workflow because it must never be cancelled: `ci.yml` cancels a
# superseded run by design, and a second push during an hour-long bootstrap
# would throw that hour away.
#
# Runs on pull requests too: `ci.yml` waits up to two hours for the release
# keyed on the four trees it is about to build against, and a branch that
# changed one of them but never got a bare `push` event would wait that out
# and fail. A pull-request checkout is the merge of head and base, the same
# commit `ci.yml` keys on.
on:
push:
branches: [main]
pull_request:
branches: [main]
merge_group:
workflow_dispatch:
concurrency:
group: toolchain-${{ github.ref }}
cancel-in-progress: false
permissions:
contents: write
jobs:
build:
runs-on: ubuntu-24.04
timeout-minutes: 350
env:
# The oldest glibc a consumer of this toolchain needs, which is the
# `ubuntu-24.04` image the hosted lane builds the host half on. The `glibc
# floor` step refuses to publish binaries that name a newer one, so the
# number the release notes carry is measured rather than promised.
GLIBC_FLOOR: "2.39"
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
# **The tag is the content hash of everything the tarball's bytes depend
# on**: the compiler's sources, the three trees that end up in the sysroot,
# the linker that ships beside them, and this file, which is the packaging.
# A key that named less would answer `build=no` for a tree whose tarball
# nobody has built — which is exactly what a linker or a packaging change
# is. Every copy of this expression is held identical by
# `src/ci.rs::every_toolchain_key_names_the_same_trees`; a copy that names
# different trees asks for a tag nothing published.
#
# The price, and it is accepted: a one-line change to `toyos-ld` or to this
# file mints a new release and so costs the whole hour-long bootstrap.
- id: key
run: |
sum=$(git rev-parse HEAD:rust HEAD:toyos-abi/src HEAD:toyos/src \
HEAD:userland/libc/src HEAD:toyos-ld/src \
HEAD:toyos-ld/Cargo.toml \
HEAD:.github/workflows/toolchain.yml \
| sha256sum | cut -c1-16)
echo "tag=toolchain-linux-x86_64-$sum" >> "$GITHUB_OUTPUT"
- id: have
env:
GH_TOKEN: ${{ github.token }}
run: |
if gh release view "${{ steps.key.outputs.tag }}" >/dev/null 2>&1; then
echo "Already published: ${{ steps.key.outputs.tag }}"
echo "build=no" >> "$GITHUB_OUTPUT"
else
echo "build=yes" >> "$GITHUB_OUTPUT"
fi
# This job runs on the bare runner and not in a container, so what it
# needs is the image's own provisioning: a disk to reclaim and packages
# to install.
- name: reclaim runner disk
if: steps.have.outputs.build == 'yes'
run: |
sudo rm -rf /usr/local/lib/android /usr/share/dotnet /opt/ghc \
/usr/local/share/boost /usr/local/.ghcup
df -h /
- name: deps
if: steps.have.outputs.build == 'yes'
run: |
sudo apt-get update -qq
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq \
cmake ninja-build build-essential pkg-config libssl-dev zstd \
qemu-system-x86 ovmf
# Full history, deliberately: bootstrap finds the upstream commit whose
# prebuilt LLVM to download by walking back to the newest bors commit,
# and a shallow clone has none.
#
# **`rust/` is whatever the last job on this machine left there.** A
# self-hosted runner's work area is one directory shared by every job in
# this repository, and `rust/` is a tracked gitlink: `actions/checkout`
# neither populates it nor cleans it — `git clean` does not touch a
# tracked path — so it survives between jobs, and `git submodule update
# --init` refuses a non-empty one with `destination path 'rust' already
# exists and is not an empty directory`. That is not hypothetical and it
# is not a partial clone: `.github/install-toolchain.sh` does `mkdir -p
# rust` and hangs the persistent cache's toolchain entry off
# `rust/build`, in every trusted guest, gate-A and probe job, so any of
# them leaves a `rust/` holding exactly that one symlink and no `.git`.
# Measured on the runner 2026-08-22: one entry, `build ->
# /toyos-cache/toolchains/toolchain-linux-x86_64-a4ad5f0abfe8a8e2`, 8 KB,
# created 04:53:43Z by a guest job — after which every toolchain job on
# the machine failed nine seconds in (run 32549542807, twice), while
# `ci.yml`'s `toolchain-ready` waited 57 minutes for the release it would
# never get. It had only ever worked because a valid checkout from an
# earlier job happened to still be there.
#
# A valid checkout carrying that same `rust/build` link is the second
# shape and the dangerous one: the bootstrap below would write the fork's
# build tree through the symlink into the cached toolchain entry, and
# `publish` would tar it back out. So the link goes and the checkout
# stays. Nothing here follows a link — the cache entry itself is never
# touched — and what was found is printed before anything is removed,
# since the next shape this hits should read as itself and not as
# `destination path already exists`.
#
# `[ -e rust/.git ]` comes before the `rev-parse`, and not after it: git
# searches upwards, so `git -C rust rev-parse --git-dir` inside a bare
# `rust/` answers about *this* repository and would call the poison a
# checkout.
- name: the rust fork
if: steps.have.outputs.build == 'yes'
run: |
t0=$(date +%s)
checkout=no
if [ -e rust/.git ] && git -C rust rev-parse --git-dir >/dev/null 2>&1; then
checkout=yes
fi
if [ "$checkout" = yes ]; then
if [ -L rust/build ]; then
echo "::warning::rust/ is a checkout and rust/build links to $(readlink rust/build)."
echo "::warning::The bootstrap would build through it. Unlinking; the target stays."
rm -f rust/build
fi
elif [ -e rust ] || [ -L rust ]; then
echo "::warning::rust/ is present and is not a checkout git can use, so"
echo "::warning::submodule init would refuse it. Removing it. What was there:"
entries=$({ find rust -mindepth 1 -maxdepth 1 2>/dev/null || true; } | wc -l)
echo " entries: $entries"
ls -la rust || true
du -sh rust 2>/dev/null || true
rm -rf rust
fi
git submodule update --init rust
echo "SUBMODULE-SECONDS: $(( $(date +%s) - t0 ))"
# `GITHUB_ACTIONS=true` puts bootstrap on its own CI path, which assumes
# HEAD is a PR merge commit and takes HEAD^1 as the upstream commit whose
# LLVM to fetch; in this fork HEAD^1 is our own merge of upstream, which
# rust-lang CI never built. Unset, bootstrap walks back to a real bors
# commit as it does on the dev host.
- name: build
id: build
if: steps.have.outputs.build == 'yes'
run: |
set -o pipefail
t0=$(date +%s)
env -u GITHUB_ACTIONS -u CI cargo run -- --build-only 2>&1 | tee /tmp/build.log
echo "BUILD-ONLY-SECONDS: $(( $(date +%s) - t0 ))"
df -h /
du -sh rust/build
# The first compile error and the panic that ended the run, lifted onto
# the run's own front page rather than left for `gh run view --log-failed`
# to find inside an hour-long log.
- name: compile error
if: failure() && steps.build.outcome == 'failure'
run: |
{
echo "### the toolchain did not build"
echo ""
echo '```'
grep -m1 -A5 -E '^error(\[|: could not compile)' /tmp/build.log \
|| echo "(no compile error in the log)"
echo ""
grep -A2 -E "^thread '.*' panicked at" /tmp/build.log | tail -20 \
|| echo "(the build system did not panic)"
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
# Every `GLIBC_x.y` the shipped host binaries name, against what this
# release states. A grep over the dynamic string table and not `objdump`:
# the answer is the same (checked against `readelf --dyn-syms` on the
# published asset) and it adds no tool to the machine's provisioning. It
# can only over-report, so the failure direction is a refused publish.
- name: glibc floor
if: steps.have.outputs.build == 'yes'
run: |
host=x86_64-unknown-linux-gnu
# `-type f`, because `bin/cargo` is a symlink into this machine's own
# rustup and the tarball excludes it for that reason.
{ find "rust/build/$host/stage2/bin" -maxdepth 1 -type f -print0
find "rust/build/$host/stage2/lib" -maxdepth 1 -type f -name '*.so*' -print0
} > /tmp/shipped
need=$(xargs -0 grep -ao 'GLIBC_[0-9][0-9]*\.[0-9][0-9]*' < /tmp/shipped \
| sed 's/.*GLIBC_//' | sort -V | tail -1)
echo "the host half names up to GLIBC_$need; this release states $GLIBC_FLOOR"
if [ "$(printf '%s\n%s\n' "$need" "$GLIBC_FLOOR" | sort -V | tail -1)" != "$GLIBC_FLOOR" ]
then
echo "::error::the host half needs GLIBC_$need and this release states"
echo "::error::$GLIBC_FLOOR, so a consumer on the stated floor cannot run it. It was"
echo "::error::built on a machine with a newer glibc than the floor allows: build the"
echo "::error::host half on the oldest supported one, or move GLIBC_FLOOR deliberately."
exit 1
fi
# **The pin a consumer writes down, and what it gets.** `TOOLCHAIN` goes
# inside the tarball and is the alias release's own asset, so the toolchain
# a tag names can be read without a 401 MiB download. `--sdk-versions` is
# asked of the build system for `publish.yml`'s reason: the set of
# published crates and their order live in `src/sdkversion.rs` alone.
- name: manifest and notes
if: steps.have.outputs.build == 'yes' || github.event_name != 'pull_request'
run: |
tag=${{ steps.key.outputs.tag }}
url=https://github.com/${{ github.repository }}/releases/download/$tag/toyos-toolchain.tar.zst
{
echo "toolchain $tag"
echo "toyos ${{ github.sha }}"
echo "rust $(git rev-parse HEAD:rust)"
echo "host x86_64-unknown-linux-gnu"
echo "glibc $GLIBC_FLOOR"
cargo run --quiet -- --sdk-versions
} > /tmp/TOOLCHAIN
cat /tmp/TOOLCHAIN
{
cat <<EOF
The \`x86_64-unknown-linux-gnu\` toolchain that cross-compiles for
\`x86_64-unknown-toyos\`, from ToyOS \`${{ github.sha }}\`.
## Install
mkdir -p toyos-toolchain
curl -sSL $url | tar --zstd -x -C toyos-toolchain
rustup toolchain link toyos toyos-toolchain/x86_64-unknown-linux-gnu/stage2
ln -s "\$(rustup which cargo)" toyos-toolchain/x86_64-unknown-linux-gnu/stage2/bin/cargo
export PATH="\$PATH:\$PWD/toyos-toolchain/x86_64-unknown-linux-gnu/stage2/bin"
cargo +toyos build --target x86_64-unknown-toyos
The last two commands before the build are not decoration. rustc's ToyOS
target names its linker \`toyos-ld\` and finds it on \`PATH\`; the tarball
carries one in that same \`bin/\`, which goes LAST on \`PATH\`: first, it
shadows rustup's \`cargo\` proxy with the real binary and \`+toyos\` fails.
The \`cargo\` symlink is not shipped because its path would be the
publisher's, and rustup narrates a fallback on every invocation without it.
Leaving \`PATH\` alone: \`CARGO_TARGET_X86_64_UNKNOWN_TOYOS_LINKER=<that bin>/toyos-ld\`.
## glibc
The host binaries name **GLIBC_$GLIBC_FLOOR** at most, so any distribution
with glibc $GLIBC_FLOOR or newer runs them — Ubuntu 24.04, Debian 13,
Fedora 40. A build that needed more is refused rather than published.
## What this is, and the SDK crates that go with it
EOF
sed 's/^/ /' /tmp/TOOLCHAIN
cat <<EOF
The five crate versions are the ones on crates.io this toolchain's std was
built from; \`sdk-<toyos-abi's version>\` is the release tag that names
them. The same lines are the file \`TOOLCHAIN\` inside the tarball.
## raw-window-handle
Until [rust-windowing/raw-window-handle#223](https://github.com/rust-windowing/raw-window-handle/pull/223)
is released, a program that opens a window carries this patch, because the
ToyOS window handle is in no published raw-window-handle yet:
[patch.crates-io]
$(grep '^raw-window-handle = ' userland/Cargo.toml)
EOF
} > /tmp/notes.md
cat /tmp/notes.md
# `lib/rustlib/<host>` and `<host>/stage2/bin/cargo` are symlinks into
# whatever toolchain this runner has, which is not a path any artifact can
# carry. `Owner::Installed` recreates both —
# `toolchain::provision_toolchain_cargo` is the one step that makes the
# second, and a link shipped from here would dangle on the consumer and
# narrate a fallback while looking provisioned.
- name: publish
if: steps.have.outputs.build == 'yes'
env:
GH_TOKEN: ${{ github.token }}
run: |
host=x86_64-unknown-linux-gnu
t0=$(date +%s)
# rustc's ToyOS target spec names the linker `toyos-ld` and finds it on
# PATH, so a consumer with only this tarball has to be given one — the
# host binary the `build` step above already made. `toyos-ld-witness`
# says which sources it is, because this tag is a function of four
# trees and `toyos-ld` is not among them.
cp "target/$host/release/toyos-ld" "rust/build/$host/stage2/bin/toyos-ld"
cp /tmp/TOOLCHAIN rust/build/TOOLCHAIN
tar -C rust/build \
--exclude="x86_64-unknown-toyos/stage2/lib/rustlib/$host" \
--exclude="$host/stage2/bin/cargo" \
-c "$host/stage2" x86_64-unknown-toyos/stage2 toyos-sysroot-witness \
toyos-ld-witness TOOLCHAIN \
| zstd -T0 -3 -o /tmp/toyos-toolchain.tar.zst
echo "PACKAGE-SECONDS: $(( $(date +%s) - t0 ))"
ls -l /tmp/toyos-toolchain.tar.zst
# Idempotent, because the tag *is* the content: two branches carrying
# the same four trees each build one, and whoever is second would
# otherwise fail having produced the same bytes.
gh release create "${{ steps.key.outputs.tag }}" \
--title "${{ steps.key.outputs.tag }}" \
--notes-file /tmp/notes.md \
/tmp/toyos-toolchain.tar.zst \
|| gh release view "${{ steps.key.outputs.tag }}"
# `create` makes the release and then uploads to it, so a failed upload
# leaves a tag that answers 200 with nothing installable — and this job
# reporting success either way is the verdict `ci.yml` reads before
# starting the shards. The wait here is for the *other* branch's upload,
# in the idempotent case the `publish` step's `||` exists for.
- name: release asset
if: steps.have.outputs.build == 'yes'
env:
GH_TOKEN: ${{ github.token }}
run: |
for _ in $(seq 20); do
if gh release view "${{ steps.key.outputs.tag }}" --json assets \
--jq '.assets[].name' | grep -qx toyos-toolchain.tar.zst; then
echo "${{ steps.key.outputs.tag }} carries toyos-toolchain.tar.zst."
exit 0
fi
echo "the release is there and the asset is not on it yet; waiting"
sleep 15
done
echo "::error::${{ steps.key.outputs.tag }} exists and carries no"
echo "::error::toyos-toolchain.tar.zst. Nothing can install this toolchain, and a"
echo "::error::green here is what lets ci.yml start thirteen shards."
exit 1
# **The SDK version names the ABI; the toolchain that goes with it carries
# the same number.** The content hash above makes a publish idempotent and
# is no name a consumer can write down, so `sdk-<toyos-abi's version>` is
# the one it pins, and it moves to whatever toolchain this tree's ABI
# belongs to.
#
# A second release and not a copy of the asset: GitHub's API hangs an
# asset off one release id, so the alternative was re-uploading 401 MiB on
# every landing. The alias carries the manifest instead — a few hundred
# bytes — and its notes name the tag the tarball is on. `create` refuses a
# tag that exists, which is what `edit` plus a clobbering upload is for.
# Never on a `pull_request`, whose head is somebody's branch and not what
# a consumer should be handed.
- name: sdk alias
if: github.event_name != 'pull_request'
env:
GH_TOKEN: ${{ github.token }}
run: |
v=$(grep '^toyos-abi ' /tmp/TOOLCHAIN | cut -d' ' -f2)
alias=toolchain-linux-x86_64-sdk-$v
gh release create "$alias" --title "$alias" --notes-file /tmp/notes.md \
/tmp/TOOLCHAIN \
|| { gh release edit "$alias" --notes-file /tmp/notes.md
gh release upload "$alias" /tmp/TOOLCHAIN --clobber; }
gh release view "$alias"