Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 42 additions & 30 deletions .github/workflows/build-binaries.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,25 @@
# Each platform builds from its OWN committed seed (boot/seed_mac / boot/seed_linux), so there is no
# cross-emit and no foreign toolchain — only the system `cc` and the vendored qbe:
# darwin-arm64 → macOS runner, native `build.sh`
# darwin-amd64 → Intel macOS runner (macos-13), native `build.sh darwin-amd64` (shares the mac seed
# IL with arm64 — the amd64 `syscall` class bit lives in the floor, not the IL)
# linux-arm64 → ubuntu-arm runner, native `build.sh linux-arm64`
# linux-amd64 → ubuntu x86-64 runner, native `build.sh linux-amd64` (its OWN seed IL — x86-64's
# linux syscall numbers + struct-stat layout differ from the generic arm64/riscv table)
# linux-riscv64 → ubuntu-arm runner, `build.sh linux-riscv64` cross-linked with the distro's
# gcc-riscv64-linux-gnu (shared linux IL, riscv floor)
# Every binary is stamped with the release version via lib/std/compiler/buildinfo.cf, so it reports
# itself through `std::comptime::version::compiler`.
#
# STATUS: all three targets validated. darwin byte-reproduces the seed. Both linux targets are static
# musl (the floor calls musl's `__init_libc`): linux-arm64 built + ran natively on a real aarch64 Linux
# VM; linux-riscv64 cross-built (musl.cc toolchain) + ran under qemu-riscv64 — cf compiles+runs programs
# and its embedded QBE works under the custom `_start` on both. The riscv leg downloads its musl cross
# from musl.cc (the one external fetch here).
# STATUS: darwin-arm64/darwin-amd64/linux-arm64/linux-amd64/linux-riscv64 validated. darwin
# byte-reproduces the seed (arm64 natively; amd64 under Rosetta 2 — arm64/amd64 emit identical IL). The
# linux targets are static musl (the floor calls musl's `__init_libc`): linux-arm64 built + ran natively
# on a real aarch64 Linux VM; linux-amd64 built + ran under qemu-x86_64 (its own seed IL, distinct x86-64
# syscall numbers); linux-riscv64 cross-built (musl cross toolchain) + ran under qemu-riscv64 — cf
# compiles+runs programs and its embedded QBE works under the custom `_start` on all. The riscv leg
# downloads its
# musl cross toolchain from the cross-tools/musl-cross GitHub release (a reliable GitHub-hosted mirror;
# the old musl.cc host chronically timed out from the runners), pinned by tag + verified by sha256.
name: build-binaries

on:
Expand Down Expand Up @@ -44,10 +52,18 @@ jobs:
runner: macos-14
target: darwin-arm64
experimental: false
- name: darwin-amd64
runner: macos-13 # macos-13 is the last Intel x86-64 image; macos-14+ are arm64
target: darwin-amd64
experimental: false
- name: linux-arm64
runner: ubuntu-24.04-arm
target: linux-arm64
experimental: false
- name: linux-amd64
runner: ubuntu-24.04 # GitHub's default x86-64 runner — native, no VM/qemu/cross
target: linux-amd64
experimental: false
- name: linux-riscv64
runner: ubuntu-24.04-arm
target: linux-riscv64
Expand Down Expand Up @@ -75,44 +91,40 @@ jobs:
if: runner.os == 'Linux'
run: sudo apt-get update && sudo apt-get install -y build-essential musl-tools

# Cache the extracted toolchain (a static, versioned tarball) so we hit
# the flaky musl.cc host at most once. Kept under $HOME so actions/cache
# can read/restore it as the non-root runner user (no sudo, no /opt).
# Cache the extracted toolchain (a static, versioned tarball) so we fetch
# the release asset at most once. Kept under $HOME so actions/cache can
# read/restore it as the non-root runner user (no sudo, no /opt).
- name: Cache the riscv64 musl cross toolchain
id: riscv-toolchain
if: matrix.target == 'linux-riscv64'
uses: actions/cache@v4
with:
path: ~/riscv64-linux-musl-cross
key: riscv64-linux-musl-cross-20211123
path: ~/riscv64-unknown-linux-musl
key: riscv64-unknown-linux-musl-cross-20260823

