name: Rolling bootstrap & tests # Triggers: # - push to master : keep the -pre binary fresh on every commit # - push to ci/** , feature/**, bootstrap/** # : validate-only pre-flight for risky changes — does # the rolling bootstrap still succeed? These runs do # NOT write the carry-forward cache or publish the # -pre artifact (see the master-only guards on those # steps), so a broken branch cannot affect master # consumers or other developers. Use a throwaway # branch under one of these prefixes to prove a # change before merging it to master. # - workflow_dispatch : manual run from the Actions UI, against any ref on: push: branches: - master - 'ci/**' - 'feature/**' - 'bootstrap/**' workflow_dispatch: inputs: force_full: description: "Ignore cached -pre binary and bootstrap from the release tag" type: boolean default: false # Cancel an in-flight run on master when a newer commit lands — no point # bootstrapping a commit that is already superseded. concurrency: group: bootstrap-${{ github.ref }} cancel-in-progress: true permissions: contents: read env: # Cold-start anchor: the newest *real* release whose binary is published as a # GitHub Release asset. Used only when no cached -pre binary is available. # v0.12.0 is the clean rolling-bootstrap anchor: the broken v0.11.0->v0.12.0 # commit range is BEFORE it, so the replay range (v0.12.0..HEAD) is all # native-default + internal-toolchain and needs neither QBE nor gcc. STAGE1_TAG: v0.12.0 jobs: bootstrap: runs-on: ubuntu-latest timeout-minutes: 120 # full ~190-commit replay is slow; incremental is fast steps: # -------------------------------------------------------------------- # 1. Check out the full history. rolling-bootstrap.sh replays commits, # so a shallow clone is useless — fetch-depth: 0 gets everything. # -------------------------------------------------------------------- - name: Checkout (full history) uses: actions/checkout@v6 with: fetch-depth: 0 # -------------------------------------------------------------------- # 2. Toolchain. From the v0.12.0 anchor onward the bootstrap chain is # native-default with the internal assembler + linker, so neither QBE # nor gcc is needed to replay it — only make (runtime Makefile) and the # distro CRT objects (build-essential) the internal linker reads. # -------------------------------------------------------------------- - name: Install build tools run: | sudo apt-get update -qq sudo apt-get install -y build-essential unzip # -------------------------------------------------------------------- # 3b. Build vendored QBE. The bootstrap chain is native-default and does # NOT need QBE, but the E2E test suite still shells out to it (the # QBE backend arm of AssertRunsOnAll, plus the toolchain-gated # round-trip tests). Without it ~218 tests fail with # "" / "[qbe] compile+run". It is vendored, so a # plain `make` builds the binary the tests' BLAISE_QBE fallback finds. # -------------------------------------------------------------------- - name: Build vendored QBE run: make -C vendor/qbe # -------------------------------------------------------------------- # 4. Restore the carry-forward cache: the previous successful master run # stashes its -pre binary + a marker file recording the commit SHA it # was built at. We restore them into releases// so the script can # resume with --from instead of replaying from the release tag. # # Keyed by run id so every run writes a fresh entry; restore-keys does # a prefix match to pull the most recent prior cache. (A fixed key # would never update — caches are immutable once written.) # -------------------------------------------------------------------- - name: Restore bootstrap cache id: cache-restore if: ${{ !inputs.force_full }} uses: actions/cache/restore@v5 with: path: .bootstrap-cache key: blaise-bootstrap-${{ github.run_id }} restore-keys: | blaise-bootstrap- # -------------------------------------------------------------------- # 5. Decide the start point. # - If a cached binary exists AND its marker SHA is an ancestor of # HEAD, stage it into releases// and set FROM=. # - Otherwise fall back to the published release tag (cold start), # downloading its binary asset from the GitHub Release. # -------------------------------------------------------------------- - name: Resolve start binary id: resolve run: | set -euo pipefail FROM="" CACHE_BIN=".bootstrap-cache/blaise" CACHE_RTL=".bootstrap-cache/blaise_rtl.a" CACHE_MARK=".bootstrap-cache/commit.sha" if [ -x "$CACHE_BIN" ] && [ -f "$CACHE_MARK" ]; then SHA="$(cat "$CACHE_MARK")" if git cat-file -e "${SHA}^{commit}" 2>/dev/null \ && git merge-base --is-ancestor "$SHA" HEAD 2>/dev/null; then echo "Cached -pre binary found at commit $SHA — resuming from there." mkdir -p "releases/$SHA" cp "$CACHE_BIN" "releases/$SHA/blaise" cp "$CACHE_RTL" "releases/$SHA/blaise_rtl.a" chmod +x "releases/$SHA/blaise" FROM="$SHA" else echo "Cached binary's commit ($SHA) is not an ancestor of HEAD — ignoring it." fi else echo "No cached binary present." fi if [ -z "$FROM" ]; then echo "Cold start from release tag $STAGE1_TAG." DEST="releases/$STAGE1_TAG" mkdir -p "$DEST" # The stage-1 release ships as a versioned tarball, not bare assets. curl -fsSL -o /tmp/stage1.tar.gz \ "https://github.com/graemeg/blaise/releases/download/${STAGE1_TAG}/blaise-${STAGE1_TAG}-linux-x86_64.tar.gz" mkdir -p /tmp/stage1 tar -xzf /tmp/stage1.tar.gz -C /tmp/stage1 # Normalise to the names rolling-bootstrap.sh expects, wherever the # files landed inside the archive. BIN="$(find /tmp/stage1 -type f -name blaise | head -1)" RTL="$(find /tmp/stage1 -type f -name blaise_rtl.a | head -1)" if [ -z "$BIN" ] || [ -z "$RTL" ]; then echo "::error::blaise / blaise_rtl.a not found inside the stage-1 tarball." find /tmp/stage1 -maxdepth 2 -type f | sed 's/^/ /' exit 1 fi cp "$BIN" "$DEST/blaise" cp "$RTL" "$DEST/blaise_rtl.a" chmod +x "$DEST/blaise" FROM="$STAGE1_TAG" fi echo "from=$FROM" >> "$GITHUB_OUTPUT" # -------------------------------------------------------------------- # 6. Run the rolling bootstrap. Installs the final -pre binary under # releases/v-pre/ (the script computes the dir from # Blaise.pas's Version constant). # -------------------------------------------------------------------- - name: Rolling bootstrap run: | set -euo pipefail ./scripts/rolling-bootstrap.sh --from "${{ steps.resolve.outputs.from }}" --to HEAD # -------------------------------------------------------------------- # 7. Locate the freshly-installed -pre binary for the steps that follow. # -------------------------------------------------------------------- - name: Locate -pre binary id: pre run: | set -euo pipefail PRE_DIR="$(ls -d releases/v*-pre 2>/dev/null | sort -V | tail -1)" if [ -z "$PRE_DIR" ] || [ ! -x "$PRE_DIR/blaise" ]; then echo "No -pre binary produced — bootstrap must have failed." >&2 exit 1 fi echo "Using $PRE_DIR/blaise" echo "dir=$PRE_DIR" >> "$GITHUB_OUTPUT" echo "bin=$PWD/$PRE_DIR/blaise" >> "$GITHUB_OUTPUT" # -------------------------------------------------------------------- # 8. Install RTL + build the test runner with the -pre binary, then run # the full test suite. PasBuild is NOT on the runner, so we mirror the # documented manual build path: make install for the RTL, then # pasbuild only if available — otherwise we fall back to the binary's # own test-compile. (Adjust here if PasBuild gets installed below.) # -------------------------------------------------------------------- - name: Build RTL with -pre binary run: | set -euo pipefail BLAISE="${{ steps.pre.outputs.bin }}" # Native default backend builds the RTL with no external tools. ( cd runtime && make BLAISE="$BLAISE" && make BLAISE="$BLAISE" install ) - name: Install PasBuild run: | set -euo pipefail curl -fsSL -o /tmp/pasbuild.zip \ https://github.com/graemeg/PasBuild/releases/download/v1.9.0/pasbuild-1.9.0-x86_64-linux.zip unzip -o -q /tmp/pasbuild.zip -d /tmp/pasbuild # The binary may sit at the zip root or inside a subdir — find it. PB="$(find /tmp/pasbuild -type f -name pasbuild | head -1)" if [ -z "$PB" ]; then echo "::error::pasbuild binary not found inside the release zip." find /tmp/pasbuild -maxdepth 2 -type f | sed 's/^/ /' exit 1 fi chmod +x "$PB" sudo install -m 0755 "$PB" /usr/local/bin/pasbuild pasbuild --version || true - name: Build test runner & run tests run: | set -euo pipefail BLAISE="${{ steps.pre.outputs.bin }}" pasbuild test-compile -m blaise-compiler --compiler "$BLAISE" # Point the CLI-contract and internal-assembler suites at the # bootstrapped -pre binary. Without this they fall back to the # /tmp/fp_blaise* fixpoint binaries (which only exist after a local # fixpoint.sh run) and silently Ignore ~24 tests in CI. # BLAISE_QBE points the E2E suite at the vendored QBE built above # (its fallback is vendor/qbe/qbe, but set it explicitly). BLAISE_QBE="$PWD/vendor/qbe/qbe" \ BLAISE_QBE_COMPILER="$BLAISE" compiler/target/TestRunner # -------------------------------------------------------------------- # 9. Save the carry-forward cache for the next run: the -pre binary, its # rtl, and a marker recording the HEAD SHA it was built at. # MASTER ONLY: the carry-forward cache is the resume anchor for master # runs. A branch run must never overwrite it — otherwise a feature # branch's -pre binary would leak into the next master run and a broken # branch could poison master's bootstrap. Branch runs are validate-only. # -------------------------------------------------------------------- - name: Stage carry-forward cache if: ${{ success() && github.ref == 'refs/heads/master' }} run: | set -euo pipefail rm -rf .bootstrap-cache mkdir -p .bootstrap-cache cp "${{ steps.pre.outputs.dir }}/blaise" .bootstrap-cache/blaise cp "${{ steps.pre.outputs.dir }}/blaise_rtl.a" .bootstrap-cache/blaise_rtl.a git rev-parse HEAD > .bootstrap-cache/commit.sha echo "Marked cache at $(cat .bootstrap-cache/commit.sha)" - name: Save bootstrap cache if: ${{ success() && github.ref == 'refs/heads/master' }} uses: actions/cache/save@v5 with: path: .bootstrap-cache key: blaise-bootstrap-${{ github.run_id }} # -------------------------------------------------------------------- # 10. Publish the -pre binary + rtl as downloadable workflow artifacts. # Runs on branches too (the name is keyed by commit SHA, so a branch # artifact never collides with master's and is handy for inspecting a # pre-flight build). Unlike the cache, an artifact is pull-only — it # is never consumed by a later run, so it cannot affect master. # -------------------------------------------------------------------- - name: Upload -pre binary artifact if: success() uses: actions/upload-artifact@v7 with: name: blaise-pre-${{ github.sha }} path: | ${{ steps.pre.outputs.dir }}/blaise ${{ steps.pre.outputs.dir }}/blaise_rtl.a retention-days: 30 if-no-files-found: error # ====================================================================== # FreeBSD cross-compile verification under emulation # (docs/freebsd-x86_64-backend-design.adoc Step 8). # # The bootstrap job's TestRunner already runs the HOST-side static ELF # checks (EI_OSABI / ET_EXEC / no PT_INTERP via # TInternalLinkerE2ETests.TestCompile_FreeBSDTarget_EmitsStaticFreeBSDExe). # Those cannot catch a wrong FreeBSD struct-stat offset or syscall number — # only running the binary on a real FreeBSD kernel does. This job # cross-compiles the fixtures on the Linux runner, then boots a FreeBSD VM # (vmactions/freebsd-vm) and executes them there. # # The FreeBSD output is a static, freestanding raw-syscall ET_EXEC (no libc, # no rtld), so the VM needs no extra packages to run it. # ====================================================================== freebsd-crosscheck: needs: bootstrap runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout uses: actions/checkout@v6 # The RTL is compiled from source by the -pre binary, so the compiler # needs no prebuilt blaise_rtl.a beside it — only the checked-out RTL # source (present after checkout). Pull the -pre binary the bootstrap # job just built and published. - name: Download -pre binary uses: actions/download-artifact@v7 with: name: blaise-pre-${{ github.sha }} path: .pre - name: Cross-compile FreeBSD fixtures (Linux host) run: | set -euo pipefail chmod +x .pre/blaise BLAISE="$PWD/.pre/blaise" scripts/freebsd-crosscheck-build.sh echo "Cross-built FreeBSD binaries:" ls -l target/freebsd-crosscheck # Boot a FreeBSD VM and run the cross-compiled binaries there. The # workspace is rsync-mounted into the VM, so target/freebsd-crosscheck/* # is available inside. release pinned to a 14.x line: the struct-stat # offsets in rtl.platform.layout.freebsd are verified against FreeBSD 14 # (stable since FreeBSD 12). Bump when 15.x is validated. - name: Run FreeBSD binaries under emulation uses: vmactions/freebsd-vm@v1 with: release: "14.2" usesh: true copyback: false run: | set -e cd target/freebsd-crosscheck echo "=== uname ===" uname -a echo "=== hello ===" OUT="$(./hello)" echo "$OUT" [ "$OUT" = "Hello" ] || { echo "FAIL: hello output '$OUT' != 'Hello'"; exit 1; } echo "=== fileio ===" OUT="$(./fileio)" echo "$OUT" # Flatten to a single line (newlines -> '|') so the compare stays a # one-line shell string inside this YAML block scalar. GOT="$(printf '%s' "$OUT" | tr '\n' '|')" [ "$GOT" = "file-io-ok|exists-ok|done" ] || { echo "FAIL: fileio output '$GOT' != 'file-io-ok|exists-ok|done'" exit 1 } echo "FREEBSD_EMULATION_OK"