Skip to content

Commit 1be3adc

Browse files
committed
ci(cross): verify the windows→linux artefact by really running it
Adds the mirror of the mingw-cross-wine row. It needs two jobs, not one, because a Windows runner cannot execute the ELF it just produced and there is no wine-equivalent in that direction: build on windows-latest, upload the artefact, download it on ubuntu and run it there. Static assertions alone were not an option. "It linked" has never implied "it runs" in this repo — the elfpatch incident is the standing reminder — so the consumer job executes `--version` and `--help` for real. The artefact is a fully static musl ELF with no PT_INTERP, so that job needs neither qemu nor a matching loader. The B2 gate lives here: before the fix a Windows host emitted a NON-static binary, so `file | grep -q "statically linked"` plus a "no INTERP segment" check on readelf is what would have caught it. Both are written as positive greps on purpose — `! cmd | grep` is exempt from errexit and can never fail. The build job also asserts that `toolchain list` shows the linux-musl row on a Windows host, which is the host gate from §1.2 having actually lifted rather than being eyeballed. The design doc is updated with everything the P0 spike turned up, including four things that cost real time and would cost it again: - -Os implies -fdeclone-ctor-dtor → C4 ctor symbols the mingw libstdc++ does not define; ELF hides this via symbol aliases, COFF cannot - --disable-libstdcxx-pch, matching how the shipped musl-gcc payload is built - do NOT rebuild target libstdc++ — reuse the same-version payload's copy; four consecutive failures there were all self-inflicted - -print-sysroot answers <prefix>/ but C++ headers still live under <prefix>/<triple>/include/c++/<ver> - libwinpthread-1.dll must land in EVERY directory holding a .exe, including x86_64-linux-musl/bin/ (as.exe) — the easy one to miss All six P0 criteria are recorded green, verified locally through wine.
1 parent 0c96978 commit 1be3adc

2 files changed

Lines changed: 279 additions & 15 deletions

File tree

.agents/docs/2026-08-03-windows-host-linux-cross-design.md