- name: Download the riscv64 musl cross toolchain
if: matrix.target == 'linux-riscv64' && steps.riscv-toolchain.outputs.cache-hit != 'true'
run: |
tgz=riscv64-linux-musl-cross.tgz
# musl.cc is a single flaky host; download to a file (never pipe a
# possibly-truncated stream into tar), retry, then fall back to the
# official more.musl.cc mirror.
ok=
for url in \
https://musl.cc/$tgz \
https://more.musl.cc/x86_64-linux-musl/$tgz; do
echo "Fetching $url"
if curl -fL --connect-timeout 30 --retry 5 --retry-delay 10 \
--retry-all-errors -o "$tgz" "$url"; then
ok=1; break
fi
echo " failed, trying next mirror"
done
[ -n "$ok" ] || { echo "all musl.cc mirrors unreachable"; exit 1; }
tar xzf "$tgz" -C "$HOME"
rm -f "$tgz"
# Pinned cross-tools/musl-cross release — a reliable GitHub-hosted
# mirror (the old musl.cc host chronically timed out from the runners).
# Download to a file (never pipe a possibly-truncated stream into tar),
# retry, then verify against the release's published sha256.
tag=20260823
txz=riscv64-unknown-linux-musl.tar.xz
base=https://github.com/cross-tools/musl-cross/releases/download/$tag
sha=aa6630f73487e5d3eb6ffa861199c3d63dfe0db8af77452581628cd762c9a64d
curl -fL --connect-timeout 30 --retry 5 --retry-delay 10 \
--retry-all-errors -o "$txz" "$base/$txz"
echo "$sha $txz" | sha256sum -c -
tar xJf "$txz" -C "$HOME"
rm -f "$txz"

- name: Add the riscv64 toolchain to PATH
if: matrix.target == 'linux-riscv64'
run: |
echo "$HOME/riscv64-linux-musl-cross/bin" >> "$GITHUB_PATH"
echo "RISCV_CC=riscv64-linux-musl-gcc" >> "$GITHUB_ENV"
echo "$HOME/riscv64-unknown-linux-musl/bin" >> "$GITHUB_PATH"
echo "RISCV_CC=riscv64-unknown-linux-musl-gcc" >> "$GITHUB_ENV"
echo "RISCV_STRIP=riscv64-unknown-linux-musl-strip" >> "$GITHUB_ENV"

- name: Build
env:
Expand Down
81 changes: 81 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ jobs:
outputs:
version: ${{ steps.compute.outputs.version }}
created: ${{ steps.compute.outputs.created }}
ring: ${{ steps.ring.outputs.ring }}
prerelease: ${{ steps.ring.outputs.prerelease }}
steps:
- uses: actions/checkout@v4
with:
Expand Down Expand Up @@ -59,6 +61,30 @@ jobs:
echo "created=false" >> "$GITHUB_OUTPUT"
else
./var/versioning changelog "$ring" > release-notes.md

# The tool credits commits by short sha `- <title> (<sha>)`; git carries no GitHub handle.
# Resolve each bullet's sha to its author `@login` via the API and append it, so GitHub
# linkifies the mention (and builds its own Contributors list). A commit with no linked
# GitHub account falls back to the plain author name (no `@`). One API call per commit.
resolve_authors() {
while IFS= read -r line; do
case "$line" in
"- "*"("*")")
sha="$(printf '%s' "$line" | sed -n 's/.*(\([0-9a-f]\{7,40\}\))$/\1/p')"
if [ -n "$sha" ]; then
who="$(gh api "repos/{owner}/{repo}/commits/$sha" \
--jq 'if .author.login then "@" + .author.login else .commit.author.name end' \
2>/dev/null || true)"
[ -n "$who" ] && line="$line - $who"
fi
;;
esac
printf '%s\n' "$line"
done
}
resolve_authors < release-notes.md > release-notes.resolved.md
mv release-notes.resolved.md release-notes.md

prerelease=""
if [ "${{ steps.ring.outputs.prerelease }}" = "true" ]; then prerelease="--prerelease"; fi
# shellcheck disable=SC2086
Expand All @@ -78,3 +104,58 @@ jobs:
contents: write
with:
version: ${{ needs.version.outputs.version }}

