66 pull_request :
77 workflow_dispatch :
88
9- # Three jobs, and the first two are ONE job written once: the gate this layer is
10- # judged by is that a single probe source runs on two genuinely different
11- # machines, so the two rows below differ only in a triple and an emulator
12- # package. If they ever need to differ in anything else, the abstraction has
13- # failed and this workflow is where that becomes visible.
9+ # Three jobs, and the first is ONE job written once: the gate this layer is
10+ # judged by is that a single probe source runs on machines that genuinely
11+ # differ, so the rows below differ only in a triple and an emulator. If they
12+ # ever need to differ in anything else, the abstraction has failed and this
13+ # workflow is where that becomes visible.
14+ #
15+ # ⭐ THE THIRD ROW IS WHAT TURNS THE GATE INTO EVIDENCE. riscv64 and aarch64 are
16+ # both load/store RISC machines with a weak memory model and a fixed instruction
17+ # width, so an interface that fits both may fit because it is right or because
18+ # they are alike. x86_64 is neither: variable-length instructions, total store
19+ # order — under which three of the four barriers need no instruction at all —
20+ # and an interrupt mechanism that is a table of 256 gates rather than a base
21+ # register. What survives all three is an abstraction.
1422jobs :
1523 gate :
1624 name : the probe runs on ${{ matrix.arch }}
@@ -20,10 +28,23 @@ jobs:
2028 fail-fast : false
2129 matrix :
2230 include :
23- - { arch: riscv64, triple: riscv64-none-elf, qemu: 'xim:qemu-riscv' }
24- - { arch: aarch64, triple: aarch64-none-elf, qemu: 'xim:qemu-arm' }
31+ - { arch: riscv64, triple: riscv64-none-elf, qemu: 'xim:qemu-riscv', apt: '' }
32+ - { arch: aarch64, triple: aarch64-none-elf, qemu: 'xim:qemu-arm', apt: '' }
33+ # ⚠️ THE THIRD ROW'S EMULATOR COMES FROM apt, AND THAT IS A GAP IN THE
34+ # ECOSYSTEM RATHER THAN A PREFERENCE.
35+ #
36+ # The other two emulators are xlings packages because xPack publishes
37+ # QEMU per target family and the index carries what it publishes.
38+ # xPack has no x86 build, so there is no `xim:qemu-x86` to install —
39+ # see .agents/docs/2026-08-21-freestanding-outstanding-four.md §4 in
40+ # the engine repository, where building one is staged work.
41+ #
42+ # Naming apt here rather than quietly relying on whatever is on the
43+ # runner keeps the difference visible: this row is the one whose
44+ # emulator the ecosystem does not yet own.
45+ - { arch: x86_64, triple: x86_64-none-elf, qemu: '', apt: 'qemu-system-x86' }
2546 env :
26- MCPP_VERSION : 2026.8.20.3
47+ MCPP_VERSION : 2026.8.21.1
2748 XLINGS_VERSION : v2026.8.17.2
2849 XLINGS_NON_INTERACTIVE : ' 1'
2950 steps :
@@ -94,18 +115,25 @@ jobs:
94115 # `qemu-arm` only the two Arm ones. Measured; no single package runs both.
95116 - name : Install the emulator
96117 run : |
97- xlings install ${{ matrix.qemu }} -y
98- XLINGS_HOME="$HOME/.mcpp/registry" xlings install ${{ matrix.qemu }} -y
118+ if [ -n "${{ matrix.qemu }}" ]; then
119+ xlings install ${{ matrix.qemu }} -y
120+ XLINGS_HOME="$HOME/.mcpp/registry" xlings install ${{ matrix.qemu }} -y
121+ else
122+ sudo apt-get update -qq
123+ sudo apt-get install -y -qq ${{ matrix.apt }}
124+ qemu-system-x86_64 --version | head -1
125+ fi
99126
100127 - name : The layer builds for ${{ matrix.arch }}
101128 run : |
102129 # Twice, the first allowed to fail: the toolchain payload is installed
103130 # during a build, so the first build on a machine that has never
104131 # targeted this triple is the one that installs it.
105- # ⚠️ THE BACKEND, NOT THE WORKSPACE ROOT. Since 0.3.1 the root is a
106- # workspace and carries no sources of its own; building the backend
107- # for this target pulls the ABI it implements and is what the probe
108- # below will link against.
132+ # ⚠️ THE BACKEND, NOT THE ROOT. Since 0.4.0 the root is BOTH the
133+ # interface package and the workspace, so building it would build the
134+ # interface; what this step wants is the machine half. Building the
135+ # backend for this target pulls the ABI it implements and is what the
136+ # probe below will link against.
109137 ( cd backends/${{ matrix.arch }} && mcpp build --target ${{ matrix.triple }} ) || true
110138 ( cd backends/${{ matrix.arch }} && mcpp build --target ${{ matrix.triple }} )
111139
@@ -120,10 +148,26 @@ jobs:
120148 working-directory : examples/switch
121149 run : |
122150 set -euo pipefail
123- mcpp run --target ${{ matrix.triple }} 2>&1 | tee run.log
151+ if [ -n "${{ matrix.qemu }}" ]; then
152+ mcpp run --target ${{ matrix.triple }} 2>&1 | tee run.log
153+ else
154+ # ⚠️ `mcpp build` AND THEN qemu BY HAND, BECAUSE `build.mcpp`
155+ # CONFIGURES NO RUNNER FOR THIS TARGET. It asks mcpp where
156+ # `xim:qemu-x86` landed and there is no such package; a build
157+ # program that fell back to a bare `qemu-system-x86_64` would make
158+ # the build depend on what happens to be installed on the machine,
159+ # which is the thing the other two rows do not do.
160+ mcpp build --target ${{ matrix.triple }}
161+ IMG=$(find target/${{ matrix.triple }} -type f -name switch | head -1)
162+ test -n "$IMG"
163+ timeout -k 5 60 qemu-system-x86_64 -machine q35 -nographic -no-reboot \
164+ -kernel "$IMG" 2>&1 | tee run.log
165+ fi
124166 grep -q "task: arg=42" run.log
125167 grep -q "witness=7 before=1234" run.log
126168 grep -q "switch ok" run.log
169+ grep -q "trap: back, witness=1" run.log
170+ grep -q "cpu: percpu round-trips" run.log
127171
128172 # The same source produced that output. Asserted rather than trusted: a
129173 # probe that had quietly grown a per-architecture branch would still pass
@@ -132,26 +176,73 @@ jobs:
132176 run : |
133177 set -euo pipefail
134178 test -f examples/switch/src/main.cpp
135- if grep -qE '__riscv|__aarch64__|MCPP_TARGET_ARCH' examples/switch/src/main.cpp; then
136- echo "the probe branches on the architecture, which is what it exists to avoid"
179+
180+ # ⚠️ THIS CHECK USED TO READ "NO ARCHITECTURE MACRO APPEARS AT ALL",
181+ # AND IT WAS WRONG IN A WAY THAT COST TWO RELEASES OF RED CI.
182+ #
183+ # The probe must name an architecture in exactly one place: the trap
184+ # instruction. `ebreak`, `brk #0` and `int3` are three spellings of
185+ # one idea and there is no portable fourth. When the trap interface
186+ # landed in 0.3.0 the old check began failing on every push, correctly
187+ # by its own wording and wrongly by its intent — and it kept failing
188+ # through 0.3.1 because nobody read the runs.
189+ #
190+ # What the gate actually claims is that the probe is not TWO PROGRAMS.
191+ # So: one conditional, and nothing inside it but instructions. A
192+ # second conditional, or a line of logic inside the first, is the
193+ # thing this is here to catch.
194+ NCOND=$(grep -c '^#if defined(__' examples/switch/src/main.cpp || true)
195+ if [ "$NCOND" != "1" ]; then
196+ grep -n '^#if defined(__' examples/switch/src/main.cpp || true
197+ echo "the probe has $NCOND architecture conditionals; it may have exactly one, for the trap instruction"
137198 exit 1
138199 fi
200+ STRAY=$(awk '
201+ /^#if defined\(__/ || /^#elif defined\(__/ { inb=1; next }
202+ /^#else/ { next }
203+ /^#endif/ { inb=0; next }
204+ inb && $0 !~ /^[[:space:]]*(\/\/)?[[:space:]]*$/ \
205+ && $0 !~ /asm volatile/ && $0 !~ /^#[[:space:]]*error/ \
206+ && $0 !~ /^[[:space:]]*\/\// { print NR": "$0 }
207+ ' examples/switch/src/main.cpp)
208+ if [ -n "$STRAY" ]; then
209+ echo "$STRAY"
210+ echo "the architecture conditional contains something other than an instruction"
211+ exit 1
212+ fi
213+ echo "the probe names an architecture once, to emit a trap instruction"
139214
140215 # ⚠️ THE SEPARATION IS ASSERTED, NOT TRUSTED TO THE DIRECTORY NAMES.
141216 #
142217 # Until 0.3.1 the backends lived under `src/arch/<arch>/` in the same
143218 # package as the specification, and the layering was a convention held
144219 # up by a path. The split made it a dependency graph; these two checks
145220 # make it a test.
146- if grep -rqE '\basm\b|__asm' spec/ src; then
147- echo "the specification contains an instruction; it must not"
221+ if grep -rqE '\basm\b|__asm' src; then
222+ echo "the interface contains an instruction; it must not"
148223 exit 1
149224 fi
150225 if grep -rq 'export module' backends; then
151- echo "a backend exports a module; it must reach the specification only through the ABI"
226+ echo "a backend exports a module; it must reach the interface only through the ABI"
152227 exit 1
153228 fi
154- echo "spec owns modules and no instruction; backends own instructions and no module"
229+ echo "the interface owns modules and no instruction; backends own instructions and no module"
230+
231+ # ⚠️ THE ROOT IS BOTH A PACKAGE AND A WORKSPACE, AND THAT IS WHAT
232+ # MAKES A CONSUMER'S SIDE ONE LINE. A virtual workspace would put the
233+ # interface in a member directory and `openarch = "0.4.0"` would have
234+ # to name it. Asserted because the two tables are ordinary TOML and
235+ # deleting one would leave a manifest that still builds.
236+ grep -q '^\[package\]' mcpp.toml
237+ grep -q '^\[workspace\]' mcpp.toml
238+
239+ # The two faces reach the same library. The C one is a header a
240+ # consumer includes; the C++ one is a module it imports. A face that
241+ # disappeared would not fail any build in this repository except the
242+ # test that names it.
243+ test -f abi/include/mcpplibs/openarch.h
244+ grep -q 'export module mcpplibs.openarch;' src/openarch.cppm
245+ echo "one package, two faces, three backends"
155246
156247 # ---------------------------------------------------------------------------
157248 # The half no emulator can check.
@@ -173,7 +264,7 @@ jobs:
173264 run :
174265 shell : bash
175266 env :
176- MCPP_VERSION : 2026.8.20.3
267+ MCPP_VERSION : 2026.8.21.1
177268 XLINGS_VERSION : v2026.8.17.2
178269 XLINGS_NON_INTERACTIVE : ' 1'
179270 steps :
@@ -218,8 +309,63 @@ jobs:
218309 # header now: a target build instantiates only what it calls — measured,
219310 # zero foreign symbols in either image — and a host build that calls both
220311 # gets both, with nothing to activate.
221- - name : Both encoders compile here and agree
222- working-directory : spec
312+ # ⚠️ THE TEMPLATE IS RENDERED BY HAND HERE, AND IT HAS TO BE.
313+ #
314+ # `mcpp new --template` resolves the package from the INDEX and takes no
315+ # path, so asking it for a template this commit ADDS would resolve the
316+ # previously published version and fail on a template that version does
317+ # not carry. The CI for a new template could never pass before the
318+ # template was published, which is the wrong way round.
319+ #
320+ # What belongs to this repository is the template's CONTENT: that the
321+ # files it ships generate a project which builds for all three machines.
322+ # That the scaffolder can fetch it is mcpp's own concern and mcpp's own
323+ # tests.
324+ #
325+ # ⚠️ The dependency is rewritten to a path. The rendered manifest names
326+ # `openarch = "<this version>"`, which is correct for a user and
327+ # unresolvable here — this version is not published yet, and a check that
328+ # silently fell back to the previous one would be testing the wrong tree.
329+ #
330+ # ⚠️ LINUX ONLY, AND THAT IS A CHOICE ABOUT PATHS RATHER THAN ABOUT
331+ # COVERAGE. The rewrite below puts `$PWD` into a manifest, and on a
332+ # Windows runner `$PWD` under Git Bash is `/d/a/openarch/openarch` while
333+ # mcpp wants a native path — a mismatch this repository has already been
334+ # bitten by once, in a C++ string literal that came out as
335+ # `"D:\a\openkal\openkal/include"`. The template's CONTENT is
336+ # host-independent; what varies per host is the toolchain payload, and
337+ # that is what the `portability` job covers.
338+ - name : The template generates a project that builds for all three machines
339+ if : runner.os == 'Linux'
340+ run : |
341+ set -euo pipefail
342+ T=templates/three-machines
343+ V=$(grep -m1 '^version' mcpp.toml | cut -d'"' -f2)
344+ D=$(mktemp -d); mkdir -p "$D/src"
345+ for f in mcpp.toml README.md build.mcpp; do
346+ sed -e "s/{{project\.name}}/k/g" -e "s/{{self\.version}}/$V/g" \
347+ "$T/$f.in" > "$D/$f"
348+ done
349+ cp "$T"/*.ld "$D/"
350+ cp "$T"/src/* "$D/src/"
351+ sed -i.bak "s|openarch = \"$V\"|openarch = { path = \"$PWD\" }|" "$D/mcpp.toml"
352+ rm -f "$D/mcpp.toml.bak"
353+ grep -q "path = " "$D/mcpp.toml" || { cat "$D/mcpp.toml"; echo "the dependency rewrite did not apply"; exit 1; }
354+ for t in riscv64-none-elf aarch64-none-elf x86_64-none-elf; do
355+ ( cd "$D" && mcpp build --target "$t" > /dev/null 2>&1 ) || true
356+ ( cd "$D" && mcpp build --target "$t" ) \
357+ || { echo "the template does not build for $t"; exit 1; }
358+ done
359+ echo "the template builds for riscv64, aarch64 and x86_64"
360+
361+ # ⚠️ THREE ENCODERS SINCE 0.4.0, AND THE THIRD SETTLED A QUESTION THE
362+ # FIRST TWO LEFT OPEN. `openarch.pte` owns `MAIR_EL1` because aarch64's
363+ # entry holds an INDEX into it rather than a memory type, while riscv's
364+ # holds the type itself — one against one, and "this layer owns the
365+ # attribute register" could fairly be called a workaround for aarch64.
366+ # x86_64 does the same thing with `IA32_PAT`, on a machine that shares no
367+ # lineage with it, so the majority is now two to one the other way.
368+ - name : The encoders compile here and agree
223369 run : mcpp test
224370
225371 # ---------------------------------------------------------------------------
@@ -257,7 +403,7 @@ jobs:
257403 run :
258404 shell : bash
259405 env :
260- MCPP_VERSION : 2026.8.20.3
406+ MCPP_VERSION : 2026.8.21.1
261407 XLINGS_VERSION : v2026.8.17.2
262408 XLINGS_NON_INTERACTIVE : ' 1'
263409 steps :
0 commit comments