-
Notifications
You must be signed in to change notification settings - Fork 9
427 lines (396 loc) · 20.7 KB
/
Copy pathcross-build-test.yml
File metadata and controls
427 lines (396 loc) · 20.7 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
name: cross-build-test
# mcpp cross-build test — the single source of truth for "which CROSS-build
# target combinations mcpp supports", verified end-to-end.
#
# Cross = host arch ≠ target arch. Verification targets are mcpp ITSELF and
# xlings (real, self-hosting C++23 module projects), cross-built from source for
# each target triple, arch-checked, and smoke-run under qemu-user.
#
# ── Supported cross matrix (built + verified below) ────────────────────────
# target | toolchain | host→target | run
# ----------------------|----------------------------------|---------------|-----
# aarch64-linux-musl | aarch64-linux-musl-gcc@16.1.0 | x86_64→arm64 | qemu
# x86_64-w64-mingw32 | mingw-cross-gcc@16.1.0 (MSVCRT) | linux→windows | wine
# x86_64-linux-musl | x86_64-linux-musl-gcc@16.1.0 | windows→linux | linux job
#
# The mingw row is OS-cross (same arch, different OS/ABI: ELF→PE), so it lives
# in its own job below with wine verification instead of the qemu arch matrix.
# See .agents/docs/2026-07-15-mingw-linux-cross-windows-design.md.
#
# The windows→linux row is the MIRROR of that one, and its verification has no
# wine-equivalent: a Windows runner cannot execute the ELF it just produced.
# So it is split across TWO jobs — build on windows-latest, upload the artefact,
# then download and really run it on ubuntu. Static assertions alone would not
# do: "it linked" has never implied "it runs" (see the elfpatch incident in
# .agents/docs/, and 2026-08-03-windows-host-linux-cross-design.md §6.1).
# The artefact is a fully static musl ELF (no PT_INTERP), so the consumer job
# needs neither qemu nor a matching loader.
#
# mcpp resolves a cross `--target <triple>-musl` build to the triple-named cross
# gcc musl toolchain from the xlings ecosystem (xim:<triple>-gcc, see
# src/build/prepare.cppm). Output is a fully static musl ELF (no PT_INTERP),
# which also makes the aarch64 artefact runnable natively in Termux/Android —
# qemu-aarch64 is the CI proxy for "does this cross artefact actually execute".
#
# ── NOT here ───────────────────────────────────────────────────────────────
# * Same-arch builds (host arch == target arch) are NOT cross. The native musl
# static build `--target x86_64-linux-musl` (x86_64 host) is exercised by
# ci-linux.yml's "Toolchain: musl-gcc" step, and release.yml for the static
# release artefact. Keep them there; this file is cross-arch only.
#
# ── Planned cross rows (documented; NOT yet wired in mcpp — keep as comments) ─
# * llvm/clang cross : clang is inherently a cross-compiler, but mcpp does not
# yet inject `-target <triple>` + a cross sysroot for a
# clang toolchain; cross `--target` resolves to gcc musl
# only. Wire the clang cross path first, then add a row.
# * riscv64-linux-musl: add once xim:riscv64-linux-musl-gcc ships to
# xlings-res + xim-pkgindex.
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
workflow_dispatch:
concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
cross-build:
name: cross-build ${{ matrix.target }} (mcpp + xlings)
runs-on: ubuntu-24.04
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
include:
- target: aarch64-linux-musl
file_arch: "ARM aarch64"
qemu_bin: qemu-aarch64-static
env:
MCPP_HOME: /home/runner/.mcpp
# Verbose every mcpp invocation for richer CI diagnostics (src/cli.cppm).
MCPP_VERBOSE: "1"
steps:
- uses: actions/checkout@v4
- name: Cache mcpp sandbox
uses: actions/cache@v4
with:
path: ~/.mcpp
key: mcpp-sandbox-${{ runner.os }}-cross-${{ matrix.target }}-${{ hashFiles('mcpp.toml', '.xlings.json') }}
restore-keys: |
mcpp-sandbox-${{ runner.os }}-cross-${{ matrix.target }}-
- name: Cache xlings
uses: actions/cache@v4
with:
path: ~/.xlings
key: xlings-${{ runner.os }}-v2-${{ hashFiles('.xlings.json') }}
restore-keys: |
xlings-${{ runner.os }}-v2-
- name: Install qemu-user-static
run: |
sudo apt-get update -qq
sudo apt-get install -y qemu-user-static
${{ matrix.qemu_bin }} --version | head -1
- name: Bootstrap mcpp via xlings
env:
XLINGS_NON_INTERACTIVE: '1'
# Must equal `pinned::kXlingsVersion` (src/xlings.cppm) and the
# xlings the release bundles — enforced by
# .github/tools/check_version_pins.sh.
#
# Floors worth remembering. 0.4.67 carried the
# multi-index_repo install fix (openxlings/xlings#374); 0.4.68 adds
# per-repo index artifact sources (openxlings/xlings#377) so the
# mcpplibs index syncs via artifact with git as fallback (mcpp#269);
# 0.4.69 keys the index by (namespace, name) so two packages sharing
# a short name in ONE index are both addressable (openxlings/xlings#381)
# — the floor for SPEC-001 short-name descriptors.
# A past 0.4.61 "download 404
# for mcpp@<pin>" was NOT a version bug — the xlings-res/mcpp GitHub
# release assets were uploaded in a broken state (records present,
# blobs missing → 404 on GET); re-uploaded clean. The stale-INDEX
# half is handled by the marker-clear below.
XLINGS_VERSION: '2026.8.5.2'
run: |
tarball="xlings-${XLINGS_VERSION}-linux-x86_64.tar.gz"
curl -fsSL -o "/tmp/${tarball}" \
"https://github.com/d2learn/xlings/releases/download/v${XLINGS_VERSION}/${tarball}"
tar -xzf "/tmp/${tarball}" -C /tmp
"/tmp/xlings-${XLINGS_VERSION}-linux-x86_64/subos/default/bin/xlings" self install
export PATH="$HOME/.xlings/subos/default/bin:$PATH"
xlings --version
# Force a real index re-sync even on a warm cache: drop the TTL refresh
# markers so `xlings update` actually pulls the latest index (sees the
# current bootstrap pin) while the toolchain payloads stay cached.
find "$HOME/.xlings" -name '.xlings-index-cache.json' -delete 2>/dev/null || true
xlings config --mirror GLOBAL 2>/dev/null || true
xlings update -y 2>/dev/null || xlings update 2>/dev/null || true
# MCPP_BOOT is what actually runs the bootstrap build below, so it —
# not just MCPP — has to be the pinned binary. It used to be the shim
# in subos/default/bin, which resolves to whatever version xvm has
# selected; pinning only MCPP would have looked right and changed
# nothing.
MCPP_BOOT=$(bash "$GITHUB_WORKSPACE/.github/tools/install_pinned_mcpp.sh" "$GITHUB_WORKSPACE")
echo "MCPP=$MCPP_BOOT" >> "$GITHUB_ENV"
echo "XLINGS_BIN=$HOME/.xlings/subos/default/bin/xlings" >> "$GITHUB_ENV"
echo "MCPP_BOOT=$MCPP_BOOT" >> "$GITHUB_ENV"
- name: Self-host build (bootstrap mcpp -> fresh host mcpp)
run: |
export MCPP_VENDORED_XLINGS="$XLINGS_BIN"
"$XLINGS_BIN" config --mirror GLOBAL 2>/dev/null || true
"$MCPP_BOOT" self config --mirror GLOBAL 2>/dev/null || true
"$MCPP_BOOT" build
MCPP=$(realpath "$(find target -type f -name mcpp -printf '%T@ %p\n' | sort -rn | head -1 | cut -d' ' -f2)")
test -x "$MCPP"
"$MCPP" self config --mirror GLOBAL
echo "MCPP=$MCPP" >> "$GITHUB_ENV"
- name: "Cross-build mcpp -> ${{ matrix.target }}"
run: |
export MCPP_VENDORED_XLINGS="$XLINGS_BIN"
"$MCPP" build --target ${{ matrix.target }}
bin=$(find target/${{ matrix.target }} -type f -name mcpp | head -1)
[ -n "$bin" ] || { echo "no mcpp artefact for ${{ matrix.target }}"; exit 1; }
echo "== file =="; file "$bin"
file "$bin" | grep -q "${{ matrix.file_arch }}" || { echo "expected ${{ matrix.file_arch }}"; exit 1; }
file "$bin" | grep -q "statically linked" || { echo "expected static"; exit 1; }
echo "MCPP_XBIN=$bin" >> "$GITHUB_ENV"
- name: "Cross-build xlings -> ${{ matrix.target }}"
run: |
export MCPP_VENDORED_XLINGS="$XLINGS_BIN"
git clone --depth 1 https://github.com/openxlings/xlings /tmp/xlings-src
cd /tmp/xlings-src
"$MCPP" self config --mirror GLOBAL 2>/dev/null || true
"$MCPP" build --target ${{ matrix.target }}
xbin=$(find target/${{ matrix.target }} -type f -name xlings | head -1)
[ -n "$xbin" ] || { echo "no xlings artefact for ${{ matrix.target }}"; exit 1; }
echo "== file =="; file "$xbin"
file "$xbin" | grep -q "${{ matrix.file_arch }}" || { echo "expected ${{ matrix.file_arch }}"; exit 1; }
file "$xbin" | grep -q "statically linked" || { echo "expected static"; exit 1; }
echo "XLINGS_XBIN=$xbin" >> "$GITHUB_ENV"
- name: "Smoke-run cross artefacts under qemu"
run: |
RUN="${{ matrix.qemu_bin }}"
# mcpp is self-contained, so --version runs cleanly under bare qemu —
# this is the hard execution proof for the cross artefact.
echo "== mcpp --version =="
mver=$($RUN "$MCPP_XBIN" --version)
echo "$mver"; echo "$mver" | grep -q "mcpp" || { echo "mcpp --version failed"; exit 1; }
# xlings expects a real runtime environment (sandbox/config) and may
# exit non-zero on a bare `--version` under qemu; its ELF arch + static
# linkage were already asserted in the build step, so treat execution
# here as best-effort rather than gating.
echo "== xlings --version (best-effort under qemu) =="
xver=$($RUN "$XLINGS_XBIN" --version 2>&1 || true)
echo "$xver"
# ── Linux → Windows MinGW cross (OS-cross, same arch: ELF→PE) ─────────────
# Builds a demo project for x86_64-w64-mingw32 with the from-source GCC-16
# MSVCRT cross toolchain, asserts the artefact is a fully-static PE, and runs
# it under wine. Delegated to the e2e harness (tests/e2e/102_mingw_cross_wine.sh,
# `# requires: mingw-cross wine`) so the run_all cap-gating stays the single
# source of truth. See 2026-07-15-mingw-linux-cross-windows-design.md Part C.
mingw-cross-wine:
name: mingw-cross linux→windows (build + wine run)
runs-on: ubuntu-24.04
timeout-minutes: 60
env:
MCPP_HOME: /home/runner/.mcpp
MCPP_VERBOSE: "1"
steps:
- uses: actions/checkout@v4
- name: Cache mcpp sandbox
uses: actions/cache@v4
with:
path: ~/.mcpp
key: mcpp-sandbox-${{ runner.os }}-mingw-cross-${{ hashFiles('mcpp.toml', '.xlings.json') }}
restore-keys: |
mcpp-sandbox-${{ runner.os }}-mingw-cross-
- name: Cache xlings
uses: actions/cache@v4
with:
path: ~/.xlings
key: xlings-${{ runner.os }}-v2-${{ hashFiles('.xlings.json') }}
restore-keys: |
xlings-${{ runner.os }}-v2-
# wine 的包集固定不变 —— 缓存整个 .deb 依赖闭包,命中时跳过 apt update
# 与下载(每轮省 ~1-2min)。镜像月度更新可能改变依赖缺口,dpkg -i 失败时
# 由 apt-get -f 兜底并重新回填缓存。
- name: Cache wine debs
uses: actions/cache@v4
with:
path: ~/wine-debs
key: wine-debs-${{ runner.os }}-ubuntu24.04-v1
- name: Install wine
run: |
sudo dpkg --add-architecture i386 || true
if ls ~/wine-debs/*.deb >/dev/null 2>&1; then
sudo dpkg -i ~/wine-debs/*.deb 2>/dev/null \
|| { sudo apt-get update -qq; sudo apt-get install -f -y; }
else
sudo apt-get update -qq
sudo apt-get install -y --download-only wine64 wine \
|| sudo apt-get install -y --download-only wine
mkdir -p ~/wine-debs
cp /var/cache/apt/archives/*.deb ~/wine-debs/ 2>/dev/null || true
sudo apt-get install -y wine64 wine || sudo apt-get install -y wine
fi
wine --version
- name: Bootstrap mcpp via xlings
env:
XLINGS_NON_INTERACTIVE: '1'
XLINGS_VERSION: '2026.8.5.2'
run: |
tarball="xlings-${XLINGS_VERSION}-linux-x86_64.tar.gz"
curl -fsSL -o "/tmp/${tarball}" \
"https://github.com/d2learn/xlings/releases/download/v${XLINGS_VERSION}/${tarball}"
tar -xzf "/tmp/${tarball}" -C /tmp
"/tmp/xlings-${XLINGS_VERSION}-linux-x86_64/subos/default/bin/xlings" self install
export PATH="$HOME/.xlings/subos/default/bin:$PATH"
find "$HOME/.xlings" -name '.xlings-index-cache.json' -delete 2>/dev/null || true
xlings config --mirror GLOBAL 2>/dev/null || true
xlings update -y 2>/dev/null || xlings update 2>/dev/null || true
# MCPP_BOOT is what actually runs the bootstrap build below, so it —
# not just MCPP — has to be the pinned binary. It used to be the shim
# in subos/default/bin, which resolves to whatever version xvm has
# selected; pinning only MCPP would have looked right and changed
# nothing.
MCPP_BOOT=$(bash "$GITHUB_WORKSPACE/.github/tools/install_pinned_mcpp.sh" "$GITHUB_WORKSPACE")
echo "MCPP=$MCPP_BOOT" >> "$GITHUB_ENV"
echo "XLINGS_BIN=$HOME/.xlings/subos/default/bin/xlings" >> "$GITHUB_ENV"
echo "MCPP_BOOT=$MCPP_BOOT" >> "$GITHUB_ENV"
- name: Self-host build (fresh host mcpp)
run: |
export MCPP_VENDORED_XLINGS="$XLINGS_BIN"
"$MCPP_BOOT" self config --mirror GLOBAL 2>/dev/null || true
"$MCPP_BOOT" build
MCPP=$(realpath "$(find target -type f -name mcpp -printf '%T@ %p\n' | sort -rn | head -1 | cut -d' ' -f2)")
test -x "$MCPP"
"$MCPP" self config --mirror GLOBAL
echo "MCPP=$MCPP" >> "$GITHUB_ENV"
- name: Install mingw-cross toolchain
run: |
export MCPP_VENDORED_XLINGS="$XLINGS_BIN"
"$MCPP" toolchain install mingw-cross 16.1.0
- name: "e2e: cross-build + wine run"
run: |
export MCPP_VENDORED_XLINGS="$XLINGS_BIN"
bash tests/e2e/102_mingw_cross_wine.sh
# ── windows → linux ───────────────────────────────────────────────────────
# The mirror of mingw-cross-wine. Two jobs because a Windows runner cannot
# execute the ELF it produces; the artefact is handed to a Linux job and
# really run there.
windows-host-linux-cross:
name: windows→linux cross-build (windows host)
runs-on: windows-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/bootstrap-mcpp
with:
# Do NOT restore target/ in a cross job. This job builds twice — once
# for the host, then once for x86_64-linux-musl — and a restored BMI
# tree makes the second build read `std` BMIs that no longer match
# what the dependency BMIs were compiled against:
#
# mcpplibs.cmdline: error: import 'std' has CRC mismatch
#
# GCC bakes a CRC of each imported module's BMI into the importer, so
# the two have to come from the same build round; a cache that
# restores one without the other is not a partial speedup, it is an
# unbuildable tree. It reproduced on rerun, and only in this job —
# the other two cross jobs below cache ~/.mcpp and ~/.xlings but
# deliberately never target/, which is the convention this now follows.
#
# It stayed hidden until a PR touched neither mcpp.toml nor
# .xlings.json: those two files key the sandbox cache, so every
# earlier run had been a cold miss.
cache-target: 'false'
- name: Build mcpp from source (self-host)
shell: bash
run: |
export MCPP_VENDORED_XLINGS="$XLINGS_BIN"
"$MCPP" build
# Newest, not first: target/ is cache-restored and keeps a directory
# per build fingerprint, so `find | head -1` can hand back the
# PREVIOUS release's binary (that is how a 0.0.106 build ran 0.0.105).
MCPP_SELF=$(find target -name "mcpp.exe" -path "*/bin/*" -printf "%T@ %p\n" \
| sort -rn | head -1 | cut -d" " -f2-)
test -n "$MCPP_SELF" || { echo "FAIL: no mcpp.exe"; exit 1; }
MCPP_SELF=$(cd "$(dirname "$MCPP_SELF")" && pwd)/$(basename "$MCPP_SELF")
"$MCPP_SELF" --version
echo "MCPP_SELF=$MCPP_SELF" >> "$GITHUB_ENV"
- name: Install the linux-musl cross toolchain (windows-hosted canadian)
shell: bash
run: |
export MCPP_VENDORED_XLINGS="$XLINGS_BIN"
"$MCPP_SELF" toolchain install gcc 16.1.0 --target x86_64-linux-musl
# The target row must now be visible on a Windows host — this is the
# host gate from design §1.2 having been lifted, asserted rather than
# eyeballed.
"$MCPP_SELF" toolchain list | tee /tmp/tclist.txt
grep -q "x86_64-linux-musl" /tmp/tclist.txt \
|| { echo "FAIL: linux-musl target not listed on windows host"; exit 1; }
- name: "Cross-build mcpp -> x86_64-linux-musl"
shell: bash
run: |
export MCPP_VENDORED_XLINGS="$XLINGS_BIN"
"$MCPP_SELF" build --target x86_64-linux-musl
# Scope the search to the TARGET's output tree — target/ also holds the
# host build from the previous step, and both are named "mcpp*".
#
# The artefact carries a `.exe` suffix even though it is an ELF:
# plan.cppm's target_output() spells the suffix from
# mcpp::platform::exe_suffix, a HOST constant. That is the same
# host-decides-target confusion as B2, and it is symmetric — a
# Linux→Windows cross produces a PE with no `.exe` today. Renaming the
# output is a behaviour change that would touch the mingw e2e and any
# user script, so it is filed as follow-up rather than folded in here;
# match both spellings so this job is correct either way.
OUT=$(find target/x86_64-linux-musl -type f -path "*/bin/*" \
\( -name "mcpp" -o -name "mcpp.exe" \) -printf "%T@ %p\n" \
| sort -rn | head -1 | cut -d" " -f2-)
if [ -z "$OUT" ]; then
echo "FAIL: no cross artefact produced; tree was:"
find target/x86_64-linux-musl -type f -path "*/bin/*" | head -20
exit 1
fi
cp "$OUT" mcpp-linux-musl
ls -la mcpp-linux-musl
- uses: actions/upload-artifact@v4
with:
name: mcpp-x86_64-linux-musl-from-windows
path: mcpp-linux-musl
retention-days: 1
windows-host-linux-cross-run:
name: windows→linux artefact really runs (linux)
needs: windows-host-linux-cross
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- uses: actions/download-artifact@v4
with:
name: mcpp-x86_64-linux-musl-from-windows
- name: Assert it is a static ELF, then run it
run: |
set -euo pipefail
chmod +x mcpp-linux-musl
file mcpp-linux-musl
# B2 regression gate (design §1.3): before the fix, `-static` was
# decided by a HOST constant (`supports_full_static = is_linux`), so
# a Windows host emitted a NON-static binary here. Written as a
# positive `grep -q` on purpose: `! cmd | grep` is exempt from
# errexit and can never fail (see build-mcpp-helper-self-containment).
file mcpp-linux-musl | grep -q "ELF 64-bit LSB"
file mcpp-linux-musl | grep -q "x86-64"
file mcpp-linux-musl | grep -q "statically linked"
# A static musl ELF has no PT_INTERP at all — the stronger form of
# the same claim, and independent of `file`'s wording.
readelf -l mcpp-linux-musl > hdrs.txt
if grep -q "INTERP" hdrs.txt; then
echo "FAIL: artefact has a PT_INTERP segment — not statically linked"
grep -A2 "INTERP" hdrs.txt
exit 1
fi
# Linked ≠ runs. This is the whole point of the second job.
./mcpp-linux-musl --version
./mcpp-linux-musl --help > /dev/null
echo "OK: windows-built linux artefact executes natively"