Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
707 changes: 707 additions & 0 deletions .agents/docs/2026-08-03-windows-host-linux-cross-design.md

Large diffs are not rendered by default.

127 changes: 124 additions & 3 deletions .github/workflows/cross-build-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,25 @@ name: cross-build-test
# each target triple, arch-checked, and smoke-run under qemu-user.
#
# ── Supported cross matrix (built + verified below) ────────────────────────
# target | toolchain | host→target | run
# ----------------------|---------------------------------|---------------|-----
# aarch64-linux-musl | aarch64-linux-musl-gcc@16.1.0 | x86_64→arm64 | qemu
# target | toolchain | host→target | run
# ----------------------|----------------------------------|---------------|-----
# aarch64-linux-musl | aarch64-linux-musl-gcc@16.1.0 | x86_64→arm64 | qemu
# x86_64-w64-mingw32 | mingw-cross-gcc@16.1.0 (MSVCRT) | linux→windows | wine
# x86_64-linux-musl | x86_64-linux-musl-gcc@16.1.0 | windows→linux | linux job
#
# The mingw row is OS-cross (same arch, different OS/ABI: ELF→PE), so it lives
# in its own job below with wine verification instead of the qemu arch matrix.
# See .agents/docs/2026-07-15-mingw-linux-cross-windows-design.md.
#
# The windows→linux row is the MIRROR of that one, and its verification has no
# wine-equivalent: a Windows runner cannot execute the ELF it just produced.
# So it is split across TWO jobs — build on windows-latest, upload the artefact,
# then download and really run it on ubuntu. Static assertions alone would not
# do: "it linked" has never implied "it runs" (see the elfpatch incident in
# .agents/docs/, and 2026-08-03-windows-host-linux-cross-design.md §6.1).
# The artefact is a fully static musl ELF (no PT_INTERP), so the consumer job
# needs neither qemu nor a matching loader.
#
# mcpp resolves a cross `--target <triple>-musl` build to the triple-named cross
# gcc musl toolchain from the xlings ecosystem (xim:<triple>-gcc, see
# src/build/prepare.cppm). Output is a fully static musl ELF (no PT_INTERP),
Expand Down Expand Up @@ -285,3 +295,114 @@ jobs:
run: |
export MCPP_VENDORED_XLINGS="$XLINGS_BIN"
bash tests/e2e/102_mingw_cross_wine.sh

# ── windows → linux ───────────────────────────────────────────────────────
# The mirror of mingw-cross-wine. Two jobs because a Windows runner cannot
# execute the ELF it produces; the artefact is handed to a Linux job and
# really run there.
windows-host-linux-cross:
name: windows→linux cross-build (windows host)
runs-on: windows-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/bootstrap-mcpp

- name: Build mcpp from source (self-host)
shell: bash
run: |
export MCPP_VENDORED_XLINGS="$XLINGS_BIN"
"$MCPP" build
# Newest, not first: target/ is cache-restored and keeps a directory
# per build fingerprint, so `find | head -1` can hand back the
# PREVIOUS release's binary (that is how a 0.0.106 build ran 0.0.105).
MCPP_SELF=$(find target -name "mcpp.exe" -path "*/bin/*" -printf "%T@ %p\n" \
| sort -rn | head -1 | cut -d" " -f2-)
test -n "$MCPP_SELF" || { echo "FAIL: no mcpp.exe"; exit 1; }
MCPP_SELF=$(cd "$(dirname "$MCPP_SELF")" && pwd)/$(basename "$MCPP_SELF")
"$MCPP_SELF" --version
echo "MCPP_SELF=$MCPP_SELF" >> "$GITHUB_ENV"

- name: Install the linux-musl cross toolchain (windows-hosted canadian)
shell: bash
run: |
export MCPP_VENDORED_XLINGS="$XLINGS_BIN"
"$MCPP_SELF" toolchain install gcc 16.1.0 --target x86_64-linux-musl
# The target row must now be visible on a Windows host — this is the
# host gate from design §1.2 having been lifted, asserted rather than
# eyeballed.
"$MCPP_SELF" toolchain list | tee /tmp/tclist.txt
grep -q "x86_64-linux-musl" /tmp/tclist.txt \
|| { echo "FAIL: linux-musl target not listed on windows host"; exit 1; }

