-
Notifications
You must be signed in to change notification settings - Fork 0
889 lines (848 loc) · 47.4 KB
/
Copy pathci.yml
File metadata and controls
889 lines (848 loc) · 47.4 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
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
name: CI
# What this workflow asserts about the specification.
#
# A specification is a claim about programs, so the claims are tested by
# building and running programs rather than by reading the text. There are four:
#
# declarations the two forms compile, under three compiler families and on
# three systems, and declare the same entities
# substitution a program's source is invariant under a change of
# implementation, and the check that says so fails when it should
# conformance the suite in this repository runs against the implementation
# for each system and every observation holds
# composability an implementation that provides three interfaces is examined
# for three, rather than failing to link
#
# The three compiler families are covered because the specification is a
# contract 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 }}
# A version verified to build these packages, not a measured minimum. The pin
# exists for reproducibility rather than because an older mcpp is known to
# fail.
MCPP_VERSION: 2026.8.27.1
XLINGS_VERSION: v2026.8.17.2
XLINGS_NON_INTERACTIVE: '1'
jobs:
# ---------------------------------------------------------------------------
# The declarations compile, everywhere, in both forms.
declarations:
name: declarations (${{ matrix.os }}, ${{ matrix.toolchain }})
runs-on: ${{ matrix.os }}
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
include:
- { os: ubuntu-24.04, toolchain: 'gcc@16.1.0' }
- { os: ubuntu-24.04, toolchain: 'llvm@22.1.8' }
- { os: macos-14, toolchain: 'llvm@20.1.7' }
- { os: windows-2022, toolchain: 'llvm@20.1.7' }
- { os: windows-2022, toolchain: 'msvc@system' }
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v4
- name: Install xlings and mcpp (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 and mcpp (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
irm https://d2learn.org/xlings-install.ps1.txt | iex
# The installer amends the user's environment; a later step in this
# job reads none of it, 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 # ⚠️ 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; 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
# ⭐⭐ 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
# The C++ form. The modules are the artefact a C++ consumer imports, and
# building them is what proves the compiler accepts them.
# ⚠️ A 32-BIT TARGET, AND NOTHING ELSE IN THIS REPOSITORY IS ONE.
#
# Every row of every job here is hosted, and every hosted target this
# specification serves is 64-bit. That is a structural blind spot rather
# than an omission: the C ABI this repository freezes is instantiated at
# two pointer widths, and only one of them was ever compiled.
#
# Measured 2026-08-20: `kal_node_info`'s frozen-layout assertion read
# `offsetof(modified_ns) == sizeof(kal_uintptr)`, which is true at 64 bits
# and false at 32 — the timestamp is a naturally-aligned `kal_u64`, so a
# 32-bit `size` is followed by four bytes of padding and the offset stays
# at eight. The failure surfaced in a CONSUMER (`mcpplibs/riscv-virt-rt`,
# whose rv32 leg activates the openkal feature), which is the only place
# the 32-bit layout existed at all.
#
# Compile-only: there is no 32-bit implementation to run against, and the
# claim under test is about layout rather than behaviour.
# ⚠️ A 32-BIT TARGET, AND NOTHING ELSE IN THIS REPOSITORY IS ONE.
#
# Every other row here is hosted, and every hosted target this
# specification serves is 64-bit. That is a structural blind spot: the C
# ABI this repository freezes is instantiated at two pointer widths, and
# only one of them was ever compiled.
#
# Measured 2026-08-20: `kal_node_info`'s frozen-layout assertion read
# `offsetof(modified_ns) == sizeof(kal_uintptr)`, true at 64 bits and
# false at 32 — the timestamp is a naturally-aligned `kal_u64`, so a
# 32-bit `size` is followed by four bytes of padding and the offset stays
# at eight. It surfaced in a CONSUMER (`mcpplibs/riscv-virt-rt`'s rv32
# leg), the only place the 32-bit layout existed at all.
#
# ⚠️ THROUGH `mcpp`, NOT THROUGH A BORROWED clang. The first version of
# this step globbed the LLVM payload for `clang++` — which is absent on
# the gcc and msvc rows, and is named `clang++.exe` on Windows, so the
# command expanded to nothing and the step died with `: command not
# found` on four rows. mcpp resolves the toolchain for a target itself;
# asking it is the only spelling that holds on every row.
# ⚠️ A 32-BIT TARGET, AND NOTHING ELSE IN THIS REPOSITORY IS ONE.
#
# Every other row here is hosted, and every hosted target this
# specification serves is 64-bit. That is a structural blind spot: the C
# ABI this repository freezes is instantiated at two pointer widths, and
# only one of them was ever compiled.
#
# Measured 2026-08-20: `kal_node_info`'s frozen-layout assertion read
# `offsetof(modified_ns) == sizeof(kal_uintptr)`, true at 64 bits and
# false at 32 — the timestamp is a naturally-aligned `kal_u64`, so a
# 32-bit `size` is followed by four bytes of padding and the offset stays
# at eight. It surfaced in a CONSUMER (`mcpplibs/riscv-virt-rt`'s rv32
# leg), the only place the 32-bit layout existed at all.
#
# ⚠️ THE PROBE LIVES INSIDE THE CHECKOUT AND USES A RELATIVE PATH. Two
# earlier spellings failed for reasons that had nothing to do with the
# ABI: globbing the LLVM payload for `clang++` died with `: command not
# found` on the rows whose toolchain is gcc or msvc, and interpolating
# `$GITHUB_WORKSPACE` into a C++ string literal produced
# `"D:\a\openkal\openkal/include"`, whose backslashes the compiler read
# as escape sequences. A relative directory has neither problem, and
# `include_dir` resolves it against the package root.
- name: The C ABI holds at thirty-two bits as well as sixty-four
run: |
set -euo pipefail
rm -rf .abi-probe && mkdir -p .abi-probe/src
cat > .abi-probe/mcpp.toml <<'EOF'
[package]
name = "abi"
version = "0.1.0"
EOF
cat > .abi-probe/build.mcpp <<'EOF'
import mcpp;
int main() { mcpp::include_dir("../include"); return 0; }
EOF
cat > .abi-probe/src/main.cpp <<'EOF'
#include <openkal/types.h>
#include <openkal/fs.h>
// `self_size' IS FIRST, AND THAT IS THE WHOLE OF THE GROWTH RULE.
// An implementation built at a later version reads this field before
// it touches anything else, which is how it learns how much of the
// structure the caller has. A layout in which it were not first would
// make the field unreadable by exactly the implementation it exists
// to inform.
static_assert(__builtin_offsetof(kal_node_info, self_size) == 0);
static_assert(__builtin_offsetof(kal_node_info, present) == 4);
// The fields that existed at 0.9.0, at the offsets they had. Growth
// is permitted only past the end; these do not move.
static_assert(__builtin_offsetof(kal_node_info, size) == 8);
static_assert(__builtin_offsetof(kal_node_info, modified_ns) == 16);
static_assert(__builtin_offsetof(kal_node_info, identity) == 24);
static_assert(__builtin_offsetof(kal_node_info, kind) == 40);
static_assert(__builtin_offsetof(kal_node_info, writable) == 44);
// The same at both pointer widths, which is the property this
// 32-bit row exists to observe: no field of it is pointer-sized.
static_assert(sizeof(kal_node_info) == 48);
static_assert(sizeof(kal_dir) == sizeof(kal_uintptr));
static_assert(sizeof(kal_file) == sizeof(kal_uintptr));
// A transfer is one signed word, and a signed word is a pointer's
// width. An implementation returning `int' here would truncate a
// count on a 64-bit machine and would be caught by nothing else.
static_assert(sizeof(kal_intptr) == sizeof(kal_uintptr));
static_assert((kal_intptr)-1 < 0);
extern "C" void _start() { for (;;) {} }
EOF
for t in riscv64-none-elf riscv32-none-elf; do
( cd .abi-probe && rm -rf target && mcpp build --target "$t" ) \
|| { echo "the frozen layout does not hold for $t"; exit 1; }
echo "$t: layout holds"
done
rm -rf .abi-probe
# ⚠️⚠️ EVERY MACRO HAS A MODULE SPELLING, AND THIS IS CHECKED RATHER THAN
# REMEMBERED --- because twice it was not. `KAL_LOCK_*' in 0.10 and
# `KAL_SPAWN_*' in 0.11 were each added to a header, used from a module
# consumer, and failed to compile there, because a macro does not cross a
# module boundary. Both were found by a consumer's build in another
# repository rather than here.
#
# ⇒ src/macros.cppm is GENERATED. Regenerating and diffing turns "somebody
# forgot" into a red build in the repository that made the change.
- name: Every macro has a module spelling
if: runner.os != 'Windows'
run: |
bash tools/gen-macros.sh
git diff --exit-code src/macros.cppm || {
echo "::error::src/macros.cppm is out of date --- run tools/gen-macros.sh and commit it"
exit 1
}
- name: The module form compiles
run: mcpp build
# The C form, with the environment's own headers excluded --- because the
# consumer this form exists for, a C library being ported onto openkal, is
# compiled that way. The tool needs a driver it can pass -nostdinc to; the
# toolchain that has no such spelling compiles the same declarations in
# the translation unit the conformance suite carries, which every row of
# the conformance job below builds.
- name: The C form compiles without the environment's headers
if: runner.os != 'Windows'
run: |
CC=cc bash tools/check-declarations.sh
command -v clang >/dev/null && CC=clang bash tools/check-declarations.sh || true
# ⭐⭐ AND NO DECLARATION NAMES A BACKEND TYPE.
#
# The job above asserts the declarations COMPILE. This asserts what they
# are written WITH, which is a different freedom and the one that decides
# whether a `kal_*` signature means the same thing on four object formats.
# `sizeof(long)` is 8 on x86_64 Linux and 4 on x86_64 Windows; a `long` in
# an interface would be one signature with two meanings.
#
# ⚠️ It runs on every row of this matrix rather than once, because a type
# is a claim about a TARGET and this matrix is where the targets are.
#
# ⚠️ Needs a clang-family driver (`-Xclang -ast-print`); rows whose
# toolchain has no such spelling are covered by the other rows, and the
# step says so rather than passing silently.
- name: No declaration names a backend type
if: runner.os != 'Windows'
run: |
cc="$(command -v clang || true)"
if [ -z "$cc" ]; then
echo "no clang on this row; the type check runs on the rows that have one"
exit 0
fi
bash tools/check-types.sh "$cc" include
# ⚠️ AND THE CHECK DETECTS WHAT IT EXISTS TO DETECT.
#
# A check that succeeds by finding nothing succeeds identically when it
# reads nothing, and every false green in this ecosystem has had that
# shape. `check-types.sh` proves it parsed the headers before reporting;
# this proves it would have failed.
- name: The type check detects a leak
if: runner.os != 'Windows'
run: |
cc="$(command -v clang || true)"
[ -n "$cc" ] || exit 0
cp include/openkal/env.h /tmp/env.h.orig
printf '\nint kal_env_probe_leak(unsigned long n);\n' >> include/openkal/env.h
if bash tools/check-types.sh "$cc" include; then
cp /tmp/env.h.orig include/openkal/env.h
echo "the type check accepted 'unsigned long' in an interface" >&2
exit 1
fi
cp /tmp/env.h.orig include/openkal/env.h
echo "the type check rejects a backend type, as it must"
# ---------------------------------------------------------------------------
# The property the specification exists for.
substitution:
name: substitution holds (${{ matrix.toolchain }})
runs-on: ubuntu-24.04
timeout-minutes: 40
strategy:
fail-fast: false
matrix:
toolchain: ['gcc@16.1.0', 'llvm@22.1.8']
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 # ⚠️ 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; 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 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: Substitution holds
working-directory: examples/substitution/app
run: |
# The property under test is that the source is invariant. The
# checksum is taken before the first build and compared after the
# second, so that a change made by either build would be detected.
before="$(sha256sum src/main.cpp | cut -d' ' -f1)"
mcpp build > /dev/null
./target/*/*/bin/app > with-fd.log 2>&1
grep -q 'the application produced this line' with-fd.log
sed -i 's|openkal-fd = { path = "../impl-fd" }|openkal-discard = { path = "../impl-discard" }|' mcpp.toml
rm -rf target
mcpp build > /dev/null
./target/*/*/bin/app > with-discard.log 2>&1
if grep -q 'the application produced this line' with-discard.log; then
echo "the discarding implementation produced output"; exit 1
fi
after="$(sha256sum src/main.cpp | cut -d' ' -f1)"
[ "$before" = "$after" ] || { echo "the source changed between builds"; exit 1; }
- name: The surface checker detects an addition
working-directory: examples/substitution/impl-fd
run: |
# The checker is only useful if it fails when it should. An
# unspecified name is introduced, the checker is required to reject
# it, and the name is then removed.
mcpp build > /dev/null
bash ../../../tools/check-surface.sh ../../../SURFACE.txt \
$(find target -name '*.o' | tr '\n' ' ')
printf 'extern "C" void kal_vendor_extension(void) {}\n' > src/extra.cpp
mcpp build > /dev/null
if bash ../../../tools/check-surface.sh ../../../SURFACE.txt \
$(find target -name '*.o' | tr '\n' ' '); then
echo "the checker accepted an unspecified name"; exit 1
fi
rm -f src/extra.cpp
# ---------------------------------------------------------------------------
# The suite, against the implementation for each system.
#
# The implementations are checked out at the branch under test where they have
# one and at their default branch otherwise, so that this job asserts what it
# is for: that the specification as written here and the implementations as
# written there agree today.
conformance:
name: conformance (${{ matrix.implementation }}, ${{ matrix.toolchain }})
runs-on: ${{ matrix.os }}
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
include:
# `features' names the optional interfaces the implementation in that
# row provides, on top of `full'. It is per-row rather than global
# because clause 6.1 makes a missing optional interface not a
# deviation: a set that demanded every one of them from every
# implementation would report a link failure where the specification
# says there is nothing to report.
- { os: ubuntu-24.04, toolchain: 'gcc@16.1.0', implementation: openkal-linux, features: 'full,optional' }
- { os: ubuntu-24.04, toolchain: 'llvm@22.1.8', implementation: openkal-linux, features: 'full,optional' }
- { os: macos-14, toolchain: 'llvm@20.1.7', implementation: openkal-macos, features: 'full' }
- { os: windows-2022, toolchain: 'llvm@20.1.7', implementation: openkal-windows, features: 'full' }
- { os: windows-2022, toolchain: 'msvc@system', implementation: openkal-windows, features: 'full' }
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v4
- name: The implementation
run: |
git clone --quiet https://github.com/mcpplibs/${{ matrix.implementation }}.git .impl
branch='${{ github.head_ref || github.ref_name }}'
if git -C .impl rev-parse --verify --quiet "origin/$branch" > /dev/null; then
git -C .impl checkout --quiet "origin/$branch"
echo "the implementation is at $branch $(git -C .impl rev-parse --short HEAD)"
else
echo "the implementation has no $branch; its default branch is used" \
"($(git -C .impl rev-parse --short HEAD))"
fi
# Whether the two are describing the same version of the contract.
#
# This job asserts that the specification as written here and the
# implementation as written there agree today, and that assertion is
# only meaningful when the implementation is written against this
# version. During a release across five repositories they are out of
# step for the seconds between one merge and the next, and the
# compiler's report on that is a hundred errors about names a header
# no longer has --- which names the symptom and not the cause.
# An implementation states which openkal it is written against in one
# of two forms, and the question the check asks is different in each.
#
# openkal = "0.5.2" a released version. The question is
# whether it is this one.
# openkal = { git = …, branch = … }
# a development line. The version is
# not stated and cannot be; what makes
# the two in step is that the branch is
# this branch, and that is what to ask.
#
# The second form was not anticipated when this check was written, and
# the check then extracted nothing and reported a mismatch against the
# empty string --- which named neither the cause nor the symptom.
here="$(sed -n 's/^version[[:space:]]*=[[:space:]]*"\([^"]*\)".*/\1/p' mcpp.toml | head -1)"
there="$(sed -n 's/^openkal[[:space:]]*=[[:space:]]*"\([^"]*\)".*/\1/p' .impl/mcpp.toml | head -1)"
if [ -n "$there" ]; then
if [ "$here" != "$there" ]; then
echo "::error::this is openkal $here and ${{ matrix.implementation }} is written against openkal $there. Nothing is wrong with either; they are not in step. Re-run after the implementation's branch has the matching version." >&2
exit 1
fi
echo "both describe openkal $here"
else
impl_branch="$(sed -n 's/^openkal[[:space:]]*=[[:space:]]*{.*branch[[:space:]]*=[[:space:]]*"\([^"]*\)".*/\1/p' .impl/mcpp.toml | head -1)"
if [ -z "$impl_branch" ]; then
echo "::error::${{ matrix.implementation }} states neither a version nor a branch for openkal, so there is nothing to be in step with." >&2
exit 1
fi
if [ "$impl_branch" != "$branch" ]; then
echo "::error::this is $branch and ${{ matrix.implementation }} follows $impl_branch. They are not in step." >&2
exit 1
fi
echo "both follow $branch; this is openkal $here"
fi
- name: Install xlings and mcpp (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 and mcpp (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: |
# ⚠️ 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 # ⚠️ 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; 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 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
# Every interface and every kind of examination. The exit status is the
# verdict: 0 when every observation held, 1 when one did not, and 2 when
# nothing was observed --- the last being the outcome a run that selected
# no interface would otherwise pass silently.
- name: Every interface, every kind of examination
run: |
bash tools/run-conformance.sh '${{ matrix.implementation }}' .impl '${{ matrix.features }}'
# The suite is composable because openkal is: an implementation provides
# an interface in whole or not at all, and a suite that examined all eight
# unconditionally would fail to link against a conforming implementation
# of three. Selecting three is therefore asserted to produce a report
# rather than a link failure.
# openkal-kit, which is in this repository and is not this specification.
#
# The kit composes facilities from openkal's interfaces --- awaiting a
# channel, writing an endpoint down and reading one back --- that the
# specification declines to admit because they can be composed. Running its
# tests here is what keeps the composition working as the interfaces it
# rests upon change.
#
# THE SURFACE CHECK ABOVE IS WHAT KEEPS THE TWO APART, and it does so by
# the shape of what is exported rather than by a declaration. The kit is
# C++ modules in `namespace kal::kit` and exports no name beginning with
# `kal_`; a C++ name is mangled to `_ZN3kal3kit…`, so an implementation
# that linked the kit still passes `check-surface.sh --complete`.
# ⭐⭐ A PUBLISHED PACKAGE MAY NOT REACH ANOTHER BY PATH.
#
# kit/mcpp.toml is published as openkal-kit, out of this repository's own
# tarball. While it read `openkal = { path = ".." }` that was true here and
# true inside the tarball, and it made the package unusable with any
# implementation: a consumer naming both openkal-kit and an implementation
# reached the specification two ways --- by path from one, by version from
# the other --- and the build refused, correctly, because two routes to one
# package are two copies of it.
#
# ⚠️ NO CONTINUOUS INTEGRATION HERE COULD HAVE CAUGHT IT. Every workflow in
# this ecosystem substitutes working trees, so both routes become paths and
# agree. The defect appears only where an implementation names the
# specification by VERSION, which is the published shape and exactly what
# no job reproduces. It was found in a sandbox against the published
# packages, after release.
#
# This check is structural rather than behavioural for that reason: it
# cannot reproduce the graph, but it can require the form that avoids it.
- name: The kit names the specification by version
run: |
set -euo pipefail
if grep -nE '^openkal = \{[^}]*path' kit/mcpp.toml; then
echo "::error::kit/mcpp.toml reaches the specification by path."
echo " openkal-kit is published from this tarball, and a path"
echo " there is a second route to a package every consumer"
echo " already reaches by version."
exit 1
fi
grep -qE '^openkal = "[0-9]' kit/mcpp.toml \
|| { echo "::error::kit/mcpp.toml does not name openkal by version at all"; exit 1; }
echo " the kit reaches the specification the way a consumer does: $(grep -E '^openkal = ' kit/mcpp.toml)"
- name: The kit, composed from the interfaces above
if: matrix.implementation == 'openkal-linux'
run: |
bash tools/run-kit-tests.sh '${{ matrix.implementation }}' .impl
# ⭐⭐ THE EXAMPLE A CONSUMER READS, BUILT AND RUN.
#
# examples/portable is the program the specification points at as the one
# every implementation must satisfy, and NOTHING BUILT IT. Its README
# stated that "an implementation's continuous integration fetches this
# source ... builds it against itself, and asserts the lines below";
# measured across all six implementation repositories, the word `portable`
# appeared in none of their workflows. A promise with nothing executing it.
#
# ⚠️ AND THE DRIFT THAT FOLLOWS FROM THAT IS VISIBLE IN ITS OWN MANIFEST:
# it named `openkal = "0.5.0"` --- three specification versions behind ---
# while claiming to demonstrate the current contract to a reader who would
# copy it. Nothing could have noticed.
#
# The manifest names published versions, because that is what a consumer
# writes. Those do not exist while a change spanning these repositories is
# under review, so the working trees stand in, exactly as
# tools/run-conformance.sh does for the suite.
#
# ⚠️ RELATIVE PATHS, NOT `pwd`. This job runs on Windows too, where MSYS
# reports `/d/a/...`, which the build tool does not resolve. Relative
# paths also cannot put a local directory into a file that might be
# committed.
#
# ⚠️ NO `standalone` FEATURE HERE. That feature makes the implementation
# supply `_start`, which collides with the C library's crt1.o on a hosted
# toolchain --- measured: "multiple definition of `_start`". The
# conformance runner defaults to no features for the same reason.
- name: The portable program builds and runs over this implementation
working-directory: examples/portable
run: |
set -euo pipefail
subst() { sed -E "$2" "$1" > "$1.next" && mv "$1.next" "$1"; }
subst mcpp.toml 's|^openkal = .*$|openkal = { path = "../.." }|'
subst mcpp.toml 's|^${{ matrix.implementation }} = .*$|${{ matrix.implementation }} = { path = "../../.impl" }|'
grep -q 'path = "../../.impl"' mcpp.toml \
|| { echo "::error::the implementation was not substituted"; exit 1; }
# ⚠️ THE IMPLEMENTATION REACHES THE SPECIFICATION TOO, AND BY ITS OWN
# FORM. Substituting only this manifest leaves two routes to one
# package and the build refuses, correctly:
#
# error: dependency 'mcpplibs.openkal' is requested as both a path
# dep (by 'portable') and a git dep (by 'openkal-linux@path')
#
# Both must name the same directory, which is what run-conformance.sh
# does for the suite. From `.impl/` the specification is `..`; the two
# spellings resolve to one canonical path and therefore to one node.
subst ../../.impl/mcpp.toml 's|^openkal = .*$|openkal = { path = ".." }|'
grep -q 'openkal = { path = ".." }' ../../.impl/mcpp.toml \
|| { echo "::error::the implementation still reaches openkal its own way"; exit 1; }
mcpp run 2>&1 | tee portable.log
# ⭐ THE COUNT IS THE CRITERION, AND SO IS THE DENOMINATOR. A program
# that failed to start would print none of these lines, and a check
# for the absence of failures would hold over an empty log.
lines=$(grep -c '^openkal: ' portable.log)
[ "$lines" -ge 10 ] \
|| { echo "::error::the program printed $lines lines; it reports one per interface"; exit 1; }
grep -q 'openkal: observations that did not hold: 0' portable.log \
|| { echo "::error::the portable program reported observations that did not hold"; exit 1; }
echo " the portable program held every observation over ${{ matrix.implementation }}"
- name: A selection of three interfaces is examined, not refused
run: |
rm -rf conformance/target
bash tools/run-conformance.sh '${{ matrix.implementation }}' .impl core,fs,task \
| tee selected.log
# A report was produced, and nothing in it failed.
grep -qE 'observations: [0-9]+ held, 0 did not hold' selected.log
# An interface that was not selected is reported as not examined and
# carries the reason, rather than being absent --- a report that
# omitted it could not be distinguished from a report on an
# implementation that provides it.
grep -q 'openkal.process --- the interface was not selected' selected.log
# And an interface that was selected was examined.
grep -qE 'held +\[behaviour\].*' selected.log