Lines changed: 171 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -259,17 +259,90 @@ endif
259259
手工三段(旧路线 3)**短期最可控但长期最差** —— 不可重复、每次升 GCC 都要手工拼装、无法自动化,
260260
会变成一个只有作者能重建的黑盒。既然选长期最合适,就不能选它。
261261
262-
#### config.mak(spike 起点)
262+
#### config.mak(实测可用)
263263
264264
```make
265265
GCC_VER = 16.1.0
266-
MUSL_VER = 1.2.5
267266
BINUTILS_VER = 2.44
267+
MUSL_VER = 1.2.5
268+
GMP_VER = 6.3.0
269+
MPC_VER = 1.3.1
270+
MPFR_VER = 4.2.2
271+
LINUX_VER = headers-4.19.88-2
272+
268273
TARGET = x86_64-linux-musl
269274
HOST = x86_64-w64-mingw32 # ← canadian:产出的编译器跑在 Windows
270-
COMMON_CONFIG += CFLAGS="-g0 -Os" CXXFLAGS="-g0 -Os" # strip,见 §4.1
275+
276+
COMMON_CONFIG += CFLAGS="-g0 -Os" CXXFLAGS="-g0 -Os -fno-declone-ctor-dtor" LDFLAGS="-s"
277+
# ^^^^^^^^^^^^^^^^^^^^^^ 硬约束,见下
271278
```
272279
280+
#### ⚠️ `-Os` 在 mingw host 上必须配 `-fno-declone-ctor-dtor`(实测,踩过)
281+
282+
**这是本期最贵的一个坑 —— 症状离根因隔了三层,值得单独记。**
283+
284+
症状:GCC 编到最后链接 `gcov-dump.exe` 时炸,报 `std::string` 的**移动构造函数** undefined:
285+
286+
```
287+
ld: libcommon.a(paths-output.o): undefined reference to
288+
`std::__cxx11::basic_string<...>::basic_string(std::__cxx11::basic_string<...>&&)'
289+
```
290+
291+
第一反应「mingw-cross-gcc payload 的 libstdc++ 坏了」——**错的**。`nm` 显示符号在
292+
`libstdc++.a` 里明明是 `T`(defined)。真相在 **mangled name**,demangle 后完全看不出来:
293+
294+
| 侧 | mangled | 含义 |
295+
|---|---|---|
296+
| 引用 | `…basic_string…C**4**EOS4_` | C4 = 统一构造函数(未 clone) |
297+
| 定义 | `…C**1**EOS4_` / `…C**2**EOS4_` | C1/C2 = complete / base |
298+
299+
`-Os` **自动开启** `-fdeclone-ctor-dtor`,令调用方引用 C4;而 libstdc++.a 只显式实例化了
300+
C1/C2。ELF 平台用符号别名把 C4 接到 C1/C2 上,**COFF 没有别名机制** —— 所以这个失配
301+
只在 mingw 上暴露,且只打在 `#include <string>` 且移动构造 `std::string` 的 host TU 上。
302+
303+
四行最小复现(任何人都能在 30 秒内自证):
304+
305+
```
306+
$ x86_64-w64-mingw32-g++ mv.cc # LINK OK
307+
$ x86_64-w64-mingw32-g++ -O2 mv.cc # LINK OK
308+
$ x86_64-w64-mingw32-g++ -Os mv.cc # LINK FAIL ← -Os 是唯一变量
309+
$ x86_64-w64-mingw32-g++ -Os -fno-declone-ctor-dtor mv.cc # LINK OK
310+
```
311+
312+
> **为什么 aarch64 那期没踩到**:`aarch64-musl-gcc-canadian-cross-rebuild.md` 用的同样是
313+
> `CFLAGS="-g0 -Os"`,但那次 HOST 是 **aarch64-linux-musl(ELF)**,符号别名把问题吃掉了。
314+
> 「同一份 config 换个 HOST 就炸」正是本条必须写进文档的原因。
315+
>
316+
> `CXXFLAGS` 只作用于 host 侧(target 库走 `CXXFLAGS_FOR_TARGET`),因此这个 flag
317+
> **不影响产物 ABI**,可以放心加。
318+
319+
#### ⚠️ 必须 `--disable-libstdcxx-pch`(实测,踩过)
320+
321+
过了上一个坑之后,构建会在 **target libstdc++** 再炸一次:
322+
323+
```
324+
libstdc++-v3/include/atomic:259:7: error: 'constexpr' constructor does not have
325+
empty body [-Wtemplate-body]
326+
make: *** [x86_64-linux-musl/bits/stdc++.h.gch/O2ggnu++0x.gch] Error 1
327+
```
328+
329+
libstdc++ 会为 `-std=gnu++0x`(C++11)额外生成一份预编译头。GCC 16 的 `<atomic>` 里
330+
那个 `constexpr` 构造函数带非空 body(C++14 起才合法),上游用
331+
`#pragma GCC diagnostic ignored "-Wc++14-extensions"` 抑制过 —— 但 **GCC 15 新增的
332+
`-Wtemplate-body`** 让诊断在模板*定义*处就触发,pragma 的作用域没覆盖到,于是变成硬 error。
333+
334+
**定位方法值得记**:不要去猜,直接看已发布的 `xim:musl-gcc@16.1.0` payload——
335+
336+
```bash
337+
$ find <payload> -name '*.gch' | wc -l
338+
0
339+
```
340+
341+
**零个 PCH**。已发布的 payload 本来就是 `--disable-libstdcxx-pch` 构建的。所以这不是
342+
"canadian 特有的新问题",而是**两个 payload 的构建配置必须对齐**。加上它,与生态既有产物一致。
343+
344+
mcpp 走 modules,PCH 对它零价值,没有任何损失。
345+
273346
前置:`x86_64-w64-mingw32-g++` 必须在 PATH —— 即 §3.2 的自举闭环,来自 mcpp 已发布的 `mingw-cross-gcc@16.1.0`。
274347
275348
#### 待解点 —— 实施中已全部解答(2026-08-03 实测)
@@ -281,20 +354,37 @@ COMMON_CONFIG += CFLAGS="-g0 -Os" CXXFLAGS="-g0 -Os" # strip,见 §4.1
281354
走 GCC 的 in-tree 依赖构建 —— 因此它们**天然继承 `--host=`**,不需要任何 `--with-gmp=` 干预。
282355
这很可能就是 issue #55 提问者卡住的地方(他大概率试图分别预编译这些库)。
283356
284-
**2. `SYSROOT = /` 的布局 —— 是 NATIVE 布局,且 mcpp 恰好已兼容。**
285-
canadian 分支下 target 库不装进 `<prefix>/<triple>/`,而是直接装在 prefix 根:
357+
**2. `SYSROOT = /` 只影响 C 库,C++ 头仍走交叉布局 —— 别被 `-print-sysroot` 骗了。**
358+
359+
canadian 分支设 `SYSROOT = /`,于是 **musl libc / kernel headers** 装进 prefix 根
360+
(`<prefix>/{include,lib}`),`-print-sysroot` 也确实回答 `<prefix>/`。**但 C++ 头不在那里**:
361+
GCC 作为交叉编译器,`GPLUSPLUS_INCLUDE_DIR` 始终是 `<prefix>/<target-triple>/include/c++/<ver>`。
362+
363+
实测踩过:按 `-print-sysroot` 的字面意思把 libstdc++ 放进 `<prefix>/include/c++/`,编译报
364+
`fatal error: iostream: No such file or directory`,而文件明明在。真相在 `-v` 的
365+
`ignoring nonexistent directory` 列表里:
366+
367+
```
368+
ignoring nonexistent directory ".../x86_64-linux-musl/include/c++/16.1.0"
369+
^^^^^^^^^^^^^^^^^^ 它要的是这里
370+
```
371+
372+
最终布局(**两种布局混用**,这正是反直觉之处):
286373
287374
```
288375
output-x86_64-w64-mingw32/
289-
├── bin/ (PE: x86_64-linux-musl-g++.exe …) ← host 侧
290-
├── include/ (musl headers, include/c++/16.1.0/…) ← target 侧
291-
├── lib/ (musl libc.a / libstdc++.a) ← target 侧
292-
└── libexec/gcc/x86_64-linux-musl/16.1.0/ (PE: cc1plus) ← host 侧
376+
├── bin/ (PE: x86_64-linux-musl-g++.exe …) ← host
377+
├── libexec/gcc/…/cc1plus.exe (PE) ← host
378+
├── include/ (musl + kernel headers) ← target, sysroot=/
379+
├── lib/ (musl libc.a, crt*.o) ← target, sysroot=/
380+
├── lib/gcc/x86_64-linux-musl/16.1.0/ (libgcc.a, crtbegin.o …) ← target
381+
└── x86_64-linux-musl/ ← target, 交叉布局
382+
├── include/c++/16.1.0/bits/std.cc
383+
└── lib/libstdc++.a
293384
```
294385
295-
**关键**:`bits/std.cc` 落在 `include/c++/16.1.0/bits/std.cc`,正是 `gcc.cppm:71` 的**第一候选**
296-
(`root/include/c++/<ver>/bits/std.cc`)—— 比 §1.1 预期的交叉布局候选更早命中。`import std` 零改可用。
297-
§2 的 asset layout 已按此更正。
386+
**对 mcpp 无影响**:`bits/std.cc` 落在 `<prefix>/<triple>/include/c++/<ver>/`,正是
387+
`gcc.cppm:75-90` 已实现的交叉候选。§1.1 的原判断成立,`import std` 零改可用。
298388
299389
#### 实施前置(实测确认,必须两者同时在 PATH)
300390
@@ -318,6 +408,75 @@ libstdc++,module 相关产物会与 driver 不匹配 —— 这是最隐蔽的
318408
319409
### 3.4 spike 的成功判据
320410
411+
#### ⚠️ target libstdc++ 不要重建 —— 从同版本 payload 复用(实测,撞了四次)
412+
413+
`make install` 会一路撞在 **target libstdc++** 上,连撞四种不同的错:
414+
415+
| # | 症状 | 处理 |
416+
|---|---|---|
417+
| 1 | `stdc++.h.gch` — `constexpr` ctor 非空 body | `--disable-libstdcxx-pch` |
418+
| 2 | `futex.lo` / `future.lo` — 同上,`-std=gnu++11` | `CXXFLAGS_FOR_TARGET += -Wno-template-body` |
419+
| 3 | `string-inst.cc` — duplicate explicit instantiation | `-fpermissive` |
420+
| 4 | `format.lo` — instantiating erroneous template | 还有下一个… |
421+
422+
打到第四个就该停下来问:**为什么要用已发布的 GCC 16.1.0 去重新编译 GCC 16.1.0 自己的 libstdc++?**
423+
424+
canadian 构建的职责是产出 **host 侧二进制**。target 库是纯 target 代码,**与谁编译它无关**;
425+
`xim:musl-gcc@16.1.0` payload 里那一份是同版本(实测 `BASE-VER` 与 `DATESTAMP` 都是
426+
16.1.0 / 20260430)、经完整 CI 验证的权威副本。canadian 模式设 `SYSROOT = /` 本就假设 target
427+
树已存在 —— 复用它才是**顺着设计走**,不是绕过困难。
428+
429+
于是构建收敛为三步(实测可复现):
430+
431+
```bash
432+
# 1) host 侧:driver / cc1plus / binutils → PE
433+
cd <build>/x86_64-w64-mingw32/x86_64-linux-musl/obj_gcc
434+
make DESTDIR=$OUT install-gcc # bin/*.exe, libexec/**/cc1plus.exe
435+
make DESTDIR=$OUT install-target-libgcc # lib/gcc/<triple>/16.1.0/{libgcc.a,crt*.o}
436+
# musl libc 已由前面的 `make install` 装进 $OUT/{include,lib}
437+
438+
# 2) target 侧 C++:从权威 payload 复用(注意是交叉布局,见上文待解点 2)
439+
cp -a $PAYLOAD/x86_64-linux-musl/include $OUT/x86_64-linux-musl/
440+
cp -a $PAYLOAD/x86_64-linux-musl/lib/. $OUT/x86_64-linux-musl/lib/
441+
442+
# 3) DLL 部署,见下
443+
```
444+
445+
#### ⚠️ `libwinpthread-1.dll` 要部署到**每个**含 .exe 的目录
446+
447+
PE 没有 RPATH,DLL 只从 exe 同目录 / PATH 解析。产出的 host 二进制唯一的非系统依赖就是它
448+
(其余 `KERNEL32/ADVAPI32/msvcrt/WS2_32` 都是 Windows 自带)。漏了就是启动即
449+
`STATUS_DLL_NOT_FOUND`,而且**不止 `bin/`**:
450+
451+
```
452+
bin/ x86_64-linux-musl-g++.exe …
453+
libexec/gcc/x86_64-linux-musl/16.1.0/ cc1.exe, cc1plus.exe
454+
x86_64-linux-musl/bin/ as.exe, ld.exe ← 最容易漏的一个
455+
```
456+
457+
实测就是漏了第三个:g++ 能跑、`--version` 正常,一编译就在汇编阶段炸 `as.exe`。
458+
459+
```bash
460+
for d in $(find "$OUT" -name "*.exe" -printf "%h\n" | sort -u); do cp -n "$DLL" "$d/"; done
461+
```
462+
463+
> **更优但更贵的替代**:给 host 侧加 `LDFLAGS=-static` 彻底消除 DLL 依赖(winlibs 就这么做)。
464+
> 需要重新 configure + 重链接,本期未做;登记为 follow-up。
465+
466+
---
467+
468+
> **P0 已完成(2026-08-03),六条全绿。** 实测记录见下。验证在本机用 **wine** 驱动 PE 编译器
469+
> 完成 —— 不需要 Windows 机器就能跑完判据 1–6,这条对后续维护很省事。
470+
>
471+
> ```
472+
> 判据 1 bin/x86_64-linux-musl-g++.exe: PE32+ executable (console) x86-64, for MS Windows ✅
473+
> 判据 2 wine …g++.exe --version → x86_64-linux-musl-g++.exe (GCC) 16.1.0 ✅
474+
> 判据 3 x86_64-linux-musl/include/c++/16.1.0/bits/std.cc 存在 ✅
475+
> 判据 4 hello → ELF 64-bit LSB executable, x86-64, statically linked ✅
476+
> 判据 5 本机(真 Linux)执行 → "windows builds linux";readelf 无 PT_INTERP ✅
477+
> 判据 6 import std 编译 + 链接 + 执行 → "import std works" ✅
478+
> ```
479+
321480
必须**全部**满足才算 P0 出口 —— 逐条断言,不要只看「编译通过」:
322481
323482
1. `file bin/x86_64-linux-musl-g++.exe` → `PE32+ executable ... x86-64`

