From 47e45d66ac83aef5b248f0a38666bb6ad3e6f3c9 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Mon, 13 Jul 2026 13:57:22 +0800 Subject: [PATCH 1/7] refactor(toolchain): CommandDialect + backend surface normalization (Part A) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New mcpp.toolchain.dialect: command-line *spellings* (gnu vs msvc) as a value-type traits table — flags.cppm and ninja rule templates now spell -I/-D/-std=/-c/-o/-O/-g/ar-invocations through it instead of hardcoding GNU syntax. BmiTraits gains the module-flag spellings (compileModulesFlag, stdBmiUsePrefix, moduleOutputPrefix, bmiSearchPrefix), replacing the is_clang()/stdlib_id branches in flags.cppm. ProviderCapabilities gains has_builtin_p1689_scan (drops ninja_backend's is_gcc gate). Toolchain gains envOverrides (EnvVar list — std::pair member trips GCC-16 module pendings) merged into ninja's child env; the MSVC backend will carry INCLUDE/LIB/PATH through it. gcc::std_module_build_commands normalizes the provider surface to the command-sequence shape clang already has. resolve_link_model returns the empty PE model explicitly on Windows. Zero-diff verified: build.ninja byte-identical (modulo the mcpp self-path var) for GCC and LLVM toolchains on a multi-module import-std project; mcpp test 31/31. --- src/build/flags.cppm | 78 +++++++++++++++++-------- src/build/ninja_backend.cppm | 39 ++++++++++--- src/toolchain/dialect.cppm | 109 +++++++++++++++++++++++++++++++++++ src/toolchain/gcc.cppm | 18 +++++- src/toolchain/linkmodel.cppm | 6 ++ src/toolchain/model.cppm | 44 ++++++++++++-- src/toolchain/provider.cppm | 6 ++ src/toolchain/stdmod.cppm | 15 +++-- 8 files changed, 268 insertions(+), 47 deletions(-) create mode 100644 src/toolchain/dialect.cppm diff --git a/src/build/flags.cppm b/src/build/flags.cppm index 235c4310..9f374ae6 100644 --- a/src/build/flags.cppm +++ b/src/build/flags.cppm @@ -16,6 +16,7 @@ import mcpp.build.plan; import mcpp.platform; import mcpp.toolchain.clang; import mcpp.toolchain.detect; +import mcpp.toolchain.dialect; import mcpp.toolchain.linkmodel; import mcpp.toolchain.provider; import mcpp.toolchain.registry; @@ -122,10 +123,13 @@ std::string atomic_link_flag(const std::vector& linkDirs, CompileFlags compute_flags(const BuildPlan& plan) { CompileFlags f; - // ProviderCapabilities: centralised query point for per-toolchain decisions. - // Prefer caps.* checks over ad-hoc is_clang()/is_musl_target() calls for - // any new branching added to this function. + // Central query points for per-toolchain decisions — prefer these over + // ad-hoc is_clang()/is_gcc() calls: + // caps — what the toolchain can do (scan-deps, stdlib id, …) + // d — how a flag is SPELT (GNU "-I" vs MSVC "/I") + // traits — BMI mechanics + module-flag spellings auto caps = mcpp::toolchain::capabilities_for(plan.toolchain); + const auto& d = mcpp::toolchain::dialect_for(plan.toolchain); // macOS minimum supported OS version for produced binaries. // Precedence: MACOSX_DEPLOYMENT_TARGET env (explicit per-invocation @@ -152,7 +156,7 @@ CompileFlags compute_flags(const BuildPlan& plan) { std::string include_flags; for (auto& inc : plan.manifest.buildConfig.includeDirs) { auto abs = inc.is_absolute() ? inc : (plan.projectRoot / inc); - include_flags += " -I" + escape_path(abs); + include_flags += std::format(" {}{}", d.includePrefix, escape_path(abs)); } // Sysroot / payload paths — resolved ONCE by the toolchain link model @@ -208,11 +212,11 @@ CompileFlags compute_flags(const BuildPlan& plan) { f.sysroot = link_toolchain_flags; } - // Binutils -B flag + // 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); - bool isClang = mcpp::toolchain::is_clang(plan.toolchain); std::filesystem::path binutilsBin; - if (!isMuslTc && !isClang) { + if (!isMuslTc && caps.stdlib_id == "libstdc++") { auto ar = mcpp::toolchain::archive_tool(plan.toolchain); if (!ar.empty()) binutilsBin = ar.parent_path(); @@ -231,8 +235,8 @@ CompileFlags compute_flags(const BuildPlan& plan) { // unless the profile pins -O0. auto& prof = plan.manifest.buildConfig; std::string opt_flag = isMuslTc && prof.optLevel != "0" - ? " -Og" : (" -O" + prof.optLevel); - if (prof.debug) opt_flag += " -g"; + ? " -Og" : std::format(" {}{}", d.optPrefix, prof.optLevel); + if (prof.debug) opt_flag += std::format(" {}", d.debugFlags); if (prof.lto) opt_flag += " -flto"; // User link flags @@ -247,20 +251,25 @@ CompileFlags compute_flags(const BuildPlan& plan) { plan.manifest.buildConfig.cStandard.empty() ? "c11" : plan.manifest.buildConfig.cStandard; // Assemble - // -fmodules is a GCC-only flag; Clang uses a different module ABI and does - // not need it. caps.stdlib_id distinguishes GCC (libstdc++) from Clang - // (libc++ / msvc-stl) without an extra is_clang() call. - std::string module_flag = (caps.stdlib_id == "libstdc++") ? " -fmodules" : ""; + // Module-flag spellings come from BmiTraits: GCC needs -fmodules on every + // TU (BMIs implicit); Clang/MSVC reference the staged std BMI and a BMI + // search dir explicitly (spelled -fmodule-file=/-fprebuilt-module-path vs + // /reference//ifcSearchDir). + auto traits = mcpp::toolchain::bmi_traits(plan.toolchain); + std::string module_flag{traits.compileModulesFlag}; std::string std_module_flag; - if (isClang && !plan.stdBmiPath.empty()) { - std_module_flag = " -fmodule-file=std=" + escape_path(staged_std_bmi_path(plan)); + if (!traits.stdBmiUsePrefix.empty() && !plan.stdBmiPath.empty()) { + std_module_flag = std::string(traits.stdBmiUsePrefix) + + escape_path(staged_std_bmi_path(plan)); } std::string std_compat_module_flag; - if (isClang && !plan.stdCompatBmiPath.empty()) { + if (!traits.stdCompatBmiUsePrefix.empty() && !plan.stdCompatBmiPath.empty()) { + // NOTE: staging path is Clang's today; registry-dispatch when the + // MSVC backend lands (std.compat.ixx staging). auto compatDst = mcpp::toolchain::clang::staged_std_compat_bmi_path(plan.outputDir); - std_compat_module_flag = " -fmodule-file=std.compat=" + escape_path(compatDst); + std_compat_module_flag = std::string(traits.stdCompatBmiUsePrefix) + + escape_path(compatDst); } - auto traits = mcpp::toolchain::bmi_traits(plan.toolchain); std::string prebuilt_module_flag; if (traits.needsPrebuiltModulePath) { // Absolute path: a bare `pcm.cache` / `gcm.cache` works at ninja @@ -272,22 +281,32 @@ CompileFlags compute_flags(const BuildPlan& plan) { // resolution fails with `module 'X' not found`. The other // `-fmodule-file=` flags in this block are already escape_path'd // (absolute) for the same reason — this one was a leftover. - prebuilt_module_flag = std::format(" -fprebuilt-module-path={}", - escape_path(plan.outputDir / traits.bmiDir)); + prebuilt_module_flag = std::string(traits.bmiSearchPrefix) + + escape_path(plan.outputDir / traits.bmiDir); } std::string cxx_std_flag = - plan.cppStandardFlag.empty() ? std::string("-std=c++23") : plan.cppStandardFlag; + plan.cppStandardFlag.empty() + ? std::format("{}c++23", d.stdPrefix) : plan.cppStandardFlag; f.cxx = std::format("{}{}{}{}{}{}{}{}{}{}", cxx_std_flag, module_flag, std_module_flag, std_compat_module_flag, prebuilt_module_flag, opt_flag, pic_flag, compile_toolchain_flags, b_flag, include_flags); - f.cc = std::format("-std={}{}{}{}{}{}", c_std, opt_flag, pic_flag, compile_toolchain_flags, - b_flag, include_flags); + f.cc = std::format("{}{}{}{}{}{}{}", d.stdPrefix, c_std, opt_flag, pic_flag, + compile_toolchain_flags, b_flag, include_flags); // 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" : ""; - std::string static_stdlib = (f.staticStdlib && !isClang && !mcpp::platform::is_windows) ? " -static-libstdc++" : ""; + // Static C++ stdlib: a libstdc++ (GCC/MinGW) concern. On Windows a MinGW + // toolchain also statically links libgcc, so produced binaries don't + // depend on libstdc++-6.dll / libgcc_s DLLs from the toolchain dir + // (portable-by-default, same spirit as the macOS static-libc++ path). + std::string static_stdlib; + if (f.staticStdlib && caps.stdlib_id == "libstdc++") { + static_stdlib = " -static-libstdc++"; + if constexpr (mcpp::platform::is_windows) + static_stdlib += " -static-libgcc"; + } std::string runtime_dirs; if constexpr (mcpp::platform::supports_rpath) { // Toolchain runtime dirs (glibc/gcc) as before... @@ -320,7 +339,16 @@ CompileFlags compute_flags(const BuildPlan& plan) { if (prof.strip) link_extra += " -s"; if constexpr (mcpp::platform::is_windows) { - f.ld = user_ldflags + link_extra; + // 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 = + (caps.stdlib_id == "libstdc++" && f.linkage == "static") + ? " -static" : ""; + f.ld = std::format("{}{}{}{}{}", mingw_static, static_stdlib, b_flag, + 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): diff --git a/src/build/ninja_backend.cppm b/src/build/ninja_backend.cppm index 2559a47b..6b79c5ec 100644 --- a/src/build/ninja_backend.cppm +++ b/src/build/ninja_backend.cppm @@ -25,6 +25,8 @@ import mcpp.build.hermetic; import mcpp.build.compile_commands; import mcpp.dyndep; import mcpp.toolchain.detect; +import mcpp.toolchain.dialect; +import mcpp.toolchain.provider; import mcpp.toolchain.registry; import mcpp.xlings; import mcpp.platform; @@ -258,10 +260,12 @@ std::string emit_ninja_string(const BuildPlan& plan) { // dyndep requires P1689 scanning capability: // GCC: built-in -fdeps-format=p1689r5 // Clang: external clang-scan-deps tool (same P1689 output format) - bool has_scanner = mcpp::toolchain::is_gcc(plan.toolchain) - || !plan.scanDepsPath.empty(); + // (MSVC /scanDependencies is the future third driver — scanner design §3a) + auto caps = mcpp::toolchain::capabilities_for(plan.toolchain); + bool has_scanner = caps.has_builtin_p1689_scan || !plan.scanDepsPath.empty(); bool dyndep = dyndep_mode_enabled() && has_scanner; auto traits = mcpp::toolchain::bmi_traits(plan.toolchain); + const auto& dial = mcpp::toolchain::dialect_for(plan.toolchain); std::string out; auto append = [&](std::string s) { out += std::move(s); }; @@ -331,25 +335,30 @@ std::string emit_ninja_string(const BuildPlan& plan) { // Runtime library paths for private toolchain executables are scoped onto // the ninja subprocess instead of being emitted into each visible rule. + // Command spellings come from the toolchain's CommandDialect (gnu vs + // msvc); the rule *structure* is shared across compilers. std::string module_output_flag = traits.needsExplicitModuleOutput - ? " -fmodule-output=$bmi_out" : ""; + ? std::string(traits.moduleOutputPrefix) + "$bmi_out" : ""; + const std::string compile_tail = std::format( + "{} $in {}$out", dial.compileOnly, dial.outputObjPrefix); append("rule cxx_module\n"); if constexpr (mcpp::platform::is_windows) { // Windows: skip BMI restat optimization (requires POSIX shell). append(std::format(" command = " - "$cxx $local_includes $cxxflags $unit_cxxflags{} -c $in -o $out\n", module_output_flag)); + "$cxx $local_includes $cxxflags $unit_cxxflags{} {}\n", + module_output_flag, compile_tail)); } else { append(std::format(" command = " "if [ -n \"$bmi_out\" ] && [ -f \"$bmi_out\" ]; then " "cp -p \"$bmi_out\" \"$bmi_out.bak\"; " "fi && " - "$cxx $local_includes $cxxflags $unit_cxxflags{} -c $in -o $out && " + "$cxx $local_includes $cxxflags $unit_cxxflags{} {} && " "if [ -n \"$bmi_out\" ] && [ -f \"$bmi_out.bak\" ] && " "cmp -s \"$bmi_out\" \"$bmi_out.bak\"; then " "mv \"$bmi_out.bak\" \"$bmi_out\"; " "else " "rm -f \"$bmi_out.bak\"; " - "fi\n", module_output_flag)); + "fi\n", module_output_flag, compile_tail)); } append(" description = MOD $out\n"); if (dyndep) @@ -357,7 +366,9 @@ std::string emit_ninja_string(const BuildPlan& plan) { append("\n"); append("rule cxx_object\n"); - append(" command = $cxx $local_includes $cxxflags $unit_cxxflags -c $in -o $out\n"); + append(std::format( + " command = $cxx $local_includes $cxxflags $unit_cxxflags {}\n", + compile_tail)); append(" description = OBJ $out\n"); if (dyndep) append(" restat = 1\n"); @@ -365,19 +376,24 @@ std::string emit_ninja_string(const BuildPlan& plan) { if (need_c_rule) { append("rule c_object\n"); - append(" command = $cc $local_includes $cflags $unit_cflags -c $in -o $out\n"); + append(std::format( + " command = $cc $local_includes $cflags $unit_cflags {}\n", + compile_tail)); append(" description = CC $out\n"); if (dyndep) append(" restat = 1\n"); append("\n"); } + // Link rule: driver-style today (g++/clang++ act as the linker). The + // dialect's LinkStyle::SeparateLinker (link.exe /OUT: + rspfile) is the + // MSVC backend's insertion point — unreachable until that lands. append("rule cxx_link\n"); append(" command = $cxx $in -o $out $ldflags $unit_ldflags\n"); append(" description = LINK $out\n\n"); append("rule cxx_archive\n"); - append(" command = $ar rcs $out $in\n"); + append(std::format(" command = {}\n", dial.archiveCmd)); append(" description = AR $out\n\n"); append("rule cxx_shared\n"); @@ -811,6 +827,11 @@ std::expected NinjaBackend::build(const BuildPlan& plan std::vector> nenv; if (r.runtimeEnvKey != "-" && !r.runtimeEnvValue.empty()) nenv.emplace_back(r.runtimeEnvKey, r.runtimeEnvValue); + // Toolchain-declared env (empty for GCC/Clang; MSVC's INCLUDE/LIB/PATH). + // NOTE: not persisted in the fast-path cache yet — revisit when the MSVC + // backend lands (its fast path must re-derive these from detection). + for (auto& ev : plan.toolchain.envOverrides) + nenv.emplace_back(ev.key, ev.value); auto cap = mcpp::platform::process::capture_exec(nargv, nenv); std::string out = cap.output; diff --git a/src/toolchain/dialect.cppm b/src/toolchain/dialect.cppm new file mode 100644 index 00000000..591d560e --- /dev/null +++ b/src/toolchain/dialect.cppm @@ -0,0 +1,109 @@ +// mcpp.toolchain.dialect — command-line *spelling* per compiler family. +// +// The single data point for how a flag is spelt: GNU driver style +// (GCC / Clang / MinGW share one instance) vs MSVC cl.exe style. Consumers +// (flags.cppm, ninja_backend.cppm, plan.cppm) concatenate these fragments +// instead of hardcoding "-I"/"-o"/"ar rcs" — adding a compiler family is a +// second row of data, not another if/else at every call site. +// +// Deliberately a value-type aggregate, not a class hierarchy — same +// rationale as BmiTraits (2026-05-15-clang-parity-and-toolchain-abstraction +// §2.3): the differences are strings and booleans, not behavior. +// +// See .agents/docs/2026-07-13-toolchain-backend-abstraction-msvc-mingw-design.md §2.1. + +export module mcpp.toolchain.dialect; + +import std; +import mcpp.toolchain.model; + +export namespace mcpp::toolchain { + +struct CommandDialect { + std::string_view id; // "gnu" | "msvc" + + // Flag spellings. Prefixes are concatenated with their (already + // ninja-escaped) argument by the caller. + std::string_view includePrefix; // "-I" | "/I" + std::string_view definePrefix; // "-D" | "/D" + std::string_view stdPrefix; // "-std=" | "/std:" + std::string_view compileOnly; // "-c" | "/c" + std::string_view outputObjPrefix; // "-o " | "/Fo:" + std::string_view optPrefix; // "-O" | "/O" + std::string_view debugFlags; // "-g" | "/Zi /FS" + std::string_view alwaysFlags; // "" | "/nologo /EHsc /utf-8" + + // Artifact naming. + std::string_view objExt; // ".o" | ".obj" + + // Ninja integration. + // deps mode for header dependencies ("" = none today for gnu — module + // deps go through the P1689/dyndep pipeline instead). + std::string_view ninjaDepsMode; // "" | "msvc" + // Whether link/archive command lines must go through a response file + // (Windows 8191-char command-line limit; cl/link/lib accept @file). + bool rspfileLink = false; + + // Link step shape: the compiler driver links (g++/clang++ $in -o $out) + // or a separate linker is invoked (link.exe /OUT:). + enum class LinkStyle { Driver, SeparateLinker }; + LinkStyle linkStyle = LinkStyle::Driver; + + // Full ninja command template for static archives. + std::string_view archiveCmd; // "$ar rcs $out $in" | "$ar /nologo /OUT:$out $in" +}; + +// Dialect lookup. GCC / Clang / MinGW → gnu; MSVC → msvc. +const CommandDialect& dialect_for(const Toolchain& tc); + +} // namespace mcpp::toolchain + +namespace mcpp::toolchain { + +namespace { + +constexpr CommandDialect kGnuDialect{ + .id = "gnu", + .includePrefix = "-I", + .definePrefix = "-D", + .stdPrefix = "-std=", + .compileOnly = "-c", + .outputObjPrefix = "-o ", + .optPrefix = "-O", + .debugFlags = "-g", + .alwaysFlags = "", + .objExt = ".o", + .ninjaDepsMode = "", + .rspfileLink = false, + .linkStyle = CommandDialect::LinkStyle::Driver, + .archiveCmd = "$ar rcs $out $in", +}; + +// Native cl.exe. Unreachable in builds until the MSVC backend lands +// (prepare.cppm gates CompilerId::MSVC) — the row exists so the data model +// is complete and unit-tested ahead of that work. +constexpr CommandDialect kMsvcDialect{ + .id = "msvc", + .includePrefix = "/I", + .definePrefix = "/D", + .stdPrefix = "/std:", + .compileOnly = "/c", + .outputObjPrefix = "/Fo:", + .optPrefix = "/O", + .debugFlags = "/Zi /FS", + .alwaysFlags = "/nologo /EHsc /utf-8", + .objExt = ".obj", + .ninjaDepsMode = "msvc", + .rspfileLink = true, + .linkStyle = CommandDialect::LinkStyle::SeparateLinker, + .archiveCmd = "$ar /nologo /OUT:$out $in", +}; + +} // namespace + +const CommandDialect& dialect_for(const Toolchain& tc) { + if (tc.compiler == CompilerId::MSVC) return kMsvcDialect; + return kGnuDialect; +} + +} // namespace mcpp::toolchain diff --git a/src/toolchain/gcc.cppm b/src/toolchain/gcc.cppm index 5a9f5491..4191c30c 100644 --- a/src/toolchain/gcc.cppm +++ b/src/toolchain/gcc.cppm @@ -29,6 +29,14 @@ std::string std_module_build_command(const Toolchain& tc, std::string_view sysrootFlag, std::string_view cppStandardFlag); +// Normalized backend surface: same shape as clang::std_module_build_commands +// (a command sequence), so consumers don't branch on arity per compiler. +std::vector std_module_build_commands( + const Toolchain& tc, + const std::filesystem::path& cacheDir, + std::string_view sysrootFlag, + std::string_view cppStandardFlag); + } // namespace mcpp::toolchain::gcc namespace mcpp::toolchain::gcc { @@ -114,7 +122,7 @@ std::string std_module_build_command(const Toolchain& tc, std::string bFlag; if (!is_musl_target(tc)) { if (auto binutilsBin = find_binutils_bin(tc.binaryPath)) { - bFlag = std::format(" -B'{}'", binutilsBin->string()); + bFlag = std::format(" -B{}", mcpp::xlings::shq(binutilsBin->string())); } } @@ -129,4 +137,12 @@ std::string std_module_build_command(const Toolchain& tc, mcpp::xlings::shq(tc.stdModuleSource.string())); } +std::vector std_module_build_commands( + const Toolchain& tc, + const std::filesystem::path& cacheDir, + std::string_view sysrootFlag, + std::string_view cppStandardFlag) { + return { std_module_build_command(tc, cacheDir, sysrootFlag, cppStandardFlag) }; +} + } // namespace mcpp::toolchain::gcc diff --git a/src/toolchain/linkmodel.cppm b/src/toolchain/linkmodel.cppm index 1b2a7e91..973dffdb 100644 --- a/src/toolchain/linkmodel.cppm +++ b/src/toolchain/linkmodel.cppm @@ -228,6 +228,12 @@ ToolchainLinkModel resolve_link_model(const Toolchain& tc) { lm.clangDriver = is_clang(tc); lm.clangWithCfg = resolve_clang_driver(tc).hasCfg; + // Windows / PE: no ELF loader, no rpath, no glibc payload/sysroot model. + // MSVC-ABI Clang gets STL+SDK via the driver, MinGW is self-contained — + // both want CLibMode::None. Explicit early-out rather than relying on + // "no payloads found" falling through to the same answer. + if constexpr (mcpp::platform::is_windows) return lm; + auto payload_first = [&] { auto& pp = *tc.payloadPaths; lm.mode = CLibMode::PayloadFirst; diff --git a/src/toolchain/model.cppm b/src/toolchain/model.cppm index 288c8dcc..dde34f43 100644 --- a/src/toolchain/model.cppm +++ b/src/toolchain/model.cppm @@ -10,6 +10,12 @@ enum class CompilerId { Unknown, GCC, Clang, MSVC }; // Fine-grained sysroot paths derived from xpkgs payloads. // When populated, flags are assembled from these paths instead of --sysroot. +// One environment variable a toolchain needs at tool-invocation time. +struct EnvVar { + std::string key; + std::string value; +}; + struct PayloadPaths { std::filesystem::path glibcInclude; // glibc headers (features.h, bits/) std::filesystem::path glibcLib; // glibc runtime (libc.so, crt*.o, ld-linux) @@ -30,6 +36,13 @@ struct Toolchain { std::optional payloadPaths; // fine-grained sysroot from xpkgs std::vector compilerRuntimeDirs; // LD_LIBRARY_PATH for private tools std::vector linkRuntimeDirs; // -L/-rpath dirs for produced binaries + // Environment the toolchain's tools need when invoked (set on the ninja + // process, inherited by compiler/linker children). Empty for GCC/Clang + // (their LD_LIBRARY_PATH need goes through compilerRuntimeDirs); the + // MSVC backend fills INCLUDE/LIB/PATH here (design §5.1). + // (Own struct, not std::pair — GCC 16 modules choke on a std::pair + // member added to this exported class: "failed to load pendings".) + std::vector envOverrides; bool hasImportStd = false; std::string label() const { @@ -54,12 +67,18 @@ bool is_musl_target(const Toolchain& tc); bool is_msvc_target(const Toolchain& tc); struct BmiTraits { - std::string_view bmiDir; // "gcm.cache" | "pcm.cache" - std::string_view bmiExt; // ".gcm" | ".pcm" - std::string_view manifestPrefix; // "gcm" | "pcm" + std::string_view bmiDir; // "gcm.cache" | "pcm.cache" | "ifc.cache" + std::string_view bmiExt; // ".gcm" | ".pcm" | ".ifc" + std::string_view manifestPrefix; // "gcm" | "pcm" | "ifc" bool needsExplicitModuleOutput = false; bool needsPrebuiltModulePath = false; bool scanNeedsFModules = true; + // Module-flag spellings (leading space included; empty = not emitted). + std::string_view compileModulesFlag; // " -fmodules" (GCC) | "" + std::string_view stdBmiUsePrefix; // "" | " -fmodule-file=std=" | " /reference std=" + std::string_view stdCompatBmiUsePrefix; // "" | " -fmodule-file=std.compat=" | " /reference std.compat=" + std::string_view moduleOutputPrefix; // "" | " -fmodule-output=" | " /ifcOutput " + std::string_view bmiSearchPrefix; // "" | " -fprebuilt-module-path=" | " /ifcSearchDir " }; BmiTraits bmi_traits(const Toolchain& tc); @@ -93,8 +112,13 @@ BmiTraits bmi_traits(const Toolchain& tc) { .bmiExt = ".ifc", .manifestPrefix = "ifc", .needsExplicitModuleOutput = true, - .needsPrebuiltModulePath = false, + .needsPrebuiltModulePath = true, .scanNeedsFModules = false, + .compileModulesFlag = "", + .stdBmiUsePrefix = " /reference std=", + .stdCompatBmiUsePrefix = " /reference std.compat=", + .moduleOutputPrefix = " /ifcOutput ", + .bmiSearchPrefix = " /ifcSearchDir ", }; } if (is_clang(tc)) { @@ -105,6 +129,11 @@ BmiTraits bmi_traits(const Toolchain& tc) { .needsExplicitModuleOutput = true, .needsPrebuiltModulePath = true, .scanNeedsFModules = false, + .compileModulesFlag = "", + .stdBmiUsePrefix = " -fmodule-file=std=", + .stdCompatBmiUsePrefix = " -fmodule-file=std.compat=", + .moduleOutputPrefix = " -fmodule-output=", + .bmiSearchPrefix = " -fprebuilt-module-path=", }; } return { @@ -114,6 +143,13 @@ BmiTraits bmi_traits(const Toolchain& tc) { .needsExplicitModuleOutput = false, .needsPrebuiltModulePath = false, .scanNeedsFModules = true, + // GCC: -fmodules on every TU; BMIs implicit in cwd/gcm.cache, no + // std=/search flags needed. + .compileModulesFlag = " -fmodules", + .stdBmiUsePrefix = "", + .stdCompatBmiUsePrefix = "", + .moduleOutputPrefix = "", + .bmiSearchPrefix = "", }; } diff --git a/src/toolchain/provider.cppm b/src/toolchain/provider.cppm index ad3e4ae5..d3246e14 100644 --- a/src/toolchain/provider.cppm +++ b/src/toolchain/provider.cppm @@ -33,6 +33,11 @@ struct ProviderCapabilities { // alongside the compiler binary. Currently only Clang provides this. bool has_scan_deps = false; + // True when the compiler itself emits P1689 during preprocessing + // (GCC -fdeps-format=p1689r5). Clang needs the external clang-scan-deps; + // MSVC's /scanDependencies is compiler-integrated but not wired yet. + bool has_builtin_p1689_scan = false; + // True when the compiler supports C++ named modules at all. // All three supported compilers do; kept for future use when we add // compilers that don't (e.g. old MSVC versions, ICC). @@ -73,6 +78,7 @@ ProviderCapabilities capabilities_for(const Toolchain& tc) { switch (tc.compiler) { case CompilerId::GCC: { caps.has_scan_deps = false; // GCC has no clang-scan-deps equivalent + caps.has_builtin_p1689_scan = true; caps.stdlib_id = "libstdc++"; caps.archive_format = "ar"; break; diff --git a/src/toolchain/stdmod.cppm b/src/toolchain/stdmod.cppm index ecbb2277..8c309eb9 100644 --- a/src/toolchain/stdmod.cppm +++ b/src/toolchain/stdmod.cppm @@ -222,14 +222,13 @@ std::expected ensure_built( macos_deployment_target); } - std::vector stdCommands; - if (is_clang(tc)) { - stdCommands = mcpp::toolchain::clang::std_module_build_commands( - tc, sm.cacheDir, sm.bmiPath, sysroot_flag, cpp_standard_flag); - } else { - stdCommands.push_back(mcpp::toolchain::gcc::std_module_build_command( - tc, sm.cacheDir, sysroot_flag, cpp_standard_flag)); - } + // Both providers expose the same command-sequence shape (A5 backend + // surface normalization) — no per-compiler arity branching here. + std::vector stdCommands = is_clang(tc) + ? mcpp::toolchain::clang::std_module_build_commands( + tc, sm.cacheDir, sm.bmiPath, sysroot_flag, cpp_standard_flag) + : mcpp::toolchain::gcc::std_module_build_commands( + tc, sm.cacheDir, sysroot_flag, cpp_standard_flag); std::vector compatCommands; if (is_clang(tc) && !tc.stdCompatSource.empty()) { auto compatBmi = mcpp::toolchain::clang::std_compat_bmi_path(sm.cacheDir); From 2ba6303ca0122c5fa5abd30dff5860521f5dfc0c Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Mon, 13 Jul 2026 14:03:00 +0800 Subject: [PATCH 2/7] feat(toolchain): MinGW-w64 GCC via the xlings ecosystem (mingw@16.1.0) (0.0.89) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User-facing 'mingw' spec → xim:mingw-gcc (winlibs GCC 16.1.0 + MinGW-w64 14.0.0 UCRT standalone build, mirrored at xlings-res/mingw-gcc). Reuses the GCC backend end-to-end (gcm module pipeline, bits/std.cc import std); Windows adds static libstdc++/libgcc defaults so produced exes run standalone, and -static under linkage="static". archive_tool resolves the bundled ar.exe; std.cc lookup gains a version-dir scan fallback. Fixes ridden along: toolchain list 'Available' + partial-version resolution read xpkg versions for the HOST platform (was hardcoded linux — empty on win/mac); toolchain install no longer pulls glibc/linux-headers on win/mac. e2e 97 (requires: windows cap) + ci-windows MinGW step + unit tests (dialect rows, mingw spec mapping); docs + CHANGELOG; version 0.0.89. --- .github/workflows/ci-windows.yml | 10 +++ CHANGELOG.md | 30 +++++++++ docs/03-toolchains.md | 24 ++++++++ mcpp.toml | 2 +- src/toolchain/fingerprint.cppm | 2 +- src/toolchain/gcc.cppm | 15 +++++ src/toolchain/lifecycle.cppm | 17 +++++- src/toolchain/model.cppm | 6 ++ src/toolchain/registry.cppm | 29 ++++++++- tests/e2e/97_mingw_toolchain.sh | 72 ++++++++++++++++++++++ tests/e2e/run_all.sh | 1 + tests/unit/test_toolchain_dialect.cpp | 88 +++++++++++++++++++++++++++ 12 files changed, 290 insertions(+), 6 deletions(-) create mode 100755 tests/e2e/97_mingw_toolchain.sh create mode 100644 tests/unit/test_toolchain_dialect.cpp diff --git a/.github/workflows/ci-windows.yml b/.github/workflows/ci-windows.yml index 2325f70a..9468d0b5 100644 --- a/.github/workflows/ci-windows.yml +++ b/.github/workflows/ci-windows.yml @@ -220,6 +220,16 @@ jobs: cd hello_win "$MCPP_SELF" run + # MinGW-w64 GCC via the xlings ecosystem (xim:mingw-gcc → xlings-res + # winlibs mirror): install → default → modules build/run → standalone + # exe. Same flow as e2e 97 but as a visible CI step. Payload is cached + # via the mcpp sandbox cache after the first run. + - name: "Toolchain: MinGW — install → build → run (xim:mingw-gcc)" + shell: bash + run: | + export MCPP_VENDORED_XLINGS="$XLINGS_BIN" + MCPP="$MCPP_SELF" bash tests/e2e/97_mingw_toolchain.sh + # windows-latest ships VS 2022 Enterprise with the VC workload, so # msvc@system detection MUST succeed here — a failure is a regression # in the discovery/identification chain (vswhere → env → paths). diff --git a/CHANGELOG.md b/CHANGELOG.md index d45949bd..e2969fdf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,36 @@ > 本文件追踪 `mcpp-community/mcpp` 公开仓的版本演进。 > 格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)。 +## [0.0.89] — 2026-07-13 + +### 新增 + +- **MinGW-w64 工具链入 xlings 生态(Windows 原生 GCC,无需 Visual Studio)**。 + `mcpp toolchain install mingw 16.1.0` / `default mingw@16.1.0`:xim 包 + `mingw-gcc`(winlibs GCC 16.1.0 + MinGW-w64 14.0.0 UCRT 独立构建,镜像于 + xlings-res/mingw-gcc,GitHub+GitCode 双端)。复用既有 GCC 后端 + (gcm 模块管线、libstdc++ `bits/std.cc` 的 `import std`);Windows 上 + libstdc++/libgcc 默认静态链接(产物免带 DLL,`[build] static_stdlib=false` + 可关),`linkage="static"` 升级全静态。e2e 97 覆盖 install→default→ + 多模块 build/run→独立 exe 验证;ci-windows 新增专项步骤。 + 连带修复:`toolchain list` 的 Available 段与部分版本解析此前硬编码按 + "linux" 平台读取 xpkg 版本(Windows/macOS 上恒空);`toolchain install` + 在 Windows/macOS 不再错误安装 glibc/linux-headers 依赖。 + +### 重构 + +- **工具链后端抽象层(Part A,对 GCC/Clang 零行为变化,build.ninja 零 diff + 实证)**。新增 `mcpp.toolchain.dialect`(命令行拼写 traits:gnu/msvc 两行 + 数据,`-I/-D/-std=/-c/-o/-O/-g/ar` 及归档命令模板经其发射);`BmiTraits` + 并入模块旗标拼写(`compileModulesFlag`/`stdBmiUsePrefix`/ + `moduleOutputPrefix`/`bmiSearchPrefix`),flags.cppm 的 is_clang 分支改由 + 数据驱动;`ProviderCapabilities.has_builtin_p1689_scan` 取代 ninja 后端的 + is_gcc 门;`Toolchain::envOverrides`(EnvVar 表,注入 ninja 子进程环境, + 为 MSVC 后端 INCLUDE/LIB/PATH 预留);gcc provider 增加与 clang 同形的 + `std_module_build_commands`(命令序列);`resolve_link_model` 在 Windows + 显式返回 PE 空模型。设计:`.agents/docs/2026-07-13-toolchain-backend- + abstraction-msvc-mingw-design.md` + 同日触点审计文档。 + ## [0.0.88] — 2026-07-13 ### 新增 diff --git a/docs/03-toolchains.md b/docs/03-toolchains.md index 1ed8ef9a..5e6b8aaa 100644 --- a/docs/03-toolchains.md +++ b/docs/03-toolchains.md @@ -69,6 +69,30 @@ Available (run `mcpp toolchain install `): The entry marked with `*` is the current default toolchain. `@mcpp/...` is shorthand for `~/.mcpp/...`, used to keep the output narrower. +## MinGW (Windows-native GCC, no Visual Studio required) + +On Windows, `mingw` installs a self-contained MinGW-w64 GCC (winlibs +standalone build, UCRT runtime) into mcpp's sandbox — the same managed-xpkg +model as `gcc`/`llvm`, no Visual Studio needed: + +```bash +mcpp toolchain install mingw 16.1.0 +mcpp toolchain default mingw@16.1.0 +``` + +It uses the regular GCC module pipeline (`gcm.cache`, `import std` via +libstdc++'s `bits/std.cc`). Produced binaries statically link libstdc++ and +libgcc by default, so they run standalone — no `libstdc++-6.dll` needs to +travel next to your exe (opt out with `[build] static_stdlib = false`). +`[build] linkage = "static"` upgrades that to a fully static link. + +In a manifest: + +```toml +[toolchain] +windows = "mingw@16.1.0" +``` + ## MSVC (System Toolchain, Windows) MSVC is different from every other toolchain mcpp manages: it is a **system diff --git a/mcpp.toml b/mcpp.toml index e75db21d..4d6cd60d 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,6 +1,6 @@ [package] name = "mcpp" -version = "0.0.88" +version = "0.0.89" description = "Modern C++ build & package management tool" license = "Apache-2.0" authors = ["mcpp-community"] diff --git a/src/toolchain/fingerprint.cppm b/src/toolchain/fingerprint.cppm index cb12886f..40980922 100644 --- a/src/toolchain/fingerprint.cppm +++ b/src/toolchain/fingerprint.cppm @@ -18,7 +18,7 @@ import mcpp.toolchain.detect; export namespace mcpp::toolchain { -inline constexpr std::string_view MCPP_VERSION = "0.0.88"; +inline constexpr std::string_view MCPP_VERSION = "0.0.89"; struct FingerprintInputs { Toolchain toolchain; diff --git a/src/toolchain/gcc.cppm b/src/toolchain/gcc.cppm index 4191c30c..5e5fdfc0 100644 --- a/src/toolchain/gcc.cppm +++ b/src/toolchain/gcc.cppm @@ -70,6 +70,21 @@ std::optional find_std_module_source( auto p = root / "include" / "c++" / std::string(version) / "bits" / "std.cc"; if (std::filesystem::exists(p)) return p; + // 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 + // this installation's — take the first. + { + std::error_code ec; + auto cxxInclude = root / "include" / "c++"; + if (std::filesystem::exists(cxxInclude, ec)) { + for (auto& entry : std::filesystem::directory_iterator(cxxInclude, ec)) { + auto cand = entry.path() / "bits" / "std.cc"; + if (std::filesystem::exists(cand, ec)) return cand; + } + } + } + auto cmd = std::format("'{}' -print-file-name=libstdc++.so 2>/dev/null", cxx_binary.string()); auto r = mcpp::toolchain::run_capture(cmd); diff --git a/src/toolchain/lifecycle.cppm b/src/toolchain/lifecycle.cppm index 220ab1dc..13434a43 100644 --- a/src/toolchain/lifecycle.cppm +++ b/src/toolchain/lifecycle.cppm @@ -123,7 +123,15 @@ list_available_xpkg_versions(const mcpp::config::GlobalConfig& cfg, if (!std::filesystem::exists(p, ec)) return std::nullopt; std::ifstream is(p); std::string body((std::istreambuf_iterator(is)), {}); - return mcpp::manifest::list_xpkg_versions(body, "linux"); + // xpkg descriptors key platforms as linux / macosx / windows — list + // the HOST's block (previously hardcoded "linux", which made the + // Available section and partial-version resolution empty on + // Windows/macOS for any package, e.g. mingw-gcc has no linux block). + constexpr std::string_view xpkgPlatform = + mcpp::platform::is_windows ? "windows" + : mcpp::platform::is_macos ? "macosx" + : "linux"; + return mcpp::manifest::list_xpkg_versions(body, xpkgPlatform); }; auto data = cfg.xlingsHome() / "data"; @@ -388,8 +396,11 @@ 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. - if (!spec->isMusl) { + // musl-gcc is self-contained and doesn't need these; neither do + // Windows (llvm/mingw — PE, own CRT) or macOS (SDK) toolchains. + // Mirrors the platform guard on prepare.cppm's first-run install. + if (!spec->isMusl + && !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)); auto depPayload = fetcher.resolve_xpkg_path(dep, /*autoInstall=*/true, &progress); diff --git a/src/toolchain/model.cppm b/src/toolchain/model.cppm index dde34f43..cf585691 100644 --- a/src/toolchain/model.cppm +++ b/src/toolchain/model.cppm @@ -65,6 +65,7 @@ bool is_gcc(const Toolchain& tc); bool is_clang(const Toolchain& tc); bool is_musl_target(const Toolchain& tc); bool is_msvc_target(const Toolchain& tc); +bool is_mingw_target(const Toolchain& tc); struct BmiTraits { std::string_view bmiDir; // "gcm.cache" | "pcm.cache" | "ifc.cache" @@ -103,6 +104,11 @@ bool is_msvc_target(const Toolchain& tc) { return tc.targetTriple.find("msvc") != std::string::npos; } +bool is_mingw_target(const Toolchain& tc) { + // "x86_64-w64-mingw32" (mingw-w64) / legacy "*-pc-mingw32". + return tc.targetTriple.find("mingw32") != std::string::npos; +} + BmiTraits bmi_traits(const Toolchain& tc) { if (tc.compiler == CompilerId::MSVC) { // Native cl.exe builds are gated off until the .ifc pipeline lands; diff --git a/src/toolchain/registry.cppm b/src/toolchain/registry.cppm index d01a71a5..813e3c55 100644 --- a/src/toolchain/registry.cppm +++ b/src/toolchain/registry.cppm @@ -3,6 +3,7 @@ export module mcpp.toolchain.registry; import std; +import mcpp.platform; import mcpp.toolchain.clang; import mcpp.toolchain.gcc; import mcpp.toolchain.llvm; @@ -101,6 +102,7 @@ std::vector frontend_candidates_for(std::string_view ximName, if (ximName == "gcc") return {"g++"}; if (ximName == "llvm") return mcpp::toolchain::llvm::frontend_candidates(); if (ximName == "msvc") return {"cl.exe"}; + if (ximName == "mingw-gcc") return {"g++.exe", "g++"}; return {"g++", "clang++", "x86_64-linux-musl-g++", "cl.exe"}; } @@ -170,6 +172,10 @@ XimToolchainPackage to_xim_package(const ToolchainSpec& spec) { std::string ximCompiler = spec.compiler; if (mcpp::toolchain::llvm::is_alias(ximCompiler)) ximCompiler = mcpp::toolchain::llvm::package_name(); + // Windows-native MinGW-w64 GCC: user-facing name `mingw`, xim package + // `mingw-gcc` (winlibs GCC+MinGW-w64 UCRT builds mirrored at xlings-res). + if (ximCompiler == "mingw") + ximCompiler = "mingw-gcc"; pkg.ximName = ximCompiler; pkg.ximVersion = spec.version; @@ -221,6 +227,8 @@ std::filesystem::path toolchain_frontend(const std::filesystem::path& binDir, std::string display_label(std::string_view compiler, std::string_view version) { if (compiler == "musl-gcc") return std::format("gcc {}-musl", version); + if (compiler == "mingw-gcc") + return std::format("mingw {}", version); return std::format("{} {}", compiler, version); } @@ -235,6 +243,12 @@ bool matches_default_toolchain(std::string_view configuredDefault, // The persisted msvc default is always the stable "msvc@system" (never a // concrete version), so it matches whatever version detection reports. if (compiler == "msvc" && configuredDefault == "msvc@system") return true; + // Installed-payload rows report the xim name (mingw-gcc); the user-facing + // spec is mingw@ (same shape as the musl-gcc clause above). + if (compiler == "mingw-gcc" + && configuredDefault == std::format("mingw@{}", version)) { + return true; + } return false; } @@ -243,11 +257,15 @@ bool is_system_toolchain(const ToolchainSpec& spec) { } std::vector> available_toolchain_indexes() { - return { + std::vector> out{ {"gcc", "gcc"}, {"musl-gcc", "musl-gcc"}, {mcpp::toolchain::llvm::package_name(), "llvm"}, }; + // Windows-native toolchain — only meaningful to list on Windows hosts. + if constexpr (mcpp::platform::is_windows) + out.emplace_back("mingw-gcc", "mingw-gcc"); + return out; } std::filesystem::path derive_c_compiler(const Toolchain& tc) { @@ -257,6 +275,15 @@ std::filesystem::path derive_c_compiler(const Toolchain& tc) { 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. + if (is_mingw_target(tc)) { + auto ar = tc.binaryPath.parent_path() / "ar.exe"; + std::error_code ec; + if (std::filesystem::exists(ar, ec)) return ar; + return {}; + } + if (!is_musl_target(tc)) { if (auto binutilsBin = mcpp::toolchain::gcc::find_binutils_bin(tc.binaryPath)) return *binutilsBin / "ar"; diff --git a/tests/e2e/97_mingw_toolchain.sh b/tests/e2e/97_mingw_toolchain.sh new file mode 100755 index 00000000..ea8a9c97 --- /dev/null +++ b/tests/e2e/97_mingw_toolchain.sh @@ -0,0 +1,72 @@ +#!/usr/bin/env bash +# requires: windows +# 97_mingw_toolchain.sh — Windows-native MinGW-w64 GCC via the xlings +# ecosystem (xim:mingw-gcc, winlibs GCC 16.1.0 UCRT): +# - `toolchain install mingw 16.1.0` resolves + installs the xpkg +# - `toolchain default mingw@16.1.0` persists; `list` stars `mingw 16.1.0` +# - new → build → run works with import std + a named module (gcm pipeline) +# - the produced exe is portable: runs from a clean dir without the +# toolchain's bin on PATH (static libstdc++/libgcc defaults) +set -e + +CONF="${MCPP_HOME:-$HOME/.mcpp}/config.toml" +ORIG_DEFAULT="" +if [[ -f "$CONF" ]]; then + ORIG_DEFAULT=$(sed -n '/^\[toolchain\]/,/^\[/p' "$CONF" \ + | grep '^default' | head -1 | cut -d'"' -f2 || true) +fi +TMP=$(mktemp -d) +restore() { + if [[ -n "$ORIG_DEFAULT" ]]; then + "$MCPP" toolchain default "$ORIG_DEFAULT" >/dev/null 2>&1 || true + fi + rm -rf "$TMP" +} +trap restore EXIT + +cd "$TMP" # neutral cwd — project mcpp.toml [toolchain] must not shadow + +# 1) install via the xlings ecosystem (xlings-res mirror) +out=$("$MCPP" toolchain install mingw 16.1.0 2>&1) \ + || { echo "FAIL: install mingw: $out"; exit 1; } +[[ "$out" == *"Installed"* || "$out" == *"already"* || "$out" == *"mingw"* ]] \ + || { echo "FAIL: install output: $out"; exit 1; } + +# 2) switch default + list shows it starred +"$MCPP" toolchain default mingw@16.1.0 \ + || { echo "FAIL: default mingw@16.1.0"; exit 1; } +out=$("$MCPP" toolchain list 2>&1) +echo "$out" | grep -E '\*\s*mingw 16\.1\.0' >/dev/null \ + || { echo "FAIL: mingw not starred: $out"; exit 1; } + +# 3) real build: import std + named module through the gcm pipeline +"$MCPP" new hello_mingw >/dev/null 2>&1 +cd hello_mingw +mkdir -p src +cat > src/greet.cppm <<'EOF' +export module hello.greet; +import std; +export namespace hello { +std::string greet() { return "mingw-ok"; } +} +EOF +cat > src/main.cpp <<'EOF' +import std; +import hello.greet; +int main() { std::println("{}", hello::greet()); return 0; } +EOF +out=$("$MCPP" build 2>&1) || { echo "FAIL: build: $out"; exit 1; } +run_out=$("$MCPP" run 2>&1) || { echo "FAIL: run: $run_out"; exit 1; } +[[ "$run_out" == *"mingw-ok"* ]] || { echo "FAIL: run output: $run_out"; exit 1; } + +# 4) portability: the exe must run from a clean dir WITHOUT the toolchain +# bin dir on PATH (no libstdc++-6.dll / libgcc_s hitchhikers needed). +EXE=$(find target -name "hello_mingw.exe" -path "*/bin/*" | head -1) +[[ -n "$EXE" ]] || { echo "FAIL: no exe produced"; exit 1; } +ISO="$TMP/iso"; mkdir -p "$ISO" +cp "$EXE" "$ISO/" +iso_out=$(cd "$ISO" && PATH="/usr/bin:/c/Windows/System32" ./hello_mingw.exe 2>&1) \ + || { echo "FAIL: exe not standalone (static libstdc++/libgcc?): $iso_out"; exit 1; } +[[ "$iso_out" == *"mingw-ok"* ]] || { echo "FAIL: standalone output: $iso_out"; exit 1; } + +echo "PASS: mingw toolchain — install, default, modules build/run, standalone exe" diff --git a/tests/e2e/run_all.sh b/tests/e2e/run_all.sh index dade949b..19142076 100755 --- a/tests/e2e/run_all.sh +++ b/tests/e2e/run_all.sh @@ -63,6 +63,7 @@ case "$OS" in # Tests requiring gcc need actual GNU GCC (modules, gcm.cache, etc.) ;; MINGW* | MSYS* | CYGWIN*) + CAPS+=(windows) # Git Bash / MSYS2 on Windows: symlinks need admin or Developer Mode if [[ "${MSYS:-}" == *winsymlinks* ]] || cmd.exe /c "mklink /?" &>/dev/null 2>&1; then CAPS+=(symlink) diff --git a/tests/unit/test_toolchain_dialect.cpp b/tests/unit/test_toolchain_dialect.cpp new file mode 100644 index 00000000..4ca3d2a1 --- /dev/null +++ b/tests/unit/test_toolchain_dialect.cpp @@ -0,0 +1,88 @@ +#include + +import std; +import mcpp.toolchain.dialect; +import mcpp.toolchain.model; +import mcpp.toolchain.registry; + +using namespace mcpp::toolchain; + +namespace { +Toolchain make_tc(CompilerId id, std::string triple = {}) { + Toolchain tc; + tc.compiler = id; + tc.targetTriple = std::move(triple); + return tc; +} +} // namespace + +TEST(CommandDialect, GnuSpellings) { + auto tc = make_tc(CompilerId::GCC); + const auto& d = dialect_for(tc); + EXPECT_EQ(d.id, "gnu"); + EXPECT_EQ(d.includePrefix, "-I"); + EXPECT_EQ(d.stdPrefix, "-std="); + EXPECT_EQ(d.outputObjPrefix, "-o "); + EXPECT_EQ(d.objExt, ".o"); + EXPECT_EQ(d.archiveCmd, "$ar rcs $out $in"); + EXPECT_FALSE(d.rspfileLink); + EXPECT_EQ(d.linkStyle, CommandDialect::LinkStyle::Driver); + // Clang and MinGW share the gnu row. + EXPECT_EQ(dialect_for(make_tc(CompilerId::Clang)).id, "gnu"); + EXPECT_EQ(dialect_for(make_tc(CompilerId::GCC, "x86_64-w64-mingw32")).id, "gnu"); +} + +TEST(CommandDialect, MsvcSpellings) { + const auto& d = dialect_for(make_tc(CompilerId::MSVC)); + EXPECT_EQ(d.id, "msvc"); + EXPECT_EQ(d.includePrefix, "/I"); + EXPECT_EQ(d.stdPrefix, "/std:"); + EXPECT_EQ(d.outputObjPrefix, "/Fo:"); + EXPECT_EQ(d.objExt, ".obj"); + EXPECT_TRUE(d.rspfileLink); + EXPECT_EQ(d.linkStyle, CommandDialect::LinkStyle::SeparateLinker); +} + +TEST(BmiTraitsSpellings, ModuleFlagRowsAreConsistent) { + auto gcc = bmi_traits(make_tc(CompilerId::GCC)); + EXPECT_EQ(gcc.compileModulesFlag, " -fmodules"); + EXPECT_TRUE(gcc.stdBmiUsePrefix.empty()); + EXPECT_TRUE(gcc.moduleOutputPrefix.empty()); + + auto clang = bmi_traits(make_tc(CompilerId::Clang)); + EXPECT_TRUE(clang.compileModulesFlag.empty()); + EXPECT_EQ(clang.stdBmiUsePrefix, " -fmodule-file=std="); + EXPECT_EQ(clang.moduleOutputPrefix, " -fmodule-output="); + EXPECT_EQ(clang.bmiSearchPrefix, " -fprebuilt-module-path="); + + auto msvc = bmi_traits(make_tc(CompilerId::MSVC)); + EXPECT_EQ(msvc.stdBmiUsePrefix, " /reference std="); + EXPECT_EQ(msvc.moduleOutputPrefix, " /ifcOutput "); + EXPECT_EQ(msvc.bmiSearchPrefix, " /ifcSearchDir "); +} + +TEST(MingwSpec, MapsToMingwGccPackage) { + auto spec = parse_toolchain_spec("mingw@16.1.0"); + ASSERT_TRUE(spec.has_value()); + EXPECT_FALSE(is_system_toolchain(*spec)); + auto pkg = to_xim_package(*spec); + EXPECT_EQ(pkg.ximName, "mingw-gcc"); + EXPECT_EQ(pkg.ximVersion, "16.1.0"); + EXPECT_EQ(pkg.display_spec(), "mingw@16.1.0"); + ASSERT_FALSE(pkg.frontendCandidates.empty()); + EXPECT_EQ(pkg.frontendCandidates.front(), "g++.exe"); + EXPECT_FALSE(pkg.needsGccPostInstallFixup); +} + +TEST(MingwSpec, DisplayAndDefaultMatching) { + EXPECT_EQ(display_label("mingw-gcc", "16.1.0"), "mingw 16.1.0"); + EXPECT_TRUE(matches_default_toolchain("mingw@16.1.0", "mingw-gcc", "16.1.0")); + EXPECT_FALSE(matches_default_toolchain("mingw@16.1.0", "mingw-gcc", "15.1.0")); + EXPECT_FALSE(matches_default_toolchain("gcc@16.1.0", "mingw-gcc", "16.1.0")); +} + +TEST(MingwModel, TargetPredicate) { + EXPECT_TRUE(is_mingw_target(make_tc(CompilerId::GCC, "x86_64-w64-mingw32"))); + EXPECT_FALSE(is_mingw_target(make_tc(CompilerId::GCC, "x86_64-linux-gnu"))); + EXPECT_FALSE(is_mingw_target(make_tc(CompilerId::Clang, "x86_64-pc-windows-msvc"))); +} From b52bff15514e212445f035289664db3074fa009b Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Mon, 13 Jul 2026 14:34:09 +0800 Subject: [PATCH 3/7] fix(linkmodel): key the PE early-out on the target, not the host The Windows-host if-constexpr broke the linkmodel contract tests on Windows CI (they resolve synthetic Linux-shaped toolchains anywhere). Target-keyed is also the semantics a future cross build needs. --- src/toolchain/linkmodel.cppm | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/toolchain/linkmodel.cppm b/src/toolchain/linkmodel.cppm index 973dffdb..97366ad5 100644 --- a/src/toolchain/linkmodel.cppm +++ b/src/toolchain/linkmodel.cppm @@ -228,11 +228,12 @@ ToolchainLinkModel resolve_link_model(const Toolchain& tc) { lm.clangDriver = is_clang(tc); lm.clangWithCfg = resolve_clang_driver(tc).hasCfg; - // Windows / PE: no ELF loader, no rpath, no glibc payload/sysroot model. + // PE targets: no ELF loader, no rpath, no glibc payload/sysroot model. // MSVC-ABI Clang gets STL+SDK via the driver, MinGW is self-contained — - // both want CLibMode::None. Explicit early-out rather than relying on - // "no payloads found" falling through to the same answer. - if constexpr (mcpp::platform::is_windows) return lm; + // both want CLibMode::None. Keyed on the TARGET (not the host) so the + // ELF resolution below stays testable anywhere and a future + // cross-compile resolves by what it builds FOR. + if (is_msvc_target(tc) || is_mingw_target(tc)) return lm; auto payload_first = [&] { auto& pp = *tc.payloadPaths; From 1a9efbfb0c181433c7784ba637f6ebf26a37a4df Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Mon, 13 Jul 2026 14:50:55 +0800 Subject: [PATCH 4/7] fix(flags): MinGW links libstdc++exp for std::print terminal symbols MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit std::vprint_unicode's __open_terminal/__write_to_terminal live in libstdc++exp.a on Windows-GCC targets — first real MinGW link on CI failed with undefined references from bits/print.h. --- src/build/flags.cppm | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/build/flags.cppm b/src/build/flags.cppm index 9f374ae6..f399192b 100644 --- a/src/build/flags.cppm +++ b/src/build/flags.cppm @@ -344,11 +344,17 @@ CompileFlags compute_flags(const BuildPlan& plan) { // 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 = - (caps.stdlib_id == "libstdc++" && f.linkage == "static") - ? " -static" : ""; - f.ld = std::format("{}{}{}{}{}", mingw_static, static_stdlib, b_flag, - user_ldflags, link_extra); + std::string mingw_static; + std::string mingw_stdexp; + if (caps.stdlib_id == "libstdc++") { + if (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); } 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): From e7ae19e1395147ae0c646bacd64f70f314d5f6e2 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Mon, 13 Jul 2026 15:09:25 +0800 Subject: [PATCH 5/7] fix(flags): force winpthread static on MinGW links MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit winlibs POSIX-threads libstdc++ otherwise leaves produced exes depending on libwinpthread-1.dll — the standalone-exe e2e caught it (empty output, missing-DLL abort). -Wl,-Bstatic -lwinpthread -Wl,-Bdynamic is the standard recipe; libwinpthread.a ships in the payload. --- src/build/flags.cppm | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/build/flags.cppm b/src/build/flags.cppm index f399192b..fd8e56ad 100644 --- a/src/build/flags.cppm +++ b/src/build/flags.cppm @@ -350,8 +350,11 @@ CompileFlags compute_flags(const BuildPlan& plan) { if (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"; + // Windows targets — plain -lstdc++ leaves them undefined. And + // POSIX-threads libstdc++ (winlibs) pulls libwinpthread-1.dll + // unless winpthread is forced static — the classic third DLL + // that -static-libstdc++/-static-libgcc do NOT cover. + mingw_stdexp = " -lstdc++exp -Wl,-Bstatic -lwinpthread -Wl,-Bdynamic"; } f.ld = std::format("{}{}{}{}{}{}", mingw_static, static_stdlib, b_flag, user_ldflags, mingw_stdexp, link_extra); From 485b0c3e20bf50ec58f24daa939ae0f099957eaf Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Mon, 13 Jul 2026 15:25:47 +0800 Subject: [PATCH 6/7] test(e2e): 97 asserts the exe import table (objdump) + diagnostic output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The silent standalone-run failure gave nothing to debug — inspect DLL imports directly and dump them on any failure. --- tests/e2e/97_mingw_toolchain.sh | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/tests/e2e/97_mingw_toolchain.sh b/tests/e2e/97_mingw_toolchain.sh index ea8a9c97..f897ab69 100755 --- a/tests/e2e/97_mingw_toolchain.sh +++ b/tests/e2e/97_mingw_toolchain.sh @@ -59,14 +59,27 @@ out=$("$MCPP" build 2>&1) || { echo "FAIL: build: $out"; exit 1; } run_out=$("$MCPP" run 2>&1) || { echo "FAIL: run: $run_out"; exit 1; } [[ "$run_out" == *"mingw-ok"* ]] || { echo "FAIL: run output: $run_out"; exit 1; } -# 4) portability: the exe must run from a clean dir WITHOUT the toolchain -# bin dir on PATH (no libstdc++-6.dll / libgcc_s hitchhikers needed). +# 4) portability: the exe's import table must not reference toolchain DLLs +# (libstdc++-6 / libgcc_s / libwinpthread — static_stdlib defaults), and +# it must run from a clean dir without the toolchain bin on PATH. EXE=$(find target -name "hello_mingw.exe" -path "*/bin/*" | head -1) [[ -n "$EXE" ]] || { echo "FAIL: no exe produced"; exit 1; } +OBJDUMP="${MCPP_HOME:-$HOME/.mcpp}/registry/data/xpkgs/xim-x-mingw-gcc/16.1.0/bin/objdump.exe" +imports="" +if [[ -x "$OBJDUMP" ]]; then + imports=$("$OBJDUMP" -p "$EXE" 2>/dev/null | grep -i "DLL Name" || true) + echo "exe imports:"; echo "$imports" + bad=$(echo "$imports" | grep -iE "libstdc|libgcc|libwinpthread" || true) + [[ -z "$bad" ]] || { echo "FAIL: toolchain DLLs in import table: $bad"; exit 1; } +fi ISO="$TMP/iso"; mkdir -p "$ISO" cp "$EXE" "$ISO/" -iso_out=$(cd "$ISO" && PATH="/usr/bin:/c/Windows/System32" ./hello_mingw.exe 2>&1) \ - || { echo "FAIL: exe not standalone (static libstdc++/libgcc?): $iso_out"; exit 1; } -[[ "$iso_out" == *"mingw-ok"* ]] || { echo "FAIL: standalone output: $iso_out"; exit 1; } +iso_rc=0 +iso_out=$(cd "$ISO" && PATH="/usr/bin:/c/Windows/System32" ./hello_mingw.exe 2>&1) || iso_rc=$? +if [[ $iso_rc -ne 0 || "$iso_out" != *"mingw-ok"* ]]; then + echo "FAIL: standalone run rc=$iso_rc out='$iso_out'" + echo "$imports" + exit 1 +fi echo "PASS: mingw toolchain — install, default, modules build/run, standalone exe" From d679297cbff8cfd5dfd9e4b2a42ceba407eb5d61 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Mon, 13 Jul 2026 15:44:09 +0800 Subject: [PATCH 7/7] =?UTF-8?q?fix(flags):=20MinGW=20default=20links=20-st?= =?UTF-8?q?atic=20=E2=80=94=20the=20piecemeal=20winpthread=20recipe=20lose?= =?UTF-8?q?s=20to=20driver=20implicit=20libs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI import table still showed libwinpthread-1.dll after -Wl,-Bstatic -lwinpthread; winlibs' documented answer for standalone exes is a full -static link (system DLLs still resolve via import libs). Gated on staticStdlib so [build] static_stdlib=false opts out. --- src/build/flags.cppm | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/build/flags.cppm b/src/build/flags.cppm index fd8e56ad..094ec9d1 100644 --- a/src/build/flags.cppm +++ b/src/build/flags.cppm @@ -347,14 +347,19 @@ CompileFlags compute_flags(const BuildPlan& plan) { std::string mingw_static; std::string mingw_stdexp; if (caps.stdlib_id == "libstdc++") { - if (f.linkage == "static") mingw_static = " -static"; + // `-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. And - // POSIX-threads libstdc++ (winlibs) pulls libwinpthread-1.dll - // unless winpthread is forced static — the classic third DLL - // that -static-libstdc++/-static-libgcc do NOT cover. - mingw_stdexp = " -lstdc++exp -Wl,-Bstatic -lwinpthread -Wl,-Bdynamic"; + // 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);