0.8.0 —— trap 组补上一个动作,第四台机器实现了它 #27
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | |
| mcpp run --target thumbv7m-none-eabi 2>&1 | tee run.log | |
| grep -q "both tasks observed preemption" run.log \ | |
| || { cat run.log; echo "the tasks were never interleaved"; 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 |