# Rolling per-ring release: a release tagged with the bare ring name (nightly/rc/latest/stable) that
# is re-pointed to this commit and carries the just-built binaries under version-independent names
# `cf-<ring>-<platform>`. This is what makes a STABLE download URL possible — GitHub embeds the
# release TAG in the download path, so "latest on a ring" needs a fixed tag, not just a fixed asset
# name. The versioned releases stay the canonical, immutable ones; these rolling releases are marked
# not-latest (and inherit the ring's prerelease flag) so they never displace them as "Latest".
# https://github.com/<owner>/cf/releases/download/<ring>/cf-<ring>-<platform>
# The bare `<ring>` tags don't collide with the `<version>-<ring>` tags `versioning` matches on.
promote:
needs: [version, binaries]
if: needs.version.outputs.created == 'true'
runs-on: ubuntu-24.04
permissions:
contents: write
steps:
- name: Refresh the rolling `${{ needs.version.outputs.ring }}` release
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
RING: ${{ needs.version.outputs.ring }}
VERSION: ${{ needs.version.outputs.version }}
PRERELEASE: ${{ needs.version.outputs.prerelease }}
SHA: ${{ github.sha }}
run: |
set -eu
mkdir dist && cd dist
# Mirror the just-built versioned binaries under ring-based names, with fresh checksums.
for name in darwin-arm64 darwin-amd64 linux-arm64 linux-amd64 linux-riscv64; do
if gh release download "$VERSION" -R "$REPO" -p "cf-$VERSION-$name" 2>/dev/null; then
mv "cf-$VERSION-$name" "cf-$RING-$name"
sha256sum "cf-$RING-$name" > "cf-$RING-$name.sha256"
else
echo "note: no cf-$VERSION-$name asset — skipping"
fi
done
cat > notes.md <<EOF
Rolling **$RING** channel — always serves the latest \`$RING\` build (currently $VERSION).

Stable per-platform download URL (never changes as the ring advances):
\`https://github.com/$REPO/releases/download/$RING/cf-$RING-<platform>\`
where \`<platform>\` ∈ { darwin-arm64, darwin-amd64, linux-arm64, linux-amd64, linux-riscv64 }.
Each binary has a \`.sha256\` sibling. For an immutable pin, use the versioned release $VERSION.
EOF
# Move the rolling tag to this commit (delete + recreate) and attach the mirrored assets.
gh release delete "$RING" -R "$REPO" --cleanup-tag --yes 2>/dev/null || true
pre=""; [ "$PRERELEASE" = "true" ] && pre="--prerelease"
# shellcheck disable=SC2086
gh release create "$RING" -R "$REPO" \
--target "$SHA" \
--title "cf $RING (rolling)" \
--latest=false \
$pre \
--notes-file notes.md \
dist/*
43 changes: 33 additions & 10 deletions boot/build.sh
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
#!/bin/sh
# Build cf — the self-hosted C! compiler (the keeper) — from a committed per-platform SEED.
#
# build.sh [target] target: darwin-arm64 | linux-arm64 | linux-riscv64 (default: host)
# build.sh [target] target: darwin-arm64 | darwin-amd64 | linux-arm64 | linux-amd64 | linux-riscv64
# (default: host)
#
# The permanent trust root is `boot/seed_<os>/` — the QBE IL + freestanding floor that cf emits FOR
# ITS OWN SOURCE (a self-reproducing fixpoint). The seed is per-OS because the compiler's own I/O bakes
Expand All @@ -26,19 +27,33 @@ out="$root/var/cf"

case "$(uname -s)/$(uname -m)" in
Darwin/arm64) host=darwin-arm64 ;;
Darwin/x86_64) host=darwin-amd64 ;;
Linux/aarch64 | Linux/arm64) host=linux-arm64 ;;
Linux/x86_64) host=linux-amd64 ;;
Linux/riscv64) host=linux-riscv64 ;;
*) echo "build: unsupported host $(uname -s)/$(uname -m)" >&2; exit 1 ;;
esac
target="${1:-$host}"

# Per-platform knobs. `seed` is the OS seed dir; `qt` the qbe `-t` target; `fa` the floor's arch name
# (floor.$fa.s in the seed); `cc`/`link`/`libs` the C toolchain + link recipe (floor owns `_start`).
knobs() { # <platform> -> sets seed qt fa cc link libs
# Per-platform knobs. `seed` is the OS seed dir; `sq` the seed IL filename inside it; `qt` the qbe `-t`
# target; `fa` the floor's arch name (floor.$fa.s in the seed); `cc`/`link`/`libs` the C toolchain +
# link recipe (floor owns `_start`); `march` extra flags threaded into BOTH the target C compile and
# the link (so a cross build targets the right machine — e.g. `-arch x86_64` for darwin-amd64). Most
# platforms share their OS seed's `cf.qbe`; linux-amd64 has its OWN IL (`sq`) because x86-64's linux
# syscall numbers are baked differently than the generic arm64/riscv64 table.
knobs() { # <platform> -> sets seed sq qt fa cc link libs march
march=""; sq="cf.qbe"
case "$1" in
darwin-arm64)
seed="$root/boot/seed_mac"; qt=arm64_apple; fa=arm64
cc="cc"; link="-nostdlib -lSystem -Wl,-e,_start"; libs="" ;;
darwin-amd64)
# Shares the mac seed IL (darwin arm64/amd64 use identical BSD syscall numbers — the amd64
# `syscall` class bit lives in the floor trampoline, not the IL); only the floor asm differs.
# `-arch x86_64` (in `march`) lets an Apple-Silicon host cross-compile the embedded QBE
# objects AND cross-assemble+link the Mach-O; run the result via Rosetta 2.
seed="$root/boot/seed_mac"; qt=amd64_apple; fa=amd64
cc="cc"; link="-nostdlib -lSystem -Wl,-e,_start"; libs=""; march="-arch x86_64" ;;
linux-arm64)
# musl (not glibc): static musl links cleanly under the floor's own `_start`, where glibc's
# static libc.a drags in crt/dynamic-loader machinery it never gets. The linux floor calls
Expand All @@ -48,6 +63,12 @@ knobs() { # <platform> -> sets seed qt fa cc link libs
# `-no-pie` because the floor's `_start` does no PIE self-relocation (some musl gccs default PIE).
seed="$root/boot/seed_linux"; qt=arm64; fa=arm64
cc="${LINUX_CC:-musl-gcc}"; link="-nostdlib -static -no-pie -Wl,-e,_start -Wl,-u,__init_libc"; libs="-lc -lgcc" ;;
linux-amd64)
# x86-64 linux syscall numbers differ from the generic arm64/riscv64 table, so linux-amd64
# has its OWN seed IL (cf.amd64.qbe), not the shared seed_linux/cf.qbe. Same static-musl link
# as the other linux targets; native `musl-gcc` on an x86-64 host already targets x86-64.
seed="$root/boot/seed_linux"; sq="cf.amd64.qbe"; qt=amd64_sysv; fa=amd64
cc="${LINUX_CC:-musl-gcc}"; link="-nostdlib -static -no-pie -Wl,-e,_start -Wl,-u,__init_libc"; libs="-lc -lgcc" ;;
linux-riscv64)
seed="$root/boot/seed_linux"; qt=rv64; fa=riscv64
cc="${RISCV_CC:-riscv64-linux-musl-gcc}"; link="-nostdlib -static -no-pie -Wl,-e,_start -Wl,-u,__init_libc"; libs="-lc -lgcc" ;;
Expand All @@ -57,7 +78,7 @@ knobs() { # <platform> -> sets seed qt fa cc link libs

# Host toolchain (for cf0 + the vendored QBE), resolved from the host knobs so the whole build uses one
# C compiler — cc on darwin, musl-gcc on linux (matching the embedded QBE objects to the libc we link).
knobs "$host"; hseed="$seed"; hqt="$qt"; hfa="$fa"; hcc="$cc"; hlink="$link"; hlibs="$libs"
knobs "$host"; hseed="$seed"; hsq="$sq"; hqt="$qt"; hfa="$fa"; hcc="$cc"; hlink="$link"; hlibs="$libs"

# The embedded QBE's C is built OPTIMIZED and without debug info (QBE's own Makefile defaults to
# `-g` and no `-O`): `-O2` makes cf's IL->asm step faster, and dropping `-g` shrinks the binary. This
Expand All @@ -77,10 +98,10 @@ trap 'rm -rf "$tmp"' EXIT
"$hcc" $cflags -I "$qbedir" -c "$root/boot/qbe_embed.c" -o "$tmp/qbe_embed_host.o"

# --- Stage 1: assemble the HOST seed into the bootstrap compiler cf0 (runs on this machine). ---
for f in "$hseed/cf.qbe" "$hseed/floor.$hfa.s"; do
for f in "$hseed/$hsq" "$hseed/floor.$hfa.s"; do
[ -f "$f" ] || { echo "build: seed missing: $f" >&2; exit 1; }
done
"$qbe" -t "$hqt" -o "$tmp/cf0.prog.s" "$hseed/cf.qbe"
"$qbe" -t "$hqt" -o "$tmp/cf0.prog.s" "$hseed/$hsq"
# shellcheck disable=SC2086
$hcc $hlink -o "$tmp/cf0" "$hseed/floor.$hfa.s" "$tmp/cf0.prog.s" $host_embed "$tmp/qbe_embed_host.o" $hlibs

Expand All @@ -98,16 +119,18 @@ else
# `main.o`, already excluded from $host_embed, stay excluded), giving each a unique object name.
mkdir -p "$tmp/tobj"; i=0
for o in $host_embed; do
"$cc" $cflags -I "$qbedir" -c "${o%.o}.c" -o "$tmp/tobj/q$i.o"
# shellcheck disable=SC2086
"$cc" $march $cflags -I "$qbedir" -c "${o%.o}.c" -o "$tmp/tobj/q$i.o"
i=$((i + 1))
done
"$cc" $cflags -I "$qbedir" -c "$root/boot/qbe_embed.c" -o "$tmp/tobj/embed.o"
# shellcheck disable=SC2086
"$cc" $march $cflags -I "$qbedir" -c "$root/boot/qbe_embed.c" -o "$tmp/tobj/embed.o"
tembed=$(ls "$tmp/tobj"/q*.o); tqembed="$tmp/tobj/embed.o"
fi

# --- Link the target cf: floor (owns _start) + program asm + embedded QBE + bridge. ---
# shellcheck disable=SC2086
$cc $link -o "$out" "$tmp/cf.floor.s" "$tmp/cf.prog.s" $tembed "$tqembed" $libs
$cc $march $link -o "$out" "$tmp/cf.floor.s" "$tmp/cf.prog.s" $tembed "$tqembed" $libs

# Release builds (`CF_STRIP=1`) strip the symbol table for a smaller artifact — the objects already
# carry no `-g`, so this is the last of the size. Local builds keep symbols (cf is debuggable with
Expand Down
25 changes: 24 additions & 1 deletion boot/reseed.sh
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,28 @@ regen_linux() { # <verified-darwin-cf>
echo "reseed: FIXPOINT FAILED — linux arm64/riscv64 IL diverged (must be identical, generic ABI)" >&2
exit 1
fi
echo "reseed: ok — linux seed regenerated (native fixpoint verified in CI) -> $lseed"
echo "reseed: ok — linux arm64/riscv64 seed regenerated (native fixpoint verified in CI) -> $lseed"

# linux-amd64 has its OWN IL: x86-64's linux syscall numbers (and struct-stat layout) differ from
# the generic arm64/riscv64 table, so it CANNOT share cf.qbe and is EXCLUDED from the identity check
# above. Emit its distinct seed; the self-hosting fixpoint is verified natively on CI (ubuntu x86-64).
"$1" --skip-embeds --target linux-amd64 "$src" "$lseed/cf.amd64.qbe" "$lseed/floor.amd64.s"
echo "reseed: ok — linux-amd64 seed regenerated (its own IL; native fixpoint verified in CI) -> $lseed"
}

# Regenerate the darwin-amd64 floor from the just-verified darwin compiler `$1`. darwin arm64 and amd64
# emit BYTE-IDENTICAL IL — they share BSD syscall NUMBERS, and the amd64 `syscall` class bit lives in
# the floor trampoline, not the IL — so amd64 REUSES the committed mac `cf.qbe`; we assert that identity
# here and commit only the new `floor.amd64.s`. The amd64 fixpoint is verified natively under Rosetta 2
# / CI (reseed runs on arm64 and cannot itself run an amd64 cf), but the emit is deterministic and
# target-driven, so this compiler's amd64 emit is exactly what an amd64-native cf reproduces.
regen_darwin_amd64() { # <verified-darwin-cf>
"$1" --skip-embeds --target darwin-amd64 "$src" "$tmp/da.qbe" "$seed/floor.amd64.s"
if ! cmp -s "$tmp/da.qbe" "$seed/cf.qbe"; then
echo "reseed: FIXPOINT FAILED — darwin-amd64 IL diverged from the mac seed (must be identical)" >&2
exit 1
fi
echo "reseed: ok — darwin-amd64 floor regenerated, IL reuses the mac seed -> $seed/floor.amd64.s"
}

# 1. cf₀ from the CURRENT (committed) seed.
Expand All @@ -99,6 +120,7 @@ if [ "$transitional" -eq 0 ]; then
cp "$tmp/new.qbe" "$seed/cf.qbe"
cp "$tmp/new.floor.s" "$seed/floor.arm64.s"
echo "reseed: ok — mac seed regenerated and fixpoint verified -> $seed"
regen_darwin_amd64 "$tmp/cf_new"
regen_linux "$tmp/cf_new"
exit 0
fi
Expand All @@ -114,4 +136,5 @@ fi
cp "$tmp/chk.qbe" "$seed/cf.qbe"
cp "$tmp/chk.floor.s" "$seed/floor.arm64.s"
echo "reseed: ok — mac seed regenerated (transitional stage-2) and fixpoint verified -> $seed"
regen_darwin_amd64 "$tmp/cf_newer"
regen_linux "$tmp/cf_newer"
Loading
Loading