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
344 changes: 344 additions & 0 deletions .agents/docs/2026-07-15-mingw-linux-cross-windows-design.md

Large diffs are not rendered by default.

82 changes: 82 additions & 0 deletions .github/workflows/cross-build-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ name: cross-build-test
# 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
#
# 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.
#
# mcpp resolves a cross `--target <triple>-musl` build to the triple-named cross
# gcc musl toolchain from the xlings ecosystem (xim:<triple>-gcc, see
Expand Down Expand Up @@ -163,3 +168,80 @@ jobs:
echo "== xlings --version (best-effort under qemu) =="
xver=$($RUN "$XLINGS_XBIN" --version 2>&1 || true)
echo "$xver"

# ── Linux → Windows MinGW cross (OS-cross, same arch: ELF→PE) ─────────────
# Builds a demo project for x86_64-w64-mingw32 with the from-source GCC-16
# MSVCRT cross toolchain, asserts the artefact is a fully-static PE, and runs
# it under wine. Delegated to the e2e harness (tests/e2e/102_mingw_cross_wine.sh,
# `# requires: mingw-cross wine`) so the run_all cap-gating stays the single
# source of truth. See 2026-07-15-mingw-linux-cross-windows-design.md Part C.
mingw-cross-wine:
name: mingw-cross linux→windows (build + wine run)
runs-on: ubuntu-24.04
timeout-minutes: 60
env:
MCPP_HOME: /home/runner/.mcpp
MCPP_VERBOSE: "1"
steps:
- uses: actions/checkout@v4

- name: Cache mcpp sandbox
uses: actions/cache@v4
with:
path: ~/.mcpp
key: mcpp-sandbox-${{ runner.os }}-mingw-cross-${{ hashFiles('mcpp.toml', '.xlings.json') }}
restore-keys: |
mcpp-sandbox-${{ runner.os }}-mingw-cross-

- name: Cache xlings
uses: actions/cache@v4
with:
path: ~/.xlings
key: xlings-${{ runner.os }}-v2-${{ hashFiles('.xlings.json') }}
restore-keys: |
xlings-${{ runner.os }}-v2-

- name: Install wine
run: |
sudo dpkg --add-architecture i386 || true
sudo apt-get update -qq
sudo apt-get install -y wine64 wine || sudo apt-get install -y wine
wine --version

- name: Bootstrap mcpp via xlings
env:
XLINGS_NON_INTERACTIVE: '1'
XLINGS_VERSION: '0.4.62'
run: |
tarball="xlings-${XLINGS_VERSION}-linux-x86_64.tar.gz"
curl -fsSL -o "/tmp/${tarball}" \
"https://github.com/d2learn/xlings/releases/download/v${XLINGS_VERSION}/${tarball}"
tar -xzf "/tmp/${tarball}" -C /tmp
"/tmp/xlings-${XLINGS_VERSION}-linux-x86_64/subos/default/bin/xlings" self install
export PATH="$HOME/.xlings/subos/default/bin:$PATH"
find "$HOME/.xlings" -name '.xlings-index-cache.json' -delete 2>/dev/null || true
xlings config --mirror GLOBAL 2>/dev/null || true
xlings update -y 2>/dev/null || xlings update 2>/dev/null || true
xlings install mcpp -y
echo "XLINGS_BIN=$HOME/.xlings/subos/default/bin/xlings" >> "$GITHUB_ENV"
echo "MCPP_BOOT=$HOME/.xlings/subos/default/bin/mcpp" >> "$GITHUB_ENV"

- name: Self-host build (fresh host mcpp)
run: |
export MCPP_VENDORED_XLINGS="$XLINGS_BIN"
"$MCPP_BOOT" self config --mirror GLOBAL 2>/dev/null || true
"$MCPP_BOOT" build
MCPP=$(realpath "$(find target -type f -name mcpp -printf '%T@ %p\n' | sort -rn | head -1 | cut -d' ' -f2)")
test -x "$MCPP"
"$MCPP" self config --mirror GLOBAL
echo "MCPP=$MCPP" >> "$GITHUB_ENV"

- name: Install mingw-cross toolchain
run: |
export MCPP_VENDORED_XLINGS="$XLINGS_BIN"
"$MCPP" toolchain install mingw-cross 16.1.0

