Host-side static ELF checks (EI_OSABI/ET_EXEC/no-PT_INTERP, already asserted by
TInternalLinkerE2ETests.TestCompile_FreeBSDTarget_EmitsStaticFreeBSDExe) cannot
catch a wrong FreeBSD struct-stat offset or syscall number — only running the
binary on a real FreeBSD kernel does.
Add a freebsd-crosscheck CI job (needs: bootstrap) that:
* downloads the -pre binary the bootstrap job built,
* cross-compiles the fixtures in compiler/src/it/freebsd (hello, fileio) with
--target freebsd-x86_64 on the Linux runner via
scripts/freebsd-crosscheck-build.sh (which also sanity-checks each emitted
ELF is a FreeBSD ET_EXEC), and
* boots a FreeBSD VM (vmactions/freebsd-vm, release 14.2 to match the
struct-stat offsets in rtl.platform.layout.freebsd) and runs the binaries
there, asserting hello prints 'Hello' and fileio round-trips a file
(write/read/stat/delete) with 'file-io-ok'/'exists-ok'/'done'.
The FreeBSD output is a static, freestanding raw-syscall ET_EXEC, so the VM
needs no extra packages to run it. fileio is the meaningful check: it exercises
open/write/read/stat/unlink through the FreeBSD leaf and layout, which the
static checks are blind to.
353 lines
16 KiB
YAML
353 lines
16 KiB
YAML
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
|
|
# "<toolchain-missing>" / "[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/<sha>/ so the script can
|
|
# resume with --from <sha> 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/<sha>/ and set FROM=<sha>.
|
|
# - 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<version>-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"
|