-
Notifications
You must be signed in to change notification settings - Fork 0
433 lines (409 loc) · 22.2 KB
/
Copy pathci.yml
File metadata and controls
433 lines (409 loc) · 22.2 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
name: CI
# What this workflow asserts.
#
# conformance the suite in the specification package runs against this
# implementation and every observation holds
# additions this package's own tests, which examine the operations
# version 0.5 added
# surface the exported names are exactly the fifty-one the
# specification lists, in both forms
# independence the objects reference no C library symbol
#
# The last is the property version 0.5 exists for, and it is asserted against
# the objects rather than against the source, because a source can reach a C
# library through a macro.
#
# Two compiler families, because an implementation is a contract's evidence and
# a contract that holds only under the compiler its author used is a description
# of that compiler.
on:
push:
branches: [main]
pull_request:
workflow_dispatch:
inputs:
mcpp_ref:
description: "Branch of mcpp-community/mcpp to build and test against (empty = the released pin)"
required: false
default: ""
env:
MCPP_SOURCE_REF: ${{ github.event.inputs.mcpp_ref || vars.MCPP_SOURCE_REF }}
# BUMPED WITH THE ANDROID STEP, BECAUSE THAT STEP NEEDS THIS ENGINE.
# `aarch64-linux-android` and `x86_64-linux-android` become real target
# rows in 2026.9.11.3; on 2026.8.27.1 they do not exist and the step
# fails with exit 2. This is the CI pin -- what this repository is
# tested against -- and not a floor: nothing here records a minimum
# engine for consumers.
MCPP_VERSION: 2026.9.11.3
XLINGS_VERSION: v2026.8.17.2
XLINGS_NON_INTERACTIVE: '1'
jobs:
conformance:
name: conformance, surface, and independence (${{ matrix.toolchain }})
runs-on: ubuntu-24.04
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
toolchain: ['gcc@16.1.0', 'llvm@22.1.8']
steps:
- uses: actions/checkout@v4
# The specification is checked out at the branch under test where it has
# one, so that this run asserts what it is for: that the specification as
# written there and this implementation as written here agree today.
# THE COMMITTED MANIFEST NAMES NO DIRECTORY OF ANYBODY'S MACHINE.
#
# Two scripts in the specification's repository rewrite this manifest to
# name a working tree --- run-conformance.sh and run-kit-tests.sh --- and
# both restore it through a trap. A trap does not fire when the process is
# killed, and a run by hand followed by `git add -A` then publishes a path
# that exists on one machine: a consumer resolving from the index is handed
# a manifest pointing at a directory that exists nowhere.
#
# ⚠️ THAT HAS HAPPENED IN THIS ECOSYSTEM, in openkal-musl, and the working
# tree here has carried the same rewrite more than once since. This step
# runs first, so what it examines is what the commit contains.
- name: The committed manifest names no local directory
run: |
set -euo pipefail
bad=$(grep -nE '^[a-z-]+ = \{[^}]*path = "(/|[A-Za-z]:)' mcpp.toml || true)
if [ -n "$bad" ]; then
echo "::error::the committed manifest names an absolute path"
printf '%s\n' "$bad" | sed 's/^/ /'
echo " run 'git checkout -- mcpp.toml' after using the"
echo " specification's conformance or kit scripts by hand."
exit 1
fi
echo " ok every dependency is named by version, branch or a relative path"
- name: The specification
run: |
git clone --quiet https://github.com/mcpplibs/openkal.git .spec
branch='${{ github.head_ref || github.ref_name }}'
if git -C .spec rev-parse --verify --quiet "origin/$branch" > /dev/null; then
git -C .spec checkout --quiet "origin/$branch"
echo "the specification is at $branch"
else
echo "the specification has no $branch; its default branch is used"
fi
- 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: |
xlings update
# ⚠️ THE PIN MAY NAME THE RELEASE THIS RUN IS VALIDATING, which does
# not exist yet — that is the whole point of MCPP_SOURCE_REF. Bootstrap
# from whatever the index has; the step below replaces it with the
# build under review, and the pin is what an ordinary run tests.
if [ -n "${MCPP_SOURCE_REF:-}" ]; then
xlings install mcpp -y -g
else
xlings install "mcpp@$MCPP_VERSION" -y -g
fi
mcpp --version
mcpp self config --mirror GLOBAL
# ⭐⭐ CROSS-VALIDATION: BUILD THE mcpp UNDER REVIEW AND USE THAT ONE.
#
# Empty in the ordinary run, so this job keeps testing the RELEASED
# mcpp the pin above names. Set it — `workflow_dispatch` input, or the
# repository variable — and the same job runs against that source.
#
# ⚠️ THIS EXISTS BECAUSE THE ORDER USED TO BE WRONG. Several mcpp
# releases went out green and only then turned this ecosystem red: the
# engine's own CI cannot see a defect that appears only in a real
# dependency graph, and this repository could not see the engine until
# it had been published. Validating before the release closes that gap.
#
# The released mcpp installed just above is the bootstrap that compiles
# it; mcpp builds itself and there is no other compiler for it here.
if [ -n "${MCPP_SOURCE_REF:-}" ]; then
src="$RUNNER_TEMP/mcpp-src"
[ -d "$src" ] || git clone --quiet --depth 1 \
--branch "$MCPP_SOURCE_REF" \
https://github.com/mcpp-community/mcpp.git "$src"
# ⚠️ THE CLONE'S OWN WORKSPACE PIN MUST NOT DECIDE WHICH mcpp
# BUILDS IT. `.xlings.json` at mcpp's root pins the mcpp that
# compiles mcpp, and that pin does not move when mcpp is released —
# so a build inside the checkout obeys it and tries to install a
# version the index may no longer carry:
#
# [error] xlings: version '2026.8.17.1' not found for 'mcpp'
# available: 2026.8.25.1
#
# What is wanted here is the source compiled by the mcpp installed
# above, which is exactly what removing the file leaves.
rm -f "$src/.xlings.json"
( cd "$src" && mcpp build --release )
# ⚠️ BOTH SPELLINGS, AND NO `-perm`. The matrix reaches Windows and
# macOS runners too: on Windows the artefact is `mcpp.exe`, and
# `-perm -u+x` is not a question that filesystem answers the way this
# expects. Measured: `Finished release [optimized] in 173.44s`
# followed by "mcpp did not build" — the build had succeeded and the
# search was looking for the wrong name.
#
# `$src` is a FRESH clone each run, so `target/` holds exactly what
# this step just built; `-printf` would be the safer form on a cached
# tree and is a GNU extension this must not use.
built=$(find "$src/target" -type f \
\( -name mcpp -o -name mcpp.exe \) | head -1)
[ -n "$built" ] || { echo "::error::mcpp did not build from $MCPP_SOURCE_REF"; exit 1; }
echo "$(cd "$(dirname "$built")" && pwd)" >> "$GITHUB_PATH"
# ⚠️ Reported, because a PATH entry that does not win looks exactly
# like one that does until something built with the wrong engine.
echo "under review: $("$built" --version) (from $MCPP_SOURCE_REF)"
fi
# The compiler family and version for this row. mcpp keeps its toolchains
# in a sandbox of its own, so this selects rather than installs into the
# system, and `mcpp test' and `mcpp run' have no flag for it --- which is
# why it is set once here rather than passed to each command.
- name: Select the toolchain
run: |
spec='${{ matrix.toolchain }}'
case "$spec" in
msvc*) mcpp toolchain default msvc ;;
*) mcpp toolchain install "${spec%@*}" "${spec#*@}"
mcpp toolchain default "$spec" ;;
esac
mcpp toolchain list
- name: Every interface, every kind of examination
run: |
# ⚠️⚠️ `optional` IS PART OF THE SET AND WAS NOT, WHICH MADE THE STEP'S
# OWN NAME UNTRUE.
#
# `full` expands to `standard,abi,stability,cost`, and `standard` is
# the HOSTED set — core, env, time, fs, process, task. The five
# interfaces openkal 0.8 added are in `optional`, and this
# implementation provides all of them; without naming that set the
# suite compiled their sections with the bodies removed and reported
# them as not examined. Nothing failed, and nothing was checked.
#
# This implementation is the one that may name `optional` entire: it
# is the only one providing every interface, which is why it is the
# reference. A backend that declines one names the others by hand.
bash .spec/tools/run-conformance.sh openkal-linux . full,optional
- name: Point at the specification's working tree
run: |
# run-conformance.sh has already done this; it is repeated because a
# reader of this file should not have to know that.
sed 's|^openkal = .*$|openkal = { path = ".spec" }|' mcpp.toml > mcpp.toml.next
mv mcpp.toml.next mcpp.toml
- name: This package's own tests
run: |
mcpp test 2>&1 | tee tests.log
# A suite that discovered nothing reports success, so every suite is
# asserted to have run. The list is derived from the files present
# rather than written out here: a hand-written list names the suites
# that existed when it was written, and one added afterwards escapes
# the assertion silently.
missing=0
for f in tests/*.cpp; do
name="$(basename "$f" .cpp)"
grep -q "^$name \.\.\. ok" tests.log || { echo "did not run or did not pass: $name" >&2; missing=1; }
done
test "$missing" -eq 0
- name: The exported surface is complete and contains nothing else
run: |
rm -rf target && mcpp build
bash .spec/tools/check-surface.sh --complete .spec/SURFACE.txt \
$(find target -path '*/obj/*' -name '*.o' ! -name '*.m.o' ! -name 'conformance*' | tr '\n' ' ')
- name: The C++ declarations are complete
run: |
# The specification's own tool examines the C form. The module form is
# examined here, where a build of the modules exists: the test names
# every entity SURFACE.txt lists, so a name the modules do not export
# fails to compile and the diagnostic names it.
list="$(grep -vE '^[[:space:]]*(#|$)' .spec/SURFACE.txt | sort -u)"
{
# ⚠️ THE MODULE LIST IS DERIVED, NOT WRITTEN OUT.
#
# It used to be written out, and it fell out of step the first time
# the specification gained an interface: SURFACE.txt listed four
# `kal_exec_*' names, no `import openkal.exec' was emitted, and the
# generated test failed with four undeclared identifiers --- which
# names the symptom (a name is missing) and not the cause (a list
# here was not updated).
#
# SURFACE.txt groups its names under `# openkal.<interface>'
# headings, so the list it already carries is the list this needs.
# `types' is not a group there --- it declares no kal_ name --- and
# is therefore named separately.
echo "import openkal.types;"
for m in $(grep -oE '^# openkal\.[a-z]+' .spec/SURFACE.txt | cut -d. -f2); do
echo "import openkal.$m;"
done
echo 'const void *const surface[] = {'
while read -r n; do [ -n "$n" ] && echo " (const void *)&$n,"; done <<< "$list"
echo '};'
echo 'int main() { return surface[0] == 0; }'
} > tests/conformance_declarations.cpp
mcpp test conformance_declarations 2>&1 | tee decl.log
grep -q '^conformance_declarations \.\.\. ok' decl.log
rm -f tests/conformance_declarations.cpp
# The property this version exists for. An implementation beneath a
# program that supplies its own runtime must not reach for that runtime's
# names, or its calls resolve to the program's and recur without bound.
- name: The objects reference no C library symbol
run: |
rm -rf target && mcpp build --features standalone
objs=$(find target -path '*/obj/*' -name '*.o' ! -name '*.m.o' ! -name 'conformance*')
test -n "$objs" || { echo "no objects were found; the check would pass vacuously" >&2; exit 1; }
# The permitted set. memcpy, memmove, memset and memcmp are the four a
# freestanding translation unit is permitted to require: a compiler
# emits calls to them from ordinary loops, and they compute rather
# than call, so none of them can re-enter this implementation.
# __libc_start_main and main are the hand-over, and are undefined here
# by construction.
permitted='^(memcpy|memmove|memset|memcmp|__libc_start_main|main|_GLOBAL_OFFSET_TABLE_|kal_[a-z_]+|__init_array_start|__init_array_end|__preinit_array_start|__preinit_array_end|_ZN3okl.*)$'
# ⭐⭐ ONE NAME IS PERMITTED ONLY IF IT IS WEAK, AND THE WEAKNESS IS
# THE WHOLE OF THE PERMISSION.
#
# `environ' is how src/env.cpp recovers the vectors the kernel placed
# on the stack when the C library above did not pass them --- glibc
# calls every `.init_array' entry with (argc, argv, envp) and musl
# calls them with none, so what arrived there was register residue.
#
# It is admissible where `puts' is not, and the difference is not that
# it is smaller. This check exists because a CALL into the runtime a
# program supplied resolves to the program's and can re-enter this
# implementation without bound. A pointer executes nothing. And being
# WEAK it is null in a program that has no C library, so it does not
# make one required --- which a strong reference to the same name
# would, silently, and is why the type letter is checked and not just
# the name.
weak_permitted='^environ$'
bad=0
nm --undefined-only $objs | awk '{ print $1, $2 }' | sort -u |
while read -r kind name; do
[ -n "$name" ] || continue
printf '%s\n' "$name" | grep -qE "$permitted" && continue
if printf '%s\n' "$name" | grep -qE "$weak_permitted"; then
case "$kind" in
w|v) continue ;;
*) echo "::error::$name is permitted only as a weak reference, and this one is '$kind'" >&2 ;;
esac
else
echo "the implementation references a symbol it must not: $name" >&2
fi
echo bad >> "$RUNNER_TEMP/independence.bad"
done
[ -s "$RUNNER_TEMP/independence.bad" ] && bad=1
rm -f "$RUNNER_TEMP/independence.bad"
test "$bad" -eq 0
echo "the implementation references no C library symbol"
# ANDROID SHARES THIS IMPLEMENTATION, AND THIS IS WHAT SAYS SO.
#
# This file is written on the Linux kernel's own system-call interface and
# borrows nothing from any C library. Android's kernel IS Linux, the
# per-architecture system-call ABI is the same, and `src/sys.h` branches
# on `__x86_64__` / `__aarch64__` -- the architecture, not the operating
# system. So the claim is that nothing here needs to change, and a claim
# about what does not change is exactly the kind that rots unmeasured.
#
# THE SECOND HALF IS THE INTERESTING ONE. Compiling is the weaker
# statement; the property this package exists for is that its objects
# name no C library symbol, and on Android the C library is a DIFFERENT
# one. A reference that resolved to glibc by habit would show up here as
# a bionic name, so the same permitted set is applied to the same kind of
# output for a different libc.
#
# ONE MATRIX LEG, AND THE AXIS DOES NOT REACH THIS TARGET. The Android
# rows carry a CAPABILITY pin (`android-ndk@...`), which means mcpp
# refuses any other toolchain for them -- so `matrix.toolchain` selects
# nothing here and running both legs would download the NDK twice for one
# signal.
#
# NOT EXECUTION. Running an Android artifact needs a device or an
# emulator: measured by hand on an API 24 x86_64 image, where a program
# written against openkal alone printed `openkal: 1-2-3` with exit 0, and
# the device ABI has no execution path from an x86_64 runner at all
# (Google's emulator refuses a foreign guest). This job asserts the two
# things a runner can.
- name: Android shares this implementation, and its objects still name no C library
if: matrix.toolchain == 'llvm@22.1.8'
run: |
permitted='^(memcpy|memmove|memset|memcmp|__libc_start_main|main|_GLOBAL_OFFSET_TABLE_|kal_[a-z_]+|__init_array_start|__init_array_end|__preinit_array_start|__preinit_array_end|_ZN3okl.*)$'
weak_permitted='^environ$'
# TWO NAMES THE NATIVE SET DOES NOT HAVE, AND THEY ARE NOT A C
# LIBRARY'S.
#
# Measured: both are defined in the NDK's own
# `libclang_rt.builtins-<arch>-android.a` and in NEITHER bionic
# `libc.so` -- so they belong to the same category the set above
# already admits for memcpy and its three neighbours: emitted by the
# compiler, computing rather than calling, and incapable of
# re-entering this implementation.
#
# __emutls_get_address emulated thread-local storage. NOT mcpp's
# choice: `build.ninja` for this target
# carries no `-femulated-tls`, and the NDK's
# clang emits this reference by itself from
# a bare `thread_local int x;` at API 21,
# which is its documented default below the
# level where bionic gained ELF TLS.
# __aarch64_swp4_acq an outline-atomics helper, aarch64 only,
# which is why it appears on one ABI and not
# the other.
#
# Kept in this step rather than widened into the native one: nothing
# about the native build has been measured to need them, and a
# permitted set that grows for a target it was not measured on is how
# a check stops being one.
permitted_compiler_rt='^(__emutls_get_address|__aarch64_[a-z0-9_]+)$'
rm -f "$RUNNER_TEMP/android.bad"
for target in aarch64-linux-android x86_64-linux-android; do
echo "== $target =="
rm -rf target
mcpp build --target "$target" --features standalone
objs=$(find target -path '*/obj/*' -name '*.o' ! -name '*.m.o' ! -name 'conformance*')
test -n "$objs" || {
echo "::error::no objects were found for $target; the check would pass vacuously" >&2
exit 1
}
# The objects are for the guest architecture, and one of the two is
# not this runner's -- so assert the architecture rather than trust
# that `--target` was honoured. `file` names it for both.
case "$target" in
aarch64-*) want='ARM aarch64' ;;
x86_64-*) want='x86-64' ;;
esac
one=$(printf '%s\n' $objs | head -1)
file "$one" | grep -q "$want" || {
echo "::error::$one is not $want: $(file "$one")" >&2
exit 1
}
nm --undefined-only $objs | awk '{ print $1, $2 }' | sort -u |
while read -r kind name; do
[ -n "$name" ] || continue
printf '%s\n' "$name" | grep -qE "$permitted" && continue
printf '%s\n' "$name" | grep -qE "$permitted_compiler_rt" && continue
if printf '%s\n' "$name" | grep -qE "$weak_permitted"; then
case "$kind" in
w|v) continue ;;
*) echo "::error::$name is permitted only as a weak reference, and this one is '$kind'" >&2 ;;
esac
else
echo "::error::$target references a symbol it must not: $name" >&2
fi
echo bad >> "$RUNNER_TEMP/android.bad"
done
done
if [ -s "$RUNNER_TEMP/android.bad" ]; then
rm -f "$RUNNER_TEMP/android.bad"
exit 1
fi
rm -f "$RUNNER_TEMP/android.bad"
echo "both Android ABIs build from this implementation unchanged, and name no C library symbol"
# A checker is only useful if it fails when it should.
- name: The independence check detects a dependence
run: |
printf 'extern "C" int puts(const char*);\nextern "C" void okl_probe(void) { puts("x"); }\n' > src/probe.cpp
rm -rf target && mcpp build --features standalone
objs=$(find target -path '*/obj/*' -name '*.o' ! -name '*.m.o')
if ! nm --undefined-only $objs | awk '{print $2}' | grep -qx puts; then
echo "the probe did not produce the reference it was written to produce" >&2
rm -f src/probe.cpp; exit 1
fi
rm -f src/probe.cpp
echo "a dependence upon a C library is visible to the check"