- name: "e2e: cross-build + wine run"
run: |
export MCPP_VENDORED_XLINGS="$XLINGS_BIN"
bash tests/e2e/102_mingw_cross_wine.sh
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,30 @@
> 本文件追踪 `mcpp-community/mcpp` 公开仓的版本演进。
> 格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)。

## [0.0.92] — 2026-07-15

### 新增

- **Linux → Windows MinGW-w64 交叉工具链(`mingw-cross`)—— host≠target 一等公民**。
在 Linux 主机上交叉编译出 Windows x86_64 PE,含 `import std`。补 0.0.89
`msvc-mingw-design` §4.4/§7 登记的延期项(交叉维:host≠target)。
- **工具链**:从源码构建的 GCC 16.1.0 mingw-w64 **MSVCRT** 交叉链(triple
`x86_64-w64-mingw32`,跟随 Rust Tier-1 `x86_64-pc-windows-gnu` 的 CRT 选型);
自包含(自带 binutils/CRT/libstdc++ 含 `bits/std.cc` + `libstdc++exp`);
发布于 `xlings-res/mingw-cross-gcc`(GitHub+GitCode)+ `xim-pkgindex`。
- **mcpp 侧**:用户名 `mingw-cross` → xim `mingw-cross-gcc`(前端
`x86_64-w64-mingw32-g++`);解除 MinGW 的 Windows-host 门(Linux 主机可装,native
`mingw` 仍 Windows-only);`--target x86_64-w64-mingw32` 约定解析(默认 static);
跳过 glibc/linux-headers(PE 自带 CRT)。
- **host≠target 化**:PE 产物形态判断一律按 target 而非 host constexpr——std 模块源
探测补 `<prefix>/<triple>/include/c++/` 子目录;自包含工具链跳过外部 binutils `-B`
(musl + mingw);MinGW link 分支(`-static` / `-lstdc++exp`)由 `if constexpr(is_windows)`
host 门提为运行期 `is_mingw_target` target 判定。
- **验证**:e2e `102_mingw_cross_wine.sh`(`# requires: mingw-cross wine`,build --target
→ PE 静态自包含断言 → wine 跑 import std + 多模块);CI `cross-build-test.yml` 加
`mingw-cross-wine` job(OS-cross,wine)。全链实测闭环(install→build→wine)。
- 设计:`.agents/docs/2026-07-15-mingw-linux-cross-windows-design.md`。

## [0.0.91] — 2026-07-15

### 新增
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 = "0.0.91"
version = "0.0.92"
description = "Modern C++ build & package management tool"
license = "Apache-2.0"
authors = ["mcpp-community"]
Expand Down
60 changes: 32 additions & 28 deletions src/build/flags.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -215,11 +215,14 @@ CompileFlags compute_flags(const BuildPlan& plan) {
f.sysroot = link_toolchain_flags;
}

