Skip to content

0.8.1 — the backend existed and the one-line usage could not reach it #30

0.8.1 — the backend existed and the one-line usage could not reach it

0.8.1 — the backend existed and the one-line usage could not reach it #30

Workflow file for this run

name: CI
on:
push:
branches: [main]
pull_request:
workflow_dispatch:
# Three jobs, and the first is ONE job written once: the gate this layer is
# judged by is that a single probe source runs on machines that genuinely
# differ, so the rows below differ only in a triple and an emulator. If they
# ever need to differ in anything else, the abstraction has failed and this
# workflow is where that becomes visible.
#
# ⭐ THE THIRD ROW IS WHAT TURNS THE GATE INTO EVIDENCE. riscv64 and aarch64 are
# both load/store RISC machines with a weak memory model and a fixed instruction
# width, so an interface that fits both may fit because it is right or because
# they are alike. x86_64 is neither: variable-length instructions, total store
# order — under which three of the four barriers need no instruction at all —
# and an interrupt mechanism that is a table of 256 gates rather than a base
# register. What survives all three is an abstraction.
jobs:
gate:
name: the probe runs on ${{ matrix.arch }}
runs-on: ubuntu-24.04
timeout-minutes: 40
strategy:
fail-fast: false
matrix:
include:
- { arch: riscv64, triple: riscv64-none-elf, qemu: 'xim:qemu-riscv' }
- { arch: aarch64, triple: aarch64-none-elf, qemu: 'xim:qemu-arm' }
# ⭐ ALL THREE ROWS NAME AN xlings PACKAGE FROM 0.5.0, AND THE THIRD
# ONE TOOK A NEW PACKAGE TO GET THERE.
#
# xPack publishes QEMU per target family and has no x86 build, so
# until now this row installed `qemu-system-x86` from apt and ran the
# emulator by hand. That made it the one row whose result depended on
# what Ubuntu happened to ship. `mcpplibs/qemu-x86` builds
# `qemu-system-x86_64` from source for five hosts, `xim:qemu-x86`
# carries it, and the row is now the same shape as the other two.
- { arch: x86_64, triple: x86_64-none-elf, qemu: 'xim:qemu-x86' }
env:
# ⚠️ THE OLDEST ENGINE THIS REPOSITORY NEEDS, NOT THE NEWEST THAT EXISTS.
# 2026.9.4.1 is the release carrying the Cortex-M target rows, which the
# fourth backend's job below builds against. Pinning something newer would
# make this repository unbuildable for the window between a merge here and
# a release there, and would say a dependency exists that does not.
MCPP_VERSION: 2026.9.4.1
XLINGS_VERSION: v2026.8.17.2
XLINGS_NON_INTERACTIVE: '1'
steps:
- uses: actions/checkout@v4
- name: Install xlings
run: |
curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh \
| bash -s "$XLINGS_VERSION"
echo "$HOME/.xlings/subos/current/bin" >> "$GITHUB_PATH"
- name: Install mcpp
run: |
# ⚠️ A LOOP, BECAUSE ONE `xlings update` CAN RETURN A STALE INDEX
# WITHOUT SAYING SO.
#
# The index is published as an artifact behind a pointer, and that
# pointer propagates asynchronously after a version bump is merged.
# Measured on release day: an update run four minutes after the merge
# printed `index updated`, and the install then failed with
#
# package 'mcpp@<ver>' not found in the synced index
# (xim@artifact:<an older sha>, ...), synced 0 seconds ago
#
# Nothing had gone wrong. The update fetched the PREVIOUS artifact,
# and "synced 0 seconds ago" describes when it was fetched rather than
# what it contains — which is why the message reads as freshness.
#
# So this is not a retry around flakiness; it is the wait that a
# single update does not perform. A pin naming a version that was
# never published still fails, after the last attempt, and says which
# of the two situations it is.
for attempt in 1 2 3 4 5 6; do
xlings update > /dev/null 2>&1 || true
if xlings install "mcpp@$MCPP_VERSION" -y -g; then break; fi
if [ "$attempt" = 6 ]; then
echo "::error::mcpp@$MCPP_VERSION never appeared in the index (6 attempts over 5 minutes). If it was just released, the pointer has not propagated; if the pin names a version that was never published, it never will."
exit 1
fi
echo "the index has not caught up yet (attempt $attempt of 6); waiting 60s"
sleep 60
done
mcpp --version
mcpp self config --mirror GLOBAL
# ⚠️ TWICE, AND THE FIRST IS ALLOWED TO FAIL. THIS IS NOT SUPERSTITION.
#
# mcpp installs the target's C library LAZILY — during a build, from the
# target's own row. So the very first build on a machine that has never
# targeted this triple compiles sources that need its headers before they
# exist, and dies on `'stdio.h' file not found` or `'inttypes.h' file not
# found` pointing inside a dependency. The second build has them. mcpp
# says as much itself for the sibling case: "expected on the first build
# in a fresh MCPP_HOME; a second build resolves it."
#
# ⚠️ Deliberately NOT `xlings install xim:picolibc-riscv`. That would make
# the target world exist without going through the engine's own install,
# and a regression in that install is exactly what this repository would
# then stop noticing. A warm-up build exercises it; a manual install
# replaces it.
# ⚠️ BOTH HOMES, AND THIS IS NOT BELT-AND-BRACES. The shim on PATH
# dispatches against whichever home owns it, while `mcpp run` starts the
# runner through mcpp's own — an emulator present in only one of them
# answers "not installed" from the other.
#
# The three packages are different builds, one per target family:
# `qemu-riscv` carries the RISC-V emulators, `qemu-arm` the Arm ones and
# `qemu-x86` the x86_64 one. Measured; no single package runs all three.
# ⭐ BEFORE THE EMULATOR IS INSTALLED, AND THAT ORDER IS THE ASSERTION.
#
# `[xlings] deps` is a DECLARATION, not an install trigger, so on a clean
# machine `xpkg_dir` answers empty and this project's build program
# configures no runner. That is correct and it used to be SILENT: mcpp
# prints a build program's output only on a non-zero exit, so a
# `std::cerr` note printed nothing on exactly the builds that needed it.
#
# mcpp 2026.8.21.2's `mcpp::warning()` is the channel. This step is the
# only place the ecosystem can observe it, because every later step has
# the emulator — so it runs here, once, and asserts the sentence a first
# -time user sees.
- name: A clean machine is told what is missing
working-directory: examples/switch
run: |
set -euo pipefail
COLD=$(mktemp -d)
MCPP_HOME="$COLD" mcpp build --target ${{ matrix.triple }} 2>&1 | tee cold.log
grep -q "is not installed" cold.log \
|| { cat cold.log; echo "the build program said nothing about the missing emulator"; exit 1; }
grep -q "xlings install" cold.log \
|| { cat cold.log; echo "the advisory does not name the command that fixes it"; exit 1; }
# An advisory is not an error: the build still succeeded.
grep -q "Finished" cold.log \
|| { cat cold.log; echo "the build did not finish"; exit 1; }
rm -rf "$COLD" target
- name: Install the emulator
run: |
xlings install ${{ matrix.qemu }} -y
XLINGS_HOME="$HOME/.mcpp/registry" xlings install ${{ matrix.qemu }} -y
- name: The layer builds for ${{ matrix.arch }}
run: |
# Twice, the first allowed to fail: the toolchain payload is installed
# during a build, so the first build on a machine that has never
# targeted this triple is the one that installs it.
# ⚠️ THE BACKEND, NOT THE ROOT. Since 0.4.0 the root is BOTH the
# interface package and the workspace, so building it would build the
# interface; what this step wants is the machine half. Building the
# backend for this target pulls the ABI it implements and is what the
# probe below will link against.
( cd backends/${{ matrix.arch }} && mcpp build --target ${{ matrix.triple }} ) || true
( cd backends/${{ matrix.arch }} && mcpp build --target ${{ matrix.triple }} )
# ⚠️ THE THIRD ASSERTION IS THE ONE THAT CATCHES A HALF-CORRECT BACKEND.
#
# A switch that saved the return address and the stack pointer and nothing
# else would reach the task and deliver its argument — the first two lines
# would appear — and would corrupt the caller's callee-saved registers.
# `before=1234` is a `volatile` local read after the round trip, and it is
# what fails when that happens.
- name: The probe switches, returns, and preserves callee-saved registers
working-directory: examples/switch
run: |
set -euo pipefail
# ⭐ ONE PATH FOR ALL THREE MACHINES. Until 0.5.0 this step branched:
# two rows went through `mcpp run` and x86_64 built and then invoked
# qemu by hand, because `build.mcpp` could configure no runner for a
# package that did not exist. The branch is gone, and with it the
# asymmetry that made the third machine the least-tested one.
mcpp run --target ${{ matrix.triple }} 2>&1 | tee run.log
grep -q "task: arg=42" run.log
grep -q "witness=7 before=1234" run.log
grep -q "switch ok" run.log
grep -q "trap: back, witness=1" run.log
# ⚠️ `steps=1` AND NOT MERELY THE LINE. Every other assertion here is
# printed by whichever context is running; only a counter the OTHER
# context advanced says the trap resumed somewhere else. Measured: a
# backend whose `arch_trap_switch` does nothing prints
# `preempt: back, steps=0` and reaches every line above.
grep -q "preempt: back, steps=1" run.log
grep -q "cpu: percpu round-trips" run.log
# ⚠️ THE TEMPLATE IS RENDERED BY HAND HERE, AND IT HAS TO BE.
#
# `mcpp new --template` resolves the package from the INDEX and takes no
# path, so asking it for a template this commit ADDS would resolve the
# previously published version and fail on a template that version does
# not carry. The CI for a new template could never pass before the
# template was published, which is the wrong way round.
#
# What belongs to this repository is the template's CONTENT: that the
# files it ships generate a project which builds for all three machines.
# That the scaffolder can fetch it is mcpp's own concern and mcpp's own
# tests.
#
# ⚠️ The dependency is rewritten to a path. The rendered manifest names
# `openarch = "<this version>"`, which is correct for a user and
# unresolvable here — this version is not published yet, and a check that
# silently fell back to the previous one would be testing the wrong tree.
#
# ⚠️ IN THE `gate` JOB, AND THE PLACEMENT IS THE FIX FOR A MISTAKE MADE
# TWICE. 0.4.0 put this in `host-encoders`, which installs no emulator —
# so the check could only ever assert `mcpp build`, and a generated project
# that ran on nothing passed it. The emulator lives here.
#
# ⚠️ AND 0.5.0 MADE THE SAME MISTAKE ONE SIZE SMALLER. This step ran only
# in the riscv64 row, which installs only the RISC-V emulator, so the
# claim it could support was "the template runs on riscv64" while the
# template's README had begun to claim all three. It did not: the
# template's own `build.mcpp.in` was left with x86_64's old early return,
# and nothing here could see that. The RUN half is per-row from 0.5.1 —
# each row asserts the machine whose emulator it installed.
#
# The BUILD half stays on one row: the template's CONTENT is
# host-independent, and the rewrite below puts `$PWD` into a manifest,
# which on a Windows runner is `/d/a/openarch/openarch` under Git Bash
# while mcpp wants a native path — a mismatch this repository has been
# bitten by before.
- name: The template generates a project that builds for all three machines
# The claim here is host-independent — the template's CONTENT builds for
# all three machines — so one row is the right place for it. The per-row
# claim, that it RUNS, is the separate step below.
if: matrix.arch == 'riscv64' # ci-lint: allow-r1: host-independent claim
run: |
set -euo pipefail
T=templates/three-machines
V=$(grep -m1 '^version' mcpp.toml | cut -d'"' -f2)
D=$(mktemp -d); mkdir -p "$D/src"
for f in mcpp.toml README.md build.mcpp; do
sed -e "s/{{project\.name}}/k/g" -e "s/{{self\.version}}/$V/g" \
"$T/$f.in" > "$D/$f"
done
cp "$T"/*.ld "$D/"
cp "$T"/src/* "$D/src/"
sed -i.bak "s|openarch = \"$V\"|openarch = { path = \"$PWD\" }|" "$D/mcpp.toml"
rm -f "$D/mcpp.toml.bak"
grep -q "path = " "$D/mcpp.toml" || { cat "$D/mcpp.toml"; echo "the dependency rewrite did not apply"; exit 1; }
for t in riscv64-none-elf aarch64-none-elf x86_64-none-elf; do
( cd "$D" && mcpp build --target "$t" > /dev/null 2>&1 ) || true
( cd "$D" && mcpp build --target "$t" ) \
|| { echo "the template does not build for $t"; exit 1; }
done
echo "the template builds for riscv64, aarch64 and x86_64"
# ⚠️ AND IT MUST RUN, NOT ONLY BUILD — THIS ASSERTION EXISTS BECAUSE ITS
# ABSENCE SHIPPED A BROKEN QUICK START, AND IT IS PER-ROW BECAUSE ITS
# BEING ON ONE ROW SHIPPED A SECOND ONE.
#
# 0.4.0's check stopped at `mcpp build`, so a generated project that
# compiled for all three machines and ran on none of them passed. What was
# missing is not in the sources: `mcpp.toml` declares the emulator packages
# under `[xlings] deps`, which is not an install trigger, so on a machine
# that has not installed them `build.mcpp` finds nothing and configures no
# runner.
#
# Each row installs one emulator, so each row asserts one machine. A row
# cannot assert a machine whose emulator it does not have — which is the
# whole reason the previous arrangement could not see that the template's
# `build.mcpp.in` still had x86_64's early return in it.
#
# This job installs the emulator in an earlier step, so the run below
# exercises the path a user gets AFTER the README's one-time install. What
# it cannot cover is the before — see the note in the README, and the
# comment in build.mcpp on why a build program cannot warn.
- name: The template's project runs on ${{ matrix.arch }}, not only builds
run: |
set -euo pipefail
T=templates/three-machines
V=$(grep -m1 '^version' mcpp.toml | cut -d'"' -f2)
D=$(mktemp -d); mkdir -p "$D/src"
for f in mcpp.toml README.md build.mcpp; do
sed -e "s/{{project\.name}}/k/g" -e "s/{{self\.version}}/$V/g" \
"$T/$f.in" > "$D/$f"
done
cp "$T"/*.ld "$D/"
cp "$T"/src/* "$D/src/"
sed -i.bak "s|openarch = \"$V\"|openarch = { path = \"$PWD\" }|" "$D/mcpp.toml"
rm -f "$D/mcpp.toml.bak"
grep -q "path = " "$D/mcpp.toml" || { cat "$D/mcpp.toml"; echo "the dependency rewrite did not apply"; exit 1; }
( cd "$D" && mcpp build --target ${{ matrix.triple }} > /dev/null 2>&1 ) || true
( cd "$D" && mcpp run --target ${{ matrix.triple }} 2>&1 | tee run.log ) \
|| { cat "$D/run.log"; echo "the generated project does not run on ${{ matrix.arch }}"; exit 1; }
grep -q "switch ok" "$D/run.log" \
|| { cat "$D/run.log"; echo "the generated project ran without reaching the end"; exit 1; }
echo "the template's project runs on ${{ matrix.arch }}"
# The same source produced that output. Asserted rather than trusted: a
# probe that had quietly grown a per-architecture branch would still pass
# every line above, and the gate would be measuring two programs.
# ⚠️ ONE PIECE OF LOGIC LIVING IN TWO FILES IS WHAT SHIPPED 0.5.0's DEFECT:
# the example's build program was updated and the template's copy was not,
# so the template's README claimed something its own program could not do.
#
# The fix is not "be more careful" — that had already been tried once. The
# pairs are declared in `.ci-identical-files` and asserted here.
- name: Files declared identical are identical
run: |
set -euo pipefail
test -s .ci-identical-files
n=0
while IFS=$'\t' read -r a b marker; do
case "${a:-}" in ''|'#'*) continue ;; esac
test -f "$a" && test -f "$b" || { echo "declared pair missing a side: $a / $b"; exit 1; }
# ⚠️ `grep -F` + `tail`, NOT a sed address. The marker is a line of
# C++ and contains `/`, which sed reads as the end of its pattern —
# measured: `unknown command: '/'`, both sides empty, and the diff
# would then have PASSED. That is the emptiness-as-success shape
# this repository has already been bitten by once.
la=$(grep -nF -m1 "$marker" "$a" | cut -d: -f1)
lb=$(grep -nF -m1 "$marker" "$b" | cut -d: -f1)
test -n "$la" && test -n "$lb" \
|| { echo "marker '$marker' not found in $a or $b"; exit 1; }
tail -n +"$la" "$a" > /tmp/ia
tail -n +"$lb" "$b" > /tmp/ib
test -s /tmp/ia && test -s /tmp/ib \
|| { echo "marker '$marker' matched an empty tail in $a or $b"; exit 1; }
diff -u /tmp/ia /tmp/ib || { echo "$a and $b diverge after '$marker'"; exit 1; }
n=$((n+1))
done < .ci-identical-files
test "$n" -ge 1 || { echo ".ci-identical-files declared no pairs"; exit 1; }
echo "$n declared pair(s) identical"
- name: The probe is one source, and the layering is a directory tree
run: |
set -euo pipefail
test -f examples/switch/src/main.cpp
# ⚠️ THIS CHECK USED TO READ "NO ARCHITECTURE MACRO APPEARS AT ALL",
# AND IT WAS WRONG IN A WAY THAT COST TWO RELEASES OF RED CI.
#
# The probe must name an architecture in exactly one place: the trap
# instruction. `ebreak`, `brk #0` and `int3` are three spellings of
# one idea and there is no portable fourth. When the trap interface
# landed in 0.3.0 the old check began failing on every push, correctly
# by its own wording and wrongly by its intent — and it kept failing
# through 0.3.1 because nobody read the runs.
#
# What the gate actually claims is that the probe is not TWO PROGRAMS.
# So: one conditional, and nothing inside it but instructions. A
# second conditional, or a line of logic inside the first, is the
# thing this is here to catch.
NCOND=$(grep -c '^#if defined(__' examples/switch/src/main.cpp || true)
if [ "$NCOND" != "1" ]; then
grep -n '^#if defined(__' examples/switch/src/main.cpp || true
echo "the probe has $NCOND architecture conditionals; it may have exactly one, for the trap instruction"
exit 1
fi
STRAY=$(awk '
/^#if defined\(__/ || /^#elif defined\(__/ { inb=1; next }
/^#else/ { next }
/^#endif/ { inb=0; next }
inb && $0 !~ /^[[:space:]]*(\/\/)?[[:space:]]*$/ \
&& $0 !~ /asm volatile/ && $0 !~ /^#[[:space:]]*error/ \
&& $0 !~ /^[[:space:]]*\/\// { print NR": "$0 }
' examples/switch/src/main.cpp)
if [ -n "$STRAY" ]; then
echo "$STRAY"
echo "the architecture conditional contains something other than an instruction"
exit 1
fi
echo "the probe names an architecture once, to emit a trap instruction"
# ⚠️ THE SEPARATION IS ASSERTED, NOT TRUSTED TO THE DIRECTORY NAMES.
#
# Until 0.3.1 the backends lived under `src/arch/<arch>/` in the same
# package as the specification, and the layering was a convention held
# up by a path. The split made it a dependency graph; these two checks
# make it a test.
if grep -rqE '\basm\b|__asm' src; then
echo "the interface contains an instruction; it must not"
exit 1
fi
if grep -rq 'export module' backends; then
echo "a backend exports a module; it must reach the interface only through the ABI"
exit 1
fi
echo "the interface owns modules and no instruction; backends own instructions and no module"
# ⚠️ THE ROOT IS BOTH A PACKAGE AND A WORKSPACE, AND THAT IS WHAT
# MAKES A CONSUMER'S SIDE ONE LINE. A virtual workspace would put the
# interface in a member directory and `openarch = "0.4.0"` would have
# to name it. Asserted because the two tables are ordinary TOML and
# deleting one would leave a manifest that still builds.
grep -q '^\[package\]' mcpp.toml
grep -q '^\[workspace\]' mcpp.toml
# The two faces reach the same library. The C one is a header a
# consumer includes; the C++ one is a module it imports. A face that
# disappeared would not fail any build in this repository except the
# test that names it.
test -f abi/include/mcpplibs/openarch.h
grep -q 'export module mcpplibs.openarch;' src/openarch.cppm
echo "one package, two faces, three backends"
# ---------------------------------------------------------------------------
# The FOURTH machine, and the only one whose backend is partial.
#
# ⭐⭐ IT IS A SEPARATE JOB BECAUSE IT CANNOT JOIN THE MATRIX ABOVE. Every row
# there runs `examples/switch`, which needs an address space and a per-CPU
# register; this machine has neither and says so in its manifest. Adding a row
# would have meant a conditional inside the gate — and a gate with a branch in
# it stops being one.
#
# What it does share is the primitive the other three grew for it:
# `arch_trap_switch`. Three machines exercise it with a synchronous trap in
# `examples/switch`; this one exercises it with a TIMER, which is the case the
# primitive exists for and the only one that shows an interrupted context
# being resumed elsewhere.
#
# ⚠️ AND THE ASSERTION IS PREEMPTION, NOT PROGRESS. Two tasks that print would
# also print if the switch never happened and one simply ran to completion.
# Measured before the primitive existed: calling `arch_context_switch` from
# PendSV built, booted, and reported that neither task ever observed the
# other. Only a counter neither task advanced itself tells them apart.
cortex-m:
name: the partial backend preempts (thumbv7m)
runs-on: ubuntu-24.04
timeout-minutes: 40
env:
MCPP_VERSION: 2026.9.4.1
XLINGS_VERSION: v2026.8.17.2
XLINGS_NON_INTERACTIVE: '1'
steps:
- uses: actions/checkout@v4
- name: Install xlings
run: |
curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh \
| bash -s "$XLINGS_VERSION"
echo "$HOME/.xlings/subos/current/bin" >> "$GITHUB_PATH"
- name: Install mcpp
run: |
# The same wait the gate job performs, and for the same reason: a
# version bump's index pointer propagates asynchronously, and a single
# `xlings update` can return a stale index while reporting freshness.
for attempt in 1 2 3 4 5 6; do
xlings update > /dev/null 2>&1 || true
if xlings install "mcpp@$MCPP_VERSION" -y -g; then break; fi
if [ "$attempt" = 6 ]; then
echo "::error::mcpp@$MCPP_VERSION never appeared in the index"
exit 1
fi
echo "the index has not caught up yet (attempt $attempt of 6); waiting 60s"
sleep 60
done
mcpp --version
mcpp self config --mirror GLOBAL
# ⚠️ BOTH HOMES. The shim on PATH dispatches against whichever home owns
# it, while `mcpp run` starts the runner through mcpp's own.
- name: Install the emulator
run: |
xlings install xim:qemu-arm -y
XLINGS_HOME="$HOME/.mcpp/registry" xlings install xim:qemu-arm -y
- name: The scheduler preempts on mps2-an385
working-directory: examples/preempt
run: |
set -euo pipefail
# Twice, the first allowed to fail: the toolchain payload is installed
# during a build, so the first build on a machine that has never
# targeted this triple is the one that installs it.
mcpp build --target thumbv7m-none-eabi > /dev/null 2>&1 || true
# ⚠️⚠️ NOT `mcpp run | tee`, AND THE PIPELINE IS THE POINT. `$?` after a
# pipeline is the LAST command's status, so `| tee` would read tee's 0
# and the exit check below would be vacuous — which is how this job
# found the defect it now guards: the example printed
# `both tasks observed preemption` and `mcpp run` exited 1, because
# the board passed `SYS_EXIT_EXTENDED`'s block to `SYS_EXIT`. Every
# assertion on the OUTPUT passed.
set +e
mcpp run --target thumbv7m-none-eabi > run.log 2>&1
rc=$?
set -e
cat run.log
grep -q "both tasks observed preemption" run.log \
|| { echo "the tasks were never interleaved"; exit 1; }
[ "$rc" = "0" ] \
|| { echo "the scheduler reported success and exited $rc — check the semihosting exit call"; exit 1; }
# ⭐⭐ THE ONE-LINE USAGE REACHES THIS BACKEND, WHICH IS A DIFFERENT CLAIM
# FROM "THE BACKEND WORKS".
#
# `examples/preempt` names `openarch-cortex-m` by PATH, so it exercises
# the backend and says nothing about whether a CONSUMER can get it. The
# documented one-line usage is `openarch = "<version>"`, and that goes
# through `backend-auto` — a table in the ROOT manifest that is a
# different file from the one declaring the backend.
#
# Measured on 0.8.0 as first written: a fresh project against the
# published package got
#
# error: no package provides capability 'openarch-backend'
#
# The backend was in the tarball and unreachable. `cfg(arch = ...)`
# compares exactly and there is no glob, so M-profile needs one row per
# arch spelling; this step is what keeps all five bound.
- name: The one-line usage reaches the Cortex-M backend
run: |
set -euo pipefail
n=0
for t in thumbv6m-none-eabi thumbv7m-none-eabi thumbv7em-none-eabi \
thumbv8m.base-none-eabi thumbv8m.main-none-eabi; do
D=$(mktemp -d); mkdir -p "$D/src"
printf '[package]\nname="oap"\nversion="0.1.0"\n[build]\ntarget="%s"\nsources=["src/main.cpp"]\n[target.%s]\nsysroot=""\n[dependencies]\nopenarch = { path = "%s" }\n' \
"$t" "$t" "$PWD" > "$D/mcpp.toml"
printf 'int main(){return 0;}\n' > "$D/src/main.cpp"
( cd "$D" && mcpp build --verbose > v.log 2>&1 ) || true
c=$(grep -c 'openarch-cortex-m' "$D/v.log" || true)
[ "$c" -gt 0 ] || {
echo "::error::$t resolved no Cortex-M backend through backend-auto"
tail -20 "$D/v.log"; exit 1; }
echo " ok $t binds the backend ($c compile lines)"
n=$((n+1)); rm -rf "$D"
done
# A count, because a loop that ran zero times also reaches this line.
[ "$n" = "5" ] || { echo "::error::checked $n arch spellings, expected 5"; exit 1; }
# ⭐ THE EXAMPLE CONTAINS NO ASSEMBLY, AND THAT IS AN ASSERTION RATHER
# THAN A REMARK. Its first version hand-wrote thirty lines of PendSV,
# because the layer had no primitive for "switch the context this trap
# will return to". If that code comes back, the primitive has stopped
# carrying its weight and this check is where it is noticed.
- name: The example is scheduling policy, not machine code
run: |
set -euo pipefail
if grep -nE '__asm__|asm volatile' examples/preempt/src/main.cpp; then
echo "the scheduler has grown assembly; arch_trap_switch should have made it unnecessary"
exit 1
fi
grep -q 'arch_trap_switch' examples/preempt/src/main.cpp \
|| { echo "the example no longer exercises arch_trap_switch"; exit 1; }
echo "the scheduler is forty lines of policy"
# ---------------------------------------------------------------------------
# The half no emulator can check.
#
# A mis-shifted field in a page-table entry still maps something, still boots,
# and faults later somewhere that names neither the encoder nor the entry. The
# encoders are pure functions in per-architecture namespaces, so a host build
# holds BOTH and compares them — which is what several of the assertions are,
# and what no single-target build could express.
host-encoders:
name: the page-table encoders agree (${{ matrix.os }})
runs-on: ${{ matrix.os }}
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
os: [ubuntu-24.04, macos-14, windows-2022]
defaults:
run:
shell: bash
env:
MCPP_VERSION: 2026.9.4.1
XLINGS_VERSION: v2026.8.17.2
XLINGS_NON_INTERACTIVE: '1'
steps:
- uses: actions/checkout@v4
- name: Install xlings (Unix)
if: runner.os != 'Windows'
run: |
curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh \
| bash -s "$XLINGS_VERSION"
echo "$HOME/.xlings/subos/current/bin" >> "$GITHUB_PATH"
- name: Install xlings (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
irm https://d2learn.org/xlings-install.ps1.txt | iex
"$env:USERPROFILE\.xlings\subos\current\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
- name: Install mcpp
run: |
for attempt in 1 2 3 4 5 6; do
xlings update > /dev/null 2>&1 || true
if xlings install "mcpp@$MCPP_VERSION" -y -g; then break; fi
if [ "$attempt" = 6 ]; then
echo "::error::mcpp@$MCPP_VERSION never appeared in the index (6 attempts over 5 minutes)."
exit 1
fi
echo "the index has not caught up yet (attempt $attempt of 6); waiting 60s"
sleep 60
done
mcpp --version
mcpp self config --mirror GLOBAL
# ⚠️ NO FEATURE FLAGS, AND THAT IS THE OUTCOME OF A MEASUREMENT. The
# encoders were feature-selected translation units until two facts ended
# that design: a file named by any feature belongs to it exclusively (with
# the feature inactive it is not compiled even when a `cfg` block lists it,
# and the library build still reports success), and compiling both
# unconditionally instead put 450 bytes of the foreign encoder into a
# riscv64 image whose whole probe is about 2000. They are `inline` in a
# header now: a target build instantiates only what it calls — measured,
# zero foreign symbols in either image — and a host build that calls both
# gets both, with nothing to activate.
# ⚠️ THREE ENCODERS SINCE 0.4.0, AND THE THIRD SETTLED A QUESTION THE
# FIRST TWO LEFT OPEN. `openarch.pte` owns `MAIR_EL1` because aarch64's
# entry holds an INDEX into it rather than a memory type, while riscv's
# holds the type itself — one against one, and "this layer owns the
# attribute register" could fairly be called a workaround for aarch64.
# x86_64 does the same thing with `IA32_PAT`, on a machine that shares no
# lineage with it, so the majority is now two to one the other way.
- name: The encoders compile here and agree
run: mcpp test
# ---------------------------------------------------------------------------
# The cross-compilation is performed FROM three systems, not only from Linux.
#
# Every target in this repository is a cross target, which makes the host a
# separate axis from the target: the compiler, the target C library and the
# emulator are payloads mcpp resolves for whichever system it is running on.
# A package that has only ever been built from Linux is a package whose
# consumers must use Linux, and nothing in these sources says so.
#
# ⚠️ A TOOLCHAIN AXIS IS ABSENT HERE, AND THAT IS MEASURED RATHER THAN
# ASSUMED. The row for a bare-metal triple names its compiler, and the
# command-line override does not displace it: `--toolchain gcc@16.1.0` on a
# `riscv64-none-elf` build resolves llvm@22.1.8 regardless. A matrix over
# compiler families would therefore run the same compiler on every row and
# report coverage it does not have. The toolchain axis belongs where the
# choice is real — openkal, whose declarations are compiled by three families
# on three systems.
#
# ⚠️ BUILD ONLY, AND DELIBERATELY. Behaviour is asserted once, above, under an
# emulator. Booting the same image from three systems would be a statement
# about the emulator rather than about this package, and "the image does what
# the README says" does not become more true for having been observed from
# macOS.
portability:
name: cross-builds from ${{ matrix.os }}
runs-on: ${{ matrix.os }}
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
os: [macos-14, windows-2022]
defaults:
run:
shell: bash
env:
MCPP_VERSION: 2026.9.4.1
XLINGS_VERSION: v2026.8.17.2
XLINGS_NON_INTERACTIVE: '1'
steps:
- uses: actions/checkout@v4
- name: Install xlings (Unix)
if: runner.os != 'Windows'
run: |
curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh \
| bash -s "$XLINGS_VERSION"
echo "$HOME/.xlings/subos/current/bin" >> "$GITHUB_PATH"
- name: Install xlings (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
irm https://d2learn.org/xlings-install.ps1.txt | iex
# The installer amends the user's environment; no later step in this
# job reads it back, so the directory is named here.
"$env:USERPROFILE\.xlings\subos\current\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
- name: Install mcpp
run: |
# ⚠️ A LOOP, BECAUSE ONE `xlings update` CAN RETURN A STALE INDEX
# WITHOUT SAYING SO.
#
# The index is published as an artifact behind a pointer, and that
# pointer propagates asynchronously after a version bump is merged.
# Measured on release day: an update run four minutes after the merge
# printed `index updated`, and the install then failed with
#
# package 'mcpp@<ver>' not found in the synced index
# (xim@artifact:<an older sha>, ...), synced 0 seconds ago
#
# Nothing had gone wrong. The update fetched the PREVIOUS artifact,
# and "synced 0 seconds ago" describes when it was fetched rather than
# what it contains — which is why the message reads as freshness.
#
# So this is not a retry around flakiness; it is the wait that a
# single update does not perform. A pin naming a version that was
# never published still fails, after the last attempt, and says which
# of the two situations it is.
for attempt in 1 2 3 4 5 6; do
xlings update > /dev/null 2>&1 || true
if xlings install "mcpp@$MCPP_VERSION" -y -g; then break; fi
if [ "$attempt" = 6 ]; then
echo "::error::mcpp@$MCPP_VERSION never appeared in the index (6 attempts over 5 minutes). If it was just released, the pointer has not propagated; if the pin names a version that was never published, it never will."
exit 1
fi
echo "the index has not caught up yet (attempt $attempt of 6); waiting 60s"
sleep 60
done
mcpp --version
mcpp self config --mirror GLOBAL
- name: The arch mechanisms cross-build
run: |
# ⚠️ TWICE, AND THE FIRST IS ALLOWED TO FAIL — every row of this
# matrix is a machine that has never targeted this triple, which is
# precisely where mcpp's lazy install of the target C library shows.
# The first build compiles sources needing its headers before they
# exist; the second has them. A single build here reported a
# portability failure that was really a cold-machine one.
mcpp build --target riscv64-none-elf || true
mcpp build --target riscv64-none-elf