Skip to content

Latest commit

 

History

History
722 lines (535 loc) · 37.5 KB

File metadata and controls

722 lines (535 loc) · 37.5 KB

Windows → Linux 交叉工具链(路径 A:canadian-cross payload)— 设计方案

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]]

0. 一句话

补上 host ≠ target最后一个象限:Windows 主机产 Linux ELF。后端零新增(GNU 方言 + ELF 链接模型早已按 target 求值),工作量集中在四处:A. 解除三处 host 门B. 修两个 host 常量污染 target 决策的真 bugC. 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)。


1. 现状评估(审计结论,file:line 落地)

方法论警告:mingw-linux-cross-windows-design.md 里点名的 mingw_wrong_host() 在 HEAD 上已不存在 (那期做完即删)。本节所有坐标以 f694e13 为准;实施前请重新 grep 一遍,别信旧文档的符号名。 —— 参见 [[v0099-feature-forwarding-238-230]] 同款教训。

1.1 已就位的抽象缝(交叉直接复用,零改)

抽象 位置 对本方案的意义
链接模型按 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)的结论一致。

1.2 需要解除的 host 门(Part A 的对象)

三处,全是 is_linux 硬编码:

  1. 可安装性门lifecycle.cppm:371-374

    if (t.os == "linux")
        return mcpp::platform::is_linux && (t.is_musl() || t.arch == hostT.arch);

    Linux 目标一律要求 Linux 主机 → toolchain list 的 Targets 块在 Windows 上不列 linux 行。

  2. payload 选择registry.cppm:225-227

    bool 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)。

  3. Available 索引 host-splitregistry.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.cppminstallable_here 改为直接调用它,自身不再做任何 is_linux 判断。 这是本方案唯一的重构项,收益是下一个 target(aarch64-linux-musl、riscv64)只改一处。

1.3 两个真 bug(host 常量污染 target 决策,Part B)

这两条都不是「缺功能」,是现存缺陷,只是今天没有 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:110flags.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-musldefaultStatic = 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:474muslStaticHelper —— 那里是 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]]:没先验证会红的测试,等于没有测试。


2. 身份与命名

遵循 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:227t.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,xpmlinuxwindows 两段,XLINGS_RES 按 host 选 asset。包名与 registry.cppm:227t.str()+"-gcc" 天然一致 → 零代码改动
两包 host-split 已排除。需在 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 的第一候选路径上。


3. 工具链构建(canadian-cross)

3.1 三元组

build  = x86_64-linux-gnu        (CI runner / 本机)
host   = x86_64-w64-mingw32      (产出的编译器跑在 Windows)
target = x86_64-linux-musl       (产出的编译器为 Linux 生成代码)

三者互不相同 = 教科书定义的 canadian cross。

3.2 自举闭环:host 编译器 mcpp 自己已经有了

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。

这是个漂亮的自举:上一期的产物直接成为这一期的构建输入,不引入任何新的外部依赖。

3.3 构建路线:musl-cross-make + HOST=(定稿)

取 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 = 一行 ⚠️ 重新 menuconfig ❌ 全程手工
可脚本化进 CI ✅ config.mak + make ⚠️ 可行但重 ❌ 不可重复

手工三段(旧路线 3)短期最可控但长期最差 —— 不可重复、每次升 GCC 都要手工拼装、无法自动化, 会变成一个只有作者能重建的黑盒。既然选长期最合适,就不能选它。

config.mak(实测可用)

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"
#                                                    ^^^^^^^^^^^^^^^^^^^^^^ 硬约束,见下

⚠️ -Os 在 mingw host 上必须配 -fno-declone-ctor-dtor(实测,踩过)

这是本期最贵的一个坑 —— 症状离根因隔了三层,值得单独记。

症状: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,可以放心加。

⚠️ 必须 --disable-libstdcxx-pch(实测,踩过)

过了上一个坑之后,构建会在 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

待解点 —— 实施中已全部解答(2026-08-03 实测)

原先登记的两个不确定项,在实际跑构建时都有了确定答案:

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,而文件明明在。真相在 -vignoring 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 零改可用。

实施前置(实测确认,必须两者同时在 PATH)

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 路径。

3.4 spike 的成功判据

⚠️ target libstdc++ 不要重建 —— 从同版本 payload 复用(实测,撞了四次)

make install 会一路撞在 target libstdc++ 上,连撞四种不同的错:

# 症状 处理
1 stdc++.h.gchconstexpr 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-VERDATESTAMP 都是 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 部署,见下

⚠️ libwinpthread-1.dll 要部署到每个含 .exe 的目录

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 出口 —— 逐条断言,不要只看「编译通过」:

  1. file bin/x86_64-linux-musl-g++.exePE32+ executable ... x86-64
  2. Windows(或 wine)上 x86_64-linux-musl-g++.exe -v 正常输出,--version 报 16.1.0
  3. x86_64-linux-musl/include/c++/16.1.0/bits/std.cc 存在 —— 没有它 import std 全盘不可用, 这是 mcpp 的硬 floor,不是可选项
  4. hello.cpp → ELF:file a.outELF 64-bit LSB executable, x86-64, statically linked
  5. 该 ELF 在真 Linux 上能跑(不是「链接成功」——见 [[e2e-inherit-toolchain-corrupts-real-payloads]]: 产物存在 ≠ 产物正确)
  6. import std 的 hello 能编能跑(判据 3 的运行期闭环)

4. 分发

4.1 xlings-res 镜像

与所有工具链 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.6ld-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」实测降级为「打包参数一行」。

4.2 xim-pkgindex 描述符

新建 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.luadeps = {"xim:patchelf@0.18.0"} —— 那是给 Linux-hosted ELF 前端做 PT_INTERP 重定位的,PE 前端没有 interp,patchelf 在 Windows 上也不存在。

4.3 发布顺序

遵循 [[release-publish-pipeline]],且索引先于代码:

  1. spike 出工具链 → strip → 打包(§4.1)
  2. 上传 xlings-res 双端(github + gitcode),真 GET + sha256 独立核验
  3. 提交 xim-pkgindex 描述符 → 等索引传播(CDN 滞后可达 ~40min,见 [[release-bootstrap-pin-two-groups]])
  4. xlings install x86_64-linux-musl-gcc@16.1.0真 Windows 上验证可装可跑
  5. 才开始合 mcpp 侧 PR

顺序反了会让 mcpp 的 CI 去装不存在的包,全线假红。


5. mcpp 侧改动清单

# 文件:行 改动 类型
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.cppmgcc.cppm(bits/std.cc 定位)、dialect.cppmtriple.cppm(target 词汇表)、lifecycle.cppm:561(sysroot 依赖门)、prepare.cppm(首运行默认)。


6. CI 验证

6.1 核心难题:Windows runner 产出的 ELF,自己跑不了

这是 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。

6.2 新 job 骨架

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

6.3 矩阵文档同步

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)。


6.5 B3 —— 同一类问题的第三例(本期发现,未修,登记 follow-up)

CI 首次真正跑通交叉构建时暴露的:产物叫 mcpp.exe,而它是个 ELF

plan.cppm:200-213target_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 定下的「提交分层、失败可归因」。事后看这个决定是对的 —— 初版对症状的描述 本身就是错的,当场改只会改错方向。


7. 边界:本方案不做什么

  • 不碰 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 的问题是默认值选错,不是缺工具链 —— 别再往默认路径加分支。

8. 分期

三期严格串行 —— 顺序反了会让 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 先行的意义。


9. 已决事项(2026-08-03 review 拍板)

# 问题 决定 依据
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 = 清空), 不再有独立价值。它只保留为最后兜底,且动用即记技术债。