- name: "Cross-build mcpp -> x86_64-linux-musl"
shell: bash
run: |
export MCPP_VENDORED_XLINGS="$XLINGS_BIN"
"$MCPP_SELF" build --target x86_64-linux-musl
# Scope the search to the TARGET's output tree — target/ also holds the
# host build from the previous step, and both are named "mcpp*".
#
# The artefact carries a `.exe` suffix even though it is an ELF:
# plan.cppm's target_output() spells the suffix from
# mcpp::platform::exe_suffix, a HOST constant. That is the same
# host-decides-target confusion as B2, and it is symmetric — a
# Linux→Windows cross produces a PE with no `.exe` today. Renaming the
# output is a behaviour change that would touch the mingw e2e and any
# user script, so it is filed as follow-up rather than folded in here;
# match both spellings so this job is correct either way.
OUT=$(find target/x86_64-linux-musl -type f -path "*/bin/*" \
\( -name "mcpp" -o -name "mcpp.exe" \) -printf "%T@ %p\n" \
| sort -rn | head -1 | cut -d" " -f2-)
if [ -z "$OUT" ]; then
echo "FAIL: no cross artefact produced; tree was:"
find target/x86_64-linux-musl -type f -path "*/bin/*" | head -20
exit 1
fi
cp "$OUT" mcpp-linux-musl
ls -la mcpp-linux-musl

- uses: actions/upload-artifact@v4
with:
name: mcpp-x86_64-linux-musl-from-windows
path: mcpp-linux-musl
retention-days: 1

windows-host-linux-cross-run:
name: windows→linux artefact really runs (linux)
needs: windows-host-linux-cross
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- uses: actions/download-artifact@v4
with:
name: mcpp-x86_64-linux-musl-from-windows

- name: Assert it is a static ELF, then run it
run: |
set -euo pipefail
chmod +x mcpp-linux-musl
file mcpp-linux-musl

# B2 regression gate (design §1.3): before the fix, `-static` was
# decided by a HOST constant (`supports_full_static = is_linux`), so
# a Windows host emitted a NON-static binary here. Written as a
# positive `grep -q` on purpose: `! cmd | grep` is exempt from
# errexit and can never fail (see build-mcpp-helper-self-containment).
file mcpp-linux-musl | grep -q "ELF 64-bit LSB"
file mcpp-linux-musl | grep -q "x86-64"
file mcpp-linux-musl | grep -q "statically linked"

# A static musl ELF has no PT_INTERP at all — the stronger form of
# the same claim, and independent of `file`'s wording.
readelf -l mcpp-linux-musl > hdrs.txt
if grep -q "INTERP" hdrs.txt; then
echo "FAIL: artefact has a PT_INTERP segment — not statically linked"
grep -A2 "INTERP" hdrs.txt
exit 1
fi

# Linked ≠ runs. This is the whole point of the second job.
./mcpp-linux-musl --version
./mcpp-linux-musl --help > /dev/null
echo "OK: windows-built linux artefact executes natively"
26 changes: 26 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,32 @@
> 本文件追踪 `mcpp-community/mcpp` 公开仓的版本演进。
> 格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)。

## [2026.8.3.2] — 2026-08-03

### 新增

- **在 Windows 上构建 Linux 程序(`--target x86_64-linux-musl`)。** 补上 `host ≠ target` 最后一个空象限:一台 Windows 机器直接产出**完全静态的 Linux ELF**,不需要 WSL、不需要容器、不往系统里装任何东西。

```bash
mcpp build --target x86_64-linux-musl # 在 Windows 与 Linux 上拼写完全相同
```

命令两边一字不差,因为「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 无关。

不支持的两种组合是明确拒绝而非碰运气:Windows → `linux-gnu`(glibc 需要 `xim:glibc` / `xim:linux-headers` 两个 sysroot 载荷,只为 Linux 主机发布),以及 Windows → 跨架构(canadian cross 载荷按主机架构构建)。`mcpp toolchain list` 只列当前主机真能装的目标,所以某个目标没出现在 Targets 块里,就是这台机器确实服务不了它。

### 修复

- **`-static` 由构建主机而非目标决定,导致 Windows→Linux 交叉产物根本不是静态的。** `supports_full_static` 是一个**主机**常量(`is_linux`),描述的是「这台机器自己的二进制能否全静态」;`flags.cppm` 却拿它来决定**产物**要不要 `-static`。在 Linux 主机上这两个问题对所有 Linux 目标恰好同解,所以它一直没被发现 —— 只有当非 Linux 主机交叉编译到 Linux 时才会现形:`-static` 被静默丢弃,而 `x86_64-linux-musl` 这个目标存在的全部理由就是产出可移植的静态 ELF。没有报错,没有警告,那个 flag 就是不在。

