-
Notifications
You must be signed in to change notification settings - Fork 15
425 lines (391 loc) · 21 KB
/
Copy pathci-linux.yml
File metadata and controls
425 lines (391 loc) · 21 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
name: ci-linux
# Self-host CI on Linux: mcpp builds mcpp. The bootstrap mcpp comes from
# `xlings install mcpp` (xim:mcpp in the xlings package index), so this
# workflow no longer depends on a previous-release tarball — the
# chicken-and-egg now lives upstream in the xlings index.
#
# SHAPE: called by ci.yml after the Linux build (build.yml). No job here builds
# mcpp to obtain the commit's binary: each takes the one binary that build
# produced (.github/actions/use-built-mcpp, rule R1 of the 2026-10-02 CI
# record). A job builds mcpp only when the build is what it tests: a cold
# rebuild, another toolchain, a cross target.
#
# This replaced four independent jobs that each paid a "warm" rebuild, chosen
# when that rebuild cost about 2.5 minutes. Measured on 2026-10-01 it cost 6.6
# minutes, and on an exact hit of the restored target/ ninja still ran 830 of
# 830 edges.
#
# The checks that need no binary run in the `docs` job of ci.yml.
#
# The e2e suite is ci-linux-e2e.yml, called beside this one.
#
# Paired workflows: ci-linux-e2e.yml, ci-macos.yml, ci-windows.yml.
on:
workflow_call:
env:
# MCPP_HOME pinned so the cache keys below restore into the same path
# mcpp resolves at runtime.
MCPP_HOME: /home/runner/.mcpp
# Verbose every mcpp invocation for richer CI diagnostics (src/cli.cppm).
# Safe here: this workflow no longer runs the e2e suite, which is what
# asserts mcpp's default quiet output (tests 48/53).
MCPP_VERBOSE: "1"
jobs:
build-test:
name: unit tests (linux x86_64, self-host)
runs-on: ubuntu-24.04
timeout-minutes: 45
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/bootstrap-mcpp
- uses: ./.github/actions/use-built-mcpp
with:
host: linux-x86_64
- name: Unit + integration tests via `mcpp test`
run: |
"$MCPP_FRESH" test
# Each package under `modules/` carries its own tests, and they are built
# as that package ALONE -- a configuration the root suite never produces,
# since there every module is linked together. A subsystem that has
# quietly come to depend on something it does not declare compiles in the
# root build and fails here, which is the whole reason to run both.
#
# Every member is run, and the loop is derived from the manifest rather
# than written out: a list maintained by hand is a list that stops
# matching, and `check_modules_wiring.sh` cannot see this file.
# WS8: docs/01 and docs/20 state, per host, the toolchain a first run
# installs; this host's row is checked against the one answer the
# resolver gives (`self env --format json` -> defaultToolchain). Each
# host's CI row checks its own row of the tables.
- name: The documented default toolchain is this host's answer
run: |
python3 tests/scripts/test_check_default_toolchain_docs.py
python3 .github/tools/check_default_toolchain_docs.py --mcpp "$MCPP_FRESH"
- name: Per-subsystem tests (`mcpp test -p <member>`)
run: |
set -euo pipefail
# `$MCPP_FRESH` is the binary of build.yml (use-built-mcpp), not one
# found under target/.
members=$(sed -n '/^\[workspace\]/,/^\[/p' mcpp.toml \
| grep -oE '"modules/[a-z0-9-]+"' | tr -d '"' | sed 's|modules/||')
[ -n "$members" ] || { echo "no workspace members found in mcpp.toml"; exit 1; }
echo "members: $(echo $members | tr '\n' ' ')"
for m in $members; do
echo "::group::mcpp test -p $m"
"$MCPP_FRESH" test -p "$m"
echo "::endgroup::"
done
# A cold, from-scratch self-host build by this commit's binary with the
# manifest-pinned GCC: mcpp building itself, where build.yml has the
# bootstrap build it. `mcpp test` is deliberately NOT repeated here; it would
# be the same suite, toolchain and binary as `build-test`.
toolchain-gcc:
name: "toolchain: gcc (cold self-host)"
runs-on: ubuntu-24.04
timeout-minutes: 45
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/bootstrap-mcpp
- uses: ./.github/actions/use-built-mcpp
with:
host: linux-x86_64
- name: "Toolchain: GCC — cold rebuild with the PR binary"
run: |
# The build's exit status is the assertion; the grep names the
# toolchain that produced it (WS7, #729).
set -o pipefail
"$MCPP" clean
"$MCPP" build 2>&1 | tee build.log; grep -q "Resolved gcc@16.1.0" build.log
# The two rebuilds share one runner, each with this commit's binary as the
# builder: a runner apiece would cost more in setup than it saves.
toolchain-cross:
name: "toolchain: musl + llvm"
runs-on: ubuntu-24.04
timeout-minutes: 45
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/bootstrap-mcpp
- uses: ./.github/actions/use-built-mcpp
with:
host: linux-x86_64
# Auto-installs gcc@16.1.0-musl on demand (cached across runs).
- name: "Toolchain: musl-gcc — build mcpp (--target)"
run: |
set -o pipefail
"$MCPP" clean
"$MCPP" build --target x86_64-linux-musl 2>&1 | tee build.log; grep -q "Resolved gcc@16.1.0 → x86_64-linux-musl" build.log
# #729. This step reported success for a year while the build failed:
# the build was piped into `tee` with no pipefail, and the only
# assertion was a grep for the resolution line. It now fails on the
# build's own status. It builds with llvm@22.1.8, the LLVM row mcpp
# develops with (`[toolchain] macos`) and that Windows CI resolves;
# libc++ 20's `std` module does not expose directory_iterator's
# comparison, so llvm@20.1.7 cannot build mcpp's own sources.
- name: "Toolchain: LLVM 22.1.8 — build mcpp"
run: |
set -o pipefail
"$MCPP" toolchain install llvm 22.1.8
"$MCPP" clean
"$MCPP" build --toolchain llvm@22.1.8 2>&1 | tee build.log
grep -q "Resolved llvm@22.1.8" build.log
# #722's function-size gate, which needs the compile database a
# successful clang build writes at the project root, and clang-tidy from
# xim:llvm-tools at the same version. It ran by hand until this job built
# mcpp with clang (#729).
- name: "Function sizes of the prepare decomposition (#722)"
run: |
"$XLINGS_BIN" install xim:llvm-tools@22.1.8 -y
bash .github/tools/check_function_sizes.sh
# Integration: the mcpp built from THIS PR's source builds & runs a real
# external C++ project — xlings (openxlings/xlings ships its own mcpp.toml).
# MCPP_VENDORED_XLINGS only supplies the xlings package backend that mcpp
# resolves deps through.
integration-xlings:
name: "integration: mcpp builds & runs xlings"
runs-on: ubuntu-24.04
timeout-minutes: 45
env:
XLINGS_NON_INTERACTIVE: '1'
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/bootstrap-mcpp
- uses: ./.github/actions/use-built-mcpp
with:
host: linux-x86_64
- name: "Integration: mcpp builds & runs xlings (openxlings/xlings)"
run: |
export MCPP_VENDORED_XLINGS="$XLINGS_BIN"
"$GITHUB_WORKSPACE/.github/tools/git_clone_retry.sh" \
--depth 1 --recurse-submodules \
https://github.com/openxlings/xlings /tmp/xlings-src
cd /tmp/xlings-src
"$MCPP" self config --mirror GLOBAL
"$MCPP" build
"$MCPP" run
# THE CURRICULUM WAS NEVER BUILT BY ANY JOB. e2e 616 checks that the examples
# and the table that documents them agree, and says so explicitly: it builds
# nothing. Every example in this repository could therefore stop compiling and
# the only signal would be a user reporting it.
#
# The list is DERIVED FROM THE TREE, not written here. An example that is in
# neither the build list nor the skip table fails this job, so adding one
# forces a decision about whether CI can build it rather than silently
# leaving it uncovered.
examples:
name: "examples: the curriculum builds"
runs-on: ubuntu-24.04
# Generous rather than tight: six cold example builds plus the Vulkan one,
# which provisions the shader compiler and a software driver as payloads.
timeout-minutes: 90
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/bootstrap-mcpp
- uses: ./.github/actions/use-built-mcpp
with:
host: linux-x86_64
- name: Build every example the runner can build
run: |
export MCPP_VENDORED_XLINGS="$XLINGS_BIN"
bash .github/tools/build_examples.sh
# TWO CRITERIA A BUILD CANNOT SHOW, from the examples whose READMEs state
# them. Both are the shape this repository has paid for before: a green
# build over a result nobody compared.
- name: "examples: the feature criterion and the device language"
run: |
export MCPP_VENDORED_XLINGS="$XLINGS_BIN"
set -eo pipefail
# 11-features. NOT "the default build works" -- that passes while the
# optional package is resolved and merely unused. The criterion is
# that the default build's RESOLUTION does not name it.
cd examples/11-features/greeter
"$MCPP" build 2>&1 | tee /tmp/f-default.log
"$MCPP" build --features metrics 2>&1 | tee /tmp/f-metrics.log
grep -qi counters /tmp/f-default.log && {
echo "FAIL: the default build resolved 'counters'"; exit 1; }
grep -qi counters /tmp/f-metrics.log || {
echo "FAIL: --features metrics did not resolve 'counters'"; exit 1; }
echo "ok: the optional package is absent without its feature"
cd - >/dev/null
# 12-a-new-device-language. Three criteria, and a probe run exits
# non-zero on purpose -- the program returns 1 when the answer is not
# 42 -- so those runs are guarded with `|| true` and judged by their
# output rather than their status.
cd examples/12-a-new-device-language/app
"$MCPP" run | tee /tmp/toy.log
grep -q 'answer() = 42' /tmp/toy.log || {
echo "FAIL: the .toy did not reach the link"; exit 1; }
# THE LANGUAGE IS EXECUTED, NOT PATTERN-MATCHED: 21 is what the
# emitted `while` loop computes, and no constant in the tree holds it.
grep -q 'gcd(1071, 462) = 21' /tmp/toy.log || {
echo "FAIL: the emitted loop did not run"; exit 1; }
# Editing the .toy reaches the artifact.
sed -i 's/scale(gcd(1071, 462), 2)/scale(gcd(1071, 462), 3)/' src/kernels/answer.toy
"$MCPP" run > /tmp/toy2.log 2>&1 || true
sed -i 's/scale(gcd(1071, 462), 3)/scale(gcd(1071, 462), 2)/' src/kernels/answer.toy
grep -q 'answer() = 63' /tmp/toy2.log || {
cat /tmp/toy2.log; echo "FAIL: editing the .toy did not reach the artifact"; exit 1; }
# THE COMPILER IS A DECLARED INPUT OF THE ACTION -- the half that
# fails silently, and the one criterion here that has to be built to
# isolate it. Bumping the tool's version is NOT that criterion: the
# tool's path is on the action's command line, so a new path re-runs
# the edge whether or not it is also declared as an input. Measured:
# with `a.input(compiler)` removed, a version bump still reached the
# artifact, and this step passed.
#
# The isolating change is different BYTES AT THE SAME PATH. Build a
# compiler that behaves differently, overwrite the cached binary in
# place, and the command line is byte-identical.
#
# BOTH DIRECTIONS ARE THE CRITERION, not either one. Falsified by
# removing `a.input(compiler)`: the artifact followed the first
# overwrite anyway and stopped following the restore, so a check that
# asserted only the first direction would have passed on a rule that
# tracks nothing.
#
# `n.value * 2` keeps `0` at `0`: a probe that changed every literal
# would turn `while (b != 0)` into a loop that divides by zero, and
# this step would report a crash rather than an answer.
# THE PROBE COMPILER IS BUILT FROM A COPY OF THE TREE. The store's
# key carries a stamp of the tool's tree (#630, item 6), so touching
# `../toyc` would move the key and the next run would rebuild the
# tool from the restored source -- the isolating change here is
# different bytes at the SAME path, which needs the tree untouched.
probe_tree=/tmp/toyc-probe
rm -rf "$probe_tree"; cp -r ../toyc "$probe_tree"; rm -rf "$probe_tree/target"
probe_on() { sed -i 's/return std::format("{}", n.value);/return std::format("{}", n.value * 2);/' "$1/src/compile.cppm"; }
probe_off() { sed -i 's/return std::format("{}", n.value \* 2);/return std::format("{}", n.value);/' "$1/src/compile.cppm"; }
install_toyc() { # $1 = the tree to build, $2 = the store path to overwrite
( cd "$1" && "$MCPP" build >/dev/null )
cp "$(find "$1/target" -name toyc -type f -perm -u+x | head -1)" "$2"
}
# `mcpp cache dir` prints a legacy-directory note on a second line.
# The newest entry, since a tree edited earlier in this job may have
# left another one.
store="$("$MCPP" cache dir | head -1)/tool"
cached="$(ls -t $(find "$store" -path '*toyc@0.1.0*/bin/toyc') 2>/dev/null | head -1)"
[ -n "$cached" ] || {
echo "FAIL: no toyc in the tool store under $store"; exit 1; }
probe_on "$probe_tree"; install_toyc "$probe_tree" "$cached"; probe_off "$probe_tree"
"$MCPP" run > /tmp/toy3.log 2>&1 || true
grep -q 'answer() = 168' /tmp/toy3.log || {
cat /tmp/toy3.log
echo "FAIL: a changed compiler binary did not reach the artifact."
echo " rules-toy must declare the compiler among the action's inputs."
exit 1; }
install_toyc "$probe_tree" "$cached"
"$MCPP" run > /tmp/toy4.log 2>&1 || true
grep -q 'answer() = 42' /tmp/toy4.log || {
cat /tmp/toy4.log
echo "FAIL: restoring the compiler binary did not reach the artifact --"
echo " the action is not tracking the compiler's bytes."
echo " rules-toy must declare the compiler among the action's inputs."
exit 1; }
# THE STORE'S KEY HOLDS THE SOURCE (#630, item 6), which the
# example's README and docs/30 both state: editing the compiler's
# sources at the same version rebuilds the tool, and so does the
# reversal. Both directions, because a key that only ever grew
# would pass the first and fail the second.
sleep 1; probe_on ../toyc
"$MCPP" run > /tmp/toy5.log 2>&1 || true
grep -q 'answer() = 168' /tmp/toy5.log || {
cat /tmp/toy5.log
echo "FAIL: editing the compiler's sources at the same version did not reach the artifact."
echo " The tool store's key must carry the path package's tree stamp."
exit 1; }
sleep 1; probe_off ../toyc
"$MCPP" run > /tmp/toy5b.log 2>&1 || true
grep -q 'answer() = 42' /tmp/toy5b.log || {
cat /tmp/toy5b.log
echo "FAIL: reverting the compiler's sources did not reach the artifact."
exit 1; }
# ... and a version bump still rebuilds it, as it always did.
sleep 1; probe_on ../toyc
sed -i 's/^version = "0.1.0"/version = "0.1.1"/' ../toyc/mcpp.toml
"$MCPP" run > /tmp/toy6.log 2>&1 || true
sed -i 's/^version = "0.1.1"/version = "0.1.0"/' ../toyc/mcpp.toml
probe_off ../toyc
grep -q 'answer() = 168' /tmp/toy6.log || {
cat /tmp/toy6.log; echo "FAIL: bumping the tool version did not rebuild it"; exit 1; }
echo "ok: the compiler is a declared input, and the store is keyed on the tool's source"
- name: "Graphics example: render offscreen on lavapipe and assert the pixels"
run: |
set -o pipefail
export MCPP_VENDORED_XLINGS="$XLINGS_BIN"
cd examples/10-graphics/offscreen
"$MCPP" build
# THE SHADERS ARE THE FIRST ASSERTION, and both of them: a rule that
# compiled only the first source would leave the second header absent
# and everything after this would still pass.
for f in triangle_vert triangle_frag; do
d="target/.build-mcpp/out/spirv"
test -f "$d/$f.h" || { echo "missing $d/$f.h"; exit 1; }
# Either file: which of the two carries the words is a property of
# the shader compiler the rule chose, not of the shader. See the
# cross-platform jobs, where that choice differs.
# ONE FILE AT A TIME, because `grep -qs a b` exits 2 when `b`
# does not exist -- even on a match in `a`, and even with `-s`,
# which suppresses the message and not the status. Written as one
# grep over both names, this criterion fails whenever the route
# that produces only a header is taken, which is a failure about
# the criterion and not about the shader.
found=""
for g in "$d/$f.h" "$d/$f.inc"; do
[ -f "$g" ] && grep -q '0x07230203' "$g" && found=1
done
[ -n "$found" ] \
|| { echo "$f carries no SPIR-V magic in either $f.h or $f.inc"; exit 1; }
done
icd=$(find "${MCPP_HOME:-$HOME/.mcpp}/registry/data/xpkgs/xim-x-mesa-lavapipe" \
"$HOME/.xlings/data/xpkgs/xim-x-mesa-lavapipe" \
-name 'lvp_icd.x86_64.json' -print -quit 2>/dev/null || true)
[ -n "$icd" ] || { echo "no lavapipe ICD in either store"; exit 1; }
out=$(VK_DRIVER_FILES="$icd" "$MCPP" run 2>&1) || { echo "$out"; exit 1; }
echo "$out"
# The program asserts the corners and the centre itself and exits
# non-zero on either. What CI adds is that the run reached the DEVICE:
# the software rasteriser produces the same pixels by construction, so
# the image cannot distinguish them and the device name is what does.
echo "$out" | grep -q 'llvmpipe' \
|| { echo "the run did not reach the lavapipe device"; exit 1; }
echo "$out" | grep -qE 'centre pixel: \([0-9]+, [0-9]+, [0-9]+, 255\)' \
|| { echo "no centre pixel was reported"; exit 1; }
# THE REVERSE LEG, AND IT COMPARES THE PIXELS RATHER THAN JUST
# RUNNING. The claim this example makes is that the image is a
# contract two independent rasterisers satisfy, not a property of one
# device -- so the criterion is that the two legs report the SAME
# centre pixel while reporting different devices. Measured: both give
# (124, 70, 62, 255), byte for byte.
gpu_px=$(echo "$out" | grep -m1 '^centre pixel:')
"$MCPP" build --no-accel
"$MCPP" run --no-accel | tee cpu.log
grep -q 'cpu rasteriser' cpu.log || { echo "the CPU leg did not run"; exit 1; }
cpu_px=$(grep -m1 '^centre pixel:' cpu.log)
[ -n "$gpu_px" ] && [ "$gpu_px" = "$cpu_px" ] || {
echo "the two legs disagree about the image:"
echo " device: $gpu_px"
echo " cpu: $cpu_px"
exit 1; }
echo "ok: both legs agree on $cpu_px"
- name: "Vulkan example: build the device half and run it on lavapipe"
run: |
export MCPP_VENDORED_XLINGS="$XLINGS_BIN"
cd examples/09-heterogeneous/vulkan/app
"$MCPP" build
# BOTH STORES, because which one holds the payload is decided by how
# this mcpp resolved its home -- and a `find` over only one of them
# is the defect this repository has already paid for twice (e2e 614,
# llama.cpp-m's Vulkan job). Ask for the file, not for a layout.
icd=$(find "${MCPP_HOME:-$HOME/.mcpp}/registry/data/xpkgs/xim-x-mesa-lavapipe" \
"$HOME/.xlings/data/xpkgs/xim-x-mesa-lavapipe" \
-name 'lvp_icd.x86_64.json' -print -quit 2>/dev/null || true)
[ -n "$icd" ] || { echo "no lavapipe ICD in either store"; exit 1; }
echo "using ICD $icd"
out=$(VK_DRIVER_FILES="$icd" "$MCPP" run 2>&1) || { echo "$out"; exit 1; }
echo "$out"
# The device leg is the point: a run that silently fell back to the
# CPU variant would print the same numbers, so the assertion is that
# the program says which device answered AND that it is the payload's.
echo "$out" | grep -q 'llvmpipe' \
|| { echo "the run did not reach the lavapipe device"; exit 1; }
echo "$out" | grep -q '12 24 36 48' \
|| { echo "the device produced the wrong result"; exit 1; }