Skip to content

Resolve the target side once, and let every translation unit read it #116

Resolve the target side once, and let every translation unit read it

Resolve the target side once, and let every translation unit read it #116

Workflow file for this run

name: openkal cross-build (3 hosts × 3 targets)
# ⭐⭐ WHAT THIS WORKFLOW ASSERTS, AND WHY IT IS A MATRIX RATHER THAN A ROW.
#
# `cross-build-test.yml` verifies the crosses served by a PAYLOAD: a toolchain
# whose driver has exactly one target. There the host and the target are joined
# — `x86_64-w64-mingw32-g++` is the Windows cross and nothing else — so one row
# per supported combination is the honest shape.
#
# openkal changes the shape of the question. The target side — the C library,
# the C++ runtime, the platform's own implementation — is a set of PACKAGES in
# the dependency graph, and the compiler is an ordinary retargetable clang. The
# claim that follows is that N hosts × N targets collapses to N implementations
# plus one tool: **the machine doing the building stops being a variable.**
#
# ⚠️ THAT IS A CLAIM, AND CLAIMS OF THIS SHAPE HAVE BEEN WRONG IN THIS
# REPOSITORY. Reaching PE from a Linux host needed four separate repairs, and
# adding the other two hosts found seven more — every one of them a decision
# that had been keyed on which machine was building rather than on which machine
# the output was for:
#
# the link line's three host-shaped branches, only one of which carried
# `--target=`; the `std` module command's Windows branch, which dropped the
# package's own include paths; `cd X && …` not changing the drive in cmd.exe;
# the artefact-format test matching LLVM's `apple` rather than mcpp's `macos`;
# the C++ runtime contract naming a library to link when one was already in
# the objects; `-nostdinc` missing so a host SDK header could be found; and
# `-lgcc` naming GCC's runtime on a link whose compiler is clang.
#
# None of those was visible from one host. So the matrix is the test.
#
# ── The shape ──────────────────────────────────────────────────────────────
#
# THREE build jobs, one per host, each producing THREE artefacts — nine builds.
# THREE run jobs, one per system, each executing the artefact FOR that system
# produced by ALL THREE hosts.
#
# build on Linux build on macOS build on Windows
# run Linux ✓ ✓ ✓
# run macOS ✓ ✓ ✓
# run Windows ✓ ✓ ✓
#
# ⭐ The diagonal is an ordinary native build. The six off-diagonal cells are
# the claim, and they are what a single-host workflow cannot reach.
#
# ⚠️ THE RUN JOBS INSTALL NOTHING — not mcpp, not a compiler, not a C runtime.
# A program above openkal carries its C library, its C++ runtime and its
# unwinder; what remains is the operating system it was built for. If a
# toolchain step is ever added to one of them because "the program needs it",
# that is the finding rather than the fix.
#
# ⚠️ AND THE ASSERTION IS ON THE OUTPUT, NOT THE EXIT STATUS. The program prints
# four lines, and `unwound: true` is the one a link cannot fake: it says a
# destructor ran while an exception was being carried out of a frame, which
# means the unwinder found this image's own frame descriptions.
on:
pull_request:
workflow_dispatch:
concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
# ⚠️ No mcpp or xlings version here. `bootstrap-mcpp` owns both, and a second
# statement of them is a second thing to keep in step — the pin check
# (.github/tools/check_version_pins.sh) enforces the ones that exist and would
# not know about a copy in this file.
XLINGS_NON_INTERACTIVE: '1'
# The branch of the openkal packages this change is verified against.
#
# ⚠️ IT WAS `feat/openkal-closure` UNTIL 2026-08-25, LONG AFTER THAT BRANCH
# MERGED. A fixed name here is a pin nobody is reminded to move: the comment
# said "when they are on `main` this becomes `main`" and the moment for that
# passed without anyone reading it again. Every run since was verifying this
# engine against a tree the ecosystem had left behind — and the two
# regressions found today both hid behind exactly this shape, a pin that
# keeps a check green by keeping it out of date.
OPENKAL_BRANCH: main
jobs:
build:
name: build 3 targets on ${{ matrix.host }}
runs-on: ${{ matrix.runner }}
timeout-minutes: 120
strategy:
fail-fast: false
matrix:
include:
- { host: linux, runner: ubuntu-24.04 }
- { host: macos, runner: macos-14 }
- { host: windows, runner: windows-2022 }
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v4
# ⚠️ THE REPOSITORY'S OWN BOOTSTRAP, NOT A SECOND ONE.
#
# This job first wrote its own: fetch xlings, then
# `xlings install mcpp@<version>`. It failed on the very first run:
#
# xlings: version '2026.8.17.1' not found for 'mcpp'
# available: 2026.8.19.4
#
# `.xlings.json` at this repository's root pins the mcpp that BUILDS mcpp,
# and that pin does not move when mcpp is released — so it names a version
# the index no longer carries, and a bare install inside the checkout
# obeys the pin rather than the argument. `bootstrap-mcpp` already knows
# this (it runs `install_pinned_mcpp.sh`), works on all three systems, and
# shares the cache lineage every other job lands on.
#
# ⇒ Two bootstraps would be two things to keep correct, and the second one
# was wrong within a day of being written.
- uses: ./.github/actions/bootstrap-mcpp
# ⭐ THE mcpp UNDER TEST. Everything after this uses the binary this step
# produces; the bootstrapped one above is only what compiles it.
- name: Build the mcpp in this pull request
run: |
set -euo pipefail
export MCPP_VENDORED_XLINGS="$XLINGS_BIN"
"$XLINGS_BIN" config --mirror GLOBAL 2>/dev/null || true
"$MCPP" self config --mirror GLOBAL 2>/dev/null || true
# ⚠️ `--dev` and not `--release`. What is under test is a set of
# decisions about compile and link flags; an optimisation level
# changes none of them and a release self-build is most of the budget
# of a job on a two-core runner.
"$MCPP" build --dev
BUILT=$(find target -type f \( -name 'mcpp' -o -name 'mcpp.exe' \) | head -1)
[ -n "$BUILT" ] || { echo "::error::mcpp did not build"; exit 1; }
BUILT=$(cd "$(dirname "$BUILT")" && pwd)/$(basename "$BUILT")
echo "MCPP_UNDER_TEST=$BUILT" >> "$GITHUB_ENV"
"$BUILT" --version
- name: Select the toolchain the openkal packages ask for
run: |
set -euo pipefail
# ⚠️ Install, then select. `toolchain default` names one and does not
# fetch it.
"$MCPP_UNDER_TEST" self config --mirror GLOBAL 2>/dev/null || true
"$MCPP_UNDER_TEST" toolchain install llvm 22.1.8
"$MCPP_UNDER_TEST" toolchain default 'llvm@22.1.8'
- name: The program — one source, three targets
run: |
set -euo pipefail
# ⚠️ THE SEVENTH CALL SITE. `git_clone_retry.sh` was written because a
# runner's DNS hiccup is not a red build, and its own note counts
# "six call sites, one failure mode" — this workflow was not among
# them, and the mode duly arrived here. Measured on this job,
# 2026-08-25:
#
# fatal: unable to access '…/openkal-llvm-runtime/':
# Could not resolve host: github.com
#
# One name that did not resolve ended a 120-minute job in its first
# minute, beside a real failure it had nothing to do with.
"$GITHUB_WORKSPACE/.github/tools/git_clone_retry.sh" \
--quiet --depth 1 -b "$OPENKAL_BRANCH" \
https://github.com/mcpplibs/openkal-llvm-runtime "$RUNNER_TEMP/okl"
cd "$RUNNER_TEMP/okl/examples/same-source"
mkdir -p "$RUNNER_TEMP/out"
# ⚠️ The three HOSTED targets. Bare metal is verified by
# `openkal-llvm-runtime`'s own CI under qemu; it has no runner here to
# execute on, and a build-only cell in a workflow whose point is
# running would be the weaker claim.
for t in x86_64-linux-gnu aarch64-macos x86_64-windows-gnu; do
rm -rf target
"$MCPP_UNDER_TEST" build --target "$t"
a=$(find target -type f \( -name 'openkal-same-source' -o -name 'openkal-same-source.exe' \) | head -1)
[ -n "$a" ] || { echo "::error::$t produced no artefact on ${{ matrix.host }}"; exit 1; }
case "$t" in
x86_64-windows-gnu) cp "$a" "$RUNNER_TEMP/out/windows.exe" ;;
aarch64-macos) cp "$a" "$RUNNER_TEMP/out/macos" ;;
*) cp "$a" "$RUNNER_TEMP/out/linux" ;;
esac
echo "${{ matrix.host }} → $t : $(ls -l "$a" | awk '{print $5}') bytes"
done
- uses: actions/upload-artifact@v4
with:
name: openkal-built-on-${{ matrix.host }}
path: ${{ runner.temp }}/out/
if-no-files-found: error
run:
name: run 3 builds on ${{ matrix.system }}
needs: build
runs-on: ${{ matrix.runner }}
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
include:
- { system: linux, runner: ubuntu-24.04, file: linux }
- { system: macos, runner: macos-14, file: macos }
- { system: windows, runner: windows-2022, file: windows.exe }
defaults:
run:
shell: bash
steps:
# ⚠️ NO checkout AND NO toolchain. This job is the claim: a program built
# above openkal needs the operating system it was built for and nothing
# else. Anything installed here would weaken what a pass means.
- uses: actions/download-artifact@v4
with: { pattern: openkal-built-on-*, path: art }
- name: The same program, from all three build hosts
run: |
set -euo pipefail
fail=0
for host in linux macos windows; do
bin="art/openkal-built-on-$host/${{ matrix.file }}"
echo "──────── built on $host, running on ${{ matrix.system }} ────────"
if [ ! -f "$bin" ]; then
echo "::error::$bin is missing"; fail=1; continue
fi
# ⚠️ The executable bit does not survive an artefact upload.
chmod +x "$bin" || true
# ⚠️ arm64 macOS refuses an unsigned image, so the signature is
# asserted before the run: a failure here is "the linker did not
# ad-hoc sign it", which is a different repair from "it crashed".
if [ "${{ matrix.system }}" = "macos" ]; then
codesign -dv "$bin" 2>&1 | grep -q 'adhoc\|Signature' \
|| { echo "::error::built on $host: no code signature"; fail=1; continue; }
fi
if ! "./$bin" > out.log 2>&1; then
echo "::error::built on $host: it did not run"; cat out.log; fail=1; continue
fi
cat out.log
ok=1
grep -q 'sorted: 2 4 7' out.log || ok=0
grep -q 'caught: 42' out.log || ok=0
# ⭐ The line a link cannot fake.
grep -q 'unwound: true' out.log || ok=0
grep -q 'import std over openkal: ok' out.log || ok=0
[ "$ok" = 1 ] || { echo "::error::built on $host: wrong output"; fail=1; }
done
[ "$fail" = 0 ] || exit 1
echo "three builds, one system, same four lines"
# ──────────────────────────────────────────────────────────────────
# The e2e scripts that BUILD the openkal ecosystem, on a runner that
# has what they ask for.
#
# ⚠️ THEY WERE WRITTEN AND THEY WERE NEVER RUN. `285`–`289` declare
# `# requires: llvm`, and the linux e2e shards report
#
# Detected capabilities: elf unix-shell fresh-sandbox gcc
# patchelf pack symlink python3 …
#
# — no `llvm`, on either shard, because the shard workflow never
# installs one. `run_all.sh` exits 0 on a skip, so the suite stayed
# green while the five tests measuring this ecosystem did not run.
#
# run_all.sh's own note says why no token can fix this: a hard-requires
# cannot tell "this runner is misconfigured" from "this platform
# legitimately lacks the capability". The guard has to know which
# runner it is, so it lives in the job — install the capability, then
# assert each script's PASS line actually appeared. Same shape as
# ci-linux-e2e.yml's `baremetal` job, for the same reason.
# ──────────────────────────────────────────────────────────────────
ecosystem-e2e:
name: openkal e2e (the scripts, on a runner that has llvm)
runs-on: ubuntu-24.04
timeout-minutes: 90
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/bootstrap-mcpp
- name: Build the mcpp in this pull request
run: |
set -euo pipefail
export MCPP_VENDORED_XLINGS="$XLINGS_BIN"
"$XLINGS_BIN" config --mirror GLOBAL 2>/dev/null || true
"$MCPP" self config --mirror GLOBAL 2>/dev/null || true
"$MCPP" build --dev
# ⚠️ NEWEST BY MTIME, NOT FIRST BY DIRECTORY ORDER. `target/` holds one
# directory per fingerprint and the runner restores a cache of it, so
# `find … | head -1` can return a binary an earlier run left behind.
# Measured: it reported the right VERSION STRING — the stale copy was
# built from an earlier push of this same release — while missing the
# last two commits, so one new test passed and two failed for reasons
# that were nowhere in the source.
BUILT=$(find target -type f -name 'mcpp' -printf '%T@ %p\n' \
| sort -rn | head -1 | cut -d' ' -f2)
[ -n "$BUILT" ] || { echo "::error::mcpp did not build"; exit 1; }
BUILT=$(cd "$(dirname "$BUILT")" && pwd)/$(basename "$BUILT")
echo "MCPP_UNDER_TEST=$BUILT" >> "$GITHUB_ENV"
"$BUILT" --version
- name: Install what the scripts declare
run: |
set -euo pipefail
"$MCPP_UNDER_TEST" self config --mirror GLOBAL 2>/dev/null || true
# Both, and both are load-bearing: 285 and 291's first half build
# with gcc (a backend running ON a platform, keeping the payload's
# C library), 286-289 and 291's second half with llvm (the whole
# stack from the graph, where openkal-llvm-runtime IS libc++).
"$MCPP_UNDER_TEST" toolchain install gcc 16.1.0
"$MCPP_UNDER_TEST" toolchain install llvm 22.1.8
# ⚠️ THE EMULATORS, OR TWO OF THE SIX MEASURE HALF OF WHAT THEY SAY.
#
# 287 and 288 both end by RUNNING what they built — an aarch64 binary and
# a riscv64 machine image — and both degrade to a SKIP when no emulator
# is here. Measured on this job's first run: 288 printed
#
# SKIP no riscv64 machine emulator here — linking is not booting
#
# and still reached its OK line, so the PASS-line assertion below would
# have called that covered. Linking is not booting, as the script itself
# says.
#
# ⚠️ BOTH homes, for the reason ci-linux-e2e.yml's baremetal job records:
# the shim on PATH dispatches against whichever home owns it, so an
# emulator installed only in the ambient one answers "not installed" when
# mcpp asks.
- name: Install the emulators the last two scripts need
run: |
set -euo pipefail
sudo apt-get update -qq && sudo apt-get install -y -qq qemu-user
"$XLINGS_BIN" install xim:qemu-riscv -y
XLINGS_HOME="${MCPP_HOME:-$HOME/.mcpp}/registry" \
"$XLINGS_BIN" install xim:qemu-riscv -y
# Reachable AND runnable, asserted before the tests: without this the
# scripts would simply skip and say so in a line nobody reads.
qemu-aarch64 --version | head -1
"$XLINGS_BIN" run qemu-system-riscv64 --version 2>/dev/null | head -1 \
|| command -v qemu-system-riscv64
- name: The scripts
run: |
set -euo pipefail
export MCPP="$MCPP_UNDER_TEST"
export MCPP_VENDORED_XLINGS="$XLINGS_BIN"
# Directly rather than through run_all.sh: it accepts no filter, and
# it exits 0 on a skip — which is the condition this job exists to
# detect.
fail=0
for t in tests/e2e/285_*.sh tests/e2e/286_*.sh tests/e2e/287_*.sh \
tests/e2e/288_*.sh tests/e2e/289_*.sh tests/e2e/291_*.sh \
tests/e2e/292_*.sh tests/e2e/293_*.sh tests/e2e/294_*.sh; do
echo "=== $t ==="
bash "$t" 2>&1 | tee "$(basename "$t").log" || true
rc=${PIPESTATUS[0]}
[ "$rc" = "0" ] || { echo "::error::$t failed (exit $rc)"; fail=1; }
done
[ "$fail" = 0 ] || exit 1
- name: Each one RAN
run: |
set -euo pipefail
# ⭐ THE ASSERTION THIS JOB EXISTS FOR. A zero exit code cannot
# distinguish "passed" from "skipped" — every one of these scripts
# has an early `exit 0` for a capability or an arrangement it did
# not find. The PASS line can.
check() {
grep -qF "$2" "$1".log || {
echo "::error::$1 did not run to its conclusion on the runner that must run it"
tail -5 "$1".log 2>/dev/null | sed 's/^/ /'
return 1
}
echo " ok $1"
}
fail=0
check 285_kernel_abi_from_graph_keeps_the_payload_c_library.sh \
"OK: a graph-supplied kernel interface leaves the payload's C library reachable" || fail=1
check 286_the_openkal_stack_still_builds.sh \
"OK: the openkal stack builds, links statically and runs" || fail=1
check 287_the_openkal_stack_crosses_to_aarch64.sh \
"OK: the openkal stack crosses to aarch64, supplies its atomics helpers and runs" || fail=1
# ⭐ AND IT REACHED THE PARTS THAT NEED A TOOL. Both of 287's last two
# assertions degrade to a SKIP, and the OK line prints either way.
check 287_the_openkal_stack_crosses_to_aarch64.sh \
"LSE instructions out of" || fail=1
check 287_the_openkal_stack_crosses_to_aarch64.sh \
"it runs under qemu-aarch64" || fail=1
check 288_the_openkal_stack_on_a_machine_with_no_os.sh \
"OK: openkal runs on a machine with no operating system and no C library" || fail=1
# ⭐ 288's name says "runs"; without this it can print that line
# having only linked.
check 288_the_openkal_stack_on_a_machine_with_no_os.sh \
"it boots" || fail=1
check 289_one_host_reaches_every_openkal_target.sh \
"OK: one host reached" || fail=1
check 291_dynamic_linkage_is_refused_only_when_the_c_library_is_the_graphs.sh \
"OK: the C library decides whether 'dynamic' can be honoured" || fail=1
check 292_a_package_that_names_a_layer_does_not_lose_the_targets_compiler.sh \
"OK: naming a layer changes the system, not the compiler that emits the target" || fail=1
check 293_the_requested_target_and_the_resolved_one_name_one_os.sh \
"OK: the requested target and the resolved one name one operating system" || fail=1
check 294_the_list_answers_what_can_be_built_not_what_has_a_payload.sh \
"OK: the list answers what can be built, not what has a payload" || fail=1
[ "$fail" = 0 ] || exit 1