Build Toolchain #48
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: Build Toolchain | |
| # Builds every host tool this repo uses, and the per-architecture cross | |
| # toolchains, from source - and publishes each output tree to the | |
| # "toolchain-latest" GitHub Release. | |
| # | |
| # This is what the other workflows start from: build-swift-sdk.yml and | |
| # build-images.yml seed their output trees from these release assets | |
| # (.github/actions/br-seed) instead of from a docker layer, and local | |
| # checkouts seed with "make <t>-seed". The per-arch container images the | |
| # consumers used to start from are retired. | |
| # Release assets rather than actions/cache because the trees are far past | |
| # the repo's 10 GB cache budget, assets are not evicted after 7 days, and | |
| # a Release gives the toolchain an inspectable, pinnable identity. | |
| # | |
| # --------------------------------------------------------------------------- | |
| # The host-tool dependency graph, and why the jobs are shaped this way | |
| # --------------------------------------------------------------------------- | |
| # One job per host tool, rather than one opaque multi-hour build: each gets its | |
| # own log, its own red mark, and can be re-run without paying for the others. | |
| # The order below is Buildroot's own dependency graph (read out of "make | |
| # show-info" on a configured tree), split by one question - does the tool | |
| # depend on the TARGET architecture? | |
| # | |
| # ARCHITECTURE-INDEPENDENT, so built ONCE in a shared tree (/mnt/br/shared) | |
| # and consumed by every arch: | |
| # | |
| # host-base host-cmake, host-ninja, host-meson, host-python3, | |
| # host-pkgconf - and the 21-package closure underneath them | |
| # (m4/autoconf/automake/libtool, expat, libffi, zlib, and the | |
| # python build backends: flit-core, installer, setuptools, | |
| # wheel, pypa-build, packaging, pyproject-hooks). | |
| # Everything else here needs this layer. | |
| # host-jdk host-openjdk-bin - the prebuilt boot JDK that the target | |
| # openjdk (BR2_PACKAGE_OPENJDK, runtimes.config) bootstraps | |
| # from. A download and an unpack; never installed on target. | |
| # host-dotnet host-mono, which pulls host-monolite and host-gettext. The | |
| # bootstrap compiler for the target mono (BR2_PACKAGE_MONO) | |
| # that Unity and XNA/FNA ports need. Note Buildroot only | |
| # offers mono when the HOST is x86 (BR2_HOSTARCH), which this | |
| # runner is. | |
| # host-swift the native Swift toolchain. Declares no dependencies at all | |
| # (nothing sets HOST_SWIFT_DEPENDENCIES) - it clones and | |
| # builds its own LLVM through build-script - so it is last | |
| # here only to keep the shared tree serialized. | |
| # | |
| # TARGET-DEPENDENT, so built per architecture, in that arch's own tree: | |
| # | |
| # host-gcc host-binutils + host-gcc-initial + glibc + host-gcc-final, | |
| # i.e. "make toolchain". This IS the cross compiler; gcc.mk | |
| # configures it --target=$(GNU_TARGET_NAME). | |
| # host-rust host-rustc -> host-rust-bin, whose EXTRA_DOWNLOADS include | |
| # rust-std-$(RUSTC_TARGET_NAME): the standard library for the | |
| # target triple. Needed by ruffle. | |
| # host-go host-go -> host-go-src plus a five-stage bootstrap, built | |
| # with GOARCH set from the target arch (go.mk: GO_GOARCH). | |
| # host-llvm host-llvm + host-clang. Per-target because llvm.mk configures | |
| # the HOST llvm with -DLLVM_DEFAULT_TARGET_TRIPLE and | |
| # -DLLVM_TARGET_ARCH taken from the target. | |
| # host-spirv host-spirv-headers, host-spirv-tools and the LLVM<->SPIR-V | |
| # translator, which links the host-llvm above. | |
| # host-libclc the OpenCL builtins as LLVM bitcode. | |
| # host-mesa3d the per-driver precompiled-shader compilers. The terminus of | |
| # that branch: target mesa3d depends on it whenever | |
| # BR2_PACKAGE_MESA3D_NEEDS_PRECOMP_COMPILER is set, which every | |
| # panfrost, iris and radeonsi board sets. | |
| # | |
| # The last four exist because mesa needs LLVM - not for the sdk profile, which | |
| # consumes none of them, but so that a device image build seeded from these | |
| # trees does not have to compile an entire LLVM before it can build its GPU | |
| # driver. Nine arm64 boards use panfrost, and gpu/panfrost.config spells out | |
| # why getting this wrong is worse than not doing it: an unmet dependency does | |
| # not fail, it silently leaves a board with llvmpipe-only GL. | |
| # | |
| # Still not built: TARGET llvm, clang and libclc, which BR2_PACKAGE_MESA3D_OPENCL | |
| # selects. A panfrost image build still compiles those itself. | |
| # --------------------------------------------------------------------------- | |
| # | |
| # Runs on the self-hosted x86 runner (see docs/build.md). That is what makes | |
| # this workflow tractable at all: no 6-hour job cap, more cores, and - the | |
| # thing that actually matters - a disk that survives between runs. /mnt/br | |
| # holds dl/, the per-arch ccaches, the shared host-tool tree and the per-arch | |
| # output trees, so each job in the chain picks the tree up where the last one | |
| # left it and a second run is incremental instead of starting cold. There are | |
| # no actions/cache steps for that reason; the state is already on the box. | |
| # | |
| # Two properties of a persistent runner the steps below are written around: | |
| # - The output trees must NOT be pruned in place to build the release | |
| # tarball. Packaging works from a hardlinked staging copy under | |
| # /mnt/br/pkg/<arch> so the live tree keeps its extracted sources. | |
| # - Container jobs run as root and leave root-owned files in the workspace, | |
| # which the next run's actions/checkout (running as the runner user) | |
| # cannot clean. Every job hands the workspace back on the way out. | |
| # | |
| # GitHub release assets still cap at 2 GiB each, and a compressed output tree | |
| # is larger; the trees ship as split .tar.zst parts that the consumer | |
| # concatenates. | |
| # | |
| # Weekly, not nightly: the toolchain changes when toolchain.config or | |
| # swift.config change, not when board overlays do. | |
| on: | |
| schedule: | |
| - cron: '0 3 * * 6' # Saturday 03:00 UTC | |
| workflow_dispatch: | |
| inputs: | |
| force_republish: | |
| description: 'Re-package and re-upload host-swift even if this version is already on the release' | |
| type: boolean | |
| default: false | |
| from_source: | |
| description: > | |
| Compile host-swift with build-script instead of unpacking the | |
| released toolchain. This is the only path that exercises the | |
| package's patches - the prebuilt install satisfies the build | |
| guard before build-script ever runs, so a patch can merge and | |
| sit uncompiled indefinitely. Dircleans host-swift first, since | |
| the persistent tree already holds a prebuilt-satisfied guard. | |
| Five to six hours from cold. Leave force_republish off to | |
| validate the compile without touching the release. | |
| type: boolean | |
| default: false | |
| from_source_clean: | |
| description: > | |
| Space-separated phase directories under swift-source/build/ | |
| buildbot_linux to delete before a from_source resume - e.g. | |
| "swift-linux-x86_64". For a tree an unclean stop corrupted: | |
| ninja trusts timestamps, so an object truncated by a power | |
| loss is "up to date" and surfaces as undefined references at | |
| link, hours later. Deleting the phase rebuilds it whole while | |
| the finished phases (llvm above all) stay cached. | |
| required: false | |
| default: '' | |
| type: string | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| # Not cancel-in-progress: this chain includes host-swift, a from-source | |
| # Swift+LLVM build that runs five to six hours from cold. The weekly | |
| # schedule and a manual workflow_dispatch both target the same ref, so | |
| # cancel-in-progress let the Saturday 03:00 UTC schedule kill a | |
| # workflow_dispatch run mid-build - at 5h33m in, past the two failures it | |
| # had already been re-dispatched to fix. Queuing costs an idle wait; a | |
| # cancellation this deep costs the run. | |
| cancel-in-progress: false | |
| env: | |
| # The old 1.5G cap existed to fit four architectures plus dl/ inside the | |
| # 10 GB repo-wide actions/cache budget. The caches live on the runner's own | |
| # disk now, so they can be sized for hit rate instead. | |
| CCACHE_MAX_SIZE: 20G | |
| # Where the architecture-independent host tools are built, once. A fixed | |
| # path, not a per-arch output tree, because these builds bake absolute paths | |
| # into their CMake caches and symlinks; the consuming jobs reference this | |
| # same path, so every one of those references still resolves. | |
| SHARED_DIR: /mnt/br/shared | |
| jobs: | |
| # ========================================================================== | |
| # Architecture-independent host tools, built once in $SHARED_DIR. | |
| # | |
| # These four are a linear chain rather than a fan-out: they all write to the | |
| # same Buildroot tree, and two of them running at once would race on its | |
| # stamps. On a single runner they serialize anyway; "needs" makes that a | |
| # guarantee rather than an accident of runner count. | |
| # ========================================================================== | |
| host-base: | |
| name: Host base (cmake, ninja, meson, python3) | |
| runs-on: [self-hosted, Linux, X64] | |
| container: | |
| image: docker.io/colemancda/buildroot-swift:latest | |
| options: --volume /mnt:/mnt --security-opt label=disable | |
| timeout-minutes: 360 | |
| env: | |
| CCACHE_DIR: /mnt/br/ccache/host-shared | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: ./.github/actions/br-setup | |
| with: | |
| output-dir: /mnt/br/shared | |
| ccache-dir: /mnt/br/ccache/host-shared | |
| ccache-max-size: ${{ env.CCACHE_MAX_SIZE }} | |
| min-free-gb: '60' | |
| # The build tools every other host package is compiled with. Buildroot | |
| # pulls the closure (autotools, m4, libtool, expat, libffi, zlib, and | |
| # python's build backends) in behind these five. | |
| - name: Build the base host tools | |
| run: | | |
| set -eu | |
| FORCE_UNSAFE_CONFIGURE=1 make -C "$BR_B" O="$BR_O" BR2_EXTERNAL="$BR_EXT" \ | |
| BR2_DL_DIR=/mnt/br/dl BR2_CCACHE_DIR="$CCACHE_DIR" -j"$BR_JOBS" \ | |
| host-cmake host-ninja host-meson host-python3 host-pkgconf | |
| "$BR_O/host/bin/cmake" --version | |
| "$BR_O/host/bin/python3" --version | |
| - name: Return the workspace to the runner user | |
| if: always() | |
| run: chown -R "$(stat -c '%u:%g' "$GITHUB_WORKSPACE")" "$GITHUB_WORKSPACE" || true | |
| host-jdk: | |
| name: Host JDK (openjdk-bin) | |
| needs: host-base | |
| runs-on: [self-hosted, Linux, X64] | |
| container: | |
| image: docker.io/colemancda/buildroot-swift:latest | |
| options: --volume /mnt:/mnt --security-opt label=disable | |
| timeout-minutes: 120 | |
| env: | |
| CCACHE_DIR: /mnt/br/ccache/host-shared | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: ./.github/actions/br-setup | |
| with: | |
| output-dir: /mnt/br/shared | |
| ccache-dir: /mnt/br/ccache/host-shared | |
| ccache-max-size: ${{ env.CCACHE_MAX_SIZE }} | |
| min-free-gb: '60' | |
| # The boot JDK the target openjdk bootstraps from. A prebuilt download, | |
| # so this job is minutes - it is separate because a broken JDK download | |
| # should not read as "the Swift build failed". | |
| - name: Build the host JDK | |
| run: | | |
| set -eu | |
| FORCE_UNSAFE_CONFIGURE=1 make -C "$BR_B" O="$BR_O" BR2_EXTERNAL="$BR_EXT" \ | |
| BR2_DL_DIR=/mnt/br/dl BR2_CCACHE_DIR="$CCACHE_DIR" -j"$BR_JOBS" \ | |
| host-openjdk-bin | |
| # openjdk-bin.mk installs the whole JDK under $(HOST_DIR)/lib/jvm, | |
| # not into host/bin. | |
| "$BR_O/host/lib/jvm/bin/java" -version | |
| - name: Return the workspace to the runner user | |
| if: always() | |
| run: chown -R "$(stat -c '%u:%g' "$GITHUB_WORKSPACE")" "$GITHUB_WORKSPACE" || true | |
| host-dotnet: | |
| name: Host .NET (mono) | |
| needs: host-jdk | |
| runs-on: [self-hosted, Linux, X64] | |
| container: | |
| image: docker.io/colemancda/buildroot-swift:latest | |
| options: --volume /mnt:/mnt --security-opt label=disable | |
| timeout-minutes: 480 | |
| env: | |
| CCACHE_DIR: /mnt/br/ccache/host-shared | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: ./.github/actions/br-setup | |
| with: | |
| output-dir: /mnt/br/shared | |
| ccache-dir: /mnt/br/ccache/host-shared | |
| ccache-max-size: ${{ env.CCACHE_MAX_SIZE }} | |
| min-free-gb: '60' | |
| # host-mono is the bootstrap C#/CLR compiler the target mono is built | |
| # with; it pulls host-monolite (the prebuilt mcs bootstrap) and | |
| # host-gettext. Buildroot only offers mono at all when BR2_HOSTARCH is | |
| # x86/x86_64, which is why this runs here and not on an arm builder. | |
| - name: Build host-mono | |
| run: | | |
| set -eu | |
| FORCE_UNSAFE_CONFIGURE=1 make -C "$BR_B" O="$BR_O" BR2_EXTERNAL="$BR_EXT" \ | |
| BR2_DL_DIR=/mnt/br/dl BR2_CCACHE_DIR="$CCACHE_DIR" -j"$BR_JOBS" \ | |
| host-mono | |
| # Run it, the way the other host tools here are checked. Searching for | |
| # the file instead fails: Buildroot installs host/bin/mono as a | |
| # symlink to mono-sgen, which "find -type f" never matches, so a | |
| # perfectly good build reported "installed no mono binary". | |
| "$BR_O/host/bin/mono" --version | head -1 | |
| - name: Return the workspace to the runner user | |
| if: always() | |
| run: chown -R "$(stat -c '%u:%g' "$GITHUB_WORKSPACE")" "$GITHUB_WORKSPACE" || true | |
| # The host Swift toolchain is the same artifact for every architecture, so it | |
| # is built once here instead of four times in the matrix - which is where the | |
| # hours went: one run spent 3h13m on it per arch and still had only reached | |
| # LLVM and clang. | |
| # | |
| # It is genuinely shareable, not just similar. host-swift declares no | |
| # dependency on the cross toolchain (nothing sets HOST_SWIFT_DEPENDENCIES), | |
| # its configure only clones the Swift sources, it builds with the host | |
| # compiler, and the buildbot_linux preset targets --host-target linux-x86_64 | |
| # whatever Buildroot is aiming at. The dependency runs the other way: the | |
| # target swift runtime needs host-swift, the cross toolchain and the sysroot. | |
| # | |
| # On the self-hosted runner $SHARED_DIR persists, so after the first | |
| # successful run this job is a stamp-guarded no-op. | |
| host-swift: | |
| name: Host Swift | |
| needs: host-dotnet | |
| runs-on: [self-hosted, Linux, X64] | |
| container: | |
| image: docker.io/colemancda/buildroot-swift:latest | |
| options: --volume /mnt:/mnt --security-opt label=disable | |
| # Generous, not calibrated: the 6-hour hosted-runner cap this used to dodge | |
| # does not apply here. It exists so a wedged build eventually releases the | |
| # runner rather than blocking every other workflow forever. | |
| timeout-minutes: 2880 | |
| permissions: | |
| contents: write # uploads assets to the host-swift-latest release | |
| outputs: | |
| version: ${{ steps.ver.outputs.version }} | |
| env: | |
| CCACHE_DIR: /mnt/br/ccache/host-shared | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: ./.github/actions/br-setup | |
| with: | |
| output-dir: /mnt/br/shared | |
| ccache-dir: /mnt/br/ccache/host-shared | |
| ccache-max-size: ${{ env.CCACHE_MAX_SIZE }} | |
| min-free-gb: '100' | |
| # The asset is keyed on this, so a Swift bump republishes rather than | |
| # silently reusing the previous toolchain. | |
| - name: Read the Swift version | |
| id: ver | |
| run: | | |
| set -eu | |
| v=$(sed -n 's/^SWIFT_VERSION *= *//p' swift/package/swift/swift.mk | head -1) | |
| [ -n "$v" ] || { echo "no SWIFT_VERSION in swift/package/swift/swift.mk" >&2; exit 1; } | |
| echo "version=$v" >> "$GITHUB_OUTPUT" | |
| echo "swift $v" | |
| # On a persistent runner the build below is a no-op once it has succeeded | |
| # once, but packaging and uploading are not: they would re-tar and re-push | |
| # gigabytes every Saturday for a byte-identical result. Decide here, and | |
| # say so in the log, rather than finding out from the upload. | |
| - name: Is this version already published? | |
| id: pub | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| set -eu | |
| VER=${{ steps.ver.outputs.version }} | |
| if [ "${{ github.event.inputs.force_republish }}" = "true" ]; then | |
| echo "needed=true" >> "$GITHUB_OUTPUT" | |
| echo "force_republish set: will package and upload regardless" | |
| exit 0 | |
| fi | |
| n=$(gh release view host-swift-latest --repo "$GITHUB_REPOSITORY" \ | |
| --json assets \ | |
| --jq "[.assets[].name | select(startswith(\"host-swift-$VER.tar.zst.part\"))] | length" \ | |
| 2>/dev/null || echo 0) | |
| if [ "${n:-0}" -gt 0 ]; then | |
| echo "needed=false" >> "$GITHUB_OUTPUT" | |
| echo "host-swift $VER is already on the release ($n parts); skipping package + upload" | |
| else | |
| echo "needed=true" >> "$GITHUB_OUTPUT" | |
| echo "host-swift $VER is not on the release yet" | |
| fi | |
| # Unpack the released toolchain into the place host-swift would have | |
| # built one, instead of compiling Swift and LLVM from source for five to | |
| # six hours. HOST_SWIFT_BUILD_CMDS is guarded on the existence of | |
| # $(SWIFT_LLVM_DIR), so once build/usr and build/llvm are populated the | |
| # build-script invocation is skipped and the package goes straight to | |
| # installing; nothing in swift.mk has to change. | |
| # | |
| # The download is the same compiler the source build was producing - the | |
| # released clang reports swiftlang/llvm-project 82cdc19, the very commit | |
| # llvm-project is pinned to - so this is not a downgrade. It is also what | |
| # fixes the -index-store-path failure: swift-sdk-generator's WASM SDK | |
| # step shells out to a bare "clang", and the distro clang-19 in this | |
| # image is built without libIndexStore and rejects the flag, while this | |
| # one accepts it. | |
| # | |
| # The script is idempotent: it tests for build/usr/bin and build/llvm | |
| # and does nothing when the tree already has them. | |
| # The prebuilt install is per-piece idempotent - it creates build/usr | |
| # only when absent, build/llvm only when absent - which is right for a | |
| # tree that is either empty or complete, and wrong for one left half | |
| # built. An interrupted from_source run leaves exactly that: build/usr | |
| # populated by build-script, no build/llvm, because that symlink is | |
| # written only on success. Installing over it would graft the distro | |
| # llvm-21 onto a partial from-source toolchain and call it done. | |
| # | |
| # This is not hypothetical either. Three from_source attempts died to | |
| # host outages last week and left the shared tree in that state, and | |
| # every image and lib32 job since has failed on the consumer's own | |
| # check - "shared host-swift has no usable build/llvm" - which is the | |
| # right error from the wrong place: the tree should never have been | |
| # handed over in that condition. Test what the consumer tests, and | |
| # start over when it does not hold. | |
| - name: Discard a half-built host-swift | |
| if: ${{ !inputs.from_source }} | |
| run: | | |
| set -eu | |
| D="$BR_O/build/host-swift-${{ steps.ver.outputs.version }}" | |
| if [ -d "$D" ] && [ ! -e "$D/build/llvm/lib/cmake/llvm" ]; then | |
| echo "$D exists without a usable build/llvm - dircleaning before the install" | |
| make -C "$BR_B" O="$BR_O" BR2_EXTERNAL="$BR_EXT" \ | |
| BR2_DL_DIR=/mnt/br/dl host-swift-dirclean | |
| else | |
| echo "host-swift tree is absent or complete; nothing to discard" | |
| fi | |
| - name: Install the prebuilt host Swift toolchain | |
| if: ${{ !inputs.from_source }} | |
| run: | | |
| set -eu | |
| BUILDROOT_OUTPUT="$BR_O" SWIFT_BUILDROOT="$GITHUB_WORKSPACE/swift" \ | |
| bash "$GITHUB_WORKSPACE/swift/.devcontainer/library-scripts/install-swift.sh" | |
| "$BR_O/build/host-swift-${{ steps.ver.outputs.version }}/build/usr/bin/swiftc" --version | |
| # from_source is a dirclean plus the absence of the step above, and the | |
| # dirclean serves two purposes: HOST_SWIFT_BUILD_CMDS is guarded on | |
| # build/llvm existing, which a prebuilt install from some earlier run | |
| # has already satisfied on this persistent tree; and patches are | |
| # applied at extract time, so only a re-extract picks up one that | |
| # merged after the tree's last extract. | |
| # | |
| # But not unconditionally. A from-source build the machine died under | |
| # leaves a tree that is already what the dirclean would produce - no | |
| # build/llvm, sources extracted with the current patches - plus hours | |
| # of compiled objects ninja can resume from. The first attempt lost | |
| # four hours of LLVM and Swift to a host outage; dircleaning on the | |
| # retry would have thrown away the four hours the outage did not. So: | |
| # dirclean when the guard is satisfied (prebuilt tree) or the applied | |
| # patch set differs from the package's (stale extract); resume | |
| # otherwise. | |
| - name: Return host-swift to source form | |
| if: ${{ inputs.from_source }} | |
| run: | | |
| set -eu | |
| D="$BR_O/build/host-swift-${{ steps.ver.outputs.version }}" | |
| want=$(ls "$GITHUB_WORKSPACE"/swift/package/swift/*.patch 2>/dev/null | xargs -rn1 basename | sort) | |
| have=$(tr ' ' '\n' < "$D/.applied_patches_list" 2>/dev/null | xargs -rn1 basename | sort || true) | |
| if [ ! -d "$D" ] || [ -d "$D/build/llvm" ] || [ "$want" != "$have" ]; then | |
| echo "tree is prebuilt-satisfied or stale; dircleaning" | |
| make -C "$BR_B" O="$BR_O" BR2_EXTERNAL="$BR_EXT" \ | |
| BR2_DL_DIR=/mnt/br/dl host-swift-dirclean | |
| else | |
| echo "tree already in source form with the current patches; resuming the build" | |
| for phase in ${{ inputs.from_source_clean }}; do | |
| p="$D/swift-source/build/buildbot_linux/$phase" | |
| if [ -d "$p" ]; then | |
| echo "deleting phase $phase (from_source_clean)" | |
| rm -rf "$p" | |
| else | |
| echo "no phase directory $phase to delete" | |
| fi | |
| done | |
| fi | |
| # Only the host-swift package: with no dependencies declared it pulls in | |
| # neither the cross toolchain nor anything else from the profile. The | |
| # architecture of the defconfig is irrelevant to what gets built - it just | |
| # has to be a configured tree. | |
| - name: Build host-swift | |
| run: | | |
| set -eu | |
| FORCE_UNSAFE_CONFIGURE=1 make -C "$BR_B" O="$BR_O" BR2_EXTERNAL="$BR_EXT" \ | |
| BR2_DL_DIR=/mnt/br/dl BR2_CCACHE_DIR="$CCACHE_DIR" -j"$BR_JOBS" host-swift | |
| # What the consumers actually need, and how big it is. The sizes are | |
| # reported because the split threshold and the wisdom of shipping the | |
| # whole build tree depend on them. | |
| # | |
| # The two tests are the same pair the matrix jobs run before trusting the | |
| # shared tree, repeated here because packaging - which used to be what | |
| # caught a missing swiftc - is now skipped when the version is already | |
| # published. Without them a broken build could go green. | |
| - name: Measure | |
| run: | | |
| set -eu | |
| D=$SHARED_DIR/build/host-swift-${{ steps.ver.outputs.version }} | |
| test -x "$D/build/usr/bin/swiftc" || { echo "no swiftc at $D/build/usr/bin" >&2; exit 1; } | |
| test -e "$D/build/llvm/lib/cmake/llvm" || { echo "no usable build/llvm at $D" >&2; exit 1; } | |
| "$D/build/usr/bin/swiftc" --version | |
| du -sh "$D" || true | |
| du -sh "$D/build/usr" "$D/swift-source" 2>/dev/null || true | |
| du -sh "$D"/swift-source/build/buildbot_linux/* 2>/dev/null || true | |
| df -h /mnt | |
| - name: Package host-swift | |
| if: steps.pub.outputs.needed == 'true' | |
| run: | | |
| set -eu | |
| VER=${{ steps.ver.outputs.version }} | |
| D=$SHARED_DIR/build/host-swift-$VER | |
| # Ship the stamps up to and including the build, so a consumer skips | |
| # download/extract/patch/configure/build outright. The install stamp | |
| # is deliberately dropped: HOST_SWIFT_INSTALL_CMDS writes the SwiftPM | |
| # destination file with target-specific flags, so it has to run per | |
| # architecture. | |
| rm -f "$D"/.stamp_host_installed "$D"/.stamp_installed* | |
| tar -C "$SHARED_DIR/build" --zstd -cf /mnt/br/host-swift-$VER.tar.zst \ | |
| host-swift-$VER | |
| # Release assets cap at 2 GiB, as with the toolchain trees. | |
| split -b 1900M -d /mnt/br/host-swift-$VER.tar.zst \ | |
| /mnt/br/host-swift-$VER.tar.zst.part | |
| rm /mnt/br/host-swift-$VER.tar.zst | |
| ls -lh /mnt/br/host-swift-$VER.tar.zst.part* | |
| - name: Publish to the host-swift-latest release | |
| if: steps.pub.outputs.needed == 'true' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| set -eu | |
| VER=${{ steps.ver.outputs.version }} | |
| gh release view host-swift-latest --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1 || \ | |
| gh release create host-swift-latest --repo "$GITHUB_REPOSITORY" \ | |
| --title "Host Swift (rolling)" \ | |
| --notes "Host Swift toolchain built from source by build-toolchain.yml and shared across the architecture matrix. Assets are split tar.zst parts: cat host-swift-<ver>.tar.zst.part* | tar --zstd -x" \ | |
| --latest=false | |
| for a in $(gh release view host-swift-latest --repo "$GITHUB_REPOSITORY" \ | |
| --json assets --jq '.assets[].name' | grep "^host-swift-$VER\." || true); do | |
| gh release delete-asset host-swift-latest "$a" --repo "$GITHUB_REPOSITORY" -y | |
| done | |
| gh release upload host-swift-latest /mnt/br/host-swift-$VER.tar.zst.part* \ | |
| --repo "$GITHUB_REPOSITORY" | |
| # The runner's disk is not thrown away after the job; the parts are | |
| # reproducible from the shared tree, so do not leave ~10 GB behind. | |
| rm -f /mnt/br/host-swift-$VER.tar.zst.part* | |
| - name: ccache stats | |
| if: always() | |
| run: | | |
| ccache -d "$CCACHE_DIR" -s 2>/dev/null || true | |
| du -sh "$CCACHE_DIR" /mnt/br/dl || true | |
| df -h /mnt | |
| # The container runs as root; the workspace belongs to the runner user on | |
| # the host and, unlike a hosted VM, survives the job. Without this the | |
| # next run's actions/checkout git-cleans as the runner user and hits EPERM | |
| # on the root-owned submodule trees this job created. | |
| - name: Return the workspace to the runner user | |
| if: always() | |
| run: chown -R "$(stat -c '%u:%g' "$GITHUB_WORKSPACE")" "$GITHUB_WORKSPACE" || true | |
| # ========================================================================== | |
| # Target-dependent host tools, one chain per architecture. | |
| # | |
| # Each stage is a matrix over arch, so its four jobs touch four DIFFERENT | |
| # trees and are safe to run concurrently; the stages themselves are ordered | |
| # because within one arch they share a tree. "needs" between matrix jobs is a | |
| # barrier across the whole matrix, which is what gives that ordering. | |
| # ========================================================================== | |
| host-gcc: | |
| name: Host GCC (${{ matrix.arch }}) | |
| needs: host-swift | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| arch: [x86_64, arm64, armv7, i386] | |
| runs-on: [self-hosted, Linux, X64] | |
| # The plain base image: Debian with the Swift toolchain and the host | |
| # dependencies both Buildroot and a from-source Swift build need. The | |
| # same image the SDK and image workflows use - they seed from this | |
| # workflow's release assets rather than carrying a prebuilt tree. | |
| container: | |
| image: docker.io/colemancda/buildroot-swift:latest | |
| options: --volume /mnt:/mnt --security-opt label=disable | |
| timeout-minutes: 600 | |
| env: | |
| CCACHE_DIR: /mnt/br/ccache/${{ matrix.arch }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: ./.github/actions/br-setup | |
| with: | |
| output-dir: /mnt/br/output/${{ matrix.arch }} | |
| arch: ${{ matrix.arch }} | |
| ccache-dir: /mnt/br/ccache/${{ matrix.arch }} | |
| ccache-max-size: ${{ env.CCACHE_MAX_SIZE }} | |
| min-free-gb: '100' | |
| # "toolchain" is Buildroot's own name for the cross compiler and its | |
| # sysroot: host-binutils, host-gcc-initial, the target glibc and | |
| # linux-headers, then host-gcc-final. This is the genuinely per-arch | |
| # part - gcc.mk configures --target=$(GNU_TARGET_NAME) - and it is why | |
| # this cannot be shared the way the tools above are. | |
| - name: Build the cross toolchain | |
| run: | | |
| set -eu | |
| FORCE_UNSAFE_CONFIGURE=1 make -C "$BR_B" O="$BR_O" BR2_EXTERNAL="$BR_EXT" \ | |
| BR2_DL_DIR=/mnt/br/dl BR2_CCACHE_DIR="$CCACHE_DIR" -j"$BR_JOBS" toolchain | |
| # One name per configuration, and the glob can match both the | |
| # triple-prefixed binary and the <arch>-linux alias gcc.mk symlinks | |
| # next to it - take the first rather than passing several to --version. | |
| cc=$(find "$BR_O/host/bin" -maxdepth 1 -name '*-linux*-gcc' | head -1) | |
| [ -n "$cc" ] || { echo "no cross gcc in $BR_O/host/bin" >&2; exit 1; } | |
| "$cc" --version | head -1 | |
| - name: Return the workspace to the runner user | |
| if: always() | |
| run: chown -R "$(stat -c '%u:%g' "$GITHUB_WORKSPACE")" "$GITHUB_WORKSPACE" || true | |
| host-rust: | |
| name: Host Rust (${{ matrix.arch }}) | |
| needs: host-gcc | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| arch: [x86_64, arm64, armv7, i386] | |
| runs-on: [self-hosted, Linux, X64] | |
| container: | |
| image: docker.io/colemancda/buildroot-swift:latest | |
| options: --volume /mnt:/mnt --security-opt label=disable | |
| timeout-minutes: 240 | |
| env: | |
| CCACHE_DIR: /mnt/br/ccache/${{ matrix.arch }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: ./.github/actions/br-setup | |
| with: | |
| output-dir: /mnt/br/output/${{ matrix.arch }} | |
| arch: ${{ matrix.arch }} | |
| ccache-dir: /mnt/br/ccache/${{ matrix.arch }} | |
| ccache-max-size: ${{ env.CCACHE_MAX_SIZE }} | |
| min-free-gb: '60' | |
| # host-rustc resolves to host-rust-bin: the prebuilt host rustc plus | |
| # rust-std for THIS target's triple (rust-bin.mk pulls | |
| # rust-std-$(RUSTC_TARGET_NAME) as an extra download), which is what | |
| # makes it per-arch rather than shared. ruffle is the consumer. | |
| - name: Build host Rust | |
| run: | | |
| set -eu | |
| FORCE_UNSAFE_CONFIGURE=1 make -C "$BR_B" O="$BR_O" BR2_EXTERNAL="$BR_EXT" \ | |
| BR2_DL_DIR=/mnt/br/dl BR2_CCACHE_DIR="$CCACHE_DIR" -j"$BR_JOBS" host-rustc | |
| "$BR_O"/host/bin/rustc --version | |
| "$BR_O"/host/bin/rustc --print target-list >/dev/null | |
| - name: Return the workspace to the runner user | |
| if: always() | |
| run: chown -R "$(stat -c '%u:%g' "$GITHUB_WORKSPACE")" "$GITHUB_WORKSPACE" || true | |
| host-go: | |
| name: Host Go (${{ matrix.arch }}) | |
| needs: host-rust | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| arch: [x86_64, arm64, armv7, i386] | |
| runs-on: [self-hosted, Linux, X64] | |
| container: | |
| image: docker.io/colemancda/buildroot-swift:latest | |
| options: --volume /mnt:/mnt --security-opt label=disable | |
| timeout-minutes: 360 | |
| env: | |
| CCACHE_DIR: /mnt/br/ccache/${{ matrix.arch }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: ./.github/actions/br-setup | |
| with: | |
| output-dir: /mnt/br/output/${{ matrix.arch }} | |
| arch: ${{ matrix.arch }} | |
| ccache-dir: /mnt/br/ccache/${{ matrix.arch }} | |
| ccache-max-size: ${{ env.CCACHE_MAX_SIZE }} | |
| min-free-gb: '60' | |
| # host-go is built from source (host-go-src) through a five-stage | |
| # bootstrap seeded by the prebuilt host-go-bin, with GOARCH taken from | |
| # the target (go.mk: GO_GOARCH), so each architecture gets its own. | |
| - name: Build host Go | |
| run: | | |
| set -eu | |
| FORCE_UNSAFE_CONFIGURE=1 make -C "$BR_B" O="$BR_O" BR2_EXTERNAL="$BR_EXT" \ | |
| BR2_DL_DIR=/mnt/br/dl BR2_CCACHE_DIR="$CCACHE_DIR" -j"$BR_JOBS" host-go | |
| # go.mk installs to $(HOST_DIR)/lib/go; the binary infers GOROOT | |
| # from its own path. | |
| "$BR_O/host/lib/go/bin/go" version | |
| - name: Return the workspace to the runner user | |
| if: always() | |
| run: chown -R "$(stat -c '%u:%g' "$GITHUB_WORKSPACE")" "$GITHUB_WORKSPACE" || true | |
| # host-llvm and host-clang, for mesa. Per-arch like the three above, and for | |
| # the same kind of reason: llvm.mk configures the HOST llvm with | |
| # -DLLVM_DEFAULT_TARGET_TRIPLE=$(GNU_TARGET_NAME) and | |
| # -DLLVM_TARGET_ARCH=$(LLVM_TARGET_ARCH), both taken from the target. | |
| # | |
| # The extra-config below is what makes the result REUSABLE by a device image | |
| # build rather than subtly wrong, and it is worth spelling out because | |
| # neither symbol is in the sdk profile: | |
| # | |
| # BR2_PACKAGE_LLVM the two knobs below live inside "if | |
| # BR2_PACKAGE_LLVM", so they are invisible without | |
| # it. Only the config is widened - no job here runs | |
| # a bare make while it is set, so the TARGET llvm is | |
| # never built, and the later jobs re-run defconfig | |
| # from the plain sdk profile which drops it again. | |
| # BR2_PACKAGE_LLVM_AMDGPU appends AMDGPU to LLVM_TARGETS_TO_BUILD, which | |
| # feeds host and target alike (llvm.mk:67: "We need | |
| # to build AMDGPU backend for both host and | |
| # target"). Wanted by every radeonsi board - | |
| # steamdeck and the x86-desktop GPU fragment. | |
| # BR2_PACKAGE_LLVM_RTTI -DLLVM_ENABLE_RTTI=ON for the host build | |
| # (llvm.mk:230). This one is not optional: RTTI | |
| # changes the C++ ABI, so a host-llvm built without | |
| # it cannot be linked against by host-clang, | |
| # host-libclc or host-spirv-llvm-translator built | |
| # expecting it. Selected by BR2_PACKAGE_MESA3D_OPENCL, | |
| # which is the precompiled-shader path panfrost, | |
| # radeonsi and iris all need. | |
| # | |
| # Building narrower than this is the failure mode gpu/panfrost.config warns | |
| # about: nothing errors, the stamps say "built", and a device build reusing | |
| # the seeded tree silently gets an LLVM that cannot serve its driver. | |
| # br-setup asserts every line above survived kconfig for that reason. | |
| host-llvm: | |
| name: Host LLVM + clang (${{ matrix.arch }}) | |
| needs: host-go | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| arch: [x86_64, arm64, armv7, i386] | |
| runs-on: [self-hosted, Linux, X64] | |
| container: | |
| image: docker.io/colemancda/buildroot-swift:latest | |
| options: --volume /mnt:/mnt --security-opt label=disable | |
| # The most expensive per-arch job by a wide margin - a full LLVM plus clang, | |
| # four times over. Weekly and incremental, but the first run is long. | |
| timeout-minutes: 1800 | |
| env: | |
| CCACHE_DIR: /mnt/br/ccache/${{ matrix.arch }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: ./.github/actions/br-setup | |
| with: | |
| output-dir: /mnt/br/output/${{ matrix.arch }} | |
| arch: ${{ matrix.arch }} | |
| ccache-dir: /mnt/br/ccache/${{ matrix.arch }} | |
| ccache-max-size: ${{ env.CCACHE_MAX_SIZE }} | |
| min-free-gb: '100' | |
| extra-config: | | |
| BR2_PACKAGE_LLVM=y | |
| BR2_PACKAGE_LLVM_AMDGPU=y | |
| BR2_PACKAGE_LLVM_RTTI=y | |
| - name: Build host LLVM and clang | |
| run: | | |
| set -eu | |
| FORCE_UNSAFE_CONFIGURE=1 make -C "$BR_B" O="$BR_O" BR2_EXTERNAL="$BR_EXT" \ | |
| BR2_DL_DIR=/mnt/br/dl BR2_CCACHE_DIR="$CCACHE_DIR" -j"$BR_JOBS" \ | |
| host-llvm host-clang | |
| "$BR_O/host/bin/llvm-config" --version | |
| # Prove the two properties a consumer depends on, rather than trusting | |
| # that the config survived: AMDGPU in the target list, RTTI on. | |
| "$BR_O/host/bin/llvm-config" --targets-built | tr ' ' '\n' | sort | tee /tmp/targets | |
| grep -qx AMDGPU /tmp/targets || { echo "host-llvm has no AMDGPU backend" >&2; exit 1; } | |
| case "$("$BR_O/host/bin/llvm-config" --has-rtti)" in | |
| YES) ;; | |
| *) echo "host-llvm was built without RTTI; host-clang and libclc will not link against it" >&2; exit 1 ;; | |
| esac | |
| "$BR_O/host/bin/clang" --version | |
| - name: Return the workspace to the runner user | |
| if: always() | |
| run: chown -R "$(stat -c '%u:%g' "$GITHUB_WORKSPACE")" "$GITHUB_WORKSPACE" || true | |
| # SPIR-V: headers, the tools, and the LLVM<->SPIR-V translator. Per-arch | |
| # because the translator links the host-llvm built one stage back, not | |
| # because anything here reads the target arch - spirv-llvm-translator.mk:11 | |
| # fixes its HOST_ dependencies and CONF_OPTS unconditionally. | |
| # | |
| # host-spirv-tools is wanted by host-mesa3d directly; the translator is what | |
| # host-libclc needs to emit SPIR-V. | |
| host-spirv: | |
| name: Host SPIR-V (${{ matrix.arch }}) | |
| needs: host-llvm | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| arch: [x86_64, arm64, armv7, i386] | |
| runs-on: [self-hosted, Linux, X64] | |
| container: | |
| image: docker.io/colemancda/buildroot-swift:latest | |
| options: --volume /mnt:/mnt --security-opt label=disable | |
| timeout-minutes: 480 | |
| env: | |
| CCACHE_DIR: /mnt/br/ccache/${{ matrix.arch }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: ./.github/actions/br-setup | |
| with: | |
| output-dir: /mnt/br/output/${{ matrix.arch }} | |
| arch: ${{ matrix.arch }} | |
| ccache-dir: /mnt/br/ccache/${{ matrix.arch }} | |
| ccache-max-size: ${{ env.CCACHE_MAX_SIZE }} | |
| min-free-gb: '60' | |
| - name: Build the host SPIR-V stack | |
| run: | | |
| set -eu | |
| FORCE_UNSAFE_CONFIGURE=1 make -C "$BR_B" O="$BR_O" BR2_EXTERNAL="$BR_EXT" \ | |
| BR2_DL_DIR=/mnt/br/dl BR2_CCACHE_DIR="$CCACHE_DIR" -j"$BR_JOBS" \ | |
| host-spirv-headers host-spirv-tools host-spirv-llvm-translator | |
| "$BR_O/host/bin/spirv-as" --version | |
| # The translator is the piece libclc needs, and the one that would | |
| # fail on an RTTI mismatch with host-llvm rather than at link time. | |
| test -x "$BR_O/host/bin/llvm-spirv" || { | |
| echo "host-spirv-llvm-translator installed no llvm-spirv" >&2; exit 1; } | |
| "$BR_O/host/bin/llvm-spirv" --version | |
| - name: Return the workspace to the runner user | |
| if: always() | |
| run: chown -R "$(stat -c '%u:%g' "$GITHUB_WORKSPACE")" "$GITHUB_WORKSPACE" || true | |
| # libclc: the OpenCL builtins, compiled to LLVM bitcode by the host clang. | |
| # No extra-config - libclc.mk:44 sets HOST_LIBCLC_CONF_OPTS with no ifeq | |
| # guards, so the host build does not vary with the profile. | |
| host-libclc: | |
| name: Host libclc (${{ matrix.arch }}) | |
| needs: host-spirv | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| arch: [x86_64, arm64, armv7, i386] | |
| runs-on: [self-hosted, Linux, X64] | |
| container: | |
| image: docker.io/colemancda/buildroot-swift:latest | |
| options: --volume /mnt:/mnt --security-opt label=disable | |
| timeout-minutes: 480 | |
| env: | |
| CCACHE_DIR: /mnt/br/ccache/${{ matrix.arch }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: ./.github/actions/br-setup | |
| with: | |
| output-dir: /mnt/br/output/${{ matrix.arch }} | |
| arch: ${{ matrix.arch }} | |
| ccache-dir: /mnt/br/ccache/${{ matrix.arch }} | |
| ccache-max-size: ${{ env.CCACHE_MAX_SIZE }} | |
| min-free-gb: '60' | |
| - name: Build host-libclc | |
| run: | | |
| set -eu | |
| FORCE_UNSAFE_CONFIGURE=1 make -C "$BR_B" O="$BR_O" BR2_EXTERNAL="$BR_EXT" \ | |
| BR2_DL_DIR=/mnt/br/dl BR2_CCACHE_DIR="$CCACHE_DIR" -j"$BR_JOBS" \ | |
| host-libclc | |
| # The deliverable is bitcode - except here it isn't. This tree's | |
| # HOST_LIBCLC_CONF_OPTS builds only the spirv64-mesa3d- target | |
| # (libclc.mk), and that target's final linked builtins are | |
| # translated to SPIR-V and installed as a .spv, not left as .bc: | |
| # | |
| # -- Installing: .../host/share/clc/spirv64-mesa3d-.spv | |
| # | |
| # Every *.cl.bc under the build tree is an intermediate object, | |
| # never installed; asserting on .bc here always failed, on every | |
| # architecture, and never caught a real regression - it just | |
| # never matched what this target actually produces. | |
| find "$BR_O/host" -name '*.spv' -path '*clc*' -print -quit | grep -q . || { | |
| echo "host-libclc installed no .spv builtins under $BR_O/host" >&2; exit 1; } | |
| find "$BR_O/host" -name '*.spv' -path '*clc*' | head -5 | |
| - name: Return the workspace to the runner user | |
| if: always() | |
| run: chown -R "$(stat -c '%u:%g' "$GITHUB_WORKSPACE")" "$GITHUB_WORKSPACE" || true | |
| # host-mesa3d: the precompiled-shader compilers a driver needs at BUILD time. | |
| # This is the terminus of the whole LLVM branch - target mesa3d depends on it | |
| # whenever BR2_PACKAGE_MESA3D_NEEDS_PRECOMP_COMPILER is set, which every | |
| # panfrost, iris and radeonsi board sets (mesa3d.mk:181, | |
| # -Dmesa-clc=system -Dprecomp-compiler=system). | |
| # | |
| # It is the one job here where a naive "make host-mesa3d" would silently | |
| # build the wrong thing. HOST_MESA3D_CONF_OPTS is assembled from the | |
| # SELECTED drivers: | |
| # | |
| # HOST_MESA3D_GALLIUM_DRIVERS-$(..._GALLIUM_DRIVER_IRIS) += iris | |
| # HOST_MESA3D_GALLIUM_DRIVERS-$(..._GALLIUM_DRIVER_PANFROST) += panfrost | |
| # HOST_MESA3D_TOOLS += panfrost # if either panfrost driver is on | |
| # | |
| # In an sdk tree none are selected, so it would configure | |
| # "-Dgallium-drivers= -Dtools=" and produce no per-driver compiler at all - | |
| # and the stamps would still say "built", so a device build reusing the | |
| # seeded tree would skip it and end up with llvmpipe-only GL. Exactly what | |
| # sdk/defconfig/gpu/panfrost.config warns about. | |
| # | |
| # So configure the union of what any board here uses. Only iris and panfrost | |
| # reach the host side; neither has an arch constraint (both only "depends on | |
| # BR2_PACKAGE_MESA3D_LLVM"), so one superset serves all four architectures. | |
| # br-setup asserts each line survived kconfig. | |
| host-mesa3d: | |
| name: Host mesa3d (${{ matrix.arch }}) | |
| needs: host-libclc | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| arch: [x86_64, arm64, armv7, i386] | |
| runs-on: [self-hosted, Linux, X64] | |
| container: | |
| image: docker.io/colemancda/buildroot-swift:latest | |
| options: --volume /mnt:/mnt --security-opt label=disable | |
| timeout-minutes: 600 | |
| env: | |
| CCACHE_DIR: /mnt/br/ccache/${{ matrix.arch }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: ./.github/actions/br-setup | |
| with: | |
| output-dir: /mnt/br/output/${{ matrix.arch }} | |
| arch: ${{ matrix.arch }} | |
| ccache-dir: /mnt/br/ccache/${{ matrix.arch }} | |
| ccache-max-size: ${{ env.CCACHE_MAX_SIZE }} | |
| min-free-gb: '60' | |
| extra-config: | | |
| BR2_PACKAGE_MESA3D=y | |
| BR2_PACKAGE_MESA3D_LLVM=y | |
| BR2_PACKAGE_MESA3D_GALLIUM_DRIVER_IRIS=y | |
| BR2_PACKAGE_MESA3D_GALLIUM_DRIVER_PANFROST=y | |
| BR2_PACKAGE_MESA3D_VULKAN_DRIVER_PANFROST=y | |
| - name: Build host-mesa3d | |
| run: | | |
| set -eu | |
| FORCE_UNSAFE_CONFIGURE=1 make -C "$BR_B" O="$BR_O" BR2_EXTERNAL="$BR_EXT" \ | |
| BR2_DL_DIR=/mnt/br/dl BR2_CCACHE_DIR="$CCACHE_DIR" -j"$BR_JOBS" \ | |
| host-mesa3d | |
| # Assert the artifacts, not the exit code. A host-mesa3d configured | |
| # with no drivers builds and installs happily; it just produces | |
| # nothing a board can use, and the stamp then hides that forever. | |
| # | |
| # The binary is mesa_clc (underscore) - Meson's target name, not the | |
| # -Dinstall-mesa-clc (hyphenated) option that requests it: | |
| # | |
| # /usr/bin/install -D -m 0755 .../src/compiler/clc/mesa_clc $BR_O/host/bin/mesa_clc | |
| # host-mesa3d installed no mesa-clc (-Dinstall-mesa-clc=true did not take) | |
| # | |
| # so the check was failing on every architecture regardless of | |
| # whether mesa_clc actually landed. | |
| test -x "$BR_O/host/bin/mesa_clc" || { | |
| echo "host-mesa3d installed no mesa_clc (-Dinstall-mesa-clc=true did not take)" >&2 | |
| exit 1; } | |
| ls "$BR_O"/host/bin/*panfrost* >/dev/null 2>&1 || { | |
| echo "host-mesa3d built no panfrost precomp compiler - it was configured" >&2 | |
| echo "with an empty driver list, which is the silent failure this job exists" >&2 | |
| echo "to catch. Check that the extra-config symbols survived kconfig." >&2 | |
| ls "$BR_O/host/bin" >&2 | |
| exit 1; } | |
| ls -la "$BR_O"/host/bin/mesa_clc "$BR_O"/host/bin/*panfrost* | |
| - name: Return the workspace to the runner user | |
| if: always() | |
| run: chown -R "$(stat -c '%u:%g' "$GITHUB_WORKSPACE")" "$GITHUB_WORKSPACE" || true | |
| # host-glslang: the GLSL-to-SPIR-V compiler, as a build-time tool. | |
| # | |
| # mesa3d-demos compiles its Vulkan demos' shaders during the build and looks | |
| # glslangValidator up as a host program, so without this every consumer of a | |
| # seeded tree stops at | |
| # | |
| # ERROR: Program 'glslangValidator' not found or not executable | |
| # | |
| # which is what failed all four App SDK jobs. It belongs here rather than in | |
| # the SDK or image workflow for the same reason every other host tool does: | |
| # built once per architecture, shipped in the toolchain tarball, and then | |
| # free for everything that seeds from it. | |
| host-glslang: | |
| name: Host glslang (${{ matrix.arch }}) | |
| needs: host-mesa3d | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| arch: [x86_64, arm64, armv7, i386] | |
| runs-on: [self-hosted, Linux, X64] | |
| container: | |
| image: docker.io/colemancda/buildroot-swift:latest | |
| options: --volume /mnt:/mnt --security-opt label=disable | |
| timeout-minutes: 240 | |
| env: | |
| CCACHE_DIR: /mnt/br/ccache/${{ matrix.arch }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: ./.github/actions/br-setup | |
| with: | |
| output-dir: /mnt/br/output/${{ matrix.arch }} | |
| arch: ${{ matrix.arch }} | |
| ccache-dir: /mnt/br/ccache/${{ matrix.arch }} | |
| ccache-max-size: ${{ env.CCACHE_MAX_SIZE }} | |
| min-free-gb: '60' | |
| - name: Build host-glslang | |
| run: | | |
| set -eu | |
| FORCE_UNSAFE_CONFIGURE=1 make -C "$BR_B" O="$BR_O" BR2_EXTERNAL="$BR_EXT" \ | |
| BR2_DL_DIR=/mnt/br/dl BR2_CCACHE_DIR="$CCACHE_DIR" -j"$BR_JOBS" \ | |
| host-glslang | |
| # Assert the name the consumers actually invoke. Upstream installs | |
| # the binary as "glslang" and leaves glslangValidator as a symlink | |
| # to it, so testing for the binary alone would pass while the name | |
| # meson looks for was missing. | |
| test -x "$BR_O/host/bin/glslangValidator" || { | |
| echo "host-glslang installed no glslangValidator under $BR_O/host" >&2 | |
| ls "$BR_O/host/bin" | grep -i glslang >&2 || true | |
| exit 1; } | |
| "$BR_O/host/bin/glslangValidator" --version | |
| - name: Return the workspace to the runner user | |
| if: always() | |
| run: chown -R "$(stat -c '%u:%g' "$GITHUB_WORKSPACE")" "$GITHUB_WORKSPACE" || true | |
| # Everything the sdk profile still wants on top of the host tools above - | |
| # the target libraries and the Swift runtime - then package and publish. | |
| toolchain: | |
| name: Toolchain (${{ matrix.arch }}) | |
| needs: [host-swift, host-glslang] | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| arch: [x86_64, arm64, armv7, i386] | |
| runs-on: [self-hosted, Linux, X64] | |
| container: | |
| image: docker.io/colemancda/buildroot-swift:latest | |
| options: --volume /mnt:/mnt --security-opt label=disable | |
| timeout-minutes: 1440 | |
| permissions: | |
| contents: write # uploads assets to the toolchain-latest release | |
| env: | |
| CCACHE_DIR: /mnt/br/ccache/${{ matrix.arch }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: ./.github/actions/br-setup | |
| with: | |
| output-dir: /mnt/br/output/${{ matrix.arch }} | |
| arch: ${{ matrix.arch }} | |
| ccache-dir: /mnt/br/ccache/${{ matrix.arch }} | |
| ccache-max-size: ${{ env.CCACHE_MAX_SIZE }} | |
| min-free-gb: '100' | |
| # Point this architecture's tree at the shared host-swift with a symlink. | |
| # swift.mk guards both expensive steps on directories existing | |
| # (swift-source for configure, build/llvm for the build), so a populated | |
| # tree makes them no-ops and only the per-arch install runs. | |
| # | |
| # On the self-hosted runner the shared tree is already there from the | |
| # producing job, and the download is skipped entirely. | |
| # | |
| # The same action runs in the SDK and image workflows. It was inline here | |
| # first, and their not having it is what let a consumer build get ninety | |
| # minutes in before the target swift package went looking for a compiler | |
| # the seed never carried. | |
| - name: Fetch the shared host-swift | |
| uses: ./.github/actions/host-swift | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| with: | |
| build-dir: /mnt/br/output/${{ matrix.arch }}/build | |
| shared-dir: ${{ env.SHARED_DIR }} | |
| version: ${{ needs['host-swift'].outputs.version }} | |
| - name: Build the rest of the sdk profile | |
| run: | | |
| set -eu | |
| FORCE_UNSAFE_CONFIGURE=1 make -C "$BR_B" O="$BR_O" BR2_EXTERNAL="$BR_EXT" \ | |
| BR2_DL_DIR=/mnt/br/dl BR2_CCACHE_DIR="$CCACHE_DIR" -j"$BR_JOBS" | |
| # The published tree is the whole output dir minus the bulky per-package | |
| # build dirs - but each build dir's stamp files must survive, because they | |
| # are what tells a consuming Buildroot "already built, skip". The | |
| # extracted sources themselves are only needed to REbuild. | |
| # | |
| # Built from a hardlinked staging copy rather than by pruning in place: | |
| # on this runner the output tree persists, and deleting its sources to | |
| # make a tarball would throw away the incremental build the next run | |
| # wants. Hardlinks make the copy cost inodes and no bytes. | |
| - name: Package output tree | |
| run: | | |
| set -eu | |
| A=${{ matrix.arch }} | |
| O=/mnt/br/output/$A | |
| S=/mnt/br/pkg/$A | |
| rm -rf "$S" | |
| mkdir -p "$S/build" | |
| if ! find "$O" -mindepth 1 -maxdepth 1 ! -name build -exec cp -al {} "$S"/ \; ; then | |
| echo "hardlinking failed (different filesystem?); falling back to a full copy" >&2 | |
| rm -rf "$S" | |
| mkdir -p "$S/build" | |
| find "$O" -mindepth 1 -maxdepth 1 ! -name build -exec cp -a {} "$S"/ \; | |
| fi | |
| # build/'s own top-level files (packages-file-list.txt, build-time.log) | |
| # ship; the package subdirectories ship only their stamps. | |
| find "$O/build" -mindepth 1 -maxdepth 1 -type f -exec cp -a {} "$S/build"/ \; | |
| # A stamp without its sources tells the consumer "already done", | |
| # which is what makes seeding cheap. Every package ships one, host | |
| # and target alike, with the exclusions below. | |
| # | |
| # Target stamps were dropped once, on the theory that a consumer | |
| # building a different profile invalidates them. That was the wrong | |
| # reading of one failure. Buildroot does not re-run a step whose | |
| # stamp exists, whatever the .config now says - which is exactly how | |
| # the self-hosted runner behaves when it builds the image profile | |
| # over the sdk profile in one tree. What actually re-ran was a | |
| # kconfig-package: those re-enter their config step on every make, | |
| # and with no sources behind the stamp busybox stopped at | |
| # | |
| # merged configuration written to .../busybox-1.38.0/.config | |
| # make[1]: *** No rule to make target 'oldconfig'. Stop. | |
| # | |
| # So the exclusion is kconfig-packages, not target packages. And | |
| # dropping every target stamp had a second cost that took three more | |
| # runs to surface: the consumer rebuilt packages the seed had already | |
| # installed into the sysroot, and swift-foundation cannot survive | |
| # that - it builds against the very headers its last install left | |
| # behind, and stops at | |
| # | |
| # sysroot/usr/lib/swift/_FoundationCShims/module.modulemap:1:8: | |
| # error: redefinition of module '_FoundationCShims' | |
| # note: previously defined here (swift-source/swift-foundation/...) | |
| # | |
| # Not rebuilding it is both faster and the only thing that is | |
| # correct: the seeded sysroot already holds what the toolchain job | |
| # built. | |
| # | |
| # host-icu is excluded: icu.mk cross-compiles the target build against | |
| # the host build TREE, not its installed output - | |
| # | |
| # --with-cross-build=$(HOST_ICU_DIR)/source | |
| # | |
| # so a stamp saying "built" with no sources behind it leaves the | |
| # target configure stopping at | |
| # | |
| # configure: error: No such directory .../build/host-icu-78.2/source | |
| # supplied as the argument to --with-cross-build | |
| # | |
| # Letting it rebuild in the consumer costs minutes and is correct. | |
| # | |
| # host-swift is the other one, and is excluded for the same reason | |
| # with a different remedy. swift.mk compiles the target stdlib with | |
| # $(HOST_SWIFT_DIR)/build/usr/bin/clang and configures it against | |
| # $(HOST_SWIFT_DIR)/swift-source/...; swift-foundation reads | |
| # $(HOST_SWIFT_SRCDIR) and pkg-swift.mk $(HOST_SWIFT_SUPPORT_DIR). | |
| # This step used to fabricate a stamps-only host-swift directory | |
| # here, and the target swift package duly stopped at | |
| # | |
| # CMAKE_C_COMPILER: .../build/host-swift-<ver>/build/usr/bin/clang | |
| # is not a full path to an existing compiler tool | |
| # | |
| # ninety minutes into the consumer's build. Rebuilding it there is | |
| # not an option the way it is for icu - that is a five-to-six-hour | |
| # build-script run - so consumers materialise the real tree from the | |
| # host-swift-latest release instead, via .github/actions/host-swift. | |
| # Shipping no host-swift directory at all is what lets that action | |
| # drop a symlink at the path without colliding with a real one. | |
| # | |
| # These two are the whole set, but the earlier version of this | |
| # comment claimed icu alone was, having grepped only package/*.mk - | |
| # swift.mk lives in the BR2_EXTERNAL tree. The check that covers it: | |
| # | |
| # grep -rE '\$\(HOST_[A-Z0-9_]+_(DIR|SRCDIR|BUILDDIR)\)' \ | |
| # buildroot/package swift/package ports/package external | |
| # | |
| # discounting a package's references to its own host variant. Adding | |
| # a package that reads another's host build directory means adding it | |
| # here, or it fails only in a consumer, only after an hour. | |
| # | |
| # The kconfig-packages, whose config step re-runs regardless of any | |
| # stamp. From `grep -rl 'kconfig-package))' buildroot --include=*.mk`; | |
| # the external trees have none. The prefix match also catches | |
| # uboot-tools and the like, which merely rebuild - the cost of being | |
| # wrong in this direction is minutes, in the other it is a red run. | |
| KCONFIG_PKGS="at91bootstrap3 barebox busybox linux linux-backports | |
| swupdate ti-k3-r5-loader uboot uclibc xvisor" | |
| SKIP="host-icu|host-swift" | |
| for p in $KCONFIG_PKGS; do SKIP="$SKIP|$p|host-$p"; done | |
| ( cd "$O/build" && find . -mindepth 2 -maxdepth 2 \ | |
| \( -name '.stamp_*' -o -name '.br2_*' -o -name '.applied_patches_list' \) \ | |
| -print | grep -vE "^\./($SKIP)-" \ | |
| | tr '\n' '\0' | tar --null -T - -cf - ) | tar -C "$S/build" -xf - | |
| du -sh "$S" | |
| tar -C /mnt/br/pkg --zstd -cf /mnt/br/toolchain-$A.tar.zst "$A" | |
| rm -rf "$S" | |
| # Release assets cap at 2 GiB apiece: ship split parts, consumers | |
| # `cat toolchain-<arch>.tar.zst.part* | tar --zstd -x`. | |
| split -b 1900M -d /mnt/br/toolchain-$A.tar.zst \ | |
| /mnt/br/toolchain-$A.tar.zst.part | |
| rm /mnt/br/toolchain-$A.tar.zst | |
| ls -lh /mnt/br/toolchain-$A.tar.zst.part* | |
| - name: Publish to the toolchain-latest release | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| set -eu | |
| gh release view toolchain-latest --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1 || \ | |
| gh release create toolchain-latest --repo "$GITHUB_REPOSITORY" \ | |
| --title "Toolchain (rolling)" \ | |
| --notes "Rolling from-source toolchains built by build-toolchain.yml. Assets are split tar.zst parts: cat toolchain-<arch>.tar.zst.part* | tar --zstd -x" \ | |
| --latest=false | |
| # An interrupted upload leaves the release with no toolchain for this | |
| # architecture at all, and anything seeding from it fails outright. | |
| # --clobber does not prevent that: it replaces an asset of the same | |
| # name by removing the old one first, so an abort part-way through | |
| # still lands in the gap. That is exactly how armv7 lost its asset - | |
| # the build succeeded, the upload of the 1.2G part ran for twenty | |
| # minutes and died, and the previous asset was already gone. | |
| # | |
| # Nothing here can close the window (the API has no atomic replace | |
| # and no rename, and br-seed globs the part names so a staged name is | |
| # not an option), so retry each part until it lands, and refuse to | |
| # prune anything until the whole set is confirmed present. | |
| for f in $(cd /mnt/br && ls toolchain-${{ matrix.arch }}.tar.zst.part*); do | |
| ok= | |
| for attempt in 1 2 3 4 5; do | |
| if gh release upload toolchain-latest "/mnt/br/$f" \ | |
| --repo "$GITHUB_REPOSITORY" --clobber; then | |
| ok=yes | |
| break | |
| fi | |
| echo "upload of $f failed (attempt $attempt of 5), retrying" >&2 | |
| sleep $((attempt * 30)) | |
| done | |
| [ -n "$ok" ] || { echo "giving up on $f - the release is now missing this part" >&2; exit 1; } | |
| done | |
| # Confirm every part is on the release at its full size before the | |
| # prune below is allowed to delete anything. A short asset means the | |
| # upload was truncated, and seeding from a truncated part fails in a | |
| # far more confusing way than failing here does. | |
| gh release view toolchain-latest --repo "$GITHUB_REPOSITORY" \ | |
| --json assets --jq '.assets[] | "\(.name) \(.size)"' > /tmp/assets.$$ | |
| for f in $(cd /mnt/br && ls toolchain-${{ matrix.arch }}.tar.zst.part*); do | |
| want=$(stat -c %s "/mnt/br/$f") | |
| got=$(awk -v n="$f" '$1 == n { print $2 }' /tmp/assets.$$) | |
| [ "$got" = "$want" ] || { | |
| echo "$f is ${got:-absent} on the release, expected $want bytes" >&2 | |
| rm -f /tmp/assets.$$ | |
| exit 1 | |
| } | |
| done | |
| rm -f /tmp/assets.$$ | |
| # Parts are numbered, so a tree that now splits into fewer parts than | |
| # last time would otherwise leave the extras behind and corrupt the | |
| # concatenation. Remove any that this run did not just upload. | |
| keep=$(cd /mnt/br && ls toolchain-${{ matrix.arch }}.tar.zst.part* | tr '\n' ' ') | |
| for a in $(gh release view toolchain-latest --repo "$GITHUB_REPOSITORY" \ | |
| --json assets --jq '.assets[].name' \ | |
| | grep "^toolchain-${{ matrix.arch }}\." || true); do | |
| case " $keep " in | |
| *" $a "*) ;; | |
| *) echo "removing stale asset $a" | |
| gh release delete-asset toolchain-latest "$a" --repo "$GITHUB_REPOSITORY" -y ;; | |
| esac | |
| done | |
| # Reproducible from the output tree, and this disk is not thrown away. | |
| rm -f /mnt/br/toolchain-${{ matrix.arch }}.tar.zst.part* | |
| - name: ccache stats | |
| if: always() | |
| run: | | |
| ccache -d "$CCACHE_DIR" -s 2>/dev/null || true | |
| du -sh "$CCACHE_DIR" /mnt/br/dl || true | |
| df -h / /mnt | |
| # See the host-swift job: root-owned files in a workspace that outlives | |
| # the job break the next run's checkout. | |
| - name: Return the workspace to the runner user | |
| if: always() | |
| run: chown -R "$(stat -c '%u:%g' "$GITHUB_WORKSPACE")" "$GITHUB_WORKSPACE" || true |