From b5609fe3ecb592e22897c5a46bd342daa832f465 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Mon, 13 Jul 2026 18:48:16 +0800 Subject: [PATCH 1/4] docs(agents): 0.0.90 single-PR implementation plan (full post-089 roadmap) --- ...07-13-single-pr-090-implementation-plan.md | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 .agents/docs/2026-07-13-single-pr-090-implementation-plan.md diff --git a/.agents/docs/2026-07-13-single-pr-090-implementation-plan.md b/.agents/docs/2026-07-13-single-pr-090-implementation-plan.md new file mode 100644 index 0000000..50824df --- /dev/null +++ b/.agents/docs/2026-07-13-single-pr-090-implementation-plan.md @@ -0,0 +1,76 @@ +# 0.0.90 单 PR 实施计划(post-089 路线图全量) + +> 2026-07-13 · 执行 `2026-07-13-post-089-roadmap-and-std-dialect-flags-design.md` 的 +> **全部内容**于一个 PR。步骤是顺序实施的里程碑(每步本地 build+test 过),不拆 PR。 + +## 步骤 + +**S1 — D1 方言旗标(#210)** +manifest 解析 `[build] dialect_cxxflags`;`extract_dialect_flags(cxxflags)` +(known-list:reflection/contracts/char8_t/`-D_GLIBCXX_USE_CXX11_ABI=`; +fno-exceptions/rtti 首版不入);`BuildPlan.dialectFlags`;注入:flags.cppm 全局 +`$cxxflags`(cxx_std_flag 旁)、`ensure_built` 新形参 → gcc/clang/msvc std 命令、 +metadata 自然失效。单测 + e2e `98_reflection_import_std.sh`(两变体,flag 探测 SKIP)。 + +**S2 — A 债务** +stdmod `shellEsc` → `shq`;std/scan 命令的 `env …` 前缀 → argv+env(执行层带 +tc.envOverrides + runtime dirs);build_program/p1689 的 is_clang → caps/dialect。 + +**S3 — C1 环境模型** +msvc.cppm:`find_windows_sdk()`(注册表→Windows Kits 目录扫描,取最高版本)+ +`build_env(inst, sdk)` → INCLUDE/LIB/PATH(+`VSLANG=1033`);`enrich_toolchain_from_cl` +填 `tc.envOverrides`;SDK 缺失=检测时警告、构建时硬错误。doctor msvc 段加 SDK 行。 + +**S4 — 对象/标准旗标方言化(C2 前置)** +`object_filename_for` 接 objExt(.o/.m.o ↔ .obj/.m.obj);ninja std staging +`obj/std.o` 名参数化;`cppStandardFlag` 由 dialect.stdPrefix 产出 +(msvc:>c++20 → `/std:c++latest`,20→`/std:c++20`);defines/`/D` 走 dialect; +CRT:`/MD` 默认、linkage=static → `/MT`。 + +**S5 — C2/C5 msvc 规则发射** +ninja_backend:LinkStyle::SeparateLinker 时 `cxx_link` = `$ld /nologo /OUT:$out @rsp` +(rspfile/rspfile_content),`cxx_shared` = `/DLL /IMPLIB:`,`cxx_archive` = +dial.archiveCmd(lib.exe @rsp);compile 规则 `deps = msvc` + `/showIncludes`; +CompileFlags 增 `ldBinary`(link.exe,cl 同目录);registry archive_tool msvc → +lib.exe;flags.cppm msvc ld 分支(dep runtime dirs → /LIBPATH,user ldflags 透传)。 + +**S6 — C3 std/std.compat staging** +msvc.cppm `std_module_build_commands`(cl /c std.ixx /ifcOutput /Fo:)+ +std.compat.ixx 同法;`std_bmi_path`/`staged_std_bmi_path`/compat 变体 registry 分发 +(flags.cppm 的 clang 硬编码 staging 调用一并解除);stdmod:objectPath 扩展名 +per-dialect、msvc 分支走新命令、执行带 envOverrides;`hasImportStd` 已由 0.0.88 检测。 + +**S7 — C4 .ifc 管线 + /scanDependencies** +ninja `cxx_module` msvc 形态(moduleOutputPrefix=` /ifcOutput ` 已就绪;确认 +`/Fo:` 同行共存);`/ifcSearchDir`(bmiSearchPrefix,已就绪);cxx_scan msvc 变体 +`cl /scanDependencies $out /std:… /c $in`;provider msvc `has_builtin_p1689_scan=true`; +dyndep DyndepOptions(ifc.cache/.ifc)零改动。 + +**S8 — C7 删门 + fast-path env** +删 prepare.cppm MSVC 构建门;BuildResult/fast-path 缓存的单对 runtime env 推广为 +env 列表(持久化 tc.envOverrides,否则 msvc 增量构建丢 INCLUDE/LIB)。 + +**S9 — MinGW 打磨 + 基建** +lifecycle:mingw 在非 Windows 明确报 windows-only;e2e 97 objdump 路径 glob; +doctor mingw 段;release.yml publish-ecosystem timeout 20→30。 + +**S10 — 测试与 CI 面** +e2e `99_msvc_native_build.sh`(requires: msvc):default msvc → new → build → run → +多模块 → import std → 增量;**95 号改造**:门断言(“not yet supported”)翻转为构建 +成功断言;ci-windows.yml MSVC 步骤同步改为 build+run 矩阵(llvm+mingw+msvc); +单测:dialect 提取器、SDK 路径解析纯函数、std: 映射、msvc std 命令拼装。 + +**S11 — 版本与文档** +0.0.90(mcpp.toml/fingerprint.cppm/CHANGELOG);docs/03-toolchains.md MSVC 节 +撤 “not yet supported”、补构建说明;#210 关联说明。 + +**S12 — 流程** +临时分支(仅 ci-windows+release.yml)迭代绿 → 真 PR(全 CI)→ --admin 合入 → +tag v0.0.90 → release(修复版 mirror 的实战验收,publish-ecosystem 应全自动)→ +`xlings install mcpp@0.0.90` 验证 → bootstrap pin → 总汇报。 + +## 风险与回退 +- MSVC 模块管线在真实 CI 的未知坑(cl 版本细节、/showIncludes 本地化、rsp 转义): + 全部集中在 S5-S7,靠临时分支 CI 循环消化;门在 S8 才删,之前任何一步可安全暂停。 +- e2e 98 的 -freflection 依赖 gcc16 驱动接受度:探测 SKIP 兜底。 +- fast-path 缓存格式变更(S8):版本化格式头,老缓存自然失效重建。 From 3470d67326a4bee16ee0b4ab3bfced9c56f9eb9c Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Mon, 13 Jul 2026 18:58:08 +0800 Subject: [PATCH 2/4] fix(build): dialect-class flags reach the std BMI prebuild and the whole module graph (#210) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [build].cxxflags like -freflection change what libstdc++ headers DECLARE ( is gated on __cpp_impl_reflection) — they are module-graph dialect, same nature as -std=. They now ride -std='s channels: the global $cxxflags (every TU incl. dependency modules — fixes the fmt.gcm-class secondary failure), the std/std.compat prebuild command, and scans. Known-list auto-promotion (reflection/contracts/char8_t/_GLIBCXX_USE_CXX11_ABI) + explicit [build] dialect_cxxflags escape hatch. The fingerprint already keyed these flags — only the command construction was missing them. Also: cppStandardFlag is now spelled per-dialect (std_flag_for — msvc /std:c++20 | /std:c++latest), groundwork for the native MSVC backend. Verified: issue #210's exact repro prints 'x 2 / y 3' via import std on gcc@16.1.0; e2e 98 covers both variants + std-module.json assertion. --- src/build/flags.cppm | 6 +- src/build/plan.cppm | 13 +++- src/build/prepare.cppm | 22 ++++++- src/manifest/toml.cppm | 5 ++ src/manifest/types.cppm | 47 +++++++++++++++ src/toolchain/dialect.cppm | 16 +++++ tests/e2e/98_reflection_import_std.sh | 86 +++++++++++++++++++++++++++ tests/unit/test_toolchain_dialect.cpp | 38 ++++++++++++ 8 files changed, 230 insertions(+), 3 deletions(-) create mode 100755 tests/e2e/98_reflection_import_std.sh diff --git a/src/build/flags.cppm b/src/build/flags.cppm index 094ec9d..bdc2205 100644 --- a/src/build/flags.cppm +++ b/src/build/flags.cppm @@ -287,7 +287,11 @@ CompileFlags compute_flags(const BuildPlan& plan) { std::string cxx_std_flag = plan.cppStandardFlag.empty() ? std::format("{}c++23", d.stdPrefix) : plan.cppStandardFlag; - f.cxx = std::format("{}{}{}{}{}{}{}{}{}{}", cxx_std_flag, module_flag, std_module_flag, + // plan.dialectFlags rides right behind -std= (issue #210): module-graph- + // global dialect flags reach every TU (deps included) via this global + // cxxflags string, exactly like the standard flag itself. + f.cxx = std::format("{}{}{}{}{}{}{}{}{}{}{}", cxx_std_flag, plan.dialectFlags, + 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("{}{}{}{}{}{}{}", d.stdPrefix, c_std, opt_flag, pic_flag, diff --git a/src/build/plan.cppm b/src/build/plan.cppm index 5a1506a..0b57e76 100644 --- a/src/build/plan.cppm +++ b/src/build/plan.cppm @@ -10,6 +10,7 @@ import mcpp.manifest; import mcpp.modgraph.graph; import mcpp.modgraph.scanner; import mcpp.toolchain.detect; +import mcpp.toolchain.dialect; import mcpp.toolchain.fingerprint; import mcpp.platform; @@ -47,6 +48,10 @@ struct BuildPlan { mcpp::toolchain::Fingerprint fingerprint; std::string cppStandard = "c++23"; std::string cppStandardFlag = "-std=c++23"; + // Module-graph-global dialect flags (issue #210), pre-joined with a + // leading space per flag (e.g. " -freflection"). Rides -std='s channels: + // global $cxxflags (all TUs incl. deps), std BMI prebuild, scans. + std::string dialectFlags; std::filesystem::path projectRoot; // where mcpp.toml lives std::filesystem::path outputDir; // target/// @@ -313,7 +318,13 @@ BuildPlan make_plan(const mcpp::manifest::Manifest& manifest, plan.fingerprint = fp; if (auto stdCfg = mcpp::manifest::normalize_cpp_standard(manifest.package.standard)) { plan.cppStandard = stdCfg->canonical; - plan.cppStandardFlag = stdCfg->flag; + // Spelled per-dialect: "-std=c++26" (gnu) vs "/std:c++latest" (msvc). + plan.cppStandardFlag = mcpp::toolchain::std_flag_for( + mcpp::toolchain::dialect_for(tc), stdCfg->canonical, stdCfg->level); + } + for (auto& f : mcpp::manifest::dialect_flags(manifest.buildConfig)) { + plan.dialectFlags += ' '; + plan.dialectFlags += f; } plan.projectRoot = projectRoot; plan.outputDir = outputDir; diff --git a/src/build/prepare.cppm b/src/build/prepare.cppm index 3157fe3..6fbb4e4 100644 --- a/src/build/prepare.cppm +++ b/src/build/prepare.cppm @@ -17,6 +17,7 @@ import mcpp.modgraph.scanner; import mcpp.modgraph.validate; import mcpp.toolchain.clang; import mcpp.toolchain.detect; +import mcpp.toolchain.dialect; import mcpp.toolchain.fingerprint; import mcpp.toolchain.msvc; import mcpp.toolchain.registry; @@ -202,6 +203,12 @@ std::string canonical_compile_flags(const mcpp::manifest::Manifest& m) { s += " cxxflag:"; s += flag; } + // Explicit [build] dialect_cxxflags (auto-promoted ones are already in + // cxxflags above) — they change every BMI in the graph. + for (auto const& flag : m.buildConfig.dialectCxxflags) { + s += " dialect:"; + s += flag; + } for (auto const& flag : m.buildConfig.ldflags) { s += " ldflag:"; s += flag; @@ -2633,8 +2640,21 @@ prepare_build(bool print_fingerprint, std::filesystem::path stdCompatBmiPath; std::filesystem::path stdCompatObjectPath; if (needsStdModule) { + // The std BMI must be compiled with the SAME dialect set its + // importers use (issue #210: -freflection gates libstdc++'s — + // a std BMI built without it structurally lacks std::meta). The + // standard flag is spelled per-dialect and the graph-global dialect + // flags ride along; both were already in the fingerprint, so this + // only fixes the COMMAND construction the fingerprint promised. + std::string stdFlagAndDialect = mcpp::toolchain::std_flag_for( + mcpp::toolchain::dialect_for(*tc), + m->cppStandard.canonical, m->cppStandard.level); + for (auto& f : mcpp::manifest::dialect_flags(m->buildConfig)) { + stdFlagAndDialect += ' '; + stdFlagAndDialect += f; + } auto sm = mcpp::toolchain::ensure_built( - *tc, fp.hex, m->package.standard, m->cppStandard.flag, + *tc, fp.hex, m->package.standard, stdFlagAndDialect, mcpp::platform::macos::deployment_target( m->buildConfig.macosDeploymentTarget)); if (!sm) return std::unexpected(sm.error().message); diff --git a/src/manifest/toml.cppm b/src/manifest/toml.cppm index 747bdc7..be18e6e 100644 --- a/src/manifest/toml.cppm +++ b/src/manifest/toml.cppm @@ -642,6 +642,11 @@ std::expected parse_string(std::string_view content, if (auto v = doc->get_bool("build.allow_host_libs")) m.buildConfig.allowHostLibs = *v; if (auto v = doc->get_string_array("build.cflags")) m.buildConfig.cflags = *v; if (auto v = doc->get_string_array("build.cxxflags")) m.buildConfig.cxxflags = *v; + // Module-graph-global dialect flags (issue #210) — see types.cppm + // dialect_flags(); this key is the explicit escape hatch for flags the + // known-list doesn't recognize yet. + if (auto v = doc->get_string_array("build.dialect_cxxflags")) + m.buildConfig.dialectCxxflags = *v; if (auto v = doc->get_string_array("build.ldflags")) m.buildConfig.ldflags = *v; if (auto v = doc->get_string("build.c_standard")) m.buildConfig.cStandard = *v; if (auto v = doc->get_string("build.default-profile")) m.buildConfig.defaultProfile = *v; diff --git a/src/manifest/types.cppm b/src/manifest/types.cppm index 7fdc928..3b5767f 100644 --- a/src/manifest/types.cppm +++ b/src/manifest/types.cppm @@ -149,6 +149,15 @@ struct BuildConfig { std::vector cflags; std::vector cxxflags; std::vector ldflags; + // Dialect-class C++ flags: flags that change what the standard library's + // headers DECLARE or participate in module dialect checks (issue #210's + // -freflection: libstdc++'s is gated on __cpp_impl_reflection). + // These are module-graph-global — they ride -std='s channels (global + // cxxflags for every TU incl. deps, the std/std.compat BMI prebuild, + // scan commands). Populated from [build] dialect_cxxflags plus + // auto-promotion of known flags found in [build] cxxflags + // (see dialect_flags()). + std::vector dialectCxxflags; std::string cStandard; // Escape hatch for the hermetic link check: a sandbox toolchain whose // CRT/loader resolve OUTSIDE the sandbox is a hard error by default @@ -393,6 +402,16 @@ struct ManifestError { std::expected normalize_cpp_standard(std::string_view raw); +// The module-graph-global dialect flag set: explicit [build] dialect_cxxflags +// plus KNOWN dialect-class flags auto-promoted out of [build] cxxflags +// (they also stay per-unit there — duplication is harmless and keeps the +// mechanism explainable). Deduplicated, declaration order preserved. +std::vector dialect_flags(const BuildConfig& bc); + +// True when `flag` belongs to the known dialect-class list (changes what +// libstdc++/libc++ headers declare, or participates in BMI dialect checks). +bool is_dialect_flag(std::string_view flag); + std::filesystem::path resolve_lib_root_path(const Manifest& manifest); // True if the manifest declares at least one `kind = "lib"` target. @@ -431,6 +450,34 @@ std::optional validate_target_soname(const Target& t, } +bool is_dialect_flag(std::string_view flag) { + // Deliberately conservative first list (design doc §1.3a): + // -fno-exceptions / -fno-rtti stay per-unit until separately reviewed + // (dependencies may assume exceptions are available). + static constexpr std::string_view exact[] = { + "-freflection", "-fno-reflection", // P2996 (GCC 16+) + "-fcontracts", "-fno-contracts", // P2900 + "-fchar8_t", "-fno-char8_t", + }; + for (auto e : exact) + if (flag == e) return true; + // libstdc++ dual-ABI switch changes declared symbols/types wholesale. + if (flag.starts_with("-D_GLIBCXX_USE_CXX11_ABI=")) return true; + return false; +} + +std::vector dialect_flags(const BuildConfig& bc) { + std::vector out; + auto add = [&](const std::string& f) { + if (std::find(out.begin(), out.end(), f) == out.end()) + out.push_back(f); + }; + for (auto& f : bc.dialectCxxflags) add(f); + for (auto& f : bc.cxxflags) + if (is_dialect_flag(f)) add(f); + return out; +} + std::expected normalize_cpp_standard(std::string_view raw) { auto trim_copy = [](std::string_view input) { std::size_t begin = 0; diff --git a/src/toolchain/dialect.cppm b/src/toolchain/dialect.cppm index 591d560..2928810 100644 --- a/src/toolchain/dialect.cppm +++ b/src/toolchain/dialect.cppm @@ -56,6 +56,13 @@ struct CommandDialect { // Dialect lookup. GCC / Clang / MinGW → gnu; MSVC → msvc. const CommandDialect& dialect_for(const Toolchain& tc); +// The full -std=/-/std: flag for a normalized standard (canonical like +// "c++26"/"gnu++23", numeric level). MSVC: /std:c++20 exists; everything +// newer maps to /std:c++latest (required for import std); gnu dialects have +// no MSVC equivalent and take the same mapping. +std::string std_flag_for(const CommandDialect& d, + std::string_view canonical, int level); + } // namespace mcpp::toolchain namespace mcpp::toolchain { @@ -106,4 +113,13 @@ const CommandDialect& dialect_for(const Toolchain& tc) { return kGnuDialect; } +std::string std_flag_for(const CommandDialect& d, + std::string_view canonical, int level) { + if (d.id == "msvc") { + if (level <= 20) return "/std:c++20"; + return "/std:c++latest"; + } + return std::format("{}{}", d.stdPrefix, canonical); +} + } // namespace mcpp::toolchain diff --git a/tests/e2e/98_reflection_import_std.sh b/tests/e2e/98_reflection_import_std.sh new file mode 100755 index 0000000..86c046a --- /dev/null +++ b/tests/e2e/98_reflection_import_std.sh @@ -0,0 +1,86 @@ +#!/usr/bin/env bash +# requires: gcc +# 98_reflection_import_std.sh — issue #210: dialect-class flags reach the std +# module BMI prebuild AND every TU in the graph: +# - [build] cxxflags = ["-freflection"] → `import std;` exposes std::meta +# - a dependency module that imports std builds in the same graph (the +# fmt.gcm-class secondary failure from the issue) +# - std-module.json records the flag in the std build command +set -e + +TMP=$(mktemp -d) +trap "rm -rf $TMP" EXIT + +# The resolved toolchain must accept -freflection (GCC 16+). Probe via the +# project's own toolchain resolution: build a trivial reflection TU. +GXX=$(find "${MCPP_HOME:-$HOME/.mcpp}/registry/data/xpkgs/xim-x-gcc" -name "g++" -path "*/bin/*" 2>/dev/null | sort | tail -1) +if [[ -z "$GXX" ]] || ! echo 'int main(){}' | "$GXX" -freflection -std=c++26 -x c++ - -fsyntax-only -o /dev/null 2>/dev/null; then + echo "SKIP-INLINE: no -freflection-capable gcc payload available" + exit 0 +fi + +mkdir -p "$TMP/proj/src" "$TMP/proj/meta_dep/src" +cd "$TMP/proj" + +# Path dependency providing a module that ITSELF imports std — must be +# compiled with the same dialect set or GCC rejects the BMI mix. +cat > meta_dep/mcpp.toml <<'EOF' +[package] +name = "meta_dep" +version = "0.1.0" +standard = "c++26" + +[targets.meta_dep] +kind = "lib" +EOF +cat > meta_dep/src/meta_dep.cppm <<'EOF' +export module meta_dep; +import std; +export namespace meta_dep { +std::string tag() { return "dep-ok"; } +} +EOF + +cat > mcpp.toml <<'EOF' +[package] +name = "refl" +version = "0.1.0" +standard = "c++26" + +[build] +cxxflags = ["-freflection"] + +[dependencies] +meta_dep = { path = "meta_dep" } +EOF +cat > src/main.cpp <<'EOF' +import std; +import meta_dep; + +void print_struct(auto &&value) { + constexpr auto info = std::meta::remove_cvref(^^decltype(value)); + constexpr auto no_check = std::meta::access_context::unchecked(); + static constexpr auto members = std::define_static_array( + std::meta::nonstatic_data_members_of(info, no_check)); + template for (constexpr auto e : members) { + auto &&member = value.[:e:]; + std::println("{} {}", identifier_of(e), member); + } +} + +struct Point { double x{}; double y{}; }; +int main() { + print_struct(Point{2, 3}); + std::println("{}", meta_dep::tag()); +} +EOF + +out=$("$MCPP" run 2>&1) || { echo "FAIL: build/run: $out"; exit 1; } +[[ "$out" == *"x 2"* && "$out" == *"y 3"* ]] || { echo "FAIL: reflection output: $out"; exit 1; } +[[ "$out" == *"dep-ok"* ]] || { echo "FAIL: dep module output: $out"; exit 1; } + +# The std BMI build command must carry the dialect flag. +grep -rl '"std_flag": "[^"]*-freflection' "${MCPP_HOME:-$HOME/.mcpp}/bmi/" >/dev/null \ + || { echo "FAIL: std-module.json lacks -freflection in std_flag"; exit 1; } + +echo "PASS: dialect flags reach std BMI + whole module graph (issue #210)" diff --git a/tests/unit/test_toolchain_dialect.cpp b/tests/unit/test_toolchain_dialect.cpp index 4ca3d2a..7e7c972 100644 --- a/tests/unit/test_toolchain_dialect.cpp +++ b/tests/unit/test_toolchain_dialect.cpp @@ -1,6 +1,7 @@ #include import std; +import mcpp.manifest; import mcpp.toolchain.dialect; import mcpp.toolchain.model; import mcpp.toolchain.registry; @@ -81,6 +82,43 @@ TEST(MingwSpec, DisplayAndDefaultMatching) { EXPECT_FALSE(matches_default_toolchain("gcc@16.1.0", "mingw-gcc", "16.1.0")); } +TEST(StdFlagFor, PerDialectSpelling) { + const auto& gnu = dialect_for(make_tc(CompilerId::GCC)); + const auto& msvc = dialect_for(make_tc(CompilerId::MSVC)); + EXPECT_EQ(std_flag_for(gnu, "c++26", 26), "-std=c++26"); + EXPECT_EQ(std_flag_for(gnu, "gnu++23", 23), "-std=gnu++23"); + EXPECT_EQ(std_flag_for(msvc, "c++20", 20), "/std:c++20"); + EXPECT_EQ(std_flag_for(msvc, "c++23", 23), "/std:c++latest"); + EXPECT_EQ(std_flag_for(msvc, "c++26", 26), "/std:c++latest"); +} + +// ─── issue #210: dialect-class flag extraction ─────────────────────────── + +TEST(DialectFlags, KnownListAndEscapeHatch) { + mcpp::manifest::BuildConfig bc; + bc.cxxflags = {"-freflection", "-O3", "-Wall", "-D_GLIBCXX_USE_CXX11_ABI=0"}; + bc.dialectCxxflags = {"-fcustom-std-thing", "-freflection"}; // dup dedups + auto flags = mcpp::manifest::dialect_flags(bc); + ASSERT_EQ(flags.size(), 3u); + EXPECT_EQ(flags[0], "-fcustom-std-thing"); // explicit first, order kept + EXPECT_EQ(flags[1], "-freflection"); // deduped against cxxflags copy + EXPECT_EQ(flags[2], "-D_GLIBCXX_USE_CXX11_ABI=0"); // auto-promoted +} + +TEST(DialectFlags, ExtractionDetails) { + using mcpp::manifest::is_dialect_flag; + EXPECT_TRUE(is_dialect_flag("-freflection")); + EXPECT_TRUE(is_dialect_flag("-fcontracts")); + EXPECT_TRUE(is_dialect_flag("-fno-char8_t")); + EXPECT_TRUE(is_dialect_flag("-D_GLIBCXX_USE_CXX11_ABI=1")); + // Conservative first list: these stay per-unit. + EXPECT_FALSE(is_dialect_flag("-fno-exceptions")); + EXPECT_FALSE(is_dialect_flag("-fno-rtti")); + EXPECT_FALSE(is_dialect_flag("-O2")); + EXPECT_FALSE(is_dialect_flag("-Wall")); + EXPECT_FALSE(is_dialect_flag("-fPIC")); +} + 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"))); From c40f0d78b234cd06c15b336c57d44db056cf9d59 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Mon, 13 Jul 2026 19:14:22 +0800 Subject: [PATCH 3/4] feat(toolchain): native MSVC cl.exe build backend (C1-C7) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - C1 env model: find_windows_sdk() + build_env_for_cl() synthesize INCLUDE/LIB/PATH/VSLANG=1033 from the detected VC tools + SDK (no vcvarsall run); enrich_toolchain_from_cl fills tc.envOverrides; missing SDK keeps detection working and fails the BUILD with guidance (replaces the 0.0.88 'not yet supported' gate — the gate is gone). - C2 emission: SeparateLinker rules (link.exe /OUT + rspfile — cmd's 8191 limit), lib.exe archives, /DLL+/IMPLIB shared, deps=msvc via /showIncludes, /interface /TP for .cppm module units, /std: mapping (std_flag_for), /MD|/MT CRT model, /Od, /nologo /EHsc /utf-8 baseline, .obj object extension end to end (plan objExt + std staging names). - C3 std/std.compat staging: single-cl commands (/ifcOutput), ifc.cache layout, registry-dispatched staged paths (clang hardcoding removed), stdmod executes with the toolchain env (capture_with_env). - C4 scanning: /scanDependencies as the third builtin P1689 producer (provider capability + ninja scan rule); dyndep ifc-parameterized as-is. - C8 fast path: '@env' multi-var encoding in the build cache env slot so incremental msvc builds re-create INCLUDE/LIB for ninja. - cd /d in std stage commands (cmd.exe won't change drive without it; D: workspace + C: BMI cache is the real CI layout). Zero-diff gate re-verified for GCC and LLVM vs the 0.0.89 release binary. --- src/build/execute.cppm | 16 +++- src/build/flags.cppm | 59 +++++++++--- src/build/ninja_backend.cppm | 114 +++++++++++++++++----- src/build/plan.cppm | 15 ++- src/build/prepare.cppm | 48 +++++----- src/toolchain/gcc.cppm | 7 +- src/toolchain/msvc.cppm | 178 ++++++++++++++++++++++++++++++++++- src/toolchain/provider.cppm | 4 +- src/toolchain/registry.cppm | 29 ++++++ src/toolchain/stdmod.cppm | 59 ++++++++---- 10 files changed, 441 insertions(+), 88 deletions(-) diff --git a/src/build/execute.cppm b/src/build/execute.cppm index cafa474..13fa9d3 100644 --- a/src/build/execute.cppm +++ b/src/build/execute.cppm @@ -335,8 +335,22 @@ export std::optional try_fast_build(const std::filesystem::path& projectRoo if (verbose) argv.push_back("-v"); std::vector> childEnv; - if (runtimeEnvKey != "-" && !runtimeEnvValue.empty()) + if (runtimeEnvKey == "@env") { + // Multi-var encoding (MSVC INCLUDE/LIB/PATH/VSLANG + optional runtime + // pair): \x1f-separated k=v records in the single value slot. + std::string_view rest = runtimeEnvValue; + while (!rest.empty()) { + auto sep = rest.find('\x1f'); + auto rec = rest.substr(0, sep); + if (auto eq = rec.find('='); eq != std::string_view::npos && eq > 0) + childEnv.emplace_back(std::string(rec.substr(0, eq)), + std::string(rec.substr(eq + 1))); + if (sep == std::string_view::npos) break; + rest.remove_prefix(sep + 1); + } + } else if (runtimeEnvKey != "-" && !runtimeEnvValue.empty()) { childEnv.emplace_back(runtimeEnvKey, runtimeEnvValue); + } auto t0 = std::chrono::steady_clock::now(); // capture_exec merges stderr into the captured output (replacing `2>&1`), diff --git a/src/build/flags.cppm b/src/build/flags.cppm index bdc2205..5417163 100644 --- a/src/build/flags.cppm +++ b/src/build/flags.cppm @@ -27,9 +27,10 @@ struct CompileFlags { std::string cxx; // full cxxflags string std::string cc; // full cflags string std::string ld; // ldflags string - std::filesystem::path cxxBinary; // g++ / clang++ - std::filesystem::path ccBinary; // gcc / clang (derived) - std::filesystem::path arBinary; // ar path (may be empty → use PATH) + std::filesystem::path cxxBinary; // g++ / clang++ / cl.exe + std::filesystem::path ccBinary; // gcc / clang (derived; cl.exe = same) + std::filesystem::path arBinary; // ar / llvm-ar / lib.exe (empty → PATH) + std::filesystem::path ldBinary; // link.exe (SeparateLinker dialects only) std::string sysroot; // --sysroot=... (for ninja ldflags) std::string bFlag; // -B (for ninja ldflags) bool staticStdlib = true; @@ -142,7 +143,9 @@ CompileFlags compute_flags(const BuildPlan& plan) { f.cxxBinary = plan.toolchain.binaryPath; f.ccBinary = mcpp::toolchain::derive_c_compiler(plan.toolchain); - // PIC? + const bool isMsvcDialect = (d.id == "msvc"); + + // PIC? (GNU-only concept; PE code is position independent by design.) bool need_pic = false; for (auto& lu : plan.linkUnits) { if (lu.kind == LinkUnit::SharedLibrary) { @@ -150,7 +153,7 @@ CompileFlags compute_flags(const BuildPlan& plan) { break; } } - std::string pic_flag = need_pic ? " -fPIC" : ""; + std::string pic_flag = (need_pic && !isMsvcDialect) ? " -fPIC" : ""; // Include dirs std::string include_flags; @@ -235,9 +238,21 @@ 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" : std::format(" {}{}", d.optPrefix, prof.optLevel); + ? " -Og" + : (isMsvcDialect && prof.optLevel == "0") + ? " /Od" // MSVC's no-opt spelling (there is no /O0) + : std::format(" {}{}", d.optPrefix, prof.optLevel); if (prof.debug) opt_flag += std::format(" {}", d.debugFlags); - if (prof.lto) opt_flag += " -flto"; + if (prof.lto && !isMsvcDialect) opt_flag += " -flto"; + + // MSVC baseline: /nologo /EHsc /utf-8 (dialect alwaysFlags) + the CRT + // model — /MD default, /MT under static linkage (portable-by-default is + // impossible on MSVC-ABI; /MT at least removes the vcruntime DLL dep). + std::string msvc_base; + if (isMsvcDialect) { + msvc_base = std::format(" {}", d.alwaysFlags); + msvc_base += (plan.manifest.buildConfig.linkage == "static") ? " /MT" : " /MD"; + } // User link flags std::string user_ldflags; @@ -264,9 +279,8 @@ CompileFlags compute_flags(const BuildPlan& plan) { } std::string std_compat_module_flag; 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); + auto compatDst = mcpp::toolchain::staged_std_compat_bmi_path( + plan.toolchain, plan.outputDir); std_compat_module_flag = std::string(traits.stdCompatBmiUsePrefix) + escape_path(compatDst); } @@ -290,12 +304,17 @@ CompileFlags compute_flags(const BuildPlan& plan) { // plan.dialectFlags rides right behind -std= (issue #210): module-graph- // global dialect flags reach every TU (deps included) via this global // cxxflags string, exactly like the standard flag itself. - f.cxx = std::format("{}{}{}{}{}{}{}{}{}{}{}", cxx_std_flag, plan.dialectFlags, - module_flag, std_module_flag, + f.cxx = std::format("{}{}{}{}{}{}{}{}{}{}{}{}", cxx_std_flag, plan.dialectFlags, + msvc_base, 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("{}{}{}{}{}{}{}", d.stdPrefix, c_std, opt_flag, pic_flag, - compile_toolchain_flags, b_flag, include_flags); + // MSVC compiles C with cl.exe too; /std: for C uses cN spellings — skip + // the C standard flag there (cl defaults are fine for the C entry TUs). + f.cc = isMsvcDialect + ? std::format("{}{}{}{}{}", msvc_base, opt_flag, compile_toolchain_flags, + b_flag, include_flags) + : 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; @@ -343,6 +362,18 @@ CompileFlags compute_flags(const BuildPlan& plan) { if (prof.strip) link_extra += " -s"; if constexpr (mcpp::platform::is_windows) { + if (isMsvcDialect) { + // Native cl.exe: link.exe does the link (SeparateLinker). Search + // paths for dependency runtime import libs via /LIBPATH; user + // ldflags pass through verbatim; GNU link_extra (-flto/-s) does + // not apply. + f.ldBinary = mcpp::toolchain::link_tool(plan.toolchain); + std::string libpaths; + for (auto& dir : plan.depRuntimeLibraryDirs) + libpaths += " /LIBPATH:" + escape_path(dir); + f.ld = libpaths + user_ldflags; + return f; + } // PE link: no rpath/loader/payload model. MSVC-ABI Clang needs // nothing extra (MSVC STL/SDK via the driver); MinGW adds the static // libstdc++/libgcc pair (static_stdlib above) and -B so its own diff --git a/src/build/ninja_backend.cppm b/src/build/ninja_backend.cppm index 6b79c5e..1efbbf9 100644 --- a/src/build/ninja_backend.cppm +++ b/src/build/ninja_backend.cppm @@ -297,6 +297,14 @@ std::string emit_ninja_string(const BuildPlan& plan) { } else { append("ar = ar\n"); } + // Separate linker (link.exe) for the msvc dialect. + const bool separateLinker = + dial.linkStyle == mcpp::toolchain::CommandDialect::LinkStyle::SeparateLinker; + if (separateLinker) { + append(std::format("ld = {}\n", + flags.ldBinary.empty() ? std::string("link.exe") + : escape_ninja_path(flags.ldBinary))); + } if (dyndep) { append(std::format("mcpp = {}\n", escape_ninja_path(mcpp_exe_path()))); if (!plan.scanDepsPath.empty()) { @@ -339,14 +347,25 @@ std::string emit_ninja_string(const BuildPlan& plan) { // msvc); the rule *structure* is shared across compilers. std::string module_output_flag = traits.needsExplicitModuleOutput ? std::string(traits.moduleOutputPrefix) + "$bmi_out" : ""; + // msvc: /showIncludes feeds ninja's deps=msvc header tracking; the + // stable-English prefix is guaranteed by VSLANG=1033 in envOverrides. + const bool msvcDeps = dial.ninjaDepsMode == std::string_view("msvc"); const std::string compile_tail = std::format( - "{} $in {}$out", dial.compileOnly, dial.outputObjPrefix); + "{}{} $in {}$out", + msvcDeps ? "/showIncludes " : "", dial.compileOnly, dial.outputObjPrefix); + auto append_deps = [&] { + if (msvcDeps) append(" deps = msvc\n"); + }; + // cl.exe needs /TP (our module interfaces are .cppm, unknown to cl) and + // /interface to treat the TU as a module interface unit. + const std::string module_src_flags = msvcDeps ? " /interface /TP" : ""; 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{} {}\n", - module_output_flag, compile_tail)); + "$cxx $local_includes $cxxflags $unit_cxxflags{}{} {}\n", + module_output_flag, module_src_flags, compile_tail)); + append_deps(); } else { append(std::format(" command = " "if [ -n \"$bmi_out\" ] && [ -f \"$bmi_out\" ]; then " @@ -370,6 +389,7 @@ std::string emit_ninja_string(const BuildPlan& plan) { " command = $cxx $local_includes $cxxflags $unit_cxxflags {}\n", compile_tail)); append(" description = OBJ $out\n"); + append_deps(); if (dyndep) append(" restat = 1\n"); append("\n"); @@ -380,25 +400,47 @@ std::string emit_ninja_string(const BuildPlan& plan) { " command = $cc $local_includes $cflags $unit_cflags {}\n", compile_tail)); append(" description = CC $out\n"); + append_deps(); 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"); + // Link/archive/shared: driver-style (g++/clang++ are the linker) vs the + // msvc dialect's separate link.exe/lib.exe. The msvc commands go through + // response files — object lists exceed cmd.exe's 8191-char limit fast. + if (separateLinker) { + append("rule cxx_link\n"); + append(" command = $ld /nologo /OUT:$out @$out.rsp $ldflags $unit_ldflags\n"); + append(" rspfile = $out.rsp\n"); + append(" rspfile_content = $in\n"); + append(" description = LINK $out\n\n"); + + append("rule cxx_archive\n"); + append(" command = $ar /nologo /OUT:$out @$out.rsp\n"); + append(" rspfile = $out.rsp\n"); + append(" rspfile_content = $in\n"); + append(" description = AR $out\n\n"); + + append("rule cxx_shared\n"); + append(" command = $ld /nologo /DLL /OUT:$out /IMPLIB:$out.lib " + "@$out.rsp $ldflags $unit_ldflags\n"); + append(" rspfile = $out.rsp\n"); + append(" rspfile_content = $in\n"); + append(" description = SHARED $out\n\n"); + } else { + 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(std::format(" command = {}\n", dial.archiveCmd)); - append(" description = AR $out\n\n"); + append("rule cxx_archive\n"); + append(std::format(" command = {}\n", dial.archiveCmd)); + append(" description = AR $out\n\n"); - append("rule cxx_shared\n"); - append(" command = $cxx -shared $in -o $out $ldflags $soname_flag $unit_ldflags\n"); - append(" description = SHARED $out\n\n"); + append("rule cxx_shared\n"); + append(" command = $cxx -shared $in -o $out $ldflags $soname_flag $unit_ldflags\n"); + append(" description = SHARED $out\n\n"); + } append("rule runtime_alias\n"); if constexpr (mcpp::platform::is_windows) { @@ -413,7 +455,12 @@ std::string emit_ninja_string(const BuildPlan& plan) { // GCC: built-in -fdeps-format=p1689r5 flags during preprocessing. // Clang: external clang-scan-deps tool with -format=p1689. append("rule cxx_scan\n"); - if (plan.scanDepsPath.empty()) { + if (msvcDeps) { + // MSVC: compiler-integrated P1689 via /scanDependencies (scan + // only — no codegen); /TP because our module units are .cppm. + append(" command = $cxx $local_includes $cxxflags $unit_cxxflags " + "/scanDependencies $out /TP /c $in /Fo:$compile_target\n"); + } else if (plan.scanDepsPath.empty()) { // GCC path: compiler-integrated P1689 scanning. append(" command = $cxx $local_includes $cxxflags -fmodules " "$unit_cxxflags " @@ -445,7 +492,8 @@ std::string emit_ninja_string(const BuildPlan& plan) { // Stage prebuilt std artifacts into the compiler-specific BMI cache. auto std_bmi_dst = mcpp::toolchain::staged_std_bmi_path(plan.toolchain, {}); - auto std_o_dst = std::filesystem::path("obj") / "std.o"; + auto std_o_dst = std::filesystem::path("obj") + / std::format("std{}", dial.objExt); bool has_std_artifacts = !plan.stdBmiPath.empty() && !plan.stdObjectPath.empty(); if (has_std_artifacts) { @@ -456,8 +504,10 @@ std::string emit_ninja_string(const BuildPlan& plan) { } bool has_std_compat = !plan.stdCompatBmiPath.empty() && !plan.stdCompatObjectPath.empty(); - auto compat_bmi_dst = std::filesystem::path("pcm.cache") / "std.compat.pcm"; - auto compat_o_dst = std::filesystem::path("obj") / "std.compat.o"; + auto compat_bmi_dst = std::filesystem::path(traits.bmiDir) + / std::format("std.compat{}", traits.bmiExt); + auto compat_o_dst = std::filesystem::path("obj") + / std::format("std.compat{}", dial.objExt); if (has_std_compat) { // std.compat.pcm depends on std.pcm — ensure std.pcm is staged first // so clang can resolve the transitive dependency when loading std.compat.pcm. @@ -803,7 +853,22 @@ std::expected NinjaBackend::build(const BuildPlan& plan // Record ninja binary for P0 fast-path cache. BuildResult r; r.ninjaProgram = ninjaProgram; - if (auto runtimeEnv = runtime_env_for_dirs(plan.toolchain.compilerRuntimeDirs)) { + if (!plan.toolchain.envOverrides.empty()) { + // Toolchain-declared env (MSVC INCLUDE/LIB/PATH/VSLANG). Encode all + // pairs (plus any runtime-dirs pair) into the fast-path cache's + // single env slot: "@env" key + \x1f-separated k=v records — the + // fast path must re-create this exact environment for ninja. + r.runtimeEnvKey = "@env"; + std::string joined; + auto add = [&](const std::string& k, const std::string& v) { + if (!joined.empty()) joined += '\x1f'; + joined += k; joined += '='; joined += v; + }; + if (auto runtimeEnv = runtime_env_for_dirs(plan.toolchain.compilerRuntimeDirs)) + add(runtimeEnv->first, runtimeEnv->second); + for (auto& ev : plan.toolchain.envOverrides) add(ev.key, ev.value); + r.runtimeEnvValue = std::move(joined); + } else if (auto runtimeEnv = runtime_env_for_dirs(plan.toolchain.compilerRuntimeDirs)) { r.runtimeEnvKey = runtimeEnv->first; r.runtimeEnvValue = runtimeEnv->second; } else { @@ -824,12 +889,11 @@ std::expected NinjaBackend::build(const BuildPlan& plan if (opts.parallelJobs) nargv.push_back(std::format("-j{}", opts.parallelJobs)); + // Real env pairs for THIS run (the "@env" cache encoding above is only + // for the fast path's later re-creation of the same environment). 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). + if (auto runtimeEnv = runtime_env_for_dirs(plan.toolchain.compilerRuntimeDirs)) + nenv.emplace_back(runtimeEnv->first, runtimeEnv->second); for (auto& ev : plan.toolchain.envOverrides) nenv.emplace_back(ev.key, ev.value); diff --git a/src/build/plan.cppm b/src/build/plan.cppm index 0b57e76..d7fcb76 100644 --- a/src/build/plan.cppm +++ b/src/build/plan.cppm @@ -130,10 +130,13 @@ std::string sanitize_for_path(std::string_view module_name) { return s; } -std::string object_filename_for(const std::filesystem::path& src) { +std::string object_filename_for(const std::filesystem::path& src, + std::string_view objExt = ".o") { auto stem = src.stem().string(); // distinguish .cppm vs .cpp by extension prefix to avoid collisions - return stem + (src.extension() == ".cppm" ? ".m.o" : ".o"); + return stem + (src.extension() == ".cppm" + ? ".m" + std::string(objExt) + : std::string(objExt)); } std::string qualified_package_name(const mcpp::manifest::Manifest& manifest) { @@ -326,6 +329,8 @@ BuildPlan make_plan(const mcpp::manifest::Manifest& manifest, plan.dialectFlags += ' '; plan.dialectFlags += f; } + // Object extension is dialect-spelled (.o vs .obj). + const std::string_view objExt = mcpp::toolchain::dialect_for(tc).objExt; plan.projectRoot = projectRoot; plan.outputDir = outputDir; plan.stdBmiPath = stdBmiPath; @@ -400,7 +405,7 @@ BuildPlan make_plan(const mcpp::manifest::Manifest& manifest, // derived from `/` so collisions are impossible. std::map basenameCount; for (auto idx : topoOrder) { - basenameCount[object_filename_for(graph.units[idx].path)]++; + basenameCount[object_filename_for(graph.units[idx].path, objExt)]++; } auto sanitize = [](const std::string& s) { std::string out; out.reserve(s.size()); @@ -417,7 +422,7 @@ BuildPlan make_plan(const mcpp::manifest::Manifest& manifest, cu.localIncludeDirs = u.localIncludeDirs; cu.packageCflags = u.packageCflags; cu.packageCxxflags = u.packageCxxflags; - const auto fname = object_filename_for(u.path); + const auto fname = object_filename_for(u.path, objExt); if (basenameCount[fname] > 1) { // Use / as prefix to handle // both cross-package (multi-version mangling) and intra-package @@ -661,7 +666,7 @@ BuildPlan make_plan(const mcpp::manifest::Manifest& manifest, // Add main.cpp -> obj/main.o CompileUnit main_cu; main_cu.source = *lu.entryMain; - main_cu.object = std::filesystem::path("obj") / object_filename_for(*lu.entryMain); + main_cu.object = std::filesystem::path("obj") / object_filename_for(*lu.entryMain, objExt); main_cu.packageName = qualified_package_name(manifest); if (!packages.empty() && packages[0].usageResolved) { main_cu.localIncludeDirs = packages[0].privateBuild.includeDirs; diff --git a/src/build/prepare.cppm b/src/build/prepare.cppm index 6fbb4e4..b4d2c4e 100644 --- a/src/build/prepare.cppm +++ b/src/build/prepare.cppm @@ -870,16 +870,17 @@ prepare_build(bool print_fingerprint, auto tc = mcpp::toolchain::detect(explicit_compiler); if (!tc) return std::unexpected(tc.error().message); - // Native MSVC builds are gated off until the cl.exe/.ifc pipeline lands. - // Selection & detection (`mcpp toolchain default msvc`, `list`, `doctor`) - // work; the build must fail here with one owned message rather than an - // incidental error deep in the GCC/Clang-shaped flag machinery. - if (tc->compiler == mcpp::toolchain::CompilerId::MSVC) { + // Native MSVC builds need the synthesized INCLUDE/LIB env — absent when + // detection found VC tools but no Windows SDK. Fail here with guidance + // instead of cl.exe's later "cannot open include file: 'corecrt.h'". + if (tc->compiler == mcpp::toolchain::CompilerId::MSVC + && tc->envOverrides.empty()) { return std::unexpected(std::format( - "native MSVC (cl.exe) builds are not yet supported by mcpp.\n" - " detected: msvc {} at {} (selection & detection work today)\n" - " for building on Windows use the MSVC-ABI Clang toolchain instead:\n" - " mcpp toolchain default llvm@20.1.7", + "msvc {} was detected at {}, but no Windows SDK was found —\n" + " cl.exe cannot compile without the UCRT/SDK headers.\n" + " Install the 'Windows 11 SDK' component via the Visual Studio\n" + " Installer (it is part of the Desktop development with C++\n" + " workload), then retry.", tc->version, tc->binaryPath.string())); } @@ -2582,6 +2583,18 @@ prepare_build(bool print_fingerprint, return false; }); + // The dialect-complete standard flag: spelled per-dialect and carrying + // the module-graph-global dialect flags (issue #210). ONE string shared + // by the p1689 scan and the std BMI prebuild so scan-time, prebuild-time + // and compile-time dialect provably agree. + std::string stdFlagAndDialect = mcpp::toolchain::std_flag_for( + mcpp::toolchain::dialect_for(*tc), + m->cppStandard.canonical, m->cppStandard.level); + for (auto& f : mcpp::manifest::dialect_flags(m->buildConfig)) { + stdFlagAndDialect += ' '; + stdFlagAndDialect += f; + } + // Modgraph: regex scanner by default; opt-in to compiler-driven P1689 // scanner via env var MCPP_SCANNER=p1689 (see docs/27). auto scan = [&] { @@ -2590,7 +2603,8 @@ prepare_build(bool print_fingerprint, auto tmp = std::filesystem::temp_directory_path() / std::format("mcpp_p1689_{}", std::random_device{}()); std::filesystem::create_directories(tmp); - return mcpp::modgraph::scan_packages_p1689(packages, *tc, tmp, m->cppStandard.flag); + return mcpp::modgraph::scan_packages_p1689(packages, *tc, tmp, + stdFlagAndDialect); } return mcpp::modgraph::scan_packages(packages); }(); @@ -2642,17 +2656,9 @@ prepare_build(bool print_fingerprint, if (needsStdModule) { // The std BMI must be compiled with the SAME dialect set its // importers use (issue #210: -freflection gates libstdc++'s — - // a std BMI built without it structurally lacks std::meta). The - // standard flag is spelled per-dialect and the graph-global dialect - // flags ride along; both were already in the fingerprint, so this - // only fixes the COMMAND construction the fingerprint promised. - std::string stdFlagAndDialect = mcpp::toolchain::std_flag_for( - mcpp::toolchain::dialect_for(*tc), - m->cppStandard.canonical, m->cppStandard.level); - for (auto& f : mcpp::manifest::dialect_flags(m->buildConfig)) { - stdFlagAndDialect += ' '; - stdFlagAndDialect += f; - } + // a std BMI built without it structurally lacks std::meta). Both + // pieces were already in the fingerprint; this fixes the COMMAND + // construction the fingerprint promised (stdFlagAndDialect above). auto sm = mcpp::toolchain::ensure_built( *tc, fp.hex, m->package.standard, stdFlagAndDialect, mcpp::platform::macos::deployment_target( diff --git a/src/toolchain/gcc.cppm b/src/toolchain/gcc.cppm index 5e5fdfc..fb5a070 100644 --- a/src/toolchain/gcc.cppm +++ b/src/toolchain/gcc.cppm @@ -3,6 +3,7 @@ export module mcpp.toolchain.gcc; import std; +import mcpp.platform; import mcpp.toolchain.model; import mcpp.toolchain.probe; import mcpp.xlings; @@ -141,8 +142,12 @@ std::string std_module_build_command(const Toolchain& tc, } } + // Windows (MinGW): cmd.exe needs `/d` to change DRIVE (project on D:, + // BMI cache on C: is the real CI layout — same-drive runs masked this). + const char* cd = mcpp::platform::is_windows ? "cd /d" : "cd"; return std::format( - "cd {} && {}{} {} -fmodules -O2{}{} -c {} -o std.o 2>&1", + "{} {} && {}{} {} -fmodules -O2{}{} -c {} -o std.o 2>&1", + cd, mcpp::xlings::shq(cacheDir.string()), mcpp::toolchain::compiler_env_prefix(tc), mcpp::xlings::shq(tc.binaryPath.string()), diff --git a/src/toolchain/msvc.cppm b/src/toolchain/msvc.cppm index 30526e2..28c98cb 100644 --- a/src/toolchain/msvc.cppm +++ b/src/toolchain/msvc.cppm @@ -20,6 +20,7 @@ import std; import mcpp.platform; import mcpp.toolchain.model; import mcpp.toolchain.probe; +import mcpp.xlings; export namespace mcpp::toolchain::msvc { @@ -72,9 +73,48 @@ std::string triple_for_arch(std::string_view arch); std::string install_guidance(); // Classify + enrich an already-probed cl.exe binary for detect(): -// version/arch from the banner, targetTriple, driverIdent, std.ixx lookup. +// version/arch from the banner, targetTriple, driverIdent, std.ixx lookup, +// and the build env (INCLUDE/LIB/PATH from VC tools + Windows SDK) into +// tc.envOverrides. Missing SDK leaves envOverrides empty — detection still +// succeeds (selection UX must work on SDK-less boxes); the build path +// checks and errors with guidance. std::expected enrich_toolchain_from_cl(Toolchain& tc); +// ─── Windows SDK + build environment (native cl.exe builds) ────────────── + +struct WindowsSdk { + std::filesystem::path root; // C:\Program Files (x86)\Windows Kits\10 + std::string version; // "10.0.26100.0" (highest usable) +}; + +// Locate the Windows 10/11 SDK (highest version with ucrt headers). +std::optional find_windows_sdk(); + +// Synthesize the environment cl.exe/link.exe need — what vcvars would set, +// derived directly from the located VC tools + SDK (no vcvarsall.bat run): +// INCLUDE = \include; \Include\\{ucrt,um,shared,winrt} +// LIB = \lib\; \Lib\\{ucrt,um}\ +// PATH = ; (mspdb*.dll etc.) +// VSLANG = 1033 (stable English /showIncludes prefix for ninja deps=msvc) +std::vector build_env_for_cl(const std::filesystem::path& clPath, + std::string_view arch, + const WindowsSdk& sdk); + +// std / std.compat module staging commands (single cl step each): +// cl /nologo /EHsc /W0 /O2 /c \modules\std.ixx +// /ifcOutput \ifc.cache\std.ifc /Fo:\std.obj +std::vector std_module_build_commands( + const Toolchain& tc, const std::filesystem::path& cacheDir, + std::string_view cppStandardFlag); +std::vector std_compat_build_commands( + const Toolchain& tc, const std::filesystem::path& cacheDir, + std::string_view cppStandardFlag); + +std::filesystem::path std_bmi_path(const std::filesystem::path& cacheDir); +std::filesystem::path staged_std_bmi_path(const std::filesystem::path& outputDir); +std::filesystem::path std_compat_bmi_path(const std::filesystem::path& cacheDir); +std::filesystem::path staged_std_compat_bmi_path(const std::filesystem::path& outputDir); + } // namespace mcpp::toolchain::msvc namespace mcpp::toolchain::msvc { @@ -358,6 +398,131 @@ std::optional detect_installation() { #endif } +std::optional find_windows_sdk() { +#if defined(_WIN32) + // Directory scan of the conventional install roots; highest version dir + // that actually carries the UCRT headers wins. (Registry Installed + // Roots would be marginally more correct — the path scan covers every + // real installer layout seen so far and needs no Win32 API surface.) + for (const char* base : {"C:\\Program Files (x86)\\Windows Kits\\10", + "C:\\Program Files\\Windows Kits\\10"}) { + std::filesystem::path root{base}; + std::error_code ec; + if (!std::filesystem::exists(root / "Include", ec)) continue; + std::string best; + for (auto& e : std::filesystem::directory_iterator(root / "Include", ec)) { + if (!e.is_directory()) continue; + auto v = e.path().filename().string(); + if (std::filesystem::exists(e.path() / "ucrt" / "corecrt.h", ec) + && v > best) + best = v; + } + if (!best.empty()) return WindowsSdk{root, best}; + } +#endif + return std::nullopt; +} + +std::vector build_env_for_cl(const std::filesystem::path& clPath, + std::string_view arch, + const WindowsSdk& sdk) { + // \bin\Host\\cl.exe → + auto clDir = clPath.parent_path(); + auto tools = clDir.parent_path().parent_path().parent_path(); + std::string a = arch.empty() ? std::string("x64") : std::string(arch); + + auto join = [](std::initializer_list ps) { + std::string s; + for (auto& p : ps) { + if (!s.empty()) s += ';'; + s += p.string(); + } + return s; + }; + + std::vector env; + env.push_back({"INCLUDE", join({ + tools / "include", + sdk.root / "Include" / sdk.version / "ucrt", + sdk.root / "Include" / sdk.version / "um", + sdk.root / "Include" / sdk.version / "shared", + sdk.root / "Include" / sdk.version / "winrt", + })}); + env.push_back({"LIB", join({ + tools / "lib" / a, + sdk.root / "Lib" / sdk.version / "ucrt" / a, + sdk.root / "Lib" / sdk.version / "um" / a, + })}); + std::string path = clDir.string(); + if (const char* p = std::getenv("PATH"); p && *p) { + path += ';'; + path += p; + } + env.push_back({"PATH", std::move(path)}); + // Stable English "Note: including file:" prefix for ninja's deps=msvc. + env.push_back({"VSLANG", "1033"}); + return env; +} + +std::filesystem::path std_bmi_path(const std::filesystem::path& cacheDir) { + return cacheDir / "ifc.cache" / "std.ifc"; +} +std::filesystem::path staged_std_bmi_path(const std::filesystem::path& outputDir) { + return outputDir / "ifc.cache" / "std.ifc"; +} +std::filesystem::path std_compat_bmi_path(const std::filesystem::path& cacheDir) { + return cacheDir / "ifc.cache" / "std.compat.ifc"; +} +std::filesystem::path staged_std_compat_bmi_path(const std::filesystem::path& outputDir) { + return outputDir / "ifc.cache" / "std.compat.ifc"; +} + +namespace { + +std::string cl_stage_command(const Toolchain& tc, + const std::filesystem::path& cacheDir, + std::string_view cppStandardFlag, + const std::filesystem::path& source, + const std::filesystem::path& ifcOut, + std::string_view objName, + std::string_view extraRef) { + // cd into the cache dir (relative outputs land there); env (INCLUDE/LIB) + // comes from tc.envOverrides via the executor, not the command string. + // `/d`: cmd.exe won't change DRIVE without it (workspace on D:, BMI + // cache on C: is the real CI layout). + return std::format( + "cd /d {} && {} /nologo {} /EHsc /O2 /W0{} /c {} /ifcOutput {} /Fo:{} 2>&1", + mcpp::xlings::shq(cacheDir.string()), + mcpp::xlings::shq(tc.binaryPath.string()), + cppStandardFlag, + extraRef, + mcpp::xlings::shq(source.string()), + mcpp::xlings::shq(ifcOut.string()), + objName); +} + +} // namespace + +std::vector std_module_build_commands( + const Toolchain& tc, const std::filesystem::path& cacheDir, + std::string_view cppStandardFlag) { + return { cl_stage_command(tc, cacheDir, cppStandardFlag, + tc.stdModuleSource, + std_bmi_path(cacheDir), "std.obj", "") }; +} + +std::vector std_compat_build_commands( + const Toolchain& tc, const std::filesystem::path& cacheDir, + std::string_view cppStandardFlag) { + // std.compat imports std — reference the freshly staged std.ifc. + auto ref = std::format(" /reference std={}", + mcpp::xlings::shq(std_bmi_path(cacheDir).string())); + return { cl_stage_command(tc, cacheDir, cppStandardFlag, + tc.stdCompatSource, + std_compat_bmi_path(cacheDir), "std.compat.obj", + ref) }; +} + std::expected enrich_toolchain_from_cl(Toolchain& tc) { auto banner = capture_cl_banner(tc.binaryPath); auto parsed = parse_cl_banner(banner); @@ -386,6 +551,17 @@ std::expected enrich_toolchain_from_cl(Toolchain& tc) { tc.stdModuleSource = *found; tc.hasImportStd = true; } + if (auto compat = toolsDir / "modules" / "std.compat.ixx"; + std::filesystem::exists(compat, ec)) { + tc.stdCompatSource = compat; + } + + // Build environment (INCLUDE/LIB/PATH/VSLANG). SDK absence keeps + // detection working (selection UX on SDK-less boxes); the build path + // errors with guidance when envOverrides is empty. + if (auto sdk = find_windows_sdk()) { + tc.envOverrides = build_env_for_cl(tc.binaryPath, parsed->second, *sdk); + } return {}; } diff --git a/src/toolchain/provider.cppm b/src/toolchain/provider.cppm index d3246e1..1bc32ad 100644 --- a/src/toolchain/provider.cppm +++ b/src/toolchain/provider.cppm @@ -95,9 +95,9 @@ ProviderCapabilities capabilities_for(const Toolchain& tc) { } case CompilerId::MSVC: { - // Pure MSVC (cl.exe) — not yet fully supported, but stubs are here - // so callers can branch on it without another unknown-compiler guard. caps.has_scan_deps = false; + // cl.exe emits P1689 itself via /scanDependencies. + caps.has_builtin_p1689_scan = true; caps.stdlib_id = "msvc-stl"; caps.archive_format = "lib.exe"; break; diff --git a/src/toolchain/registry.cppm b/src/toolchain/registry.cppm index 813e3c5..ec793de 100644 --- a/src/toolchain/registry.cppm +++ b/src/toolchain/registry.cppm @@ -8,6 +8,7 @@ import mcpp.toolchain.clang; import mcpp.toolchain.gcc; import mcpp.toolchain.llvm; import mcpp.toolchain.model; +import mcpp.toolchain.msvc; export namespace mcpp::toolchain { @@ -68,8 +69,11 @@ std::vector> available_toolchain_indexes(); std::filesystem::path derive_c_compiler(const Toolchain& tc); std::filesystem::path archive_tool(const Toolchain& tc); +std::filesystem::path link_tool(const Toolchain& tc); std::filesystem::path staged_std_bmi_path(const Toolchain& tc, const std::filesystem::path& outputDir); +std::filesystem::path staged_std_compat_bmi_path(const Toolchain& tc, + const std::filesystem::path& outputDir); } // namespace mcpp::toolchain @@ -273,6 +277,12 @@ std::filesystem::path derive_c_compiler(const Toolchain& tc) { } std::filesystem::path archive_tool(const Toolchain& tc) { + if (tc.compiler == CompilerId::MSVC) { + auto lib = tc.binaryPath.parent_path() / "lib.exe"; + std::error_code ec; + if (std::filesystem::exists(lib, ec)) return lib; + return {}; + } if (is_clang(tc)) return mcpp::toolchain::clang::archive_tool(tc); // MinGW bundles its own binutils next to the frontend (self-contained, @@ -302,8 +312,27 @@ std::filesystem::path archive_tool(const Toolchain& tc) { std::filesystem::path staged_std_bmi_path(const Toolchain& tc, const std::filesystem::path& outputDir) { + if (tc.compiler == CompilerId::MSVC) + return mcpp::toolchain::msvc::staged_std_bmi_path(outputDir); if (is_clang(tc)) return mcpp::toolchain::clang::staged_std_bmi_path(outputDir); return mcpp::toolchain::gcc::staged_std_bmi_path(outputDir); } +std::filesystem::path staged_std_compat_bmi_path(const Toolchain& tc, + const std::filesystem::path& outputDir) { + if (tc.compiler == CompilerId::MSVC) + return mcpp::toolchain::msvc::staged_std_compat_bmi_path(outputDir); + return mcpp::toolchain::clang::staged_std_compat_bmi_path(outputDir); +} + +// Separate linker binary for SeparateLinker dialects (link.exe beside cl). +// Empty for driver-link toolchains. +std::filesystem::path link_tool(const Toolchain& tc) { + if (tc.compiler != CompilerId::MSVC) return {}; + auto link = tc.binaryPath.parent_path() / "link.exe"; + std::error_code ec; + if (std::filesystem::exists(link, ec)) return link; + return {}; +} + } // namespace mcpp::toolchain diff --git a/src/toolchain/stdmod.cppm b/src/toolchain/stdmod.cppm index 8c309eb..d2251c9 100644 --- a/src/toolchain/stdmod.cppm +++ b/src/toolchain/stdmod.cppm @@ -30,6 +30,7 @@ import mcpp.toolchain.detect; import mcpp.toolchain.fingerprint; import mcpp.toolchain.gcc; import mcpp.toolchain.linkmodel; +import mcpp.toolchain.msvc; export namespace mcpp::toolchain { @@ -64,8 +65,12 @@ namespace mcpp::toolchain { namespace { -std::expected run_capture_command(const std::string& cmd) { - auto r = mcpp::platform::process::capture(cmd); +std::expected run_capture_command( + const std::string& cmd, + const std::vector>& env) { + auto r = env.empty() + ? mcpp::platform::process::capture(cmd) + : mcpp::platform::process::capture_with_env(cmd, env); if (r.exit_code != 0) { // Include the command: its --sysroot/-isystem flags are the first // thing needed to diagnose header-resolution failures. @@ -154,10 +159,16 @@ std::expected write_metadata(const std::filesystem::path& pat return {}; } -std::expected run_commands(const std::vector& commands) { +// Toolchain-declared env (MSVC's INCLUDE/LIB/PATH) applies to every std +// module build command; empty for GCC/Clang (their LD_LIBRARY_PATH need is +// carried as an in-command `env` prefix on POSIX for now). +std::expected run_commands( + const std::vector& commands, const Toolchain& tc) { + std::vector> env; + for (auto& ev : tc.envOverrides) env.emplace_back(ev.key, ev.value); std::string out; for (auto const& cmd : commands) { - if (auto r = run_capture_command(cmd); !r) return std::unexpected(r.error()); + if (auto r = run_capture_command(cmd, env); !r) return std::unexpected(r.error()); else out += *r; } return out; @@ -188,12 +199,14 @@ std::expected ensure_built( "toolchain has no std module source (import std unsupported on this compiler)"}); } + const bool isMsvc = tc.compiler == CompilerId::MSVC; StdModule sm; sm.cacheDir = cache_root / std::string(fingerprint_hex); - sm.bmiPath = is_clang(tc) + sm.bmiPath = isMsvc ? mcpp::toolchain::msvc::std_bmi_path(sm.cacheDir) + : is_clang(tc) ? mcpp::toolchain::clang::std_bmi_path(sm.cacheDir) : mcpp::toolchain::gcc::std_bmi_path(sm.cacheDir); - sm.objectPath = sm.cacheDir / "std.o"; + sm.objectPath = sm.cacheDir / (isMsvc ? "std.obj" : "std.o"); // Build sysroot + include flags for std module precompilation, derived // from the shared toolchain link model (same resolver as flags.cppm — @@ -222,18 +235,26 @@ std::expected ensure_built( macos_deployment_target); } - // Both providers expose the same command-sequence shape (A5 backend + // All three providers expose the same command-sequence shape (A5 backend // surface normalization) — no per-compiler arity branching here. - std::vector stdCommands = is_clang(tc) + std::vector stdCommands = + isMsvc ? mcpp::toolchain::msvc::std_module_build_commands( + tc, sm.cacheDir, cpp_standard_flag) + : 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); - compatCommands = mcpp::toolchain::clang::std_compat_build_commands( - tc, sm.cacheDir, compatBmi, sm.bmiPath, sysroot_flag, cpp_standard_flag); + if (!tc.stdCompatSource.empty()) { + if (isMsvc) { + compatCommands = mcpp::toolchain::msvc::std_compat_build_commands( + tc, sm.cacheDir, cpp_standard_flag); + } else if (is_clang(tc)) { + auto compatBmi = mcpp::toolchain::clang::std_compat_bmi_path(sm.cacheDir); + compatCommands = mcpp::toolchain::clang::std_compat_build_commands( + tc, sm.cacheDir, compatBmi, sm.bmiPath, sysroot_flag, cpp_standard_flag); + } } auto metadata = metadata_for(tc, cpp_standard, cpp_standard_flag, stdCommands, compatCommands); auto metaPath = metadata_path(sm.cacheDir); @@ -248,7 +269,7 @@ std::expected ensure_built( if (ec) return std::unexpected(StdModError{ std::format("cannot create '{}': {}", sm.bmiPath.parent_path().string(), ec.message())}); - auto out = run_commands(stdCommands); + auto out = run_commands(stdCommands, tc); if (!out) return std::unexpected(out.error()); if (!std::filesystem::exists(sm.bmiPath)) { @@ -259,17 +280,19 @@ std::expected ensure_built( rebuiltStd = true; } - // Build std.compat after std (std.compat depends on std, Clang only). - if (is_clang(tc) && !tc.stdCompatSource.empty()) { - auto compatBmi = mcpp::toolchain::clang::std_compat_bmi_path(sm.cacheDir); + // Build std.compat after std (std.compat imports std; Clang + MSVC). + if (!compatCommands.empty()) { + auto compatBmi = isMsvc + ? mcpp::toolchain::msvc::std_compat_bmi_path(sm.cacheDir) + : mcpp::toolchain::clang::std_compat_bmi_path(sm.cacheDir); if (rebuiltStd || !std::filesystem::exists(compatBmi) || !metadata_matches(metaPath, metadata)) { - if (auto out = run_commands(compatCommands); !out) { + if (auto out = run_commands(compatCommands, tc); !out) { return std::unexpected(out.error()); } } sm.compatBmiPath = compatBmi; - sm.compatObjectPath = sm.cacheDir / "std.compat.o"; + sm.compatObjectPath = sm.cacheDir / (isMsvc ? "std.compat.obj" : "std.compat.o"); } if (auto r = write_metadata(metaPath, metadata); !r) { From 856a292a79a516a99d435c4bc497790023270db5 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Mon, 13 Jul 2026 19:19:15 +0800 Subject: [PATCH 4/4] feat: e2e 99 msvc native build, 95 gate-flip, mingw polish, 0.0.90 version/docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - e2e 99 (requires: msvc): default msvc → modules build/run → import std → incremental; 95's build-gate assertion flips to a real build+run; the ci-windows MSVC step now drives 99. - mingw: windows-only guard with a clear message (was 'invalid xpkg target xim:mingw-gcc@'); e2e 97 objdump path unhardcoded; doctor gains Windows SDK + mingw sections. - release.yml publish-ecosystem timeout 20→30 (backstop for the fixed mirror script's worst case). - version 0.0.90 + CHANGELOG + docs/03 MSVC build section. --- .github/workflows/ci-windows.yml | 12 ++---- .github/workflows/release.yml | 6 ++- CHANGELOG.md | 36 ++++++++++++++++ docs/03-toolchains.md | 14 ++++--- mcpp.toml | 2 +- src/doctor.cppm | 28 +++++++++++++ src/toolchain/fingerprint.cppm | 2 +- src/toolchain/lifecycle.cppm | 14 +++++++ tests/e2e/95_msvc_system_toolchain.sh | 13 +++--- tests/e2e/97_mingw_toolchain.sh | 2 +- tests/e2e/99_msvc_native_build.sh | 59 +++++++++++++++++++++++++++ 11 files changed, 162 insertions(+), 26 deletions(-) create mode 100755 tests/e2e/99_msvc_native_build.sh diff --git a/.github/workflows/ci-windows.yml b/.github/workflows/ci-windows.yml index 9468d0b..34c99f5 100644 --- a/.github/workflows/ci-windows.yml +++ b/.github/workflows/ci-windows.yml @@ -254,17 +254,11 @@ jobs: "$MCPP_SELF" self doctor 2>&1 | tee doctor.txt || true grep -qi "msvc" doctor.txt - # native cl.exe builds are gated with one owned message - "$MCPP_SELF" new hello_msvc && cd hello_msvc - set +e - bout=$("$MCPP_SELF" build 2>&1); brc=$? - set -e - echo "$bout" - test $brc -ne 0 - grep -q "not yet supported" <<<"$bout" + # native cl.exe build: full e2e (modules, import std, incremental) + cd "$GITHUB_WORKSPACE" + MCPP="$MCPP_SELF" bash tests/e2e/99_msvc_native_build.sh # restore the LLVM default for the remaining steps - cd "$GITHUB_WORKSPACE" "$MCPP_SELF" toolchain default llvm@20.1.7 - name: "Toolchain: LLVM — build mcpp (self-host)" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 59d2679..dda5c71 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -708,8 +708,10 @@ jobs: needs: [build-release, build-linux-aarch64, build-macos, build-windows] runs-on: ubuntu-latest # A4 hardening: a single stuck upload once held this job >1h (6h default - # ceiling). The mirror script now has per-file timeouts, so 20min is ample. - timeout-minutes: 20 + # ceiling). The mirror script has per-file timeouts and (post-0.0.89) + # batch-upload + ranged-GET verification — normal runs are minutes; 30 + # is the generous backstop (20 was hit by the old per-asset verify loop). + timeout-minutes: 30 env: XLINGS_RES_TOKEN: ${{ secrets.XLINGS_RES_TOKEN }} GITCODE_TOKEN: ${{ secrets.GITCODE_TOKEN }} diff --git a/CHANGELOG.md b/CHANGELOG.md index e2969fd..8f55b80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,42 @@ > 本文件追踪 `mcpp-community/mcpp` 公开仓的版本演进。 > 格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)。 +## [0.0.90] — 2026-07-13 + +### 新增 + +- **MSVC 原生构建后端(cl.exe)落地,0.0.88 的构建门移除**。选定 `msvc@system` + 后 `mcpp build/run` 直接用系统 MSVC 编译链接: + - 环境模型:`find_windows_sdk()` + 从检测到的 VC tools/SDK 直接合成 + INCLUDE/LIB/PATH(+`VSLANG=1033`),不跑 vcvarsall;SDK 缺失时检测/选择仍可用, + 构建报带指引的明确错误;doctor 新增 SDK 与 mingw 检查行。 + - 模块管线:std/std.compat.ixx 单命令 staging(`/ifcOutput` → ifc.cache), + 命名模块 `.cppm` 经 `/interface /TP` 编译、`/ifcSearchDir` 消费; + `/scanDependencies` 作为第三个编译器内建 P1689 扫描驱动接入 dyndep。 + - 链接:link.exe/lib.exe(SeparateLinker)+ 响应文件(绕 cmd 8191 限制), + DLL=`/DLL /IMPLIB:`;`deps=msvc`(/showIncludes)头文件依赖;`/MD|/MT` CRT + 随 linkage;`/std:c++20|c++latest` 映射;`.obj` 扩展名全链路。 + - fast-path 增量:构建缓存 env 槽新增 `@env` 多变量编码,增量构建重建 + INCLUDE/LIB 环境。e2e 99(模块/import std/增量)+ 95 改造为真实构建断言。 +- **`[build] dialect_cxxflags` + 方言旗标全图化(issue #210 修复)**。 + `-freflection` 等"改变标准库头声明集"的 flag 现随 `-std=` 的通道到达: + 全局 cxxflags(项目+依赖所有 TU)、std/std.compat BMI 预构建命令、P1689 扫描。 + known-list 自动提升(reflection/contracts/char8_t/`_GLIBCXX_USE_CXX11_ABI`)+ + 显式 `dialect_cxxflags` 逃生舱;指纹早已包含这些 flag,修的是命令构造。 + 实证:#210 的最小复现(gcc16 + `import std;` + `std::meta`)输出 `x 2/y 3`; + e2e 98 含依赖模块变体。 + +### 修复与优化 + +- mingw 在非 Windows 主机的 `toolchain install/default` 现在明确报 + windows-only(此前是 `invalid xpkg target 'xim:mingw-gcc@'`)。 +- std 模块 staging 命令在 Windows 用 `cd /d`(跨盘;工作区 D: + 缓存 C: 的 + 真实 CI 布局)。 +- release 的 publish-ecosystem:镜像脚本改为批量上传+带耐心的 ranged-GET 验证、 + **验证超时不再删除资产**(0.0.89 因逐资产"18s 即删重传"+全量 GET 探测触顶 + 20min 被杀);timeout 兜底 20→30。 +- stdmod 执行层支持工具链声明环境(capture_with_env);shell 引用平台化。 + ## [0.0.89] — 2026-07-13 ### 新增 diff --git a/docs/03-toolchains.md b/docs/03-toolchains.md index 5e6b8aa..560d7a4 100644 --- a/docs/03-toolchains.md +++ b/docs/03-toolchains.md @@ -132,12 +132,14 @@ windows = "msvc@system" newest installed VC tools, but errors if the detected version doesn't match the prefix. -> [!NOTE] -> Selection and detection are supported today; **building with native MSVC -> (cl.exe) is not yet supported** — `mcpp build` fails with a clear message -> naming the detected version. For building on Windows use the MSVC-ABI Clang -> toolchain: `mcpp toolchain default llvm@20.1.7` (it borrows the MSVC STL -> from the same detected installation). +Since 0.0.90, **native cl.exe builds work**: mcpp synthesizes the +INCLUDE/LIB environment from the detected VC tools + Windows SDK (no +`vcvarsall` involved), stages `std.ixx`/`std.compat.ixx` as `.ifc` BMIs, +compiles `.cppm` module units via `/interface /TP /ifcOutput`, scans with +`/scanDependencies`, and links with `link.exe`/`lib.exe` through response +files. `[build] linkage = "static"` selects the `/MT` CRT. A missing Windows +SDK fails the build with installation guidance (`mcpp self doctor` reports +SDK status). ## Project-Level Version Pinning diff --git a/mcpp.toml b/mcpp.toml index 4d6cd60..29b5397 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,6 +1,6 @@ [package] name = "mcpp" -version = "0.0.89" +version = "0.0.90" description = "Modern C++ build & package management tool" license = "Apache-2.0" authors = ["mcpp-community"] diff --git a/src/doctor.cppm b/src/doctor.cppm index 60e168a..a077b47 100644 --- a/src/doctor.cppm +++ b/src/doctor.cppm @@ -120,6 +120,34 @@ export int doctor_report() { warn("msvc not detected — run `mcpp toolchain default msvc` for " "setup guidance (mcpp does not install MSVC)"); } + // Windows SDK (native cl.exe builds need its UCRT/um headers). + if (auto sdk = mcpp::toolchain::msvc::find_windows_sdk()) { + ok(std::format("Windows SDK {} at {}", sdk->version, + sdk->root.string())); + } else { + warn("no Windows SDK found — native msvc builds will fail " + "(install the 'Windows 11 SDK' VS component)"); + } + + mcpp::ui::status("Checking", "mingw (xim:mingw-gcc)"); + { + auto pkgs = std::filesystem::path( + std::getenv("MCPP_HOME") ? std::getenv("MCPP_HOME") + : (std::string(std::getenv("USERPROFILE") + ? std::getenv("USERPROFILE") : "") + "\\.mcpp")) + / "registry" / "data" / "xpkgs" / "xim-x-mingw-gcc"; + std::error_code ec; + bool any = false; + if (std::filesystem::exists(pkgs, ec)) { + for (auto& v : std::filesystem::directory_iterator(pkgs, ec)) { + if (!v.is_directory(ec)) continue; + ok(std::format("mingw {} installed", v.path().filename().string())); + any = true; + } + } + if (!any) + ok("mingw not installed (optional — `mcpp toolchain install mingw 16.1.0`)"); + } } mcpp::ui::status("Checking", "std module"); diff --git a/src/toolchain/fingerprint.cppm b/src/toolchain/fingerprint.cppm index 4098092..45047bb 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.89"; +inline constexpr std::string_view MCPP_VERSION = "0.0.90"; struct FingerprintInputs { Toolchain toolchain; diff --git a/src/toolchain/lifecycle.cppm b/src/toolchain/lifecycle.cppm index 13434a4..da926ad 100644 --- a/src/toolchain/lifecycle.cppm +++ b/src/toolchain/lifecycle.cppm @@ -183,6 +183,18 @@ int msvc_wrong_host() { return 1; } +// MinGW is a Windows-native toolchain package; on other hosts fail with a +// clear message instead of the confusing empty-version xim error +// (`invalid xpkg target 'xim:mingw-gcc@'`). +bool mingw_wrong_host(const mcpp::toolchain::XimToolchainPackage& pkg) { + if (pkg.ximName == "mingw-gcc" && !mcpp::platform::is_windows) { + mcpp::ui::error("mingw is a Windows-only toolchain (MinGW-w64 GCC); " + "on this host use gcc/llvm instead"); + return true; + } + return false; +} + void msvc_print_detected(const mcpp::toolchain::msvc::MsvcInstallation& inst) { mcpp::ui::status("Detected", std::format( "msvc {}{} (VC tools {})", @@ -370,6 +382,7 @@ export int toolchain_install(const mcpp::config::GlobalConfig& cfg, } auto pkg = mcpp::toolchain::to_xim_package(*spec); + if (mingw_wrong_host(pkg)) return 1; // Partial-version resolution: `gcc 15` → highest available 15.x.y in // the synced index. Empty version → latest of any major. @@ -490,6 +503,7 @@ export int toolchain_set_default(const mcpp::config::GlobalConfig& cfg, } auto pkg = mcpp::toolchain::to_xim_package(*spec); + if (mingw_wrong_host(pkg)) return 1; // Partial-version resolution against installed payloads. if (auto picked = resolve_version_match( diff --git a/tests/e2e/95_msvc_system_toolchain.sh b/tests/e2e/95_msvc_system_toolchain.sh index 2cdd9f8..0057c92 100755 --- a/tests/e2e/95_msvc_system_toolchain.sh +++ b/tests/e2e/95_msvc_system_toolchain.sh @@ -58,12 +58,13 @@ out=$("$MCPP" toolchain install msvc 2>&1) \ [[ "$out" == *"already installed"* ]] \ || { echo "FAIL: install msvc message: $out"; exit 1; } -# 5) native cl.exe builds are gated with one owned message +# 5) native cl.exe builds WORK (0.0.90; the 0.0.88 gate is gone) — the full +# modules/import-std flow is covered by 99_msvc_native_build.sh; here we +# assert the basic hello builds and runs under msvc@system. "$MCPP" new hello_msvc >/dev/null 2>&1 cd hello_msvc -rc=0; out=$("$MCPP" build 2>&1) || rc=$? -[[ $rc -ne 0 ]] || { echo "FAIL: build should be gated"; exit 1; } -[[ "$out" == *"not yet supported"* ]] || { echo "FAIL: gate message: $out"; exit 1; } -[[ "$out" == *"llvm@20.1.7"* ]] || { echo "FAIL: no alternative hint: $out"; exit 1; } +out=$("$MCPP" run 2>&1) || { echo "FAIL: msvc hello build/run: $out"; exit 1; } +[[ "$out" == *"Hello"* || "$out" == *"hello"* ]] \ + || { echo "FAIL: hello output: $out"; exit 1; } -echo "PASS: msvc@system detection, selection, guidance, and build gate" +echo "PASS: msvc@system detection, selection, guidance, and native build" diff --git a/tests/e2e/97_mingw_toolchain.sh b/tests/e2e/97_mingw_toolchain.sh index f897ab6..a81864c 100755 --- a/tests/e2e/97_mingw_toolchain.sh +++ b/tests/e2e/97_mingw_toolchain.sh @@ -64,7 +64,7 @@ run_out=$("$MCPP" run 2>&1) || { echo "FAIL: run: $run_out"; exit 1; } # 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" +OBJDUMP=$(ls "${MCPP_HOME:-$HOME/.mcpp}"/registry/data/xpkgs/xim-x-mingw-gcc/*/bin/objdump.exe 2>/dev/null | head -1) imports="" if [[ -x "$OBJDUMP" ]]; then imports=$("$OBJDUMP" -p "$EXE" 2>/dev/null | grep -i "DLL Name" || true) diff --git a/tests/e2e/99_msvc_native_build.sh b/tests/e2e/99_msvc_native_build.sh new file mode 100755 index 0000000..3a91af4 --- /dev/null +++ b/tests/e2e/99_msvc_native_build.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +# requires: msvc +# 99_msvc_native_build.sh — native cl.exe builds (0.0.90): +# - toolchain default msvc → new → build → run (import std via std.ixx/.ifc) +# - a named module (.cppm through /interface /TP + /ifcOutput) +# - incremental: touching one .cppm rebuilds, run still correct +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" +"$MCPP" toolchain default msvc >/dev/null \ + || { echo "FAIL: toolchain default msvc"; exit 1; } + +"$MCPP" new hello_cl >/dev/null 2>&1 +cd hello_cl +mkdir -p src +cat > src/greet.cppm <<'EOF' +export module hello.greet; +import std; +export namespace hello { +std::string greet() { return "cl-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: msvc build: $out"; exit 1; } +run_out=$("$MCPP" run 2>&1) || { echo "FAIL: msvc run: $run_out"; exit 1; } +[[ "$run_out" == *"cl-ok"* ]] || { echo "FAIL: run output: $run_out"; exit 1; } + +# .obj + .ifc artifacts really came from the msvc pipeline +find target -name "*.obj" | grep -q . || { echo "FAIL: no .obj produced"; exit 1; } +find target -path "*ifc.cache*" -name "*.ifc" | grep -q . \ + || { echo "FAIL: no .ifc produced"; exit 1; } + +# incremental: edit the module, expect rebuild + updated output +sleep 1 +sed -i 's/cl-ok/cl-ok2/' src/greet.cppm +run_out=$("$MCPP" run 2>&1) || { echo "FAIL: incremental run: $run_out"; exit 1; } +[[ "$run_out" == *"cl-ok2"* ]] || { echo "FAIL: incremental output: $run_out"; exit 1; } + +echo "PASS: native MSVC build — modules, import std, incremental"