From 47e45d66ac83aef5b248f0a38666bb6ad3e6f3c9 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Mon, 13 Jul 2026 13:57:22 +0800 Subject: [PATCH 1/8] 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/8] 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 ab96534e2c44413148169cc027b43783ecee878f Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Mon, 13 Jul 2026 14:25:52 +0800 Subject: [PATCH 3/8] ci(temp): strip workflows except ci-windows + release (e2e 45 asserts release.yml) --- .github/workflows/aur-publish.yml | 102 ----- .github/workflows/bootstrap-macos.yml | 146 ------ .../workflows/ci-aarch64-fresh-install.yml | 102 ----- .github/workflows/ci-fresh-install.yml | 379 ---------------- .github/workflows/ci-linux-e2e.yml | 192 -------- .github/workflows/ci-linux.yml | 168 ------- .github/workflows/ci-macos.yml | 421 ------------------ .github/workflows/cross-build-test.yml | 165 ------- 8 files changed, 1675 deletions(-) delete mode 100644 .github/workflows/aur-publish.yml delete mode 100644 .github/workflows/bootstrap-macos.yml delete mode 100644 .github/workflows/ci-aarch64-fresh-install.yml delete mode 100644 .github/workflows/ci-fresh-install.yml delete mode 100644 .github/workflows/ci-linux-e2e.yml delete mode 100644 .github/workflows/ci-linux.yml delete mode 100644 .github/workflows/ci-macos.yml delete mode 100644 .github/workflows/cross-build-test.yml diff --git a/.github/workflows/aur-publish.yml b/.github/workflows/aur-publish.yml deleted file mode 100644 index 661cb893..00000000 --- a/.github/workflows/aur-publish.yml +++ /dev/null @@ -1,102 +0,0 @@ -name: aur-publish - -# Publish the `mcpp-bin` and `mcpp` AUR packages after a release. -# -# Triggers on COMPLETION of the `release` workflow (not on `release: -# published`): release.yml creates the GitHub Release in its first job but -# uploads the aarch64 / macOS / Windows assets in LATER jobs, so the aarch64 -# .sha256 that mcpp-bin needs only exists once the whole workflow finishes. -# -# Requires one repository secret: -# AUR_SSH_PRIVATE_KEY — private key whose public half is registered on the -# AUR account that owns mcpp / mcpp-bin. -# See scripts/aur/README.md → "Automated publishing" for the full setup. -on: - workflow_run: - workflows: [release] - types: [completed] - workflow_dispatch: - inputs: - version: - description: "Version to publish (default: [package].version in mcpp.toml)" - required: false - -concurrency: - group: aur-publish - cancel-in-progress: false - -jobs: - publish: - runs-on: ubuntu-latest - # On the workflow_run trigger, only proceed if the release actually - # succeeded (skip failed/cancelled release runs). - if: >- - github.event_name == 'workflow_dispatch' || - github.event.workflow_run.conclusion == 'success' - steps: - - name: Checkout released commit - uses: actions/checkout@v4 - with: - # workflow_run: the exact commit the release was built from. - # workflow_dispatch: default ref (HEAD of the branch). - ref: ${{ github.event.workflow_run.head_sha || github.ref }} - - - name: Refresh both PKGBUILDs to the release version - id: refresh - env: - # CI runs as root; force update.sh's template .SRCINFO path. - MCPP_AUR_NO_MAKEPKG: "1" - run: | - VER="${{ github.event.inputs.version }}" - if [ -z "$VER" ]; then - # mcpp.toml at the released commit carries the right version. - VER=$(grep -m1 -E '^\s*version\s*=' mcpp.toml | sed -E 's/.*"([^"]+)".*/\1/') - fi - echo "version=$VER" >> "$GITHUB_OUTPUT" - ./scripts/aur/update.sh "$VER" - - - name: Configure AUR SSH - run: | - install -dm700 ~/.ssh - printf '%s\n' "${{ secrets.AUR_SSH_PRIVATE_KEY }}" > ~/.ssh/aur - chmod 600 ~/.ssh/aur - ssh-keyscan -t rsa,ed25519 aur.archlinux.org >> ~/.ssh/known_hosts 2>/dev/null - cat > ~/.ssh/config <<'EOF' - Host aur.archlinux.org - User aur - IdentityFile ~/.ssh/aur - IdentitiesOnly yes - EOF - - - name: Push to the AUR - env: - VER: ${{ steps.refresh.outputs.version }} - run: | - set -eu - git config --global user.name "mcpp-ci" - git config --global user.email "x.d2learn.org@gmail.com" - - publish() { # $1 = package name (= dir under scripts/aur/) - pkg="$1"; src="scripts/aur/${pkg}"; work="/tmp/aur-${pkg}" - # Clone the existing AUR repo; if the package doesn't exist yet - # (first publish), start an empty repo — AUR creates it on push. - if git clone "ssh://aur@aur.archlinux.org/${pkg}.git" "$work" 2>/dev/null \ - && [ -e "$work/.git" ]; then :; else - rm -rf "$work"; mkdir -p "$work" - git -C "$work" init -q - git -C "$work" remote add origin "ssh://aur@aur.archlinux.org/${pkg}.git" - fi - # AUR repos contain only PKGBUILD, .SRCINFO and local sources. - cp "$src/PKGBUILD" "$src/.SRCINFO" "$src/mcpp.sh" "$work/" - git -C "$work" add -A - if git -C "$work" diff --cached --quiet; then - echo ":: ${pkg}: no changes, skipping" - return 0 - fi - git -C "$work" commit -q -m "${pkg} ${VER}" - git -C "$work" push origin HEAD:master - echo ":: ${pkg}: published ${VER}" - } - - publish mcpp-bin - publish mcpp-m diff --git a/.github/workflows/bootstrap-macos.yml b/.github/workflows/bootstrap-macos.yml deleted file mode 100644 index 33fffc4d..00000000 --- a/.github/workflows/bootstrap-macos.yml +++ /dev/null @@ -1,146 +0,0 @@ -name: bootstrap-macos - -# One-shot workflow to produce the first macOS mcpp binary. -# Uses xmake + xlings LLVM to compile mcpp from source. -# Once a macOS binary exists, mcpp can self-host for future releases. - -on: - workflow_dispatch: - -jobs: - bootstrap: - name: Bootstrap mcpp (macOS ARM64) - runs-on: macos-15 - timeout-minutes: 30 - env: - XLINGS_NON_INTERACTIVE: '1' - XLINGS_VERSION: '0.4.30' - steps: - - uses: actions/checkout@v4 - - - name: System info - run: | - uname -a - sw_vers - xcrun --show-sdk-path - - - name: Install xlings - run: | - WORK=$(mktemp -d) - tarball="xlings-${XLINGS_VERSION}-macosx-arm64.tar.gz" - curl -fsSL -o "${WORK}/${tarball}" \ - "https://github.com/d2learn/xlings/releases/download/v${XLINGS_VERSION}/${tarball}" - tar -xzf "${WORK}/${tarball}" -C "${WORK}" - "${WORK}/xlings-${XLINGS_VERSION}-macosx-arm64/subos/default/bin/xlings" self install - echo "$HOME/.xlings/subos/default/bin" >> "$GITHUB_PATH" - echo "$HOME/.xlings/bin" >> "$GITHUB_PATH" - - - name: Install LLVM + xmake - run: | - xlings install llvm -y || xlings install llvm@20.1.7 -y - brew install xmake - LLVM_ROOT=$(find "$HOME/.xlings" -path "*/xpkgs/xim-x-llvm/*/bin/clang++" | head -1 | xargs dirname | xargs dirname) - echo "LLVM_ROOT=$LLVM_ROOT" >> "$GITHUB_ENV" - "$LLVM_ROOT/bin/clang++" --version - xmake --version - - - name: Build mcpp with xmake - run: | - # Generate xmake.lua if not present - if [ ! -f xmake.lua ]; then - cat > xmake.lua << 'EOF' - add_rules("mode.release") - set_languages("c++23") - - package("cmdline") - set_homepage("https://github.com/mcpplibs/cmdline") - set_description("Modern C++ command-line parsing library") - set_license("Apache-2.0") - add_urls("https://github.com/mcpplibs/cmdline/archive/refs/tags/$(version).tar.gz") - add_versions("0.0.1", "3fb2f5495c1a144485b3cbb2e43e27059151633460f702af0f3851cbff387ef0") - on_install(function (package) - import("package.tools.xmake").install(package) - end) - package_end() - - add_requires("cmdline 0.0.1") - - target("mcpp") - set_kind("binary") - add_files("src/main.cpp") - add_files("src/**.cppm") - add_packages("cmdline") - add_includedirs("src/libs/json") - set_policy("build.c++.modules", true) - -- Static link libc++ for minimal runtime dependencies - add_ldflags("-static-libstdc++", {force = true}) - add_cxxflags("-stdlib=libc++", {force = true}) - add_ldflags("-stdlib=libc++", {force = true}) - EOF - fi - - # Configure with xlings LLVM - xmake f -y -m release --toolchain=llvm --sdk="$LLVM_ROOT" - # Build - xmake build -y mcpp - - - name: Verify built binary - run: | - MCPP=$(find build -name mcpp -type f -perm +111 | head -1) - test -x "$MCPP" - echo "=== file ===" - file "$MCPP" - echo "=== otool -L (dynamic deps) ===" - otool -L "$MCPP" - echo "=== version ===" - "$MCPP" --version - echo "MCPP=$MCPP" >> "$GITHUB_ENV" - - - name: Package - id: package - run: | - VERSION=$(awk -F '"' '/^version[[:space:]]*=/{print $2; exit}' mcpp.toml) - TARBALL="mcpp-${VERSION}-macosx-arm64.tar.gz" - WRAPPER="mcpp-${VERSION}-macosx-arm64" - - mkdir -p "dist/$WRAPPER/bin" - cp "$MCPP" "dist/$WRAPPER/bin/mcpp" - strip "dist/$WRAPPER/bin/mcpp" 2>/dev/null || true - cp LICENSE "dist/$WRAPPER/" 2>/dev/null || true - cp README.md "dist/$WRAPPER/" 2>/dev/null || true - - cat > "dist/$WRAPPER/mcpp" << 'LAUNCHER' - #!/bin/sh - exec "$(dirname "$0")/bin/mcpp" "$@" - LAUNCHER - chmod +x "dist/$WRAPPER/mcpp" - - # Bundle xlings - XLINGS_BIN="$HOME/.xlings/subos/default/bin/xlings" - if [ -x "$XLINGS_BIN" ]; then - mkdir -p "dist/$WRAPPER/registry/bin" - cp "$XLINGS_BIN" "dist/$WRAPPER/registry/bin/xlings" - fi - - (cd dist && tar -czf "$TARBALL" "$WRAPPER") - (cd dist && shasum -a 256 "$TARBALL" > "$TARBALL.sha256") - - echo "tarball=dist/$TARBALL" >> "$GITHUB_OUTPUT" - echo "version=$VERSION" >> "$GITHUB_OUTPUT" - ls -la dist/ - - - name: Smoke test - run: | - SMOKE=$(mktemp -d) - tar -xzf "${{ steps.package.outputs.tarball }}" -C "$SMOKE" - VERSION="${{ steps.package.outputs.version }}" - "$SMOKE/mcpp-${VERSION}-macosx-arm64/bin/mcpp" --version - "$SMOKE/mcpp-${VERSION}-macosx-arm64/mcpp" --version - - - name: Upload artifact - uses: actions/upload-artifact@v4 - with: - name: mcpp-macosx-arm64 - path: | - dist/mcpp-*-macosx-arm64.tar.gz - dist/mcpp-*-macosx-arm64.tar.gz.sha256 diff --git a/.github/workflows/ci-aarch64-fresh-install.yml b/.github/workflows/ci-aarch64-fresh-install.yml deleted file mode 100644 index 5f6b6e7c..00000000 --- a/.github/workflows/ci-aarch64-fresh-install.yml +++ /dev/null @@ -1,102 +0,0 @@ -name: ci-aarch64-fresh-install - -# End-to-end "fresh install" of the whole ecosystem on a NATIVE aarch64 host, -# exactly as a new aarch64-Linux / Termux(-proot) user would: -# -# curl quick_install.sh | bash -> installs aarch64 xlings (static musl) -# xlings install mcpp -> installs aarch64 mcpp (static musl) -# mcpp new / build / run -> NATIVE aarch64 build (pulls the native -# musl-gcc toolchain from the ecosystem) -# -# Validates that every published aarch64 asset (xlings, mcpp, musl-gcc) lines -# up and that mcpp can build & run a real `import std` program natively on -# aarch64 — no cross, no qemu. Runs on GitHub's native ARM64 runner. - -on: - workflow_dispatch: - schedule: - - cron: '0 6 * * 1' # weekly Mon 06:00 UTC - push: - branches: [ main ] - paths: - - '.github/workflows/ci-aarch64-fresh-install.yml' - -concurrency: - group: ci-${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - fresh-install: - name: fresh install + native build (aarch64 / glibc) - runs-on: ubuntu-24.04-arm - timeout-minutes: 60 - env: - # Verbose every mcpp invocation — cold bootstrap path (src/cli.cppm). - MCPP_VERBOSE: "1" - steps: - - name: System info - run: | - uname -a - echo "arch: $(uname -m)" # aarch64 on this runner - - - name: Fresh-install xlings (curl | bash) - env: - XLINGS_NON_INTERACTIVE: '1' - run: | - curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh | bash - echo "$HOME/.xlings/subos/current/bin" >> "$GITHUB_PATH" - echo "$HOME/.xlings/bin" >> "$GITHUB_PATH" - - - name: Verify xlings + GLOBAL mirror - run: | - xlings --version - xlings config --mirror GLOBAL 2>/dev/null || true - xlings update -y 2>/dev/null || xlings update 2>/dev/null || true - - - name: Fresh-install mcpp via xlings - run: | - xlings install mcpp -y - mcpp --version - mcpp self config --mirror GLOBAL 2>/dev/null || true - - - name: Refresh mcpp package index (force latest xim-pkgindex) - run: | - # mcpp seeds a baseline index with a freshness TTL marker, so a plain - # `index update` can no-op within the window. Force the latest index - # so this run validates the native build against current packages. - mcpp index update || true - idx="$HOME/.mcpp/registry/data/xim-pkgindex" - rm -rf "$idx" - git clone --depth 1 https://github.com/openxlings/xim-pkgindex "$idx" - grep -n "skipping relocation\|os.isfile(path.join(bindir" "$idx/pkgs/m/musl-gcc.lua" | head -2 || true - - - name: Native build + run an `import std` program - run: | - work=$(mktemp -d); cd "$work" - mcpp new hello - cd hello - # default src uses import std (C++23) - mcpp build - out=$(mcpp run 2>/dev/null || true) - echo "program output: $out" - bin=$(find target -type f -name hello | head -1) - file "$bin" - file "$bin" | grep -q "ARM aarch64" || { echo "expected aarch64 ELF"; exit 1; } - - - name: Self-host — build mcpp + xlings from source natively - run: | - # mcpp/xlings manifests pin a glibc default toolchain; on aarch64 the - # musl-static target is the published path, so build with --target. - git clone --depth 1 https://github.com/mcpp-community/mcpp /tmp/mcpp-src - cd /tmp/mcpp-src - mcpp self config --mirror GLOBAL 2>/dev/null || true - mcpp build --target aarch64-linux-musl - m=$(find target/aarch64-linux-musl -type f -name mcpp | head -1) - file "$m" | grep -q "ARM aarch64" || { echo "expected aarch64 mcpp"; exit 1; } - "$m" --version - git clone --depth 1 https://github.com/openxlings/xlings /tmp/xlings-src - cd /tmp/xlings-src - mcpp build --target aarch64-linux-musl - x=$(find target/aarch64-linux-musl -type f -name xlings | head -1) - file "$x" | grep -q "ARM aarch64" || { echo "expected aarch64 xlings"; exit 1; } - "$x" --version diff --git a/.github/workflows/ci-fresh-install.yml b/.github/workflows/ci-fresh-install.yml deleted file mode 100644 index bd5649c8..00000000 --- a/.github/workflows/ci-fresh-install.yml +++ /dev/null @@ -1,379 +0,0 @@ -name: ci-fresh-install - -# Fresh install CI — validates the released mcpp binary via xlings. -# Simulates a real first-time user on a clean machine (no caches). -# -# For each platform, tests every supported toolchain: -# 1. mcpp new hello → mcpp run (basic project) -# 2. mcpp build (build mcpp itself from source) -# -# This workflow tests released mcpp, not PR code. -# It runs on release publish, manual trigger, and daily schedule. - -on: - # NOTE: `release: published` never fires from the release pipeline — the - # release is created by release.yml with GITHUB_TOKEN, and GitHub - # suppresses workflow triggers from GITHUB_TOKEN-generated events. Kept - # only for releases created manually outside the pipeline. The reliable - # post-release hook is `workflow_run` below: a platform-generated event, - # exempt from that suppression, and requiring no cross-repo PAT. - release: - types: [ published ] - workflow_run: - workflows: [ release ] - types: [ completed ] - workflow_dispatch: - schedule: - # Run daily at 06:00 UTC to catch issues from xlings/runner updates - - cron: '0 6 * * *' - -concurrency: - group: ci-fresh-install - cancel-in-progress: false # use false to test in PRs, true to only test released mcpp - -jobs: - # ────────────────────────────────────────────────────────────────── - # Linux: gcc@16.1.0, musl-gcc@16.1.0, llvm@20.1.7 - # ────────────────────────────────────────────────────────────────── - # A4: post-release runs race the xim-pkgindex bump (a PR a maintainer - # merges asynchronously) — the 0.0.85 fresh-install failed with - # "version not found: available 0.0.84" 55s after the bump merged. - # Convert the race into a bounded wait: poll the index's mcpp.lua until - # it carries the released version (<=15 min), then let every job run. - # Non-workflow_run triggers (manual, cron, PR) skip the wait. - wait-index: - runs-on: ubuntu-latest - if: ${{ github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' }} - timeout-minutes: 20 - steps: - - name: Wait for xim-pkgindex to track the released mcpp - if: ${{ github.event_name == 'workflow_run' }} - run: | - VER=$(curl -fsSL "https://api.github.com/repos/mcpp-community/mcpp/releases/latest" | python3 -c "import json,sys; print(json.load(sys.stdin)['tag_name'].lstrip('v'))") - echo "released: $VER — waiting for index..." - for i in $(seq 1 30); do - if curl -fsSL "https://raw.githubusercontent.com/openxlings/xim-pkgindex/main/pkgs/m/mcpp.lua" | grep -q "\"$VER\""; then - echo "index tracks $VER (after $((i*30))s)"; exit 0 - fi - sleep 30 - done - echo "::error::index never tracked $VER within 15min — merge the bump PR (openxlings/xim-pkgindex) and re-run" - exit 1 - - name: No wait needed (manual/cron trigger) - if: ${{ github.event_name != 'workflow_run' }} - run: echo "not a post-release run; skipping index wait" - - linux-fresh: - needs: [wait-index] - name: Linux fresh install - if: ${{ github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' }} - runs-on: ubuntu-24.04 - timeout-minutes: 60 - env: - # Verbose every mcpp invocation — fresh-install is the cold index/sandbox - # bootstrap path, exactly where extra diagnostics matter (src/cli.cppm). - MCPP_VERBOSE: "1" - steps: - - uses: actions/checkout@v4 - - - name: Install xlings + mcpp - env: - XLINGS_NON_INTERACTIVE: '1' - run: | - curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh | bash -s v0.4.38 - echo "$HOME/.xlings/subos/current/bin" >> "$GITHUB_PATH" - - - name: Install mcpp and config mirror - run: | - # The release tarball bundles a pkgindex snapshot frozen at - # build time; refresh it so the workspace pin in .xlings.json - # (latest mcpp) resolves. - xlings update - xlings install mcpp -y -g # install to global - mcpp --version - mcpp self config --mirror GLOBAL - - echo "mcpp debug info:" - which mcpp - cat $HOME/.xlings/.xlings.json - - - name: "Default: mcpp new → run" - run: | - cd "$(mktemp -d)" - mcpp new hello_gcc - cd hello_gcc - mcpp run - - # Template packages exercise the sha256-pinned mcpp-index fetch - # path (user report: `mcpp new ... --template imgui` failed with - # fetch 'imgui@0.0.6' exit 1 on hosts without a sha256sum binary). - - name: "Template: mcpp new --template imgui (fetch path)" - run: | - cd "$(mktemp -d)" - mcpp new abc1 --template imgui - test -f abc1/mcpp.toml - - - name: "Default: build mcpp" - run: | - mcpp clean - mcpp run - - - name: "musl-gcc: mcpp new → run" - run: | - mcpp toolchain install gcc 16.1.0-musl - mcpp toolchain default gcc@16.1.0-musl - cd "$(mktemp -d)" - mcpp new hello_musl - cd hello_musl - mcpp run - - - name: "musl-gcc: build mcpp" - run: | - mcpp toolchain default gcc@16.1.0-musl - mcpp clean - mcpp run - - - name: "gcc 16: mcpp new → run" - run: | - mcpp toolchain install gcc 16.1.0 - mcpp toolchain default gcc@16.1.0 - cd "$(mktemp -d)" - mcpp new hello_gcc16 - cd hello_gcc16 - mcpp run - - - name: "gcc 16: build mcpp" - run: | - mcpp toolchain default gcc@16.1.0 - mcpp clean - mcpp run - - - name: "LLVM: mcpp new → run" - run: | - mcpp toolchain install llvm 20.1.7 - mcpp toolchain default llvm@20.1.7 - cd "$(mktemp -d)" - mcpp new hello_llvm - cd hello_llvm - mcpp run - - - name: "LLVM: build mcpp" - run: | - mcpp toolchain default llvm@20.1.7 - mcpp clean - mcpp run - - # ────────────────────────────────────────────────────────────────── - # Newer/rolling-glibc distros — reproduction surface for the - # bundled-glibc-vs-host-libtinfo `sh:` crash (host glibc > bundled). - # Plus older-glibc legs (the safe reverse direction) proving the - # musl-static mcpp + self-contained toolchain run end-to-end on old - # hosts. Runs the released mcpp inside distro containers. - # ────────────────────────────────────────────────────────────────── - linux-distro-matrix: - needs: [wait-index] - name: Linux distro (${{ matrix.distro }}) - if: ${{ github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' }} - runs-on: ubuntu-24.04 - container: - image: ${{ matrix.image }} - timeout-minutes: 45 - strategy: - fail-fast: false - matrix: - # findutils (find) is mandatory on every leg: quick_install.sh - # locates the extracted xlings dir with `find`, so a missing find - # fails the install with exit 127 *before* mcpp ever runs. Minimal - # images (opensuse/tumbleweed) ship without it; arch's base merely - # bundles it by luck. List it explicitly everywhere — don't rely on - # the base image. - include: - - distro: fedora-latest - image: fedora:latest - setup: dnf -y install curl bash tar gzip xz git findutils binutils file glibc-langpack-en - - distro: arch - image: archlinux:latest - setup: pacman -Sy --noconfirm curl bash tar gzip xz git findutils binutils file - - distro: tumbleweed - image: opensuse/tumbleweed:latest - setup: zypper -n install curl bash tar gzip xz git findutils binutils file - - distro: debian-testing - image: debian:testing - setup: apt-get update && apt-get -y install curl bash tar gzip xz-utils git ca-certificates binutils findutils file - - distro: ubuntu-2004 - image: ubuntu:20.04 - setup: apt-get update && DEBIAN_FRONTEND=noninteractive apt-get -y install curl bash tar gzip xz-utils git ca-certificates binutils findutils file - - distro: debian-11 - image: debian:11 - setup: apt-get update && apt-get -y install curl bash tar gzip xz-utils git ca-certificates binutils findutils file - env: - XLINGS_NON_INTERACTIVE: '1' - HOME: /root - steps: - - uses: actions/checkout@v4 - - - name: Install prerequisites (${{ matrix.distro }}) - run: ${{ matrix.setup }} - - - name: Install xlings + mcpp - run: | - curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh | bash -s v0.4.38 - # Deliberately NOT writing to $GITHUB_PATH here. On container - # images that declare no PATH in their config (opensuse/ - # tumbleweed), appending a single dir to GITHUB_PATH makes the - # runner exec later steps' `sh` with only that dir on PATH — - # `sh` (in /usr/bin) vanishes and the next step dies with - # `exec: "sh": ... not found` / exit 127. Each step below already - # exports PATH itself, so the append is redundant anyway. - - - name: Configure mcpp - run: | - export PATH="$HOME/.xlings/subos/current/bin:$PATH" - xlings update - xlings install mcpp -y -g - mcpp --version - mcpp self config --mirror GLOBAL - - - name: "Regression: new → run (loader env must not crash /bin/sh)" - run: | - export PATH="$HOME/.xlings/subos/current/bin:$PATH" - cd "$(mktemp -d)" - mcpp new hello_distro - cd hello_distro - mcpp run - - - name: "Self-containment: produced binary uses bundled loader" - run: | - export PATH="$HOME/.xlings/subos/current/bin:$PATH" - cd "$(mktemp -d)" && mcpp new hc && cd hc && mcpp build - bin="$(find target -type f -name hc | head -1)" - interp="$(file "$bin" | grep -o 'interpreter [^,]*' | awk '{print $2}')" - echo "interp=$interp" - case "$interp" in - */.mcpp/*|*/registry/*|*xpkgs*) echo "OK bundled loader" ;; - *) echo "FAIL host loader: $interp"; exit 1 ;; - esac - - # ────────────────────────────────────────────────────────────────── - # macOS: llvm@20.1.7 - # ────────────────────────────────────────────────────────────────── - macos-fresh: - needs: [wait-index] - name: macOS fresh install - if: ${{ github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' }} - # macos-14: the support floor (mcpp ≥0.0.50 / xlings ≥0.4.50 ship - # minos=14.0 static-libc++ binaries). A fresh install passing here - # is the continuous proof of the macOS 14 floor — and of host-tool - # independence (this image has no sha256sum; macos-15 does). - runs-on: macos-14 - timeout-minutes: 30 - steps: - - uses: actions/checkout@v4 - - - name: Install xlings - env: - XLINGS_NON_INTERACTIVE: '1' - run: | - # v0.4.50+: first xlings release whose macosx binary runs on - # macOS 14 (older ones carry minos=15 and refuse to start). - # v0.4.51: in-process sha256 — required on this image, which - # has no sha256sum binary (pinned fetches failed before). - curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh | bash -s v0.4.51 - echo "$HOME/.xlings/subos/current/bin" >> "$GITHUB_PATH" - - - name: Install mcpp and config mirror - run: | - # Refresh the bundled pkgindex snapshot so the workspace pin - # in .xlings.json (latest mcpp) resolves. - xlings update - xlings install mcpp -y -g # install to global - mcpp --version - mcpp self config --mirror GLOBAL - - echo "mcpp debug info:" - which mcpp - cat $HOME/.xlings/.xlings.json - - - name: "LLVM: mcpp new → run" - run: | - cd "$(mktemp -d)" - mcpp new hello_mac - cd hello_mac - mcpp run - - # Template packages exercise the sha256-pinned mcpp-index fetch - # path — this is what broke on hosts without a sha256sum binary - # (stock macOS / bare Windows) before xlings 0.4.51 hashed - # in-process. - - name: "Template: mcpp new --template imgui (fetch path)" - run: | - cd "$(mktemp -d)" - mcpp new abc1 --template imgui - test -f abc1/mcpp.toml - - - name: "LLVM: build mcpp" - run: | - mcpp clean - mcpp run - - # ────────────────────────────────────────────────────────────────── - # Windows: llvm@20.1.7 + MSVC STL - # ────────────────────────────────────────────────────────────────── - windows-fresh: - needs: [wait-index] - name: Windows fresh install - if: ${{ github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' }} - runs-on: windows-latest - timeout-minutes: 30 - steps: - - uses: actions/checkout@v4 - - - name: Install xlings - shell: pwsh - env: - XLINGS_NON_INTERACTIVE: '1' - run: | - irm https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.ps1 | iex - - $xlingsbin = "$env:USERPROFILE\.xlings\subos\current\bin" - $env:PATH = "$xlingsbin;$env:PATH" - $xlingsbin | Out-File -Append -FilePath $env:GITHUB_PATH -Encoding utf8 - - - name: Install mcpp and config mirror - shell: pwsh - run: | - # Refresh the bundled pkgindex snapshot so the workspace pin - # in .xlings.json (latest mcpp) resolves. - xlings update - xlings install mcpp -y -g --verbose - - cat "$env:USERPROFILE\.xlings\.xlings.json" - mcpp --version - mcpp self config --mirror GLOBAL - - - name: "LLVM: mcpp new → run" - shell: pwsh - run: | - $tmp = New-TemporaryFile | ForEach-Object { Remove-Item $_; New-Item -ItemType Directory -Path $_ } - Set-Location $tmp - mcpp new hello_win - Set-Location hello_win - mcpp run - - # Template packages exercise the sha256-pinned mcpp-index fetch - # path (user report: `mcpp new abc1 --template imgui` failed with - # fetch 'imgui@0.0.6' exit 1 on bare Windows — no sha256sum binary - # outside git-bash; fixed by xlings 0.4.51 in-process hashing). - - name: "Template: mcpp new --template imgui (fetch path)" - shell: pwsh - run: | - $tmp = New-TemporaryFile | ForEach-Object { Remove-Item $_; New-Item -ItemType Directory -Path $_ } - Set-Location $tmp - mcpp new abc1 --template imgui - if (!(Test-Path abc1/mcpp.toml)) { exit 1 } - - - name: "LLVM: build mcpp" - shell: pwsh - run: | - mcpp clean - mcpp run diff --git a/.github/workflows/ci-linux-e2e.yml b/.github/workflows/ci-linux-e2e.yml deleted file mode 100644 index 7e823b74..00000000 --- a/.github/workflows/ci-linux-e2e.yml +++ /dev/null @@ -1,192 +0,0 @@ -name: ci-linux-e2e - -# The e2e suite (tests/e2e/run_all.sh, ~18 min) split out of ci-linux.yml so it -# runs in PARALLEL with the build/unit/toolchain-matrix job instead of tacked on -# after it. Both workflows share the same cache lineage (mcpp sandbox + xlings + -# target/), so this job restores a warm build and the only added wall-clock vs. -# the inline version is one extra warm `mcpp build`. Net per-PR critical path -# drops from "build + … + e2e" to max(build+matrix, build+e2e). -# -# Paired workflows: ci-linux.yml (build + unit + toolchain matrix + integration), -# ci-macos.yml, ci-windows.yml. - -on: - push: - branches: [ main ] - pull_request: - branches: [ main ] - workflow_dispatch: - -concurrency: - group: ci-${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - e2e: - name: e2e suite (linux x86_64, self-host) - runs-on: ubuntu-24.04 - timeout-minutes: 45 - env: - MCPP_HOME: /home/runner/.mcpp - # NOTE: do NOT force MCPP_VERBOSE here. The e2e suite includes tests that - # assert mcpp's DEFAULT (quiet) output — e.g. 48_build_error_output and - # 53_namespaced_cache_label — which forced verbose would break. Verbose is - # set only in the fresh-install workflows (cold bootstrap, no such asserts). - # A specific test that needs verbose passes `--verbose` itself. - steps: - - uses: actions/checkout@v4 - - # Same cache lineage as ci-linux.yml so this job lands on a warm - # toolchain/sandbox instead of re-installing it. Both workflows read - # (and may save) the same keys; actions/cache tolerates concurrent - # "already exists" saves. - - name: Cache mcpp sandbox - uses: actions/cache@v4 - with: - path: ~/.mcpp - key: mcpp-sandbox-${{ runner.os }}-ci-${{ hashFiles('mcpp.toml', '.xlings.json') }} - restore-keys: | - mcpp-sandbox-${{ runner.os }}-ci- - - - 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: Bootstrap mcpp via xlings - env: - XLINGS_NON_INTERACTIVE: '1' - XLINGS_VERSION: '0.4.30' - 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" - xlings --version - xlings install mcpp -y - MCPP="$HOME/.xlings/subos/default/bin/mcpp" - test -x "$MCPP" - "$MCPP" --version - echo "MCPP=$MCPP" >> "$GITHUB_ENV" - echo "XLINGS_BIN=$HOME/.xlings/subos/default/bin/xlings" >> "$GITHUB_ENV" - - - name: Cache target/ (build artifacts + BMIs) - uses: actions/cache@v4 - with: - path: target - key: mcpp-target-${{ runner.os }}-${{ hashFiles('src/**', 'tests/**', 'mcpp.toml', 'mcpp.lock') }} - restore-keys: | - mcpp-target-${{ runner.os }}- - - - name: Configure mirror + Build mcpp from source (self-host) - run: | - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - "$XLINGS_BIN" config --mirror GLOBAL 2>/dev/null || true - "$MCPP" self config --mirror GLOBAL 2>/dev/null || true - "$MCPP" build - - - name: E2E suite - # Per-test 600s timeout lives in tests/e2e/run_all.sh and identifies - # WHICH test hung; this caps the whole suite so a hang fails fast. - timeout-minutes: 25 - run: | - # Point the e2e runner at the freshly-built binary, not the - # bootstrap one. Tests cd into mktemp -d, so $MCPP must be - # absolute or the relative path breaks under the temp cwd. - MCPP=$(realpath "$(find target -type f -name mcpp -printf '%T@ %p\n' | sort -rn | head -1 | cut -d' ' -f2)") - test -x "$MCPP" - export MCPP - # Tests that set MCPP_HOME to a fresh tmpdir need an xlings to - # bootstrap from; surface the xlings binary installed above. - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - test -x "$MCPP_VENDORED_XLINGS" - # GitHub-hosted runners are outside CN; keep CI toolchain downloads on - # the global mirror while mcpp's default remains CN for fresh local - # sandboxes. E2E tests with their own MCPP_HOME read this variable. - export MCPP_E2E_TOOLCHAIN_MIRROR=GLOBAL - "$MCPP" self config --mirror "$MCPP_E2E_TOOLCHAIN_MIRROR" - "$MCPP" self config - # Pin the global default so test 28 (default-toolchain path) gets a - # deterministic GNU answer instead of an auto-install pick. - "$MCPP" toolchain default gcc@16.1.0 - # Warm musl once so fresh-home e2e tests inherit the payload. - "$MCPP" toolchain install gcc 16.1.0-musl - bash tests/e2e/run_all.sh - - # ────────────────────────────────────────────────────────────────── - # Hermetic (no host toolchain): the ONLY environment class that - # faithfully reproduces issue #195. Standard runners ship gcc + - # libc6-dev, so a sandbox toolchain that leaks to the host's CRT - # still links "green" there; this container has no compiler and no - # host Scrt1.o, so any leak fails loudly. Builds PR code with the - # bootstrap mcpp, then runs the llvm flow end-to-end. - # ────────────────────────────────────────────────────────────────── - hermetic: - name: hermetic e2e (no host toolchain, container) - runs-on: ubuntu-24.04 - container: debian:stable-slim - timeout-minutes: 60 - env: - XLINGS_NON_INTERACTIVE: '1' - steps: - - name: Install base utilities (NO compiler) - run: | - apt-get update -qq - apt-get install -y -qq curl ca-certificates git xz-utils unzip - # The whole point of this job: no host toolchain, no host CRT. - ! command -v gcc - ! command -v cc - test ! -e /usr/lib/x86_64-linux-gnu/Scrt1.o - test ! -e /usr/lib/gcc - - - uses: actions/checkout@v4 - - # Payload cache (downloads only — the container still has no host - # toolchain, which is the property under test). - - name: Cache mcpp sandbox payloads - uses: actions/cache@v4 - with: - path: ~/.mcpp - key: mcpp-hermetic-${{ hashFiles('mcpp.toml') }} - restore-keys: | - mcpp-hermetic- - - - name: Bootstrap xlings + released mcpp - run: | - curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh | bash -s v0.4.62 - export PATH="$HOME/.xlings/subos/current/bin:$PATH" - xlings update - xlings install mcpp -y -g - MCPP_BOOT="$HOME/.xlings/subos/current/bin/mcpp" - "$MCPP_BOOT" --version - "$MCPP_BOOT" self config --mirror GLOBAL - echo "MCPP_BOOT=$MCPP_BOOT" >> "$GITHUB_ENV" - - - name: Build PR mcpp from source (sandbox gcc only) - run: | - "$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" --version - echo "MCPP=$MCPP" >> "$GITHUB_ENV" - - - name: "issue #195 reproduction: manifest llvm toolchain, fresh" - run: | - cd "$(mktemp -d)" - "$MCPP" new hello195 - cd hello195 - printf '\n[toolchain]\nlinux = "llvm@22.1.8"\n' >> mcpp.toml - printf 'import std;\nint main() { std::println("hello {}", 195); return 0; }\n' > src/main.cpp - "$MCPP" run - - - name: Hermetic llvm e2e subset - run: | - export PATH="$HOME/.xlings/subos/current/bin:$PATH" - export MCPP - bash tests/e2e/86_llvm_hermetic_link.sh - bash tests/e2e/37_llvm_import_std.sh diff --git a/.github/workflows/ci-linux.yml b/.github/workflows/ci-linux.yml deleted file mode 100644 index fc3a3640..00000000 --- a/.github/workflows/ci-linux.yml +++ /dev/null @@ -1,168 +0,0 @@ -name: ci-linux - -# Self-host CI on Linux: mcpp builds mcpp. The bootstrap mcpp comes from -# `xlings install mcpp` (xim:mcpp in the xlings package index), so this -# workflow no longer depends on a previous-release tarball — the -# chicken-and-egg now lives upstream in the xlings index. -# -# This job covers build + unit/integration tests + the toolchain matrix + -# the xlings integration build. The ~18 min e2e suite is a SEPARATE workflow -# (ci-linux-e2e.yml) that runs in parallel on the same warm caches, so the -# per-PR critical path is max(this, e2e) instead of their sum. -# -# Paired workflows: ci-linux-e2e.yml, ci-macos.yml, ci-windows.yml. - -on: - push: - branches: [ main ] - pull_request: - branches: [ main ] - workflow_dispatch: - -concurrency: - group: ci-${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - build-and-test: - name: build + test (linux x86_64, self-host) - runs-on: ubuntu-24.04 - timeout-minutes: 60 - env: - # MCPP_HOME pinned so the cache key below restores into the - # same path mcpp resolves at runtime. - MCPP_HOME: /home/runner/.mcpp - # Verbose every mcpp invocation for richer CI diagnostics (src/cli.cppm). - MCPP_VERBOSE: "1" - steps: - - uses: actions/checkout@v4 - - # Cache mcpp's sandbox: the toolchain (musl-gcc + binutils + - # glibc + linux-headers + patchelf + ninja) takes minutes to - # install on a cold runner. Key on the workspace manifest so a - # toolchain change in mcpp.toml refreshes the cache; restore-keys - # provide a layered fallback so near-misses still skip the slow - # toolchain installs. - - name: Cache mcpp sandbox - uses: actions/cache@v4 - with: - path: ~/.mcpp - # NOTE: the "-ci-" segment keeps this lineage disjoint from - # release.yml's "-release-" caches. A bare "mcpp-sandbox--" - # restore prefix used to match the release sandbox too, silently - # swapping in a differently-populated registry (issue #120). - key: mcpp-sandbox-${{ runner.os }}-ci-${{ hashFiles('mcpp.toml', '.xlings.json') }} - restore-keys: | - mcpp-sandbox-${{ runner.os }}-ci- - - # Cache xlings + its locally installed packages (xim:mcpp etc.). - # Saves the xlings bootstrap roundtrip + the mcpp xpkg download - # on hot runs. - - 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: Bootstrap mcpp via xlings - env: - XLINGS_NON_INTERACTIVE: '1' - XLINGS_VERSION: '0.4.30' - run: | - # Always install the pinned version — cache may hold an older - # xlings whose sysroot/packages are incompatible. - 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" - xlings --version - # xim:mcpp — `xlings install` is idempotent so cache hits skip - # the download. - xlings install mcpp -y - MCPP="$HOME/.xlings/subos/default/bin/mcpp" - test -x "$MCPP" - "$MCPP" --version - echo "MCPP=$MCPP" >> "$GITHUB_ENV" - echo "XLINGS_BIN=$HOME/.xlings/subos/default/bin/xlings" >> "$GITHUB_ENV" - - # Cache the build directory: precise key on src/ + manifest - # so a no-source-change run lands on a full hit. Layered - # restore-keys let mid-run partial hits keep BMI/dyndep state - # for proper incremental builds. - - name: Cache target/ (build artifacts + BMIs) - uses: actions/cache@v4 - with: - path: target - key: mcpp-target-${{ runner.os }}-${{ hashFiles('src/**', 'tests/**', 'mcpp.toml', 'mcpp.lock') }} - restore-keys: | - mcpp-target-${{ runner.os }}- - - - name: Configure mirror + Build mcpp from source (self-host) - run: | - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - # Set GLOBAL mirror via xlings directly (bootstrap mcpp may lack --mirror flag) - "$XLINGS_BIN" config --mirror GLOBAL 2>/dev/null || true - "$MCPP" self config --mirror GLOBAL 2>/dev/null || true - "$MCPP" build - - - name: Unit + integration tests via `mcpp test` - run: | - # Use freshly-built mcpp for test (it has --mirror support) - MCPP_FRESH=$(realpath "$(find target -type f -name mcpp -printf '%T@ %p\n' | sort -rn | head -1 | cut -d' ' -f2)") - "$MCPP_FRESH" self config --mirror GLOBAL - "$MCPP_FRESH" test - - # NOTE: the e2e suite (tests/e2e/run_all.sh) moved to ci-linux-e2e.yml - # so it runs in parallel with this job. The toolchain matrix below no - # longer relies on e2e's toolchain warm-ups: the GCC build uses the - # mcpp.toml-pinned default (gcc@16.1.0, already in the sandbox from the - # self-host build above), and the musl `--target` build auto-installs - # gcc@16.1.0-musl on demand (cached across runs). - - - name: Save freshly-built mcpp for toolchain tests - run: | - MCPP=$(realpath "$(find target -type f -name mcpp -printf '%T@ %p\n' | sort -rn | head -1 | cut -d' ' -f2)") - cp "$MCPP" /tmp/mcpp-fresh - echo "MCPP=/tmp/mcpp-fresh" >> "$GITHUB_ENV" - - - name: "Toolchain: GCC — build mcpp + test" - run: | - "$MCPP" clean - "$MCPP" build 2>&1 | tee build.log; grep -q "Resolved gcc@16.1.0" build.log - "$MCPP" test - - - name: "Toolchain: musl-gcc — build mcpp (--target)" - run: | - "$MCPP" clean - "$MCPP" build --target x86_64-linux-musl 2>&1 | tee build.log; grep -q "Resolved gcc@16.1.0-musl" build.log - - - name: "Toolchain: LLVM — build mcpp" - run: | - "$MCPP" toolchain install llvm 20.1.7 - # Override project toolchain to use LLVM for this build - sed -i 's/^default = "gcc@16.1.0"/default = "llvm@20.1.7"/' mcpp.toml - "$MCPP" clean - "$MCPP" build 2>&1 | tee build.log; grep -q "Resolved llvm@20.1.7" build.log - # Restore - sed -i 's/^default = "llvm@20.1.7"/default = "gcc@16.1.0"/' mcpp.toml - - # Integration: the mcpp built from THIS PR's source (the self-host binary, - # $MCPP = /tmp/mcpp-fresh) builds & runs a real external C++ project — - # xlings (openxlings/xlings ships its own mcpp.toml). MCPP_VENDORED_XLINGS - # only supplies the xlings package backend that mcpp resolves deps through. - - name: "Integration: mcpp builds & runs xlings (openxlings/xlings)" - env: - XLINGS_NON_INTERACTIVE: '1' - run: | - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - # $MCPP is the freshly self-hosted binary built from this PR. - git clone --depth 1 --recurse-submodules \ - https://github.com/openxlings/xlings /tmp/xlings-src - cd /tmp/xlings-src - "$MCPP" self config --mirror GLOBAL - "$MCPP" build - "$MCPP" run diff --git a/.github/workflows/ci-macos.yml b/.github/workflows/ci-macos.yml deleted file mode 100644 index f1d15f41..00000000 --- a/.github/workflows/ci-macos.yml +++ /dev/null @@ -1,421 +0,0 @@ -name: ci-macos - -# macOS CI for mcpp — validates LLVM/Clang as the default macOS toolchain. -# Tests the full xlings → LLVM → C++23 import std pipeline on macOS ARM64. - -on: - push: - branches: [ main ] - pull_request: - branches: [ main ] - workflow_dispatch: - -concurrency: - group: ci-macos-${{ github.ref }} - cancel-in-progress: true - -jobs: - macos-xlings-llvm: - name: macOS ARM64 — xlings LLVM end-to-end - runs-on: macos-15 - timeout-minutes: 30 - # NOTE: no MCPP_VERBOSE here — this job runs the e2e suite, which includes - # tests asserting mcpp's default quiet output (48/53). Verbose is forced only - # in the fresh-install workflows. - steps: - - uses: actions/checkout@v4 - - - name: System info - run: | - uname -a - sw_vers - xcrun --show-sdk-path - echo "SDK: $(xcrun --show-sdk-version)" - - - name: Cache xlings - uses: actions/cache@v4 - with: - path: ~/.xlings - key: xlings-macos15-arm64-v3-${{ hashFiles('.xlings.json') }} - restore-keys: | - xlings-macos15-arm64-v3- - - - name: Bootstrap xlings - env: - XLINGS_NON_INTERACTIVE: '1' - XLINGS_VERSION: '0.4.30' - run: | - WORK=$(mktemp -d) - tarball="xlings-${XLINGS_VERSION}-macosx-arm64.tar.gz" - curl -fsSL -o "${WORK}/${tarball}" \ - "https://github.com/d2learn/xlings/releases/download/v${XLINGS_VERSION}/${tarball}" - tar -xzf "${WORK}/${tarball}" -C "${WORK}" - XLINGS_DIR="${WORK}/xlings-${XLINGS_VERSION}-macosx-arm64" - "$XLINGS_DIR/subos/default/bin/xlings" self install - export PATH="$HOME/.xlings/subos/default/bin:$PATH" - xlings --version - echo "PATH=$HOME/.xlings/subos/default/bin:$PATH" >> "$GITHUB_ENV" - - - name: Install LLVM via xlings - run: | - # latest-first: test binaries now link the toolchain's own libc++ - # (A1 root fix in flags.cppm), so new llvm releases are - # self-consistent — this job is the proof for each new default. - xlings install llvm -y || xlings install llvm@20.1.7 -y - # Verify clang++ is available - LLVM_ROOT=$(find "$HOME/.xlings" -path "*/xpkgs/xim-x-llvm/*/bin/clang++" | head -1 | xargs dirname | xargs dirname) - echo "LLVM_ROOT=$LLVM_ROOT" - ls "$LLVM_ROOT/bin/clang++" - "$LLVM_ROOT/bin/clang++" --version - echo "LLVM_ROOT=$LLVM_ROOT" >> "$GITHUB_ENV" - echo "CXX=$LLVM_ROOT/bin/clang++" >> "$GITHUB_ENV" - - - name: Inspect LLVM package structure - run: | - echo "=== bin/ ===" - ls "$LLVM_ROOT/bin/" | grep -E "^(clang|llvm|lld|ld)" | head -20 - echo "=== lib/ ===" - ls "$LLVM_ROOT/lib/" 2>/dev/null | head -10 - echo "=== share/libc++/ ===" - find "$LLVM_ROOT" -name "std.cppm" -o -name "std.compat.cppm" 2>/dev/null - echo "=== clang++.cfg ===" - cat "$LLVM_ROOT/bin/clang++.cfg" 2>/dev/null || echo "(no cfg file)" - echo "=== Target triple ===" - "$CXX" -dumpmachine - echo "=== Module manifest ===" - "$CXX" -print-library-module-manifest-path 2>/dev/null || echo "(not available)" - - - name: Test — non-module C++23 compilation - run: | - WORK=$(mktemp -d) - cd "$WORK" - cat > main.cpp << 'EOF' - #include - #include - int main() { - std::cout << std::format("Hello from LLVM on macOS! clang {}", __clang_version__) << std::endl; - return 0; - } - EOF - "$CXX" -std=c++23 -o hello main.cpp - ./hello - - - name: Test — import std (two-stage module compilation) - run: | - WORK=$(mktemp -d) - cd "$WORK" - - # Find std.cppm - STD_CPPM=$(find "$LLVM_ROOT" -name "std.cppm" -path "*/libc++/*" | head -1) - if [ -z "$STD_CPPM" ]; then - echo "::error::std.cppm not found in LLVM package" - find "$LLVM_ROOT" -name "*.cppm" 2>/dev/null - exit 1 - fi - echo "std.cppm at: $STD_CPPM" - - echo "=== Step 1: Precompile std module ===" - mkdir -p pcm.cache - "$CXX" -std=c++23 -Wno-reserved-module-identifier \ - --precompile "$STD_CPPM" -o pcm.cache/std.pcm - - echo "=== Step 2: Compile std.pcm → std.o ===" - "$CXX" -std=c++23 -Wno-reserved-module-identifier \ - pcm.cache/std.pcm -c -o std.o - - echo "=== Step 3: Compile main.cpp with import std ===" - cat > main.cpp << 'EOF' - import std; - int main() { - std::println("C++23 import std works on macOS via xlings LLVM!"); - return 0; - } - EOF - "$CXX" -std=c++23 -fmodule-file=std=pcm.cache/std.pcm -c main.cpp -o main.o - - echo "=== Step 4: Link ===" - "$CXX" main.o std.o -o hello_modules - echo "=== Step 5: Run ===" - ./hello_modules - - - name: Test — import std.compat - run: | - WORK=$(mktemp -d) - cd "$WORK" - - STD_CPPM=$(find "$LLVM_ROOT" -name "std.cppm" -path "*/libc++/*" | head -1) - STD_COMPAT_CPPM=$(find "$LLVM_ROOT" -name "std.compat.cppm" -path "*/libc++/*" | head -1) - - if [ -z "$STD_COMPAT_CPPM" ]; then - echo "::warning::std.compat.cppm not found, skipping" - exit 0 - fi - echo "std.compat.cppm at: $STD_COMPAT_CPPM" - - mkdir -p pcm.cache - # Build std first - "$CXX" -std=c++23 -Wno-reserved-module-identifier \ - --precompile "$STD_CPPM" -o pcm.cache/std.pcm - "$CXX" -std=c++23 -Wno-reserved-module-identifier \ - pcm.cache/std.pcm -c -o std.o - - # Build std.compat (depends on std) - "$CXX" -std=c++23 -Wno-reserved-module-identifier \ - -fmodule-file=std=pcm.cache/std.pcm \ - --precompile "$STD_COMPAT_CPPM" -o pcm.cache/std.compat.pcm - "$CXX" -std=c++23 -Wno-reserved-module-identifier \ - -fmodule-file=std=pcm.cache/std.pcm \ - pcm.cache/std.compat.pcm -c -o std.compat.o - - cat > main.cpp << 'EOF' - import std.compat; - #include - int main() { - printf("std.compat works on macOS! %s\n", "success"); - return 0; - } - EOF - "$CXX" -std=c++23 \ - -fmodule-file=std=pcm.cache/std.pcm \ - -fmodule-file=std.compat=pcm.cache/std.compat.pcm \ - -c main.cpp -o main.o - "$CXX" main.o std.o std.compat.o -o compat_test - ./compat_test - - - name: Test — multi-module project - run: | - WORK=$(mktemp -d) - cd "$WORK" - - STD_CPPM=$(find "$LLVM_ROOT" -name "std.cppm" -path "*/libc++/*" | head -1) - mkdir -p pcm.cache - - # Build std - "$CXX" -std=c++23 -Wno-reserved-module-identifier \ - --precompile "$STD_CPPM" -o pcm.cache/std.pcm - "$CXX" -std=c++23 -Wno-reserved-module-identifier \ - pcm.cache/std.pcm -c -o std.o - - # User module: greeter - cat > greeter.cppm << 'EOF' - export module greeter; - import std; - export namespace greeter { - std::string hello(std::string_view name) { - return std::format("Hello, {}! (from macOS module)", name); - } - } - EOF - "$CXX" -std=c++23 -fmodule-file=std=pcm.cache/std.pcm \ - --precompile greeter.cppm -o pcm.cache/greeter.pcm - "$CXX" -std=c++23 -fmodule-file=std=pcm.cache/std.pcm \ - pcm.cache/greeter.pcm -c -o greeter.o - - # Main - cat > main.cpp << 'EOF' - import std; - import greeter; - int main() { - std::println("{}", greeter::hello("mcpp")); - return 0; - } - EOF - "$CXX" -std=c++23 \ - -fmodule-file=std=pcm.cache/std.pcm \ - -fmodule-file=greeter=pcm.cache/greeter.pcm \ - -c main.cpp -o main.o - "$CXX" main.o greeter.o std.o -o multimod - ./multimod - - - name: Validate mcpp probe logic expectations - run: | - echo "=== Verifying mcpp's assumptions ===" - echo "1. -print-sysroot returns empty (mcpp falls back to xcrun):" - result=$("$CXX" -print-sysroot 2>/dev/null || true) - if [ -z "$result" ]; then - echo " PASS: empty (xcrun fallback needed)" - else - echo " INFO: $result" - fi - - echo "2. xcrun --show-sdk-path works:" - xcrun --show-sdk-path && echo " PASS" - - echo "3. -dumpmachine returns darwin triple:" - triple=$("$CXX" -dumpmachine) - echo " $triple" - echo "$triple" | grep -q "darwin" && echo " PASS: contains 'darwin'" - - echo "4. libc++ module manifest discoverable:" - manifest=$("$CXX" -print-library-module-manifest-path 2>/dev/null || true) - if [ -n "$manifest" ] && [ -f "$manifest" ]; then - echo " PASS: $manifest" - echo " Content:" - cat "$manifest" | head -20 - else - echo " INFO: manifest not via flag, using fallback path" - find "$LLVM_ROOT/share/libc++" -name "*.cppm" 2>/dev/null && echo " PASS: fallback exists" - fi - - echo "5. llvm-ar available:" - ls "$LLVM_ROOT/bin/llvm-ar" && echo " PASS" - - echo "6. clang-scan-deps available:" - ls "$LLVM_ROOT/bin/clang-scan-deps" && echo " PASS" || echo " WARN: not found" - - - name: Validate install.sh platform detection - run: | - uname_s=$(uname -s) - uname_m=$(uname -m) - echo "Platform: ${uname_s}-${uname_m}" - case "${uname_s}-${uname_m}" in - Darwin-arm64) echo "PASS: would select darwin-arm64" ;; - Darwin-x86_64) echo "PASS: would select darwin-x86_64" ;; - *) echo "FAIL: unexpected platform"; exit 1 ;; - esac - - - name: Bootstrap mcpp via xlings - run: | - # Same pattern as Linux CI: xlings install mcpp - xlings install mcpp -y - MCPP="$HOME/.xlings/subos/default/bin/mcpp" - test -x "$MCPP" - "$MCPP" --version - echo "MCPP=$MCPP" >> "$GITHUB_ENV" - echo "XLINGS_BIN=$HOME/.xlings/subos/default/bin/xlings" >> "$GITHUB_ENV" - - - name: Configure dev mcpp sandbox to reuse xlings LLVM - run: | - # Use EXACTLY the LLVM the install step resolved (env LLVM_ROOT) — - # a version-glob pick chose a stale cached 20.1.7 next to the - # freshly installed 22.1.8 and silently tested the wrong toolchain. - LLVM_PKG="$LLVM_ROOT" - test -d "$LLVM_PKG" - LLVM_VER=$(basename "$LLVM_PKG") - echo "MCPP_LLVM_VER=$LLVM_VER" >> "$GITHUB_ENV" - MCPP_LLVM_LINK="$HOME/.mcpp/registry/data/xpkgs/xim-x-llvm/$LLVM_VER" - printf '1\n' > "$LLVM_PKG/.mcpp_ok" - - mkdir -p "$HOME/.mcpp/registry/data/xpkgs/xim-x-llvm" - rm -rf "$MCPP_LLVM_LINK" - ln -s "$LLVM_PKG" "$MCPP_LLVM_LINK" - - mkdir -p "$HOME/.mcpp" - cat > "$HOME/.mcpp/config.toml" </dev/null | grep -cE "T __ZNSt3__1" || echo "0 (good: no libc++ code in binary)" - echo "--- direct run ---" - set +e - "$BIN" > run.out 2>&1 - echo "exit=$?" - head -20 run.out - sleep 5 - echo "--- newest crash report (termination) ---" - CR=$(ls -t "$HOME/Library/Logs/DiagnosticReports"/*.ips 2>/dev/null | head -1) - if [ -n "$CR" ]; then - python3 - "$CR" <<'PY' - import json, sys - lines = open(sys.argv[1]).read().splitlines() - meta = json.loads(lines[0]); body = json.loads("\n".join(lines[1:])) - print("proc:", meta.get("app_name"), "| exc:", body.get("exception", {})) - print("termination:", body.get("termination", {})) - t = [th for th in body.get("threads", []) if th.get("triggered")] - for fr in (t[0].get("frames", [])[:12] if t else []): - print(" ", fr.get("imageIndex"), fr.get("symbol", fr.get("imageOffset"))) - imgs = body.get("usedImages", []) - for i, im in enumerate(imgs[:12]): - print("img", i, im.get("path")) - PY - else - echo "none" - fi - - - name: E2E suite - # See ci-linux.yml — fail-fast on hung tests instead of burning the - # whole job budget. Per-test 600s timeout lives in run_all.sh. - timeout-minutes: 25 - run: | - MCPP=$(find target -path "*/bin/mcpp" | head -1) - MCPP=$(cd "$(dirname "$MCPP")" && pwd)/$(basename "$MCPP") - test -x "$MCPP" - export MCPP - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - test -x "$MCPP_VENDORED_XLINGS" - export MCPP_E2E_TOOLCHAIN_MIRROR=GLOBAL - "$MCPP" self config --mirror "$MCPP_E2E_TOOLCHAIN_MIRROR" - "$MCPP" self config - # macOS default toolchain is LLVM - "$MCPP" toolchain default "llvm@${MCPP_LLVM_VER}" - bash tests/e2e/run_all.sh - - - name: "Toolchain: LLVM — build mcpp (self-host)" - run: | - MCPP=$(find target -path "*/bin/mcpp" | head -1) - MCPP=$(cd "$(dirname "$MCPP")" && pwd)/$(basename "$MCPP") - test -x "$MCPP" - cp "$MCPP" /tmp/mcpp-fresh - MCPP=/tmp/mcpp-fresh - "$MCPP" toolchain default "llvm@${MCPP_LLVM_VER}" - "$MCPP" clean - "$MCPP" build - "$MCPP" --version - - # Integration: the mcpp built from THIS PR's source (the self-host binary, - # $MCPP = /tmp/mcpp-fresh) builds & runs a real external C++ project — - # xlings (openxlings/xlings ships its own mcpp.toml). - - name: "Integration: mcpp builds & runs xlings (openxlings/xlings)" - env: - XLINGS_NON_INTERACTIVE: '1' - run: | - MCPP=/tmp/mcpp-fresh # the freshly self-hosted binary built from this PR - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - git clone --depth 1 --recurse-submodules \ - https://github.com/openxlings/xlings /tmp/xlings-src - cd /tmp/xlings-src - "$MCPP" self config --mirror GLOBAL - "$MCPP" build - "$MCPP" run diff --git a/.github/workflows/cross-build-test.yml b/.github/workflows/cross-build-test.yml deleted file mode 100644 index 50a1d4f6..00000000 --- a/.github/workflows/cross-build-test.yml +++ /dev/null @@ -1,165 +0,0 @@ -name: cross-build-test - -# mcpp cross-build test — the single source of truth for "which CROSS-build -# target combinations mcpp supports", verified end-to-end. -# -# Cross = host arch ≠ target arch. Verification targets are mcpp ITSELF and -# xlings (real, self-hosting C++23 module projects), cross-built from source for -# 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 -# -# mcpp resolves a cross `--target -musl` build to the triple-named cross -# gcc musl toolchain from the xlings ecosystem (xim:-gcc, see -# src/build/prepare.cppm). Output is a fully static musl ELF (no PT_INTERP), -# which also makes the aarch64 artefact runnable natively in Termux/Android — -# qemu-aarch64 is the CI proxy for "does this cross artefact actually execute". -# -# ── NOT here ─────────────────────────────────────────────────────────────── -# * Same-arch builds (host arch == target arch) are NOT cross. The native musl -# static build `--target x86_64-linux-musl` (x86_64 host) is exercised by -# ci-linux.yml's "Toolchain: musl-gcc" step, and release.yml for the static -# release artefact. Keep them there; this file is cross-arch only. -# -# ── Planned cross rows (documented; NOT yet wired in mcpp — keep as comments) ─ -# * llvm/clang cross : clang is inherently a cross-compiler, but mcpp does not -# yet inject `-target ` + a cross sysroot for a -# clang toolchain; cross `--target` resolves to gcc musl -# only. Wire the clang cross path first, then add a row. -# * riscv64-linux-musl: add once xim:riscv64-linux-musl-gcc ships to -# xlings-res + xim-pkgindex. - -on: - push: - branches: [ main ] - pull_request: - branches: [ main ] - workflow_dispatch: - -concurrency: - group: ci-${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - cross-build: - name: cross-build ${{ matrix.target }} (mcpp + xlings) - runs-on: ubuntu-24.04 - timeout-minutes: 60 - strategy: - fail-fast: false - matrix: - include: - - target: aarch64-linux-musl - file_arch: "ARM aarch64" - qemu_bin: qemu-aarch64-static - env: - MCPP_HOME: /home/runner/.mcpp - # Verbose every mcpp invocation for richer CI diagnostics (src/cli.cppm). - MCPP_VERBOSE: "1" - steps: - - uses: actions/checkout@v4 - - - name: Cache mcpp sandbox - uses: actions/cache@v4 - with: - path: ~/.mcpp - key: mcpp-sandbox-${{ runner.os }}-cross-${{ matrix.target }}-${{ hashFiles('mcpp.toml', '.xlings.json') }} - restore-keys: | - mcpp-sandbox-${{ runner.os }}-cross-${{ matrix.target }}- - - - 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 qemu-user-static - run: | - sudo apt-get update -qq - sudo apt-get install -y qemu-user-static - ${{ matrix.qemu_bin }} --version | head -1 - - - name: Bootstrap mcpp via xlings - env: - XLINGS_NON_INTERACTIVE: '1' - # 0.4.62 (current) — kept in lock-step with the xlings the release - # is built/bundled against (release.yml). A past 0.4.61 "download 404 - # for mcpp@" was NOT a version bug — the xlings-res/mcpp GitHub - # release assets were uploaded in a broken state (records present, - # blobs missing → 404 on GET); re-uploaded clean. The stale-INDEX - # half is handled by the marker-clear below. - 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" - xlings --version - # Force a real index re-sync even on a warm cache: drop the TTL refresh - # markers so `xlings update` actually pulls the latest index (sees the - # current bootstrap pin) while the toolchain payloads stay cached. - 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 (bootstrap mcpp -> fresh host mcpp) - run: | - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - "$XLINGS_BIN" config --mirror GLOBAL 2>/dev/null || true - "$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: "Cross-build mcpp -> ${{ matrix.target }}" - run: | - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - "$MCPP" build --target ${{ matrix.target }} - bin=$(find target/${{ matrix.target }} -type f -name mcpp | head -1) - [ -n "$bin" ] || { echo "no mcpp artefact for ${{ matrix.target }}"; exit 1; } - echo "== file =="; file "$bin" - file "$bin" | grep -q "${{ matrix.file_arch }}" || { echo "expected ${{ matrix.file_arch }}"; exit 1; } - file "$bin" | grep -q "statically linked" || { echo "expected static"; exit 1; } - echo "MCPP_XBIN=$bin" >> "$GITHUB_ENV" - - - name: "Cross-build xlings -> ${{ matrix.target }}" - run: | - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - git clone --depth 1 https://github.com/openxlings/xlings /tmp/xlings-src - cd /tmp/xlings-src - "$MCPP" self config --mirror GLOBAL 2>/dev/null || true - "$MCPP" build --target ${{ matrix.target }} - xbin=$(find target/${{ matrix.target }} -type f -name xlings | head -1) - [ -n "$xbin" ] || { echo "no xlings artefact for ${{ matrix.target }}"; exit 1; } - echo "== file =="; file "$xbin" - file "$xbin" | grep -q "${{ matrix.file_arch }}" || { echo "expected ${{ matrix.file_arch }}"; exit 1; } - file "$xbin" | grep -q "statically linked" || { echo "expected static"; exit 1; } - echo "XLINGS_XBIN=$xbin" >> "$GITHUB_ENV" - - - name: "Smoke-run cross artefacts under qemu" - run: | - RUN="${{ matrix.qemu_bin }}" - # mcpp is self-contained, so --version runs cleanly under bare qemu — - # this is the hard execution proof for the cross artefact. - echo "== mcpp --version ==" - mver=$($RUN "$MCPP_XBIN" --version) - echo "$mver"; echo "$mver" | grep -q "mcpp" || { echo "mcpp --version failed"; exit 1; } - # xlings expects a real runtime environment (sandbox/config) and may - # exit non-zero on a bare `--version` under qemu; its ELF arch + static - # linkage were already asserted in the build step, so treat execution - # here as best-effort rather than gating. - echo "== xlings --version (best-effort under qemu) ==" - xver=$($RUN "$XLINGS_XBIN" --version 2>&1 || true) - echo "$xver" From b5c340dd8556491f9ef99fc60660cf8fd594b337 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Mon, 13 Jul 2026 14:34:09 +0800 Subject: [PATCH 4/8] 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 fdcb3c5429bd5a8a261f16725a93613935b8a4cf Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Mon, 13 Jul 2026 14:50:55 +0800 Subject: [PATCH 5/8] 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 06550917a1d10ac6b2f8a6a1e1a6893926199cbe Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Mon, 13 Jul 2026 15:09:25 +0800 Subject: [PATCH 6/8] 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 06ee2a10b961b9753d1116950e4308e3f99e7dae Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Mon, 13 Jul 2026 15:25:47 +0800 Subject: [PATCH 7/8] 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 23cf62cfe1b7a6d34613382abca5f3c522a50209 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Mon, 13 Jul 2026 15:44:09 +0800 Subject: [PATCH 8/8] =?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);