Skip to content

Commit b165890

Browse files
authored
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.
1 parent f694e13 commit b165890

13 files changed

Lines changed: 1130 additions & 20 deletions

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

Lines changed: 707 additions & 0 deletions
Large diffs are not rendered by default.

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

Lines changed: 124 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,114 @@ 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+
# Scope the search to the TARGET's output tree — target/ also holds the
344+
# host build from the previous step, and both are named "mcpp*".
345+
#
346+
# The artefact carries a `.exe` suffix even though it is an ELF:
347+
# plan.cppm's target_output() spells the suffix from
348+
# mcpp::platform::exe_suffix, a HOST constant. That is the same
349+
# host-decides-target confusion as B2, and it is symmetric — a
350+
# Linux→Windows cross produces a PE with no `.exe` today. Renaming the
351+
# output is a behaviour change that would touch the mingw e2e and any
352+
# user script, so it is filed as follow-up rather than folded in here;
353+
# match both spellings so this job is correct either way.
354+
OUT=$(find target/x86_64-linux-musl -type f -path "*/bin/*" \
355+
\( -name "mcpp" -o -name "mcpp.exe" \) -printf "%T@ %p\n" \
356+
| sort -rn | head -1 | cut -d" " -f2-)
357+
if [ -z "$OUT" ]; then
358+
echo "FAIL: no cross artefact produced; tree was:"
359+
find target/x86_64-linux-musl -type f -path "*/bin/*" | head -20
360+
exit 1
361+
fi
362+
cp "$OUT" mcpp-linux-musl
363+
ls -la mcpp-linux-musl
364+
365+
- uses: actions/upload-artifact@v4
366+
with:
367+
name: mcpp-x86_64-linux-musl-from-windows
368+
path: mcpp-linux-musl
369+
retention-days: 1
370+
371+
windows-host-linux-cross-run:
372+
name: windows→linux artefact really runs (linux)
373+
needs: windows-host-linux-cross
374+
runs-on: ubuntu-24.04
375+
timeout-minutes: 10
376+
steps:
377+
- uses: actions/download-artifact@v4
378+
with:
379+
name: mcpp-x86_64-linux-musl-from-windows
380+
381+
- name: Assert it is a static ELF, then run it
382+
run: |
383+
set -euo pipefail
384+
chmod +x mcpp-linux-musl
385+
file mcpp-linux-musl
386+
387+
# B2 regression gate (design §1.3): before the fix, `-static` was
388+
# decided by a HOST constant (`supports_full_static = is_linux`), so
389+
# a Windows host emitted a NON-static binary here. Written as a
390+
# positive `grep -q` on purpose: `! cmd | grep` is exempt from
391+
# errexit and can never fail (see build-mcpp-helper-self-containment).
392+
file mcpp-linux-musl | grep -q "ELF 64-bit LSB"
393+
file mcpp-linux-musl | grep -q "x86-64"
394+
file mcpp-linux-musl | grep -q "statically linked"
395+
396+
# A static musl ELF has no PT_INTERP at all — the stronger form of
397+
# the same claim, and independent of `file`'s wording.
398+
readelf -l mcpp-linux-musl > hdrs.txt
399+
if grep -q "INTERP" hdrs.txt; then
400+
echo "FAIL: artefact has a PT_INTERP segment — not statically linked"
401+
grep -A2 "INTERP" hdrs.txt
402+
exit 1
403+
fi
404+
405+
# Linked ≠ runs. This is the whole point of the second job.
406+
./mcpp-linux-musl --version
407+
./mcpp-linux-musl --help > /dev/null
408+
echo "OK: windows-built linux artefact executes natively"

