You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(toolchain): build Linux binaries on Windows (x86_64-linux-musl canadian cross) (#339)
* test(toolchain): pin that full-static is a TARGET property, not a host one
Adds the regression assertions for a bug that exists on HEAD today: mcpp
decides whether to emit `-static` from `mcpp::platform::supports_full_static`,
a host constant defined as `is_linux`. On a Linux host that coincides with the
right answer for every Linux target, so nothing ever caught it — it only bites
once a non-Linux host cross-compiles to Linux, where `-static` silently
vanishes and the musl targets lose the very property they exist for.
`target_supports_full_static` takes the host capability as an explicit
parameter so the decision is pinnable from any host: passing false models a
Windows/macOS host. The implementation added here is a deliberate STUB
reproducing the old behaviour, so these tests go RED now and the next commit
turns them green — proving the assertions actually discriminate rather than
passing vacuously.
Verified red:
[ FAILED ] TargetSupportsFullStatic.LinuxTargetFromNonLinuxHost
[ FAILED ] TargetSupportsFullStatic.MacosTargetNeverStatic
[ FAILED ] TargetSupportsFullStatic.PeTargetsDeferToContractTable
3 FAILED TESTS
Refs .agents/docs/2026-08-03-windows-host-linux-cross-design.md §1.3 (B2)
* fix(build): decide full-static from the TARGET, not from the build host
`supports_full_static` is a host constant (`is_linux`) describing what THIS
machine's own binaries can be. flags.cppm read it to decide whether the
ARTIFACT gets `-static` — a different question with a different answer the
moment host != target.
On a Linux host the two coincide for every Linux target, which is why this
survived: it is invisible until a non-Linux host cross-compiles to Linux.
There, `-static` was silently dropped and `x86_64-linux-musl` — a target whose
entire reason to exist is a portable, fully static ELF — produced something
else. No error, no warning; the flag just was not there.
The predicate now reads the parsed triple:
- empty triple → host target, so the host capability IS the answer
- PE targets → false; their `-static` comes from the C++ runtime
distribution contract (dist::Format::Pe), and answering
true here would make both mechanisms emit it
- macOS → false; libSystem must stay dynamic
- linux → true, glibc or musl, native or cross
- unparseable → fall back to the host answer rather than guess
Every path that works today is bit-for-bit unchanged: on a Linux host every
branch returns exactly what the old constant returned. Only the previously
broken host!=target case changes.
Turns the previous commit's assertions green:
[==========] 6 tests from 1 test suite ran.
[ PASSED ] 6 tests.
Refs .agents/docs/2026-08-03-windows-host-linux-cross-design.md §1.3 (B2)
* fix(toolchain): resolve .exe tool names on Windows; collapse the host/target gate
Two changes to one code region, which is why they share a commit — splitting
them would leave a middle state that does not compile.
B1 — the frontend was unfindable on a Windows host.
musl payload candidates were { "<triple>-g++", "g++" }, resolved with
filesystem::exists. On Windows the file is `<triple>-g++.exe`, so exists()
said no and a payload that installed perfectly was then unusable. The mingw
branch has carried the .exe spelling since it shipped; this branch never did,
because nothing had ever installed a musl payload on a Windows host. Same
omission in archive_tool's musl branch (`<triple>-ar`), fixed alongside.
.exe is listed first: on a case-insensitive filesystem both spellings match
and the executable is the one we want.
host_can_serve — one derivation instead of two.
"Can this host serve that target" was computed independently in two places:
registry.cppm picking the xim payload, and lifecycle.cppm deciding whether
`toolchain list` may show a target as available. They had already drifted —
the payload side would resolve a windows-hosted musl package that the
availability side declared impossible. The predicate now lives once, in
registry.cppm next to the payload resolution it has to agree with, and
lifecycle calls it.
It also gains the case this series exists for: a non-Linux host may serve a
Linux target when the payload is self-contained (musl) and built for that
host's own arch. Deliberately narrow — the canadian-cross payload is built
per host arch, and macOS has no Linux-targeting payload at all, so both stay
unserviceable rather than failing at install time.
available_toolchain_indexes now lists the windows-hosted cross under the same
name to_xim_package() derives (`<triple>-gcc`), so the Available listing and
the install path cannot disagree.
Refs .agents/docs/2026-08-03-windows-host-linux-cross-design.md §1.2, §1.3 (B1)
* feat(toolchain): surface the windows-hosted linux cross in doctor and docs
doctor gains the mirror of its existing mingw probe: on a Windows host it now
also reports whether the linux-musl cross payload is installed. Same shape,
same optional-not-an-error wording, and it derives the package name the same
way to_xim_package() does (`<triple>-gcc`) so the two cannot disagree.
docs/03-toolchains.md gains the section this feature is for, written as the
mirror of "Windows PE via MinGW-w64": the command is spelled identically on
both hosts because cross is not a name in mcpp, just host != target. It also
states the two limits explicitly rather than leaving users to discover them —
linux-gnu from Windows is unsupported (glibc needs the sysroot payloads, which
are Linux-only), and cross-arch from Windows is unsupported (the canadian-cross
payload is built per host arch).
* 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.
* release: 2026.8.3.2
Bumps the BUILDING pair only (mcpp.toml + fingerprint.cppm). The bootstrap pin
in .xlings.json stays at 2026.8.3.1 on purpose — it is the self-hosting starting
point, not a mirror of the version being built. Bumping it in the same commit
would point every CI job at a release that does not exist yet.
* ci(cross): scope the artefact search to the target tree; record B3
The cross-build itself succeeded on the first real Windows run — 'Finished
release [optimized] in 119.09s', resolving through
x86_64-linux-musl-g++.exe, which is B1's fix working. The job still failed,
for two reasons worth separating:
1. target/ holds the host build from the previous step alongside the cross
build, and both are named mcpp*. The search now starts at
target/x86_64-linux-musl/ so it cannot pick up the wrong one.
2. The artefact is named mcpp.exe despite being an ELF. plan.cppm's
target_output() spells the suffix from mcpp::platform::exe_suffix — a HOST
constant — so this is the same host-decides-target confusion as B2, and it
is symmetric: a Linux→Windows cross produces a PE with no .exe today.
B3 is filed in the design doc (§6.5) rather than fixed here. Renaming build
outputs is a behaviour change that reaches the mingw e2e, the release packaging
paths and any user script, and neither direction is actually broken today —
folding a regression-prone rename into this PR would defeat the layered-commit
discipline the series was built around. The job matches both spellings so it is
correct before and after that fix.
Also prints the tree on failure, so the next person does not have to fetch the
job log to find out what was actually produced.
* test(toolchain): make musl frontend assertions host-aware
B1 changed the musl frontend candidate list on Windows hosts (.exe first), and
these two assertions pinned the bare spelling as front(). They pass on Linux
either way, which is exactly why the break only showed up in Windows CI — the
same blind spot that let B1 itself survive.
Adds expected_musl_frontend() next to the existing expected_musl_xim() helper,
mirroring how the mingw test already handles the host split. The helper states
why .exe comes first rather than leaving a bare constant.
-**`-static` 由构建主机而非目标决定,导致 Windows→Linux 交叉产物根本不是静态的。**`supports_full_static` 是一个**主机**常量(`is_linux`),描述的是「这台机器自己的二进制能否全静态」;`flags.cppm` 却拿它来决定**产物**要不要 `-static`。在 Linux 主机上这两个问题对所有 Linux 目标恰好同解,所以它一直没被发现 —— 只有当非 Linux 主机交叉编译到 Linux 时才会现形:`-static` 被静默丢弃,而 `x86_64-linux-musl` 这个目标存在的全部理由就是产出可移植的静态 ELF。没有报错,没有警告,那个 flag 就是不在。
0 commit comments