-
Notifications
You must be signed in to change notification settings - Fork 0
443 lines (418 loc) · 21.1 KB
/
Copy pathci.yml
File metadata and controls
443 lines (418 loc) · 21.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
name: CI
# What this workflow asserts.
#
# A C++ standard library is not portable in the sense a program is: it is
# configured for one C library and compiled against that library's headers. The
# claim here is therefore not "it builds" but "a program above it does the
# things a C++ program does", and the one that settles it is an exception thrown
# across frames and caught --- because that is the path a build proves nothing
# about. Everything else this package could check, a runtime that was linked but
# never worked would also satisfy.
on:
push:
branches: [main]
pull_request:
workflow_dispatch:
env:
MCPP_VERSION: 2026.8.19.4
XLINGS_VERSION: v2026.8.17.2
XLINGS_NON_INTERACTIVE: '1'
jobs:
runtime:
name: build the runtime and run what stands above it (${{ matrix.toolchain }})
runs-on: ubuntu-24.04
# ⚠️ Raised for the self-build step below, which is temporary. See the note
# there: when mcpp#486 ships, that step goes and so does this.
timeout-minutes: 90
strategy:
fail-fast: false
matrix:
toolchain: ['llvm@22.1.8']
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v4
- name: Install xlings and mcpp
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: |
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
sleep 60
done
mcpp --version
mcpp self config --mirror GLOBAL
- name: Select the toolchain
run: |
spec='${{ matrix.toolchain }}'
mcpp toolchain install "${spec%@*}" "${spec#*@}"
mcpp toolchain default "$spec"
- name: The runtime builds
run: mcpp build
# ⚠️ THE ONE OBSERVATION A BUILD CANNOT MAKE.
#
# An unwinder that cannot find the program's frame descriptions compiles,
# links, and runs every path that does not throw. Measured while this
# package was written: the whole example above printed its first line and
# then `libc++abi: terminating due to uncaught exception', with
# _Unwind_Backtrace walking zero frames --- and nothing named the cause.
# So the throw is the check, and the destructor beside it is what says the
# unwind was correct rather than merely non-fatal.
- name: A C++ program above it throws across frames and catches
run: |
cd examples/cxx && mcpp run 2>&1 | tee out.log
grep -q -- '-- failures: 0 --' out.log
- name: import std above it
run: |
cd examples/import-std && mcpp run 2>&1 | tee out.log
grep -q 'import std above openkal: 2 4 7' out.log
# ⭐⭐ THE SAME PROGRAM ON A MACHINE WITH NO OPERATING SYSTEM.
#
# Everything above this step runs on a host, and a host has a C library, a
# C++ runtime and an unwinder already installed. A program that reaches
# one of them by mistake still works — so those steps can pass without
# having exercised this package's own copies at all.
#
# There is nothing here to reach. The C library is openkal-musl, the
# standard library and the unwinder are this package's, and beneath them
# is firmware whose whole interface is `ecall`. ⇒ This is the step that
# cannot go green by accident, which is why the design document makes it
# the acceptance criterion rather than one more row.
#
# ⚠️ The assertion is on the OUTPUT and not on the exit status: firmware
# that never reaches the payload exits zero, and so does a payload whose
# console writes go nowhere.
- name: Install the emulator
if: matrix.toolchain == 'llvm@22.1.8'
run: |
xlings install xim:qemu-riscv -y
XLINGS_HOME="$HOME/.mcpp/registry" xlings install xim:qemu-riscv -y
# ⚠️ THE BUILD TOOL IS BUILT FROM A BRANCH, AND THAT IS TEMPORARY.
#
# This step needs four decisions that no released mcpp makes yet, each of
# which belongs to the tool rather than to this package:
#
# `import std` is gated on whether a package PROVIDES a standard
# library for the target rather than on whether the target is
# freestanding; a package may carry its own std module source;
# `-fno-exceptions` / `-fno-rtti` / `-ffreestanding` come off when the
# graph supplies a C++ runtime built for the target; and unwind tables
# go on, because the compiler turns them off for this kind of target
# and a partial set of tables stops the walk rather than degrading it.
#
# They are on mcpp-community/mcpp#486. Until that is released this job
# builds the tool the same way every other dependency here is taken —
# from the branch — so the criterion below is actually enforced instead
# of being a comment saying it was verified once on a laptop.
#
# ⇒ When #486 ships, delete this step and raise MCPP_VERSION.
- name: The build tool, from the branch that has what this needs
if: matrix.toolchain == 'llvm@22.1.8'
run: |
set -euo pipefail
git clone --depth 1 -b feat/import-std-capability \
https://github.com/mcpp-community/mcpp "$RUNNER_TEMP/mcpp-src"
cd "$RUNNER_TEMP/mcpp-src"
# ⚠️ THE CLONE CARRIES A WORKSPACE PIN, AND IT NAMES A VERSION THE
# INDEX NO LONGER HAS.
#
# xlings: version '2026.8.17.1' not found for 'mcpp'
# available: 2026.8.19.4
#
# `.xlings.json` at a repository root says which mcpp a build in that
# tree uses, and mcpp's own bootstrap pin does not move when mcpp is
# released — it is the version that was current when the pin was last
# touched. Cloning the branch therefore imports a pin that is only
# valid inside that repository's own CI, where the same file selects
# what gets installed.
#
# Rewriting it to the version this job already installed is what makes
# the two agree. It changes nothing about what is being tested: the
# pin selects the tool that BUILDS mcpp, and what is under test is the
# mcpp that comes out.
printf '{\n "workspace": {\n "mcpp": "%s"\n }\n}\n' "$MCPP_VERSION" > .xlings.json
# ⚠️ `--dev` AND NOT `--release`. What is under test is a set of
# decisions the tool makes about compile flags; an optimisation level
# changes none of them. Measured: the release self-build took over
# half an hour of a sixty-minute job on a two-core runner, which is
# most of the budget spent on something the test does not observe.
mcpp build --dev
BUILT=$(find target -type f -name mcpp -perm -u+x | head -1)
[ -n "$BUILT" ] || { echo "::error::mcpp did not build"; exit 1; }
echo "$(cd "$(dirname "$BUILT")" && pwd)" >> "$GITHUB_PATH"
"$BUILT" --version
- name: The same source on bare metal, with exceptions
if: matrix.toolchain == 'llvm@22.1.8'
run: |
set -euo pipefail
cd examples/same-source
Q=$(ls -d "$HOME"/.mcpp/registry/data/xpkgs/xim-x-qemu-riscv/*/bin/qemu-system-riscv64 | head -1)
# ⚠️ Anchored on the BARE NAME, so a checkout that already carries a
# path is left alone rather than getting a path inside a path. The
# manifest is required to carry the bare name; the note beside it says
# why, and this is the step that relies on it.
grep -q '"qemu-system-riscv64"' mcpp.toml \
|| { echo "::error::the manifest no longer carries the bare emulator name"; exit 1; }
sed -i "s|\"qemu-system-riscv64\"|\"$Q\"|" mcpp.toml
mcpp run 2>&1 | tee out.log
grep -q 'sorted: 2 4 7' out.log # containers + algorithms + the allocator
grep -q 'caught: 42' out.log # the unwinder found the handler
grep -q 'unwound: true' out.log # ⭐ and ran a destructor on the way
grep -q 'import std over openkal: ok' out.log
# ⭐⭐ AND THE SAME SOURCE ON THIS MACHINE, WHICH IS WHAT MAKES THE STEP
# ABOVE A DEMONSTRATION RATHER THAN AN ILLUSTRATION.
#
# Nothing is edited between the two commands — no `#if`, no second
# directory, no second source. The only thing that differs is the memory
# layout, and `build.mcpp` states it behind a condition on the target OS
# because that is a statement about the machine.
- name: The same source, on this machine, over openkal-linux
if: matrix.toolchain == 'llvm@22.1.8'
run: |
set -euo pipefail
cd examples/same-source && mcpp run 2>&1 | tee host.log
grep -q 'import std over openkal: ok' host.log
# The four lines are the same four lines.
diff <(grep -E '^(sorted|caught|unwound|import std over openkal):' out.log) \
<(grep -E '^(sorted|caught|unwound|import std over openkal):' host.log)
# ⭐⭐ AND THE SAME SOURCE FOR TWO MACHINES THIS ONE IS NOT.
#
# The two steps above prove the source does not know which machine it is
# for. These prove the BUILD does not need to be on it: one Linux host
# produces a PE and a Mach-O, and the jobs below run them on the real
# thing with nothing installed.
#
# ⚠️ THE ARTEFACT IS THE ARGUMENT, WHICH IS WHY THOSE JOBS INSTALL NOTHING.
# Not mcpp, not a compiler, not a C runtime — the program carries its C
# library, its C++ runtime and its unwinder, and what remains is the
# operating system it was built for. A run that needed a redistributable
# installed first would be demonstrating something weaker.
- name: The same source, built here for Windows and for macOS
if: matrix.toolchain == 'llvm@22.1.8'
run: |
set -euo pipefail
cd examples/same-source
mkdir -p "$RUNNER_TEMP/cross"
for t in x86_64-windows-gnu aarch64-macos; do
rm -rf target
mcpp build --target "$t"
a=$(find target -type f \( -name 'openkal-same-source' -o -name '*.exe' \) | head -1)
[ -n "$a" ] || { echo "::error::$t produced no artefact"; exit 1; }
echo "$t → $(file -b "$a")"
cp "$a" "$RUNNER_TEMP/cross/"
done
# ⚠️ The format is asserted here rather than left to the run jobs. A
# run that fails tells you the program did not work; this tells you
# what was produced, and the two failures need different fixes.
file "$RUNNER_TEMP/cross/openkal-same-source.exe" | grep -q 'PE32+ executable'
file "$RUNNER_TEMP/cross/openkal-same-source" | grep -q 'Mach-O 64-bit arm64'
- uses: actions/upload-artifact@v4
if: matrix.toolchain == 'llvm@22.1.8'
with:
name: cross-artifacts
path: ${{ runner.temp }}/cross/
if-no-files-found: error
# ---------------------------------------------------------------------------
# ⭐⭐ THE ACCEPTANCE CRITERION FOR PORTABILITY OF THE ARTEFACT.
#
# A cross build that produces a well-formed file proves the compiler was told
# the right target. It does not prove the program runs, and every difference
# this ecosystem has had to find on these two platforms — the loader-bootstrapped
# thread-local, the unwinder's search for its own tables, the personality
# routine — links successfully and fails at run time.
#
# ⚠️ These jobs deliberately have NO toolchain steps. If one is ever added
# because "the program needs it", that is the finding, not the fix.
run-on-windows:
name: the artefact built on Linux runs on Windows
needs: runtime
runs-on: windows-2022
timeout-minutes: 10
defaults:
run:
shell: bash
steps:
- uses: actions/download-artifact@v4
with: { name: cross-artifacts, path: art }
- name: It runs, and it unwinds
run: |
set -euo pipefail
./art/openkal-same-source.exe 2>&1 | tee out.log
grep -q 'sorted: 2 4 7' out.log
grep -q 'caught: 42' out.log
# ⭐ The line a link cannot fake: a destructor ran during the unwind,
# so libunwind found `.eh_frame` by reading the image rather than by
# asking the operating system to enumerate modules.
grep -q 'unwound: true' out.log
grep -q 'import std over openkal: ok' out.log
run-on-macos:
name: the artefact built on Linux runs on macOS
needs: runtime
runs-on: macos-14
timeout-minutes: 10
steps:
- uses: actions/download-artifact@v4
with: { name: cross-artifacts, path: art }
- name: It runs, and it unwinds
run: |
set -euo pipefail
# ⚠️ The executable bit does not survive an artefact upload.
chmod +x art/openkal-same-source
# ⚠️ AND THE SIGNATURE DOES. arm64 macOS refuses an unsigned image, so
# this is asserted before the run: a failure here is "the linker did
# not ad-hoc sign it", which is a different repair from "the program
# crashed".
codesign -dv art/openkal-same-source 2>&1 | grep -q 'adhoc\|Signature'
# ⚠️ A CRASH IS DIAGNOSED HERE RATHER THAN GUESSED AT LATER. This is
# the first time an artefact of this stack has run on this system, and
# the interesting failures — the entry point's assumptions about what
# the kernel hands it, the thread pointer, the two borrowed names —
# all look identical from outside: `Segmentation fault: 11`.
# One CI cycle that prints a backtrace is worth several that do not.
if ! ./art/openkal-same-source > out.log 2>&1; then
echo "--- how far the loader got ---"
# ⚠️ To a file, then read. Piping into `tail` interleaves dyld's
# output with the shell's own report of the signal, and the last
# lines — the ones that say which initializer was running — are the
# ones that get lost.
DYLD_PRINT_INITIALIZERS=1 ./art/openkal-same-source > dyld.log 2>&1 || true
tail -40 dyld.log
echo "--- it did not run; what the debugger saw ---"
# ⭐ `lr` IS THE DATUM. A jump to address 0 leaves no frame to
# unwind, so `bt` says only "frame #0: 0x0" — which is the symptom
# restated. The link register still holds the return address of
# whoever made that call, and `image lookup` turns it into a name.
# ⚠️ `-k` AND NOT `-o`. In `--batch` lldb abandons the remaining
# `-o` commands after the first one that errors, and reading
# register state at a PC of 0 errors — so the three commands that
# would have said something never ran. `-k` is the list lldb
# executes WHEN THE PROCESS CRASHES, which is the case at hand.
lldb --batch \
-k 'register read pc lr sp fp x8 x9 x16 x17' \
-k 'image lookup --address $lr' \
-k 'thread backtrace all' \
-k 'image list -o -f' \
-o run \
-- ./art/openkal-same-source 2>&1 | tail -70 || true
echo "--- the image's own dependencies ---"
otool -L art/openkal-same-source || true
otool -l art/openkal-same-source | grep -A4 LC_MAIN || true
cat out.log
exit 1
fi
cat out.log
grep -q 'sorted: 2 4 7' out.log
grep -q 'caught: 42' out.log
grep -q 'unwound: true' out.log
grep -q 'import std over openkal: ok' out.log
# ---------------------------------------------------------------------------
# ⭐⭐ THE HOST DIMENSION — THE HALF OF THE CLAIM THE JOBS ABOVE DO NOT TOUCH.
#
# Everything above builds on Linux. That establishes "one host reaches every
# target" and leaves open the thing an N×N matrix would otherwise have to
# enumerate: whether the HOST matters. The scheme's answer is that it does not
# — the target side is a set of packages and the compiler is a retargetable
# clang, so N hosts × N targets collapses to N implementations plus one tool.
#
# ⚠️ THAT IS A CLAIM, AND CLAIMS OF THIS SHAPE HAVE BEEN WRONG HERE BEFORE:
# the Linux host needed four separate repairs before it reached PE, and every
# one of them was invisible until a build was actually run. Two more hosts
# cost two jobs; asserting the collapse without running them costs a paragraph
# and proves nothing.
#
# Each host builds all four targets and RUNS the one that is itself, which is
# the same criterion the Linux job applies to itself.
host-dimension:
name: ${{ matrix.host }} host reaches every target
needs: runtime
runs-on: ${{ matrix.runner }}
timeout-minutes: 120
strategy:
fail-fast: false
matrix:
include:
- { host: macOS, runner: macos-14, native: aarch64-macos }
- { host: Windows, runner: windows-2022, native: x86_64-windows-gnu }
defaults:
run:
shell: bash
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"
exit 1
fi
sleep 60
done
mcpp self config --mirror GLOBAL
# ⚠️ INSTALL, THEN SELECT. `toolchain default` names a toolchain and
# does not fetch one, so selecting an absent payload fails with
# `llvm@22.1.8 is not installed` — measured on both rows of this job.
mcpp toolchain install llvm 22.1.8
mcpp toolchain default 'llvm@22.1.8'
# ⚠️ Same temporary step as the Linux job: the decisions this exercises are
# on mcpp#486 and not in any release. When that ships, this goes and
# MCPP_VERSION rises. See the long note in the `runtime` job.
- name: The build tool, from the branch that has what this needs
run: |
set -euo pipefail
git clone --depth 1 -b feat/import-std-capability \
https://github.com/mcpp-community/mcpp "$RUNNER_TEMP/mcpp-src"
cd "$RUNNER_TEMP/mcpp-src"
printf '{\n "workspace": {\n "mcpp": "%s"\n }\n}\n' "$MCPP_VERSION" > .xlings.json
mcpp build --dev
# ⚠️ Named exactly, and `-perm` is not used. On Windows every file
# reads as executable, so a permission test selects nothing useful;
# `mcpp.exe` is the name there and `mcpp` everywhere else.
BUILT=$(find target -type f \( -name 'mcpp' -o -name 'mcpp.exe' \) | head -1)
[ -n "$BUILT" ] || { echo "::error::mcpp did not build"; exit 1; }
echo "$(cd "$(dirname "$BUILT")" && pwd)" >> "$GITHUB_PATH"
"$BUILT" --version
- name: Every target, from this host
run: |
set -euo pipefail
cd examples/same-source
for t in x86_64-linux-gnu x86_64-windows-gnu aarch64-macos riscv64-none-elf; do
rm -rf target
mcpp 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 this host"; exit 1; }
echo "$t → $(file -b "$a" 2>/dev/null || echo built)"
done
# ⭐ And the one that is this machine, run rather than inspected — the same
# criterion the Linux job holds itself to, applied from a different host.
- name: The artefact for this host runs on it
run: |
set -euo pipefail
cd examples/same-source
rm -rf target
mcpp run --target '${{ matrix.native }}' 2>&1 | tee out.log
grep -q 'sorted: 2 4 7' out.log
grep -q 'caught: 42' out.log
grep -q 'unwound: true' out.log
grep -q 'import std over openkal: ok' out.log