判据改读解析后的 triple:空 triple(目标即主机)沿用主机答案;PE 目标返回 false,它们的 `-static` 来自 C++ 运行时分发契约,在这里也答 true 会让两套机制都发一遍;macOS 返回 false(libSystem 必须动态);Linux 返回 true。今天所有能工作的路径逐位不变。

- **Windows 主机上交叉工具链的前端永远找不到。** 候选名是 `{ "<triple>-g++", "g++" }`,用 `filesystem::exists` 解析,而 Windows 上的文件叫 `<triple>-g++.exe` —— 载荷装得好好的,然后不可用。`archive_tool` 的 musl 分支(`<triple>-ar`)有同样的遗漏。

### 改进

- **「这台主机能否服务这个目标」收敛为单一判据 `host_can_serve`。** 它此前在两处独立推导:`registry.cppm` 选载荷时一次,`lifecycle.cppm` 决定 `toolchain list` 能否把目标标为 `available` 时又一次 —— 而且两者**已经漂移**:载荷侧会解析出一个可用性侧宣称不可能存在的 windows-hosted musl 包。现在判据只有一份,就放在它必须与之一致的载荷解析旁边。

## [2026.8.3.1] — 2026-08-03

### 修复
Expand Down
35 changes: 35 additions & 0 deletions docs/03-toolchains.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,41 @@ windows = "gcc@16" # gcc family on Windows = MinGW-w64
# legacy value "mingw@16.1.0" keeps working
```

## Linux ELF from Windows (`x86_64-linux-musl`, no WSL required)

The mirror of the section above: a Windows machine producing a **fully static
Linux binary**, with no WSL, no container, and nothing installed system-wide.

```bash
mcpp build --target x86_64-linux-musl # from Windows OR Linux
```

The command is spelled *identically* on both hosts, because "cross" is not a
name in mcpp — it is just the relation `host ≠ target`. Which payload serves
the target is resolved automatically: a Linux x86_64 host installs the native
`musl-gcc`; a Windows host installs a **canadian-cross** GCC
(built `x86_64-linux-gnu` → runs on `x86_64-w64-mingw32` → emits
`x86_64-linux-musl`). Both are GCC 16.1.0 and both ship `bits/std.cc`, so
`import std` works the same either way.

The output is a fully static ELF with no `PT_INTERP` — it runs on any Linux
distribution regardless of its libc, which is exactly why musl is the target
that got wired up first:

```console
$ file mcpp
mcpp: ELF 64-bit LSB executable, x86-64, statically linked, stripped
```

`x86_64-linux-gnu` from Windows is **not** supported: a glibc target needs the
`xim:glibc` and `xim:linux-headers` sysroot payloads, which are published for
Linux hosts only. The musl target is self-contained and needs neither.

Cross-arch from Windows (e.g. `aarch64-linux-musl`) is not available either —
the canadian-cross payload is built per host arch. `mcpp toolchain list` shows
only what the current host can actually install, so if a target is missing from
the Targets block, that host genuinely cannot serve it.

## MSVC (System Toolchain, Windows)

MSVC is different from every other toolchain mcpp manages: it is a **system
Expand Down
2 changes: 1 addition & 1 deletion mcpp.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "mcpp"
version = "2026.8.3.1"
version = "2026.8.3.2"
description = "Modern C++ build & package management tool"
license = "Apache-2.0"
authors = ["mcpp-community"]
Expand Down
11 changes: 10 additions & 1 deletion src/build/flags.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import mcpp.toolchain.detect;
import mcpp.toolchain.dialect;
import mcpp.toolchain.hostflags;
import mcpp.toolchain.linkmodel;
import mcpp.toolchain.model;
import mcpp.toolchain.provider;
import mcpp.toolchain.registry;

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

// ---- C++ runtime distribution contract (issue #336) -------------------
//
Expand Down
25 changes: 25 additions & 0 deletions src/doctor.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,31 @@ export int doctor_report() {
if (!any)
ok("mingw not installed (optional — `mcpp toolchain install mingw 16.1.0`)");
}

// The other direction: a windows-hosted cross toolchain that produces
// Linux ELF. Same shape as the mingw probe above; the package is named
// by triple, matching to_xim_package()'s `<triple>-gcc`.
{
auto triple = std::string(mcpp::platform::host_arch) + "-linux-musl";
auto label = std::format("linux cross (xim:{}-gcc)", triple);
mcpp::ui::status("Checking", label);
auto pkgs = mcpp::home::root() / "registry" / "data" / "xpkgs"
/ std::format("xim-x-{}-gcc", triple);
std::error_code ec;
bool any = false;
if (std::filesystem::exists(pkgs, ec)) {
for (auto& v : std::filesystem::directory_iterator(pkgs, ec)) {
if (!v.is_directory(ec)) continue;
ok(std::format("{} {} installed",
triple, v.path().filename().string()));
any = true;
}
}
if (!any)
ok(std::format("{} not installed (optional — "
"`mcpp toolchain install gcc 16.1.0 --target {}`)",
triple, triple));
}
}

mcpp::ui::status("Checking", "std module");
Expand Down
2 changes: 1 addition & 1 deletion src/toolchain/fingerprint.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import mcpp.toolchain.detect;

export namespace mcpp::toolchain {

inline constexpr std::string_view MCPP_VERSION = "2026.8.3.1";
inline constexpr std::string_view MCPP_VERSION = "2026.8.3.2";

struct FingerprintInputs {
Toolchain toolchain;
Expand Down
13 changes: 5 additions & 8 deletions src/toolchain/lifecycle.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -368,15 +368,12 @@ export int toolchain_list(const mcpp::config::GlobalConfig& cfg) {

// Vocabulary rows not covered by an installed payload. Only list a
// verified target as "available" when this host can actually install it.
//
// The predicate lives in registry.cppm next to the payload resolution it
// must agree with — this used to be a second, independent derivation of
// the same question and the two had already drifted apart.
auto installable_here = [&](const mcpp::toolchain::triple::Triple& t) {
if (t.os == "linux")
return mcpp::platform::is_linux
&& (t.is_musl() || t.arch == hostT.arch);
if (t.is_windows_gnu())
return mcpp::platform::is_linux || mcpp::platform::is_windows;
if (t.os == "windows") return bool(mcpp::platform::is_windows);
if (t.os == "macos") return bool(mcpp::platform::is_macos);
return false;
return mcpp::toolchain::host_can_serve(t);
};
for (auto& info : mcpp::toolchain::triple::known_targets()) {
bool covered = std::any_of(targetRows.begin(), targetRows.end(),
Expand Down
41 changes: 41 additions & 0 deletions src/toolchain/model.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,22 @@ bool is_musl_target(const Toolchain& tc);
bool is_msvc_target(const Toolchain& tc);
bool is_mingw_target(const Toolchain& tc);

// Can the artifact we are building be fully statically linked (`-static`)?
//
// This is a property of the TARGET, not of the machine doing the build —
// a Windows host cross-compiling to x86_64-linux-musl still produces a fully
// static ELF. `mcpp::platform::supports_full_static` answers a DIFFERENT
// question ("can THIS machine's own binaries be static"), and using it here
// silently dropped `-static` from every Windows→Linux cross build.
//
// `hostCapability` is threaded in explicitly rather than read from
// mcpp::platform so the decision is testable on any host: passing false
// models a Windows/macOS host. It is only consulted for the host target
// (empty triple), where target *is* host. Callers pass
// mcpp::platform::supports_full_static — keeping that dependency at the call
// site leaves this module free of any platform import.
bool target_supports_full_static(std::string_view targetTriple, bool hostCapability);

struct BmiTraits {
std::string_view bmiDir; // "gcm.cache" | "pcm.cache" | "ifc.cache"
std::string_view bmiExt; // ".gcm" | ".pcm" | ".ifc"
Expand Down Expand Up @@ -126,6 +142,31 @@ bool is_mingw_target(const Toolchain& tc) {
return tc.targetTriple.find("mingw32") != std::string::npos;
}

bool target_supports_full_static(std::string_view targetTriple, bool hostCapability) {
// Empty triple means "build for this machine" — target IS host, so the
// host answer is the correct one. This is the only case where the host
// capability legitimately decides.
if (targetTriple.empty()) return hostCapability;

auto t = triple::parse(targetTriple);
// Outside the triple language we have nothing to reason from. Fall back
// to the host answer rather than guessing from a substring — a wrong
// `true` here would emit `-static` at a target that cannot honour it.
if (!t) return hostCapability;

// PE targets get their `-static` from the C++ runtime distribution
// contract (dist::Format::Pe in flags.cppm), never from here. Returning
// false is what keeps the two mechanisms from both emitting the flag.
if (t->is_pe()) return false;

// macOS cannot fully static-link: libSystem must stay dynamic.
if (t->os == "macos") return false;

// Linux ELF — glibc or musl, native or cross. This is the line that was
// previously gated on the HOST being Linux.
return t->os == "linux";
}

BmiTraits bmi_traits(const Toolchain& tc) {
if (tc.compiler == CompilerId::MSVC) {
// Native cl.exe builds are gated off until the .ifc pipeline lands;
Expand Down
Loading
Loading