CHANGELOG.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,32 @@
33
> 本文件追踪 `mcpp-community/mcpp` 公开仓的版本演进。
44
> 格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)
55
6+
## [2026.8.3.2] — 2026-08-03
7+
8+
### 新增
9+
10+
- **在 Windows 上构建 Linux 程序(`--target x86_64-linux-musl`)。** 补上 `host ≠ target` 最后一个空象限:一台 Windows 机器直接产出**完全静态的 Linux ELF**,不需要 WSL、不需要容器、不往系统里装任何东西。
11+
12+
```bash
13+
mcpp build --target x86_64-linux-musl # 在 Windows 与 Linux 上拼写完全相同
14+
```
15+
16+
命令两边一字不差,因为「cross」在 mcpp 里从来不是一个名字,只是 `host ≠ target` 这个关系的取值。由谁来服务这个目标是自动解析的:Linux x86_64 主机装原生 `musl-gcc`,Windows 主机装 **canadian cross**(build=`x86_64-linux-gnu` / host=`x86_64-w64-mingw32` / target=`x86_64-linux-musl`)。两者都是 GCC 16.1.0,都带 `bits/std.cc`,所以 `import std` 在哪边都一样能用。产物无 `PT_INTERP`,任何 Linux 发行版上都能跑,与它的 libc 无关。
17+
18+
不支持的两种组合是明确拒绝而非碰运气:Windows`linux-gnu`(glibc 需要 `xim:glibc` / `xim:linux-headers` 两个 sysroot 载荷,只为 Linux 主机发布),以及 Windows → 跨架构(canadian cross 载荷按主机架构构建)。`mcpp toolchain list` 只列当前主机真能装的目标,所以某个目标没出现在 Targets 块里,就是这台机器确实服务不了它。
19+
20+
### 修复
21+
22+
- **`-static` 由构建主机而非目标决定,导致 Windows→Linux 交叉产物根本不是静态的。** `supports_full_static` 是一个**主机**常量(`is_linux`),描述的是「这台机器自己的二进制能否全静态」;`flags.cppm` 却拿它来决定**产物**要不要 `-static`。在 Linux 主机上这两个问题对所有 Linux 目标恰好同解,所以它一直没被发现 —— 只有当非 Linux 主机交叉编译到 Linux 时才会现形:`-static` 被静默丢弃,而 `x86_64-linux-musl` 这个目标存在的全部理由就是产出可移植的静态 ELF。没有报错,没有警告,那个 flag 就是不在。
23+
24+
判据改读解析后的 triple:空 triple(目标即主机)沿用主机答案;PE 目标返回 false,它们的 `-static` 来自 C++ 运行时分发契约,在这里也答 true 会让两套机制都发一遍;macOS 返回 false(libSystem 必须动态);Linux 返回 true。今天所有能工作的路径逐位不变。
25+
26+
- **Windows 主机上交叉工具链的前端永远找不到。** 候选名是 `{ "<triple>-g++", "g++" }`,用 `filesystem::exists` 解析,而 Windows 上的文件叫 `<triple>-g++.exe` —— 载荷装得好好的,然后不可用。`archive_tool` 的 musl 分支(`<triple>-ar`)有同样的遗漏。
27+
28+
### 改进
29+
30+
- **「这台主机能否服务这个目标」收敛为单一判据 `host_can_serve`** 它此前在两处独立推导:`registry.cppm` 选载荷时一次,`lifecycle.cppm` 决定 `toolchain list` 能否把目标标为 `available` 时又一次 —— 而且两者**已经漂移**:载荷侧会解析出一个可用性侧宣称不可能存在的 windows-hosted musl 包。现在判据只有一份,就放在它必须与之一致的载荷解析旁边。
31+
632
## [2026.8.3.1] — 2026-08-03
733

834
### 修复

