Repository navigation
The orchestrator answers the bench's questions on ToyOS, and designs … #2235
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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" |