// Binutils -B flag — a GCC/libstdc++ payload concern (musl bundles its
// own as/ld; Clang and MSVC never take an external binutils).
bool isMuslTc = mcpp::toolchain::is_musl_target(plan.toolchain);
// Binutils -B flag — a GCC/libstdc++ payload concern (musl and MinGW-w64
// cross both bundle their own as/ld; Clang and MSVC never take an external
// binutils). MinGW must not get the Linux binutils -B — its PE/SEH output
// is only assemblable by its own x86_64-w64-mingw32-as.
bool isMuslTc = mcpp::toolchain::is_musl_target(plan.toolchain);
bool isMingwTc = mcpp::toolchain::is_mingw_target(plan.toolchain);
std::filesystem::path binutilsBin;
if (!isMuslTc && caps.stdlib_id == "libstdc++") {
if (!isMuslTc && !isMingwTc && caps.stdlib_id == "libstdc++") {
auto ar = mcpp::toolchain::archive_tool(plan.toolchain);
if (!ar.empty())
binutilsBin = ar.parent_path();
Expand Down Expand Up @@ -361,6 +364,27 @@ CompileFlags compute_flags(const BuildPlan& plan) {
if (prof.lto) link_extra += " -flto";
if (prof.strip) link_extra += " -s";

// MinGW PE link — keyed on the TARGET (is_mingw_target), NOT the host: a
// Linux-hosted cross build produces exactly the same PE link as a native
// Windows MinGW build (host≠target). No rpath/loader/payload model. Static
// + libstdc++exp (std::print's __open_terminal/__write_to_terminal live in
// libstdc++exp.a, not plain libstdc++). Self-contained binutils → no -B.
if (isMingwTc) {
std::string mingw_static;
std::string mingw_stdexp;
if (caps.stdlib_id == "libstdc++") {
// `-static` for the whole link — MinGW's standalone-exe convention
// (the piecemeal -static-libstdc++ recipe still pulls
// libwinpthread-1.dll). Opt out via [build] static_stdlib=false.
if (f.staticStdlib || f.linkage == "static")
mingw_static = " -static";
mingw_stdexp = " -lstdc++exp";
}
f.ld = std::format("{}{}{}{}{}", mingw_static, static_stdlib,
user_ldflags, mingw_stdexp, link_extra);
return f;
}

if constexpr (mcpp::platform::is_windows) {
if (isMsvcDialect) {
// Native cl.exe: link.exe does the link (SeparateLinker). Search
Expand All @@ -374,30 +398,10 @@ CompileFlags compute_flags(const BuildPlan& plan) {
f.ld = libpaths + user_ldflags;
return f;
}
// PE link: no rpath/loader/payload model. MSVC-ABI Clang needs
// nothing extra (MSVC STL/SDK via the driver); MinGW adds the static
// libstdc++/libgcc pair (static_stdlib above) and -B so its own
// binutils resolve, plus `-static` for full static when requested
// (MinGW supports it, unlike MSVC-ABI links).
std::string mingw_static;
std::string mingw_stdexp;
if (caps.stdlib_id == "libstdc++") {
// `-static` for the whole link — winlibs' own recommendation for
// standalone exes. The piecemeal recipe (-static-libstdc++ +
// -Wl,-Bstatic -lwinpthread) verifiably loses to the driver's
// implicit closing libs: CI import tables still showed
// libwinpthread-1.dll. System DLLs (KERNEL32/UCRT) still resolve
// via their import libs. Tied to staticStdlib so
// [build] static_stdlib=false opts back into DLL-coupled links.
if (f.staticStdlib || f.linkage == "static")
mingw_static = " -static";
// std::print's terminal probe (__open_terminal /
// __write_to_terminal, bits/print.h) lives in libstdc++exp.a on
// Windows targets — plain -lstdc++ leaves them undefined.
mingw_stdexp = " -lstdc++exp";
}
f.ld = std::format("{}{}{}{}{}{}", mingw_static, static_stdlib, b_flag,
user_ldflags, mingw_stdexp, link_extra);
// PE link, MSVC-ABI Clang (native MinGW is handled by the target-keyed
// branch above and has already returned): no rpath/loader/payload —
// MSVC STL/SDK come via the driver, nothing extra needed.
f.ld = std::format("{}{}{}", static_stdlib, user_ldflags, link_extra);
} else if constexpr (mcpp::platform::needs_explicit_libcxx) {
// macOS. Two min-version concerns (see xlings
// .agents/docs/2026-06-05-macos-min-version-support.md):
Expand Down
13 changes: 13 additions & 0 deletions src/build/prepare.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -664,6 +664,19 @@ prepare_build(bool print_fingerprint,
&& m->buildConfig.linkage.empty()) {
m->buildConfig.linkage = "static";
}
// Convention: the Windows PE cross target `x86_64-w64-mingw32` without
// an explicit [target.X] override resolves to the from-source GCC-16
// MSVCRT cross toolchain. host≠target — an ELF frontend producing PE.
// Default static linkage: MinGW standalone-exe convention and the clean
// path for running the artifact under wine (no DLL deployment needed).
// See .agents/docs/2026-07-15-mingw-linux-cross-windows-design.md.
if (overrides.target_triple == "x86_64-w64-mingw32"
&& (it == m->targetOverrides.end() || it->second.toolchain.empty()))
{
tcSpec = "mingw-cross@16.1.0";
if (m->buildConfig.linkage.empty())
m->buildConfig.linkage = "static";
}
}
if (overrides.force_static) m->buildConfig.linkage = "static";

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 = "0.0.91";
inline constexpr std::string_view MCPP_VERSION = "0.0.92";

struct FingerprintInputs {
Toolchain toolchain;
Expand Down
36 changes: 31 additions & 5 deletions src/toolchain/gcc.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,31 @@ std::optional<std::filesystem::path> find_std_module_source(
auto p = root / "include" / "c++" / std::string(version) / "bits" / "std.cc";
if (std::filesystem::exists(p)) return p;

// Cross toolchains (e.g. MinGW-w64 `x86_64-w64-mingw32-g++`) install the
// TARGET libstdc++ — and its bits/std.cc — under a target-triple subdir:
// <prefix>/<triple>/include/c++/<version>/bits/std.cc
// rather than the host-native <prefix>/include/c++/. Derive the triple from
// the frontend filename (strip the -g++/-gcc/-c++ suffix) and look there.
{
std::error_code ec;
auto stem = cxx_binary.stem().string();
for (std::string_view suf : {"-g++", "-gcc", "-c++"}) {
if (stem.size() > suf.size() && stem.ends_with(suf)) {
std::string triple = stem.substr(0, stem.size() - suf.size());
auto base = root / triple / "include" / "c++";
auto pv = base / std::string(version) / "bits" / "std.cc";
if (std::filesystem::exists(pv, ec)) return pv;
if (std::filesystem::exists(base, ec)) {
for (auto& entry : std::filesystem::directory_iterator(base, ec)) {
auto cand = entry.path() / "bits" / "std.cc";
if (std::filesystem::exists(cand, ec)) return cand;
}
}
break;
}
}
}

// Version-dir scan fallback: the header dir doesn't always equal the
// full driver version (e.g. distro / MinGW-w64 builds using the major
// version, or a patched banner). Any bits/std.cc under include/c++ is
Expand Down Expand Up @@ -131,12 +156,13 @@ std::string std_module_build_command(const Toolchain& tc,
const std::filesystem::path& cacheDir,
std::string_view sysrootFlag,
std::string_view cppStandardFlag) {
// musl-cross-make toolchains bundle their own as/ld (and for a cross
// target the host's binutils would mis-assemble, e.g. `as: unrecognized
// option '-EL'`). Only the glibc gcc needs an external binutils package
// wired via -B. Mirrors the guard in build/flags.cppm.
// musl-cross-make AND MinGW-w64 cross toolchains bundle their own as/ld
// (and for a cross target the host's binutils would mis-assemble — e.g.
// the Linux `as` chokes on MinGW's PE/SEH directives `.def`/`.seh_proc`).
// Only the glibc gcc needs an external binutils package wired via -B.
// Mirrors the guard in build/flags.cppm.
std::string bFlag;
if (!is_musl_target(tc)) {
if (!is_musl_target(tc) && !is_mingw_target(tc)) {
if (auto binutilsBin = find_binutils_bin(tc.binaryPath)) {
bFlag = std::format(" -B{}", mcpp::xlings::shq(binutilsBin->string()));
}
Expand Down
5 changes: 4 additions & 1 deletion src/toolchain/lifecycle.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -410,9 +410,12 @@ export int toolchain_install(const mcpp::config::GlobalConfig& cfg,
// Ensure sysroot dependencies (glibc, linux-headers) are installed.
// These are required for C library + kernel headers during compilation.
// musl-gcc is self-contained and doesn't need these; neither do
// Windows (llvm/mingw — PE, own CRT) or macOS (SDK) toolchains.
// Windows (llvm/mingw — PE, own CRT) or macOS (SDK) toolchains, nor a
// Linux-hosted mingw CROSS toolchain (host is Linux but the TARGET is
// Windows PE with its own CRT — glibc/linux-headers are irrelevant).
// Mirrors the platform guard on prepare.cppm's first-run install.
if (!spec->isMusl
&& pkg.ximName != "mingw-cross-gcc"
&& !mcpp::platform::is_windows && !mcpp::platform::is_macos) {
for (auto dep : {"xim:glibc", "xim:linux-headers"}) {
mcpp::log::verbose("toolchain", std::format("installing dep: {}", dep));
Expand Down
34 changes: 31 additions & 3 deletions src/toolchain/registry.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,10 @@ std::vector<std::string> frontend_candidates_for(std::string_view ximName,
if (ximName == "llvm") return mcpp::toolchain::llvm::frontend_candidates();
if (ximName == "msvc") return {"cl.exe"};
if (ximName == "mingw-gcc") return {"g++.exe", "g++"};
// Linux-hosted MinGW-w64 cross toolchain: an ELF frontend that produces
// Windows PE. Triple-prefixed name (like the musl cross tools), never the
// host g++ — a cross build must never silently fall back to native.
if (ximName == "mingw-cross-gcc") return {"x86_64-w64-mingw32-g++"};
return {"g++", "clang++", "x86_64-linux-musl-g++", "cl.exe"};
}

Expand Down Expand Up @@ -180,6 +184,11 @@ XimToolchainPackage to_xim_package(const ToolchainSpec& spec) {
// `mingw-gcc` (winlibs GCC+MinGW-w64 UCRT builds mirrored at xlings-res).
if (ximCompiler == "mingw")
ximCompiler = "mingw-gcc";
// Linux→Windows MinGW-w64 cross: user-facing `mingw-cross`, xim package
// `mingw-cross-gcc` (from-source GCC-16 MSVCRT cross, xlings-res). host≠target:
// an ELF toolchain producing PE. See 2026-07-15-mingw-linux-cross-windows-design.
if (ximCompiler == "mingw-cross")
ximCompiler = "mingw-cross-gcc";

pkg.ximName = ximCompiler;
pkg.ximVersion = spec.version;
Expand Down Expand Up @@ -233,6 +242,8 @@ std::string display_label(std::string_view compiler, std::string_view version) {
return std::format("gcc {}-musl", version);
if (compiler == "mingw-gcc")
return std::format("mingw {}", version);
if (compiler == "mingw-cross-gcc")
return std::format("mingw-cross {}", version);
return std::format("{} {}", compiler, version);
}

Expand All @@ -253,6 +264,12 @@ bool matches_default_toolchain(std::string_view configuredDefault,
&& configuredDefault == std::format("mingw@{}", version)) {
return true;
}
// Cross MinGW: installed-payload row reports xim name (mingw-cross-gcc);
// user-facing spec is mingw-cross@<ver>.
if (compiler == "mingw-cross-gcc"
&& configuredDefault == std::format("mingw-cross@{}", version)) {
return true;
}
return false;
}

Expand All @@ -266,9 +283,13 @@ std::vector<std::pair<std::string, std::string>> available_toolchain_indexes() {
{"musl-gcc", "musl-gcc"},
{mcpp::toolchain::llvm::package_name(), "llvm"},
};
// Windows-native toolchain — only meaningful to list on Windows hosts.
// MinGW-w64 toolchains are host-conditional (host≠target axis):
// - native mingw-gcc (produces PE, runs on Windows) → Windows hosts
// - cross mingw-cross-gcc (ELF frontend, produces PE) → Linux hosts
if constexpr (mcpp::platform::is_windows)
out.emplace_back("mingw-gcc", "mingw-gcc");
else if constexpr (mcpp::platform::is_linux)
out.emplace_back("mingw-cross-gcc", "mingw-cross-gcc");
return out;
}

Expand All @@ -286,10 +307,17 @@ std::filesystem::path archive_tool(const Toolchain& tc) {
if (is_clang(tc)) return mcpp::toolchain::clang::archive_tool(tc);

// MinGW bundles its own binutils next to the frontend (self-contained,
// like musl) — never an external binutils xpkg.
// like musl) — never an external binutils xpkg. Native (Windows-host) ships
// `ar.exe`; the Linux-hosted cross ships the triple-prefixed ELF tool
// `x86_64-w64-mingw32-ar`. Try the cross form first, then native.
if (is_mingw_target(tc)) {
auto ar = tc.binaryPath.parent_path() / "ar.exe";
std::error_code ec;
auto dir = tc.binaryPath.parent_path();
if (!tc.targetTriple.empty()) {
auto crossAr = dir / (tc.targetTriple + "-ar");
if (std::filesystem::exists(crossAr, ec)) return crossAr;
}
auto ar = dir / "ar.exe";
if (std::filesystem::exists(ar, ec)) return ar;
return {};
}
Expand Down
Loading
Loading