.github/workflows/cross-build-test.yml

Lines changed: 108 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,25 @@ name: cross-build-test
88
# each target triple, arch-checked, and smoke-run under qemu-user.
99
#
1010
# ── Supported cross matrix (built + verified below) ────────────────────────
11-
# target | toolchain | host→target | run
12-
# ----------------------|---------------------------------|---------------|-----
13-
# aarch64-linux-musl | aarch64-linux-musl-gcc@16.1.0 | x86_64→arm64 | qemu
11+
# target | toolchain | host→target | run
12+
# ----------------------|----------------------------------|---------------|-----
13+
# aarch64-linux-musl | aarch64-linux-musl-gcc@16.1.0 | x86_64→arm64 | qemu
1414
# x86_64-w64-mingw32 | mingw-cross-gcc@16.1.0 (MSVCRT) | linux→windows | wine
15+
# x86_64-linux-musl | x86_64-linux-musl-gcc@16.1.0 | windows→linux | linux job
1516
#
1617
# The mingw row is OS-cross (same arch, different OS/ABI: ELF→PE), so it lives
1718
# in its own job below with wine verification instead of the qemu arch matrix.
1819
# See .agents/docs/2026-07-15-mingw-linux-cross-windows-design.md.
1920
#
21+
# The windows→linux row is the MIRROR of that one, and its verification has no
22+
# wine-equivalent: a Windows runner cannot execute the ELF it just produced.
23+
# So it is split across TWO jobs — build on windows-latest, upload the artefact,
24+
# then download and really run it on ubuntu. Static assertions alone would not
25+
# do: "it linked" has never implied "it runs" (see the elfpatch incident in
26+
# .agents/docs/, and 2026-08-03-windows-host-linux-cross-design.md §6.1).
27+
# The artefact is a fully static musl ELF (no PT_INTERP), so the consumer job
28+
# needs neither qemu nor a matching loader.
29+
#
2030
# mcpp resolves a cross `--target <triple>-musl` build to the triple-named cross
2131
# gcc musl toolchain from the xlings ecosystem (xim:<triple>-gcc, see
2232
# src/build/prepare.cppm). Output is a fully static musl ELF (no PT_INTERP),
@@ -285,3 +295,98 @@ jobs:
285295
run: |
286296
export MCPP_VENDORED_XLINGS="$XLINGS_BIN"
287297
bash tests/e2e/102_mingw_cross_wine.sh
298+
299+
# ── windows → linux ───────────────────────────────────────────────────────
300+
# The mirror of mingw-cross-wine. Two jobs because a Windows runner cannot
301+
# execute the ELF it produces; the artefact is handed to a Linux job and
302+
# really run there.
303+
windows-host-linux-cross:
304+
name: windows→linux cross-build (windows host)
305+
runs-on: windows-latest
306+
timeout-minutes: 60
307+
steps:
308+
- uses: actions/checkout@v4
309+
- uses: ./.github/actions/bootstrap-mcpp
310+
311+
- name: Build mcpp from source (self-host)
312+
shell: bash
313+
run: |
314+
export MCPP_VENDORED_XLINGS="$XLINGS_BIN"
315+
"$MCPP" build
316+
# Newest, not first: target/ is cache-restored and keeps a directory
317+
# per build fingerprint, so `find | head -1` can hand back the
318+
# PREVIOUS release's binary (that is how a 0.0.106 build ran 0.0.105).
319+
MCPP_SELF=$(find target -name "mcpp.exe" -path "*/bin/*" -printf "%T@ %p\n" \
320+
| sort -rn | head -1 | cut -d" " -f2-)
321+
test -n "$MCPP_SELF" || { echo "FAIL: no mcpp.exe"; exit 1; }
322+
MCPP_SELF=$(cd "$(dirname "$MCPP_SELF")" && pwd)/$(basename "$MCPP_SELF")
323+
"$MCPP_SELF" --version
324+
echo "MCPP_SELF=$MCPP_SELF" >> "$GITHUB_ENV"
325+
326+
- name: Install the linux-musl cross toolchain (windows-hosted canadian)
327+
shell: bash
328+
run: |
329+
export MCPP_VENDORED_XLINGS="$XLINGS_BIN"
330+
"$MCPP_SELF" toolchain install gcc 16.1.0 --target x86_64-linux-musl
331+
# The target row must now be visible on a Windows host — this is the
332+
# host gate from design §1.2 having been lifted, asserted rather than
333+
# eyeballed.
334+
"$MCPP_SELF" toolchain list | tee /tmp/tclist.txt
335+
grep -q "x86_64-linux-musl" /tmp/tclist.txt \
336+
|| { echo "FAIL: linux-musl target not listed on windows host"; exit 1; }
337+
338+
- name: "Cross-build mcpp -> x86_64-linux-musl"
339+
shell: bash
340+
run: |
341+
export MCPP_VENDORED_XLINGS="$XLINGS_BIN"
342+
"$MCPP_SELF" build --target x86_64-linux-musl
343+
OUT=$(find target -name "mcpp" -type f -path "*/bin/*" -printf "%T@ %p\n" \
344+
| sort -rn | head -1 | cut -d" " -f2-)
345+
test -n "$OUT" || { echo "FAIL: no cross artefact produced"; exit 1; }
346+
cp "$OUT" mcpp-linux-musl
347+
ls -la mcpp-linux-musl
348+
349+
- uses: actions/upload-artifact@v4
350+
with:
351+
name: mcpp-x86_64-linux-musl-from-windows
352+
path: mcpp-linux-musl
353+
retention-days: 1
354+
355+
windows-host-linux-cross-run:
356+
name: windows→linux artefact really runs (linux)
357+
needs: windows-host-linux-cross
358+
runs-on: ubuntu-24.04
359+
timeout-minutes: 10
360+
steps:
361+
- uses: actions/download-artifact@v4
362+
with:
363+
name: mcpp-x86_64-linux-musl-from-windows
364+
365+
- name: Assert it is a static ELF, then run it
366+
run: |
367+
set -euo pipefail
368+
chmod +x mcpp-linux-musl
369+
file mcpp-linux-musl
370+
371+
# B2 regression gate (design §1.3): before the fix, `-static` was
372+
# decided by a HOST constant (`supports_full_static = is_linux`), so
373+
# a Windows host emitted a NON-static binary here. Written as a
374+
# positive `grep -q` on purpose: `! cmd | grep` is exempt from
375+
# errexit and can never fail (see build-mcpp-helper-self-containment).
376+
file mcpp-linux-musl | grep -q "ELF 64-bit LSB"
377+
file mcpp-linux-musl | grep -q "x86-64"
378+
file mcpp-linux-musl | grep -q "statically linked"
379+
380+
# A static musl ELF has no PT_INTERP at all — the stronger form of
381+
# the same claim, and independent of `file`'s wording.
382+
readelf -l mcpp-linux-musl > hdrs.txt
383+
if grep -q "INTERP" hdrs.txt; then
384+
echo "FAIL: artefact has a PT_INTERP segment — not statically linked"
385+
grep -A2 "INTERP" hdrs.txt
386+
exit 1
387+
fi
388+
389+
# Linked ≠ runs. This is the whole point of the second job.
390+
./mcpp-linux-musl --version
391+
./mcpp-linux-musl --help > /dev/null
392+
echo "OK: windows-built linux artefact executes natively"

0 commit comments

Comments
 (0)