2026-08-03 · 基于 f694e13(2026.8.3.1)代码审计 姊妹文档:
2026-07-15-mingw-linux-cross-windows-design.md(本方案的镜像,Linux→Windows,已发布)、2026-06-23-aarch64-musl-gcc-canadian-cross-rebuild.md(canadian-cross 打包先例)、2026-07-15-toolchain-target-naming-unification-design.md(工具链×目标二轴命名)、2026-07-07-hermetic-toolchain-link-model-design.md(链接模型) 记忆:[[release-publish-pipeline]]、[[toolchain-target-naming-unification]]、[[mingw-linux-cross-windows]]、[[aarch64-musl-static-ninja-closure]]
补上 host ≠ target 的最后一个象限:Windows 主机产 Linux ELF。后端零新增(GNU 方言 + ELF 链接模型早已按
target 求值),工作量集中在四处:A. 解除三处 host 门、B. 修两个 host 常量污染 target 决策的真 bug、
C. canadian-cross 打包(build=linux-gnu / host=x86_64-w64-mingw32 / target=x86_64-linux-musl)、
D. CI 验证(Windows runner 产 ELF 自己跑不了,需跨 job 回传 —— wine 那一维的镜像难题)。
选 musl 而非 glibc 作为首个 Linux target,理由与现有交叉行一致:自包含。musl 交叉链自带 libc + libstdc++ +
binutils,不需要 xim:glibc / xim:linux-headers 这两个 sysroot payload —— 而它们在 Windows 主机上根本没有发布。
lifecycle.cppm:561 的依赖门已经写好了这个豁免,零改即正确(见 §1.1)。
方法论警告:
mingw-linux-cross-windows-design.md里点名的mingw_wrong_host()在 HEAD 上已不存在 (那期做完即删)。本节所有坐标以 f694e13 为准;实施前请重新 grep 一遍,别信旧文档的符号名。 —— 参见 [[v0099-feature-forwarding-238-230]] 同款教训。
| 抽象 | 位置 | 对本方案的意义 |
|---|---|---|
| 链接模型按 target 求值 | linkmodel.cppm:281-286 |
PE target 早退,ELF 路径按 target 解析;注释明说「so a future cross-compile resolves by what it builds FOR」 |
交叉工具链的 bits/std.cc 定位已实现 |
gcc.cppm:75-90 |
显式支持 <prefix>/<triple>/include/c++/<ver>/bits/std.cc —— 正是 musl 交叉链布局,import std 开箱可用 |
| sysroot payload 依赖门按 target 豁免 | lifecycle.cppm:561-562 |
!target.is_musl() && !target.is_pe() && !is_windows && !is_macos → musl target 且 Windows host,双重豁免,不会去找不存在的 xim:glibc |
<triple>-g++ 交叉前端解析 |
registry.cppm:227-228 |
交叉前端的现成机制(但 Windows 上缺 .exe,见 §1.3) |
kGnuDialect 命令拼写 |
dialect.cppm |
交叉链是 GCC 家族,拼写零差异 |
BuildOverrides.target_triple 入口 |
prepare.cppm:403-411 |
--target x86_64-linux-musl 入口已在 |
交叉下跳过 L3 build.mcpp |
prepare.cppm:914-919 |
host 构建程序不为 target 跑,语义已对 |
| 首次运行默认值不受影响 | prepare.cppm:1416-1421 |
Windows host 走 kFirstRunWinMsvc / windows-gnu,本方案纯 opt-in |
| 目标词汇表已含该 triple | triple.cppm:104 |
x86_64-linux-musl 已是 verified 行,无需新增 target 身份 |
结论:mcpp 引擎侧改动极小。真正的工作在打包与验证 —— 与镜像方案(Linux→Windows)的结论一致。
三处,全是 is_linux 硬编码:
-
可安装性门 —
lifecycle.cppm:371-374if (t.os == "linux") return mcpp::platform::is_linux && (t.is_musl() || t.arch == hostT.arch);
Linux 目标一律要求 Linux 主机 →
toolchain list的 Targets 块在 Windows 上不列 linux 行。 -
payload 选择 —
registry.cppm:225-227bool native = mcpp::platform::is_linux && t.arch == mcpp::platform::host_arch; pkg.ximName = native ? "musl-gcc" : t.str() + "-gcc";
Windows host 上
native恒 false → 已经会去要x86_64-linux-musl-gcc这个包名。逻辑碰巧是对的,缺的是包 (§3)与.exe前端(§1.3)。 -
Available 索引 host-split —
registry.cppm:308-313仅在 Windows host 列mingw-gcc、Linux host 列mingw-cross-gcc。需要在 Windows 分支补列新包。
架构债 —— 本期收敛(已拍板):门 1(可安装性)与门 2(payload 选择)是同一决策的两处推导 —— 「这个 host 能不能服务这个 target」。历史上这个模式每次都在加新语义时变成构建失败 (见 [[object-path-disambiguation-239-240]])。
本期把判据提成
registry.cppm里的单一谓词:// registry.cppm — 唯一真源:这个 host 能否服务这个 target bool host_can_serve(const triple::Triple& target);
lifecycle.cppm的installable_here改为直接调用它,自身不再做任何is_linux判断。 这是本方案唯一的重构项,收益是下一个 target(aarch64-linux-musl、riscv64)只改一处。
这两条都不是「缺功能」,是现存缺陷,只是今天没有 Windows→Linux 这条路径把它们暴露出来。
B1 — 交叉前端在 Windows 上永远找不到(registry.cppm:228)
pkg.frontendCandidates = { t.str() + "-g++", "g++" };toolchain_frontend()(registry.cppm:264-271)用 std::filesystem::exists(binDir / cand) 逐个试。Windows 上
真实文件是 x86_64-linux-musl-g++.exe,exists("...-g++") 返回 false → 前端解析失败 → 安装成功但工具链不可用。
mingw 分支早就踩过并修了(registry.cppm:242:{"g++.exe", "g++"}),musl 分支没跟上。修法同款:候选表按 host
补 .exe 变体,.exe 在前。
B2 — -static 在 Windows host 上不会发(platform/common.cppm:110 → flags.cppm:539)
// platform/common.cppm:110
constexpr bool supports_full_static = is_linux; // macOS/Windows cannot
// flags.cppm:539
std::string full_static = (mcpp::platform::supports_full_static && f.linkage == "static") ? " -static" : "";这是个 host 常量,注释说的是「macOS/Windows 主机的产物不能全静态」—— 语义正确,但被 flags.cppm 拿来决定
target 的链接方式。x86_64-linux-musl 的 defaultStatic = true(triple.cppm:104),在 Windows host 上
supports_full_static 为 false → -static 不发 → musl 目标的核心卖点(全静态 ELF)直接丢失。
PE target 不受影响,因为它的 -static 走 contract table 的 unit_ldflags(dist::Format::Pe) 另一条路
(flags.cppm:707-708)—— 也就是说这条常量今天只服务 ELF,把它按 host 求值从来就是错的。
修法:改为按 target 求值。语义应是「目标平台能否全静态链接」,即 target.os == "linux"(macOS 目标因
libSystem 仍为 false)。注意这会同时影响 build_program.cppm:474 的 muslStaticHelper —— 那里是 host 语义
(build.mcpp helper 跑在主机上),不能一起改,必须两处分开。参见 [[build-mcpp-helper-self-containment]]:
-static 绝不进 base,helper 的静态性是逐平台机制。
交付形态(已拍板):B1/B2 与主体合并为单 PR。
代价是 [[soname-alias-explicit-ninja-goals]] 记着的那条:混在大改动里的跨平台修复,失败时无法归因。 单 PR 下用提交分层保住归因能力,这是硬要求不是建议:
commit 内容 1 先加 B2 的回归断言(§6.2 的 grep -q "statically linked")—— 此时 CI 必红,红即证明断言有效2 修 B2( supports_full_static按 target 求值)—— CI 转绿3 修 B1(前端 .exe候选)4 host_can_serve谓词收敛 + 三处门5 doctor + 文档 + CI job commit 1 单独存在是关键 —— 它把「测试真的会失败」变成可查的历史记录,而不是口头保证。 参见 [[explicit-ninja-goals-two-regressions]]:没先验证会红的测试,等于没有测试。
遵循 toolchain-target-naming-unification 的二轴模型 —— 不新增任何用户可见身份:
- toolchain 轴:
gcc@16.1.0(与现有 pin 一致) - target 轴:
x86_64-linux-musl(triple.cppm:104已是 verified 行) - 用户命令与 Linux 主机上逐字相同:
mcpp build --target x86_64-linux-musl mcpp toolchain default gcc@16 --target x86_64-linux-musl
「Windows-hosted」不是名字,是 host ≠ target 关系的一个取值 —— 与 mingw 那期的结论同构。
xim 包名:x86_64-linux-musl-gcc,由 registry.cppm:227 的 t.str() + "-gcc" 自动导出,无需改代码。
包形态:一包双 host asset(已确证 xlings 支持,定稿)
原本的未决项是「xlings 的 xpm 是否允许同一个包同时挂 linux 与 windows 段」。已在本机索引上实测确证:
$ cd ~/.xlings/data/xim-pkgindex/pkgs && \
for f in $(grep -rl "windows = {" .); do grep -q "linux = {" "$f" && echo "BOTH: $f"; done
BOTH: n/ninja.lua BOTH: c/cmake.lua BOTH: x/xmake.lua
BOTH: g/git.lua BOTH: r/rust.lua BOTH: x/xvm.lua ... (20+ 个)ninja / cmake / git / rust 全是一包双 host。取一包:
| 方案 | 结论 |
|---|---|
| 一包双 host asset ✅ | 单个 x86_64-linux-musl-gcc,xpm 下 linux 与 windows 两段,XLINGS_RES 按 host 选 asset。包名与 registry.cppm:227 的 t.str()+"-gcc" 天然一致 → 零代码改动 |
已排除。需在 registry.cppm 加 host-split 分支,多一处推导 —— 与 §1.2 的收敛方向相反 |
install() / config() 的 Lua 需按 host 分叉枚举 bin(ls -1 vs dir /b),这在描述符内部解决,不外溢到 mcpp。
Asset 命名遵循 xlings-res 约定:
x86_64-linux-musl-gcc-16.1.0-linux-x86_64.tar.gz (已有,如未发布则本期一并发)
x86_64-linux-musl-gcc-16.1.0-windows-x86_64.zip (本期新增)
└── x86_64-linux-musl-gcc-16.1.0-windows-x86_64/
├── bin/ (PE: x86_64-linux-musl-g++.exe, -gcc.exe, -ar.exe, ...) ← host 侧
├── include/ (musl headers;include/c++/16.1.0/bits/std.cc) ← target 侧
├── lib/ (musl libc.a、libstdc++.a、lib/gcc/x86_64-linux-musl/…) ← target 侧
└── libexec/gcc/x86_64-linux-musl/16.1.0/ (PE: cc1.exe, cc1plus.exe) ← host 侧
注意这是 NATIVE 布局(target 库在 prefix 根),不是交叉工具链惯常的
<prefix>/<triple>/布局 —— musl-cross-make 的 canadian 分支设SYSROOT = /所致(§3.3 待解点 2)。对 mcpp 无影响甚至更好:bits/std.cc落在gcc.cppm:71的第一候选路径上。
build = x86_64-linux-gnu (CI runner / 本机)
host = x86_64-w64-mingw32 (产出的编译器跑在 Windows)
target = x86_64-linux-musl (产出的编译器为 Linux 生成代码)
三者互不相同 = 教科书定义的 canadian cross。
canadian cross 需要一个 --host 编译器(能产 Windows PE 的 GCC)。mcpp 已发布的 mingw-cross-gcc@16.1.0
正是它 —— Linux 主机上的 x86_64-w64-mingw32-g++,GCC 16.1.0,自带 mingw-w64 CRT。
这是个漂亮的自举:上一期的产物直接成为这一期的构建输入,不引入任何新的外部依赖。
取 musl-cross-make 的 canadian 路径。机制已在源码上确证,不再是「无先例的赌」。
上一版曾把这条标为「风险最高」,依据是上游 issue #55 提过 mingw host 却无解关闭。读了本机 payload 里的实际源码后,这个判断被推翻:
证据 1 — HOST 是一等公民,NATIVE 只是它的语法糖(litecross/Makefile:87-89):
ifneq ($(NATIVE),)
HOST:=$(TARGET) # NATIVE=1 ⟺ HOST=TARGET,仅此而已
endif证据 2 — canadian 是内建代码路径,直接把 --build/--host 喂给 configure(litecross/Makefile:97-125):
ifeq ($(HOST),) # 普通交叉
SYSROOT = /$(TARGET)
FULL_BINUTILS_CONFIG += --build=$(BUILD) --host=$(BUILD)
...
else # ← canadian / native 共用这一支,HOST 可以是任意 triple
SYSROOT = /
FULL_BINUTILS_CONFIG += --build=$(BUILD) --host=$(HOST)
FULL_GCC_CONFIG += --build=$(BUILD) --host=$(HOST)
MUSL_VARS =
endif证据 3 — 用户 HOST= 不会被默认值吃掉(Makefile:32-36):HOST = $(if $(NATIVE),$(TARGET)) 之后第 36 行
才 -include config.mak,GNU make 后赋值胜出;BUILD_DIR/OUTPUT 是递归展开变量,使用时才求值。
这三条合起来说明:HOST=x86_64-w64-mingw32 走的是与 aarch64 那期 NATIVE=1 完全相同的代码路径
(aarch64-musl-gcc-canadian-cross-rebuild.md 已实战跑通)。issue #55 的失败因此可以准确归因为
配置问题而非机制缺失(2019 年、ppc64 target、提问后无跟进),不构成对本方案的否证。
证据 4 — canadian 模式下 target 库本就不重建:上一版路线 3 的「关键洞察」(target 侧产物可复用)
其实是 musl-cross-make canadian 模式的既有行为 —— SYSROOT = / 且 MUSL_VARS = 清空,它不编 musl。
换言之路线 1 已经把路线 3 的优点包含在内,路线 3 没有独立存在价值。
| 维度 | musl-cross-make(选中) | crosstool-NG | 手工三段 |
|---|---|---|---|
| 与生态一致 | ✅ xim:musl-cross-make 已在索引,所有现有 musl payload 都出自它 |
❌ 引入第二套构建体系 | ❌ 无体系 |
| 与既有先例同路径 | ✅ 与 aarch64 NATIVE=1 同一分支 |
❌ | ❌ |
| 扩展到下一个 target | ✅ 改 TARGET = 一行(aarch64 / riscv64) |
❌ 每次手工重来 | |
| GCC 版本升级 | ✅ 改 GCC_VER = 一行 |
❌ 全程手工 | |
| 可脚本化进 CI | ✅ config.mak + make |
❌ 不可重复 |
手工三段(旧路线 3)短期最可控但长期最差 —— 不可重复、每次升 GCC 都要手工拼装、无法自动化, 会变成一个只有作者能重建的黑盒。既然选长期最合适,就不能选它。
GCC_VER = 16.1.0
BINUTILS_VER = 2.44
MUSL_VER = 1.2.5
GMP_VER = 6.3.0
MPC_VER = 1.3.1
MPFR_VER = 4.2.2
LINUX_VER = headers-4.19.88-2
TARGET = x86_64-linux-musl
HOST = x86_64-w64-mingw32 # ← canadian:产出的编译器跑在 Windows
COMMON_CONFIG += CFLAGS="-g0 -Os" CXXFLAGS="-g0 -Os -fno-declone-ctor-dtor" LDFLAGS="-s"
# ^^^^^^^^^^^^^^^^^^^^^^ 硬约束,见下这是本期最贵的一个坑 —— 症状离根因隔了三层,值得单独记。
症状:GCC 编到最后链接 gcov-dump.exe 时炸,报 std::string 的移动构造函数 undefined:
ld: libcommon.a(paths-output.o): undefined reference to
`std::__cxx11::basic_string<...>::basic_string(std::__cxx11::basic_string<...>&&)'
第一反应「mingw-cross-gcc payload 的 libstdc++ 坏了」——错的。nm 显示符号在
libstdc++.a 里明明是 T(defined)。真相在 mangled name,demangle 后完全看不出来:
| 侧 | mangled | 含义 |
|---|---|---|
| 引用 | …basic_string…C**4**EOS4_ |
C4 = 统一构造函数(未 clone) |
| 定义 | …C**1**EOS4_ / …C**2**EOS4_ |
C1/C2 = complete / base |
-Os 自动开启 -fdeclone-ctor-dtor,令调用方引用 C4;而 libstdc++.a 只显式实例化了
C1/C2。ELF 平台用符号别名把 C4 接到 C1/C2 上,COFF 没有别名机制 —— 所以这个失配
只在 mingw 上暴露,且只打在 #include <string> 且移动构造 std::string 的 host TU 上。
四行最小复现(任何人都能在 30 秒内自证):
$ x86_64-w64-mingw32-g++ mv.cc # LINK OK
$ x86_64-w64-mingw32-g++ -O2 mv.cc # LINK OK
$ x86_64-w64-mingw32-g++ -Os mv.cc # LINK FAIL ← -Os 是唯一变量
$ x86_64-w64-mingw32-g++ -Os -fno-declone-ctor-dtor mv.cc # LINK OK
为什么 aarch64 那期没踩到:
aarch64-musl-gcc-canadian-cross-rebuild.md用的同样是CFLAGS="-g0 -Os",但那次 HOST 是 aarch64-linux-musl(ELF),符号别名把问题吃掉了。 「同一份 config 换个 HOST 就炸」正是本条必须写进文档的原因。
CXXFLAGS只作用于 host 侧(target 库走CXXFLAGS_FOR_TARGET),因此这个 flag 不影响产物 ABI,可以放心加。
过了上一个坑之后,构建会在 target libstdc++ 再炸一次:
libstdc++-v3/include/atomic:259:7: error: 'constexpr' constructor does not have
empty body [-Wtemplate-body]
make: *** [x86_64-linux-musl/bits/stdc++.h.gch/O2ggnu++0x.gch] Error 1
libstdc++ 会为 -std=gnu++0x(C++11)额外生成一份预编译头。GCC 16 的 <atomic> 里
那个 constexpr 构造函数带非空 body(C++14 起才合法),上游用
#pragma GCC diagnostic ignored "-Wc++14-extensions" 抑制过 —— 但 GCC 15 新增的
-Wtemplate-body 让诊断在模板定义处就触发,pragma 的作用域没覆盖到,于是变成硬 error。
定位方法值得记:不要去猜,直接看已发布的 xim:musl-gcc@16.1.0 payload——
$ find <payload> -name '*.gch' | wc -l
0零个 PCH。已发布的 payload 本来就是 --disable-libstdcxx-pch 构建的。所以这不是
"canadian 特有的新问题",而是两个 payload 的构建配置必须对齐。加上它,与生态既有产物一致。
mcpp 走 modules,PCH 对它零价值,没有任何损失。
前置:x86_64-w64-mingw32-g++ 必须在 PATH —— 即 §3.2 的自举闭环,来自 mcpp 已发布的 mingw-cross-gcc@16.1.0。
原先登记的两个不确定项,在实际跑构建时都有了确定答案:
1. host 侧 gmp/mpfr/mpc/isl —— 不是问题,GCC in-tree 构建自动处理。
litecross/Makefile:160-190 把这些库 symlink 进 GCC 源码树(src_gcc/{gmp,mpc,mpfr,isl}),
走 GCC 的 in-tree 依赖构建 —— 因此它们天然继承 --host=,不需要任何 --with-gmp= 干预。
这很可能就是 issue #55 提问者卡住的地方(他大概率试图分别预编译这些库)。
2. SYSROOT = / 只影响 C 库,C++ 头仍走交叉布局 —— 别被 -print-sysroot 骗了。
canadian 分支设 SYSROOT = /,于是 musl libc / kernel headers 装进 prefix 根
(<prefix>/{include,lib}),-print-sysroot 也确实回答 <prefix>/。但 C++ 头不在那里:
GCC 作为交叉编译器,GPLUSPLUS_INCLUDE_DIR 始终是 <prefix>/<target-triple>/include/c++/<ver>。
实测踩过:按 -print-sysroot 的字面意思把 libstdc++ 放进 <prefix>/include/c++/,编译报
fatal error: iostream: No such file or directory,而文件明明在。真相在 -v 的
ignoring nonexistent directory 列表里:
ignoring nonexistent directory ".../x86_64-linux-musl/include/c++/16.1.0"
^^^^^^^^^^^^^^^^^^ 它要的是这里
最终布局(两种布局混用,这正是反直觉之处):
output-x86_64-w64-mingw32/
├── bin/ (PE: x86_64-linux-musl-g++.exe …) ← host
├── libexec/gcc/…/cc1plus.exe (PE) ← host
├── include/ (musl + kernel headers) ← target, sysroot=/
├── lib/ (musl libc.a, crt*.o) ← target, sysroot=/
├── lib/gcc/x86_64-linux-musl/16.1.0/ (libgcc.a, crtbegin.o …) ← target
└── x86_64-linux-musl/ ← target, 交叉布局
├── include/c++/16.1.0/bits/std.cc
└── lib/libstdc++.a
对 mcpp 无影响:bits/std.cc 落在 <prefix>/<triple>/include/c++/<ver>/,正是
gcc.cppm:75-90 已实现的交叉候选。§1.1 的原判断成立,import std 零改可用。
canadian 构建需要两个现成工具链,mcpp 生态里都已发布:
| 工具链 | 作用 | 来源 |
|---|---|---|
x86_64-w64-mingw32-* |
编 host 侧(driver、cc1plus、binutils → PE) | xim:mingw-cross-gcc@16.1.0 |
x86_64-linux-musl-* |
编 target 侧(libgcc、libstdc++、musl) | xim:musl-gcc@16.1.0 |
target 编译器的版本必须与 GCC_VER 一致(本期同为 16.1.0)。若用 15.1.0 去编 16.1.0 的
libstdc++,module 相关产物会与 driver 不匹配 —— 这是最隐蔽的一类失败,不会在构建期报错。
make 时间戳陷阱(实测踩到):新增
hashes/<tarball>.sha1必须早于已下载的 tarball。Makefile:86的规则是$(SOURCES)/%: hashes/%.sha1,hash 文件更新则视 tarball 过期 → 重新下载 → 撞上 ftpmirror 502 直接失败。先下载后写 hash 的话,补一次touch sources/<tarball>即可。
回退顺序:若上述两点在合理时间内解不掉,回退 crosstool-NG canadian sample
(ct-ng x86_64-w64-mingw32,x86_64-linux-musl,需先建 host 工具链);手工三段仅作最后兜底,
且一旦动用必须在文档里标注为技术债,后续补回 musl-cross-make 路径。
make install 会一路撞在 target libstdc++ 上,连撞四种不同的错:
| # | 症状 | 处理 |
|---|---|---|
| 1 | stdc++.h.gch — constexpr ctor 非空 body |
--disable-libstdcxx-pch |
| 2 | futex.lo / future.lo — 同上,-std=gnu++11 |
CXXFLAGS_FOR_TARGET += -Wno-template-body |
| 3 | string-inst.cc — duplicate explicit instantiation |
-fpermissive |
| 4 | format.lo — instantiating erroneous template |
还有下一个… |
打到第四个就该停下来问:为什么要用已发布的 GCC 16.1.0 去重新编译 GCC 16.1.0 自己的 libstdc++?
canadian 构建的职责是产出 host 侧二进制。target 库是纯 target 代码,与谁编译它无关;
而 xim:musl-gcc@16.1.0 payload 里那一份是同版本(实测 BASE-VER 与 DATESTAMP 都是
16.1.0 / 20260430)、经完整 CI 验证的权威副本。canadian 模式设 SYSROOT = / 本就假设 target
树已存在 —— 复用它才是顺着设计走,不是绕过困难。
于是构建收敛为三步(实测可复现):
# 1) host 侧:driver / cc1plus / binutils → PE
cd <build>/x86_64-w64-mingw32/x86_64-linux-musl/obj_gcc
make DESTDIR=$OUT install-gcc # bin/*.exe, libexec/**/cc1plus.exe
make DESTDIR=$OUT install-target-libgcc # lib/gcc/<triple>/16.1.0/{libgcc.a,crt*.o}
# musl libc 已由前面的 `make install` 装进 $OUT/{include,lib}
# 2) target 侧 C++:从权威 payload 复用(注意是交叉布局,见上文待解点 2)
cp -a $PAYLOAD/x86_64-linux-musl/include $OUT/x86_64-linux-musl/
cp -a $PAYLOAD/x86_64-linux-musl/lib/. $OUT/x86_64-linux-musl/lib/
# 3) DLL 部署,见下PE 没有 RPATH,DLL 只从 exe 同目录 / PATH 解析。产出的 host 二进制唯一的非系统依赖就是它
(其余 KERNEL32/ADVAPI32/msvcrt/WS2_32 都是 Windows 自带)。漏了就是启动即
STATUS_DLL_NOT_FOUND,而且不止 bin/:
bin/ x86_64-linux-musl-g++.exe …
libexec/gcc/x86_64-linux-musl/16.1.0/ cc1.exe, cc1plus.exe
x86_64-linux-musl/bin/ as.exe, ld.exe ← 最容易漏的一个
实测就是漏了第三个:g++ 能跑、--version 正常,一编译就在汇编阶段炸 as.exe。
for d in $(find "$OUT" -name "*.exe" -printf "%h\n" | sort -u); do cp -n "$DLL" "$d/"; done更优但更贵的替代:给 host 侧加
LDFLAGS=-static彻底消除 DLL 依赖(winlibs 就这么做)。 需要重新 configure + 重链接,本期未做;登记为 follow-up。
P0 已完成(2026-08-03),六条全绿。 实测记录见下。验证在本机用 wine 驱动 PE 编译器 完成 —— 不需要 Windows 机器就能跑完判据 1–6,这条对后续维护很省事。
判据 1 bin/x86_64-linux-musl-g++.exe: PE32+ executable (console) x86-64, for MS Windows ✅ 判据 2 wine …g++.exe --version → x86_64-linux-musl-g++.exe (GCC) 16.1.0 ✅ 判据 3 x86_64-linux-musl/include/c++/16.1.0/bits/std.cc 存在 ✅ 判据 4 hello → ELF 64-bit LSB executable, x86-64, statically linked ✅ 判据 5 本机(真 Linux)执行 → "windows builds linux";readelf 无 PT_INTERP ✅ 判据 6 import std 编译 + 链接 + 执行 → "import std works" ✅
必须全部满足才算 P0 出口 —— 逐条断言,不要只看「编译通过」:
file bin/x86_64-linux-musl-g++.exe→PE32+ executable ... x86-64- Windows(或 wine)上
x86_64-linux-musl-g++.exe -v正常输出,--version报 16.1.0 x86_64-linux-musl/include/c++/16.1.0/bits/std.cc存在 —— 没有它import std全盘不可用, 这是 mcpp 的硬 floor,不是可选项hello.cpp→ ELF:file a.out报ELF 64-bit LSB executable, x86-64, statically linked- 该 ELF 在真 Linux 上能跑(不是「链接成功」——见 [[e2e-inherit-toolchain-corrupts-real-payloads]]: 产物存在 ≠ 产物正确)
import std的 hello 能编能跑(判据 3 的运行期闭环)
与所有工具链 payload 同模式:GLOBAL → github,CN → gitcode 双端镜像。
必须 strip:[[gitcode-mirror-upload-shaping]] 记着上一次的教训 —— 未 strip 的载荷 34.81MB → strip 后 4.62MB,
而 file.gitcode.com 入境逐连接限速 ~12KB/s,不 strip 上传必挂。canadian cross 产物同样要 strip
(COMMON_CONFIG += CFLAGS="-g0 -Os" 已在 §3.3 的 config 里)。
symlink 处理(本机实测,风险已量化):
xim-x-musl-gcc/15.1.0/ symlinks: 14 files: 2369
14 个,全是 .so 别名(libstdc++.so → .so.6、ld-musl-x86_64.so.1)和 x86_64-linux-musl-cc。
Windows 无 Developer Mode 的普通用户无权创建 symlink,而 mcpp 安装以普通用户身份运行。
修法:打包时 tar --dereference(或 zip 默认行为即存副本)。静态链接场景下 .so 别名本就用不到,
-cc 别名可留实体副本。14 个文件的代价换掉一整类 Windows 权限故障,直接做,不要留 symlink。
这条是 §1 之外唯一的 Windows 特有约束,且已从「可能是 blocker」实测降级为「打包参数一行」。
新建 pkgs/x/x86_64-linux-musl-gcc.lua。骨架取自 mingw-cross-gcc.lua(cross 语义)+ mingw-gcc.lua
(Windows host 的 dir /b 枚举)+ musl-gcc.lua(一包多 asset):
package = {
spec = "1",
name = "x86_64-linux-musl-gcc",
description = "GCC cross toolchain targeting x86_64-linux-musl (musl, static)",
-- ...
xpm = {
-- Linux host: 非 x86_64 主机交叉编 x86_64(x86_64 主机走 native `musl-gcc`)
linux = { ["latest"] = { ref = "16.1.0" },
["16.1.0"] = { url = "XLINGS_RES", sha256 = { x86_64 = "..." } } },
-- Windows host: 本期新增的 canadian-cross 产物
windows = { ["latest"] = { ref = "16.1.0" },
["16.1.0"] = { url = "XLINGS_RES", sha256 = { x86_64 = "..." } } },
},
}install() / config() 需按 host 分叉枚举 bin(ls -1 / dir /b),并在 Windows 分支跳过 .dll
(mingw-gcc.lua:is_registerable_bin 现成)。
无 deps:musl 交叉链自包含。特别注意 不要照抄 musl-gcc.lua 的 deps = {"xim:patchelf@0.18.0"} ——
那是给 Linux-hosted ELF 前端做 PT_INTERP 重定位的,PE 前端没有 interp,patchelf 在 Windows 上也不存在。
遵循 [[release-publish-pipeline]],且索引先于代码:
- spike 出工具链 → strip → 打包(§4.1)
- 上传 xlings-res 双端(github + gitcode),真 GET + sha256 独立核验
- 提交 xim-pkgindex 描述符 → 等索引传播(CDN 滞后可达 ~40min,见 [[release-bootstrap-pin-two-groups]])
xlings install x86_64-linux-musl-gcc@16.1.0在真 Windows 上验证可装可跑- 才开始合 mcpp 侧 PR
顺序反了会让 mcpp 的 CI 去装不存在的包,全线假红。
| # | 文件:行 | 改动 | 类型 |
|---|---|---|---|
| 1 | platform/common.cppm:110 + build/flags.cppm:539 |
supports_full_static 由 host 常量改为 target 谓词;build_program.cppm:474 保持 host 语义不动 |
bug 修复 B2 |
| 2 | toolchain/registry.cppm:228 |
musl 前端候选补 .exe 变体(.exe 在前) |
bug 修复 B1 |
| 3 | toolchain/registry.cppm |
新增 host_can_serve(const Triple&) 单一谓词 |
重构(收敛两处推导) |
| 4 | toolchain/lifecycle.cppm:371-374 |
installable_here 改为调用 #3 |
门 |
| 5 | toolchain/registry.cppm:308-313 |
Windows 分支补列 x86_64-linux-musl-gcc |
门 |
| 6 | toolchain/registry.cppm:334-350 |
archive_tool 的 musl 分支在 Windows 上试 <triple>-ar.exe |
适配 |
| 7 | doctor.cppm:134-148 |
Windows 分支补一行 linux-musl 交叉链探测(仿 mingw 那段) | 可观测性 |
| 8 | docs/03-toolchains.md |
Targets 表补 Windows host 行;新增一节说明该组合 | 文档 |
零改确认(审计过,勿动):linkmodel.cppm、gcc.cppm(bits/std.cc 定位)、dialect.cppm、
triple.cppm(target 词汇表)、lifecycle.cppm:561(sysroot 依赖门)、prepare.cppm(首运行默认)。
这是 wine 那一维的镜像问题,但更麻烦:Linux 上有 wine 能跑 PE,Windows 上跑 ELF 没有对等物。
三个选项:
| 方案 | 说明 | 取舍 |
|---|---|---|
| 跨 job artifact 回传(推荐) | Windows job 交叉构建 → upload-artifact → Linux job download-artifact 并真实执行 |
真实执行,无模拟层;代价是两个 job + 一次 artifact 往返 |
| Windows runner 内 WSL | wsl bash -c ./a.out |
GitHub windows runner 的 WSL 可用性/成本不稳定 |
| 只做静态断言 | file 报 ELF + readelf 检查 |
不够 —— 见 [[e2e-inherit-toolchain-corrupts-real-payloads]],链接成功 ≠ 能跑 |
取跨 job artifact 回传。产物是全静态 musl ELF(无 PT_INTERP),任何 Linux 上都能直接执行,回传后
ubuntu-24.04 裸跑即可,不需要 qemu。
在 cross-build-test.yml 新增(结构照抄 mingw-cross-wine job,:194-289):
windows-host-linux-cross:
name: windows→linux cross (build on windows, run on linux)
runs-on: windows-2022
steps:
- Bootstrap mcpp via xlings(Windows 版,仿 ci-windows.yml)
- mcpp toolchain install gcc 16.1.0 --target x86_64-linux-musl
- mcpp build --target x86_64-linux-musl # 被测:mcpp 自身
- upload-artifact: target/**/mcpp
windows-host-linux-cross-run:
needs: windows-host-linux-cross
runs-on: ubuntu-24.04
steps:
- download-artifact
- file ./mcpp | grep -q "statically linked" # 断言 B2 已修
- chmod +x ./mcpp && ./mcpp --version # 真实执行file ... | grep -q "statically linked" 这一行就是 B2 的回归测试 —— B2 未修时它必红。
测试自伤坑:
! cmd | grep在 errexit 下被豁免,永不失败(见 [[build-mcpp-helper-self-containment]])。 断言一律写成正向grep -q,不要写! ... | grep。
cross-build-test.yml 头部的注释矩阵是「mcpp 支持哪些交叉组合」的单一真源,必须同步加行:
# x86_64-linux-musl | gcc@16.1.0 (canadian) | windows→linux | linux job
并把 "Planned cross rows" 里的 clang cross 那条保留(本方案不碰 clang 轴,见 §7)。
CI 首次真正跑通交叉构建时暴露的:产物叫 mcpp.exe,而它是个 ELF。
plan.cppm:200-213 的 target_output():
return std::filesystem::path("bin") /
std::format("{}{}", t.name, mcpp::platform::exe_suffix); // ← host 常量exe_suffix / lib_prefix / static_lib_ext / shared_lib_ext 四个都是 host 常量
(platform/common.cppm:18-33),却用来命名 target 产物。与 B2 同构。
初版写的是「对称地错:Linux→Windows 产出
mcpp却是 PE」。后半句是错的 —— 实测 Linux 主机交叉到x86_64-windows-gnu,产物就是b3probe.exe(PE32+), 因为 mingw 的 GCC driver 自己会补.exe,mcpp 从未参与这个决定。真正的症状是另一件事,而且更实际:ninja 声明的输出是
bin/foo,GCC 写出bin/foo.exe, 声明的那个文件从来不存在 ⇒ 每次mcpp build都重跑链接边。实测连续两次构建 产物 mtime 会变。也就是说 Linux→Windows 是功能缺陷(增量构建失效), Windows→Linux 才只是「ELF 顶着.exe」的观感问题。
完整分析、修复方案与验证判据见专项文档:
2026-08-03-b3-target-aware-artifact-naming.md
要点摘录:
- 改动面只有
src/build/plan.cppm一个文件;其余 15 处exe_suffix引用都是找主机上的ninja/xlings/clang++等,是正确的 host 语义,不要动 - 正确的命名是 (os, env) 二元函数:
windows-gnu用 GNU 约定(libfoo.a),windows-msvc才是foo.lib—— 现行_WIN32分支写死后者,Windows 主机上用 mingw 构建静态库今天就已经命名错了,与交叉无关的存量缺陷 - e2e/CI 基本不用改(与本节初版的担心相反):
102_mingw_cross_wine.sh找的是真实产物 而非 ninja 声明,改前改后都匹配 runtime_aliases_for_target()(plan.cppm:215)依赖target_output()比对 soname, 必须一起改并补断言 —— 见 [[soname-alias-explicit-ninja-goals]]
为什么当期没修:发现时 #339 已进入 CI 验证阶段,把一个尚未查清真实形态的改动塞进去, 会毁掉 §1.3 定下的「提交分层、失败可归因」。事后看这个决定是对的 —— 初版对症状的描述 本身就是错的,当场改只会改错方向。
- 不碰 clang cross 轴。
cross-build-test.yml:38-40记着 "mcpp does not yet inject-target <triple>+ a cross sysroot"。让一个 clang 覆盖所有 target 长期更优(不必每个 triple 造一套编译器),但那是 另一根轴:mcpp 今天从不发-target给编译器,targetTriple只用于选 payload / 选目录 / 算缓存键。 改它要 touch flags + linkmodel + stdmod + probe,比本方案大一个量级。本期不动,登记为 follow。 - 不做 Windows → linux-gnu。glibc 目标依赖
xim:glibc/xim:linux-headers两个 sysroot payload, 它们没有 Windows 分发。musl 静态覆盖了「在 Windows 上产可分发 Linux 二进制」的主要诉求。 - 不做 Windows → aarch64-linux-musl。同一机制的第二个 triple,等 x86_64 跑通后按 §5#3 的单一谓词加一行即可。
- 不改首次运行默认值。Windows 主机的默认仍是 MSVC / windows-gnu,本方案纯 opt-in。 参见 [[issue331-windows-msvc-triage]]:裸 Windows 的问题是默认值选错,不是缺工具链 —— 别再往默认路径加分支。
三期严格串行 —— 顺序反了会让 CI 去装不存在的包,全线假红(§4.3)。
| 期 | 位置 | 内容 | 出口判据 |
|---|---|---|---|
| P0 | mcpp 仓库外 | §3.3 spike:musl-cross-make HOST=x86_64-w64-mingw32 |
§3.4 六条判据全绿 |
| P1 | xlings-res + xim-pkgindex | §4 strip → 打包(去 symlink)→ 双端镜像 → 描述符 | 真 Windows 上 xlings install x86_64-linux-musl-gcc@16.1.0 可装可跑 |
| P2 | mcpp 单 PR | §5 全部 8 项改动 + §6 CI 双 job,按 §1.3 的五段 commit 分层 | Windows 上 mcpp toolchain list 列出 linux-musl 行;交叉产物在 Linux job 上真实执行通过 |
P0 是唯一的高不确定性环节,且完全在 mcpp 代码之外 —— 它失败不会污染仓库。
不要因为 B1/B2「今天就已存在」而提前单独修。它们缺少能证伪的测试环境:在没有 Windows→Linux 这条路径 之前,B2 的回归断言无处可挂,改了也只是「看起来对」。等 P0/P1 就位,断言才有落点 —— 这正是 §1.3 commit 1 先行的意义。
| # | 问题 | 决定 | 依据 |
|---|---|---|---|
| 1 | 构建路线 | musl-cross-make + HOST=(选长期最合适,非最省事) |
§3.3:litecross/Makefile:87-125 三条源码证据;与生态同工具、扩展/升级各改一行 |
| 2 | host_can_serve 谓词收敛 |
本期做 | §1.2:避免下一个 target 再踩两处推导 |
| 3 | B1/B2 交付形态 | 与主体合并为单 PR | §1.3:用五段 commit 分层保住归因能力,commit 1 先证明断言会红 |
| 4 | 包形态 | 一包双 host asset | §2:本机索引实测 20+ 包(ninja/cmake/git/rust)已用双段,xlings 原生支持 |
决定 1 的连带效果:原「路线 3 手工三段」被取消而非降级为并行选项 —— 其核心优点(target 库复用)
已被证实是 musl-cross-make canadian 模式的既有行为(SYSROOT = / + MUSL_VARS = 清空),
不再有独立价值。它只保留为最后兜底,且动用即记技术债。