docs/03-toolchains.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,41 @@ windows = "gcc@16" # gcc family on Windows = MinGW-w64
158158
# legacy value "mingw@16.1.0" keeps working
159159
```
160160

161+
## Linux ELF from Windows (`x86_64-linux-musl`, no WSL required)
162+
163+
The mirror of the section above: a Windows machine producing a **fully static
164+
Linux binary**, with no WSL, no container, and nothing installed system-wide.
165+
166+
```bash
167+
mcpp build --target x86_64-linux-musl # from Windows OR Linux
168+
```
169+
170+
The command is spelled *identically* on both hosts, because "cross" is not a
171+
name in mcpp — it is just the relation `host ≠ target`. Which payload serves
172+
the target is resolved automatically: a Linux x86_64 host installs the native
173+
`musl-gcc`; a Windows host installs a **canadian-cross** GCC
174+
(built `x86_64-linux-gnu` → runs on `x86_64-w64-mingw32` → emits
175+
`x86_64-linux-musl`). Both are GCC 16.1.0 and both ship `bits/std.cc`, so
176+
`import std` works the same either way.
177+
178+
The output is a fully static ELF with no `PT_INTERP` — it runs on any Linux
179+
distribution regardless of its libc, which is exactly why musl is the target
180+
that got wired up first:
181+
182+
```console
183+
$ file mcpp
184+
mcpp: ELF 64-bit LSB executable, x86-64, statically linked, stripped
185+
```
186+
187+
`x86_64-linux-gnu` from Windows is **not** supported: a glibc target needs the
188+
`xim:glibc` and `xim:linux-headers` sysroot payloads, which are published for
189+
Linux hosts only. The musl target is self-contained and needs neither.
190+
191+
Cross-arch from Windows (e.g. `aarch64-linux-musl`) is not available either —
192+
the canadian-cross payload is built per host arch. `mcpp toolchain list` shows
193+
only what the current host can actually install, so if a target is missing from
194+
the Targets block, that host genuinely cannot serve it.
195+
161196
## MSVC (System Toolchain, Windows)
162197

163198
MSVC is different from every other toolchain mcpp manages: it is a **system

mcpp.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "mcpp"
3-
version = "2026.8.3.1"
3+
version = "2026.8.3.2"
44
description = "Modern C++ build & package management tool"
55
license = "Apache-2.0"
66
authors = ["mcpp-community"]

src/build/flags.cppm

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import mcpp.toolchain.detect;
2121
import mcpp.toolchain.dialect;
2222
import mcpp.toolchain.hostflags;
2323
import mcpp.toolchain.linkmodel;
24+
import mcpp.toolchain.model;
2425
import mcpp.toolchain.provider;
2526
import mcpp.toolchain.registry;
2627

@@ -536,7 +537,15 @@ CompileFlags compute_flags(const BuildPlan& plan) {
536537
// Link flags
537538
f.staticStdlib = plan.manifest.buildConfig.staticStdlib;
538539
f.linkage = plan.manifest.buildConfig.linkage;
539-
std::string full_static = (mcpp::platform::supports_full_static && f.linkage == "static") ? " -static" : "";
540+
// Whether the ARTIFACT can be fully static is a property of the target,
541+
// not of this machine. Reading the host constant directly here dropped
542+
// `-static` from every Windows→Linux cross build, silently turning the
543+
// musl targets into something they are not. The host constant is still
544+
// the right answer for a host-target build, so it is threaded in as the
545+
// fallback rather than discarded.
546+
const bool full_static_ok = mcpp::toolchain::target_supports_full_static(
547+
plan.toolchain.targetTriple, mcpp::platform::supports_full_static);
548+
std::string full_static = (full_static_ok && f.linkage == "static") ? " -static" : "";
540549

541550
// ---- C++ runtime distribution contract (issue #336) -------------------
542551
//

src/doctor.cppm

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,31 @@ export int doctor_report() {
147147
if (!any)
148148
ok("mingw not installed (optional — `mcpp toolchain install mingw 16.1.0`)");
149149
}
150+
151+
// The other direction: a windows-hosted cross toolchain that produces
152+
// Linux ELF. Same shape as the mingw probe above; the package is named
153+
// by triple, matching to_xim_package()'s `<triple>-gcc`.
154+
{
155+
auto triple = std::string(mcpp::platform::host_arch) + "-linux-musl";
156+
auto label = std::format("linux cross (xim:{}-gcc)", triple);
157+
mcpp::ui::status("Checking", label);
158+
auto pkgs = mcpp::home::root() / "registry" / "data" / "xpkgs"
159+
/ std::format("xim-x-{}-gcc", triple);
160+
std::error_code ec;
161+
bool any = false;
162+
if (std::filesystem::exists(pkgs, ec)) {
163+
for (auto& v : std::filesystem::directory_iterator(pkgs, ec)) {
164+
if (!v.is_directory(ec)) continue;
165+
ok(std::format("{} {} installed",
166+
triple, v.path().filename().string()));
167+
any = true;
168+
}
169+
}
170+
if (!any)
171+
ok(std::format("{} not installed (optional — "
172+
"`mcpp toolchain install gcc 16.1.0 --target {}`)",
173+
triple, triple));
174+
}
150175
}
151176

152177
mcpp::ui::status("Checking", "std module");

src/toolchain/fingerprint.cppm

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import mcpp.toolchain.detect;
1818

1919
export namespace mcpp::toolchain {
2020

21-
inline constexpr std::string_view MCPP_VERSION = "2026.8.3.1";
21+
inline constexpr std::string_view MCPP_VERSION = "2026.8.3.2";
2222

2323
struct FingerprintInputs {
2424
Toolchain toolchain;

src/toolchain/lifecycle.cppm

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -368,15 +368,12 @@ export int toolchain_list(const mcpp::config::GlobalConfig& cfg) {
368368

369369
// Vocabulary rows not covered by an installed payload. Only list a
370370
// verified target as "available" when this host can actually install it.
371+
//
372+
// The predicate lives in registry.cppm next to the payload resolution it
373+
// must agree with — this used to be a second, independent derivation of
374+
// the same question and the two had already drifted apart.
371375
auto installable_here = [&](const mcpp::toolchain::triple::Triple& t) {
372-
if (t.os == "linux")
373-
return mcpp::platform::is_linux
374-
&& (t.is_musl() || t.arch == hostT.arch);
375-
if (t.is_windows_gnu())
376-
return mcpp::platform::is_linux || mcpp::platform::is_windows;
377-
if (t.os == "windows") return bool(mcpp::platform::is_windows);
378-
if (t.os == "macos") return bool(mcpp::platform::is_macos);
379-
return false;
376+
return mcpp::toolchain::host_can_serve(t);
380377
};
381378
for (auto& info : mcpp::toolchain::triple::known_targets()) {
382379
bool covered = std::any_of(targetRows.begin(), targetRows.end(),

src/toolchain/model.cppm

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,22 @@ bool is_musl_target(const Toolchain& tc);
7676
bool is_msvc_target(const Toolchain& tc);
7777
bool is_mingw_target(const Toolchain& tc);
7878

79+
// Can the artifact we are building be fully statically linked (`-static`)?
80+
//
81+
// This is a property of the TARGET, not of the machine doing the build —
82+
// a Windows host cross-compiling to x86_64-linux-musl still produces a fully
83+
// static ELF. `mcpp::platform::supports_full_static` answers a DIFFERENT
84+
// question ("can THIS machine's own binaries be static"), and using it here
85+
// silently dropped `-static` from every Windows→Linux cross build.
86+
//
87+
// `hostCapability` is threaded in explicitly rather than read from
88+
// mcpp::platform so the decision is testable on any host: passing false
89+
// models a Windows/macOS host. It is only consulted for the host target
90+
// (empty triple), where target *is* host. Callers pass
91+
// mcpp::platform::supports_full_static — keeping that dependency at the call
92+
// site leaves this module free of any platform import.
93+
bool target_supports_full_static(std::string_view targetTriple, bool hostCapability);
94+
7995
struct BmiTraits {
8096
std::string_view bmiDir; // "gcm.cache" | "pcm.cache" | "ifc.cache"
8197
std::string_view bmiExt; // ".gcm" | ".pcm" | ".ifc"
@@ -126,6 +142,31 @@ bool is_mingw_target(const Toolchain& tc) {
126142
return tc.targetTriple.find("mingw32") != std::string::npos;
127143
}
128144

145+
bool target_supports_full_static(std::string_view targetTriple, bool hostCapability) {
146+
// Empty triple means "build for this machine" — target IS host, so the
147+
// host answer is the correct one. This is the only case where the host
148+
// capability legitimately decides.
149+
if (targetTriple.empty()) return hostCapability;
150+
151+
auto t = triple::parse(targetTriple);
152+
// Outside the triple language we have nothing to reason from. Fall back
153+
// to the host answer rather than guessing from a substring — a wrong
154+
// `true` here would emit `-static` at a target that cannot honour it.
155+
if (!t) return hostCapability;
156+
157+
// PE targets get their `-static` from the C++ runtime distribution
158+
// contract (dist::Format::Pe in flags.cppm), never from here. Returning
159+
// false is what keeps the two mechanisms from both emitting the flag.
160+
if (t->is_pe()) return false;
161+
162+
// macOS cannot fully static-link: libSystem must stay dynamic.
163+
if (t->os == "macos") return false;
164+
165+
// Linux ELF — glibc or musl, native or cross. This is the line that was
166+
// previously gated on the HOST being Linux.
167+
return t->os == "linux";
168+
}
169+
129170
BmiTraits bmi_traits(const Toolchain& tc) {
130171
if (tc.compiler == CompilerId::MSVC) {
131172
// Native cl.exe builds are gated off until the .ifc pipeline lands;

0 commit comments

Comments
 (0)