Skip to content

Latest commit

 

History

History
1045 lines (783 loc) · 42.1 KB

File metadata and controls

1045 lines (783 loc) · 42.1 KB

Windows 可用性 — 实施计划

For agentic workers: 本计划按任务逐条执行,每个任务自带测试循环与提交点。 步骤用 - [ ] 复选框跟踪。

Goal: 让 mcpp 在裸 Windows(无 MSVC STL)上无感可用,补齐含空格路径与 build.mcpp 的 方言/环境缺口,给 build.mcpp 接上 import std;,并把测试面从 1 个 Windows 镜像扩到 3 轴(含无 MSVC 轴)。

Architecture: 五个正交组件 A–E,见 .agents/docs/2026-08-02-windows-usability-design.md。 A(默认值分档)、B(引号收敛)、D(import std;)互不触碰同一函数,可并行推进; C(build.mcpp 方言+环境)串行且依赖 B 的 argv[0] 修复;E(CI/e2e)随对应组件落地。

Tech Stack: C++23 modules,mcpp 自举构建(mcpp build / mcpp test), gtest 单测(tests/unit/),bash e2e(tests/e2e/,# requires: 能力门), GitHub Actions。


Global Constraints

  • 版本号:本批次发 2026.8.2.1。规范 YYYY.M.D.N,月日不补零,.0 保留给稳定版。
  • 版本号只改两处:mcpp.toml:3src/toolchain/fingerprint.cppm:21.xlings.json:3 是 bootstrap pin,是自举起点,不随本次发布走 —— 它只在 release 完成、包已上架之后单独 bump。提前改会让全部 CI 去装一个不存在的版本。
  • 校验:任何版本改动后必须 bash .github/tools/check_version_pins.sh 通过。
  • 不引入 clang + libc++ 的任何形态(设计 §7.1)。
  • 不新增配置字段:意图来源分档复用现有的两层配置(项目 mcpp.toml vs 全局 config.toml),不落盘任何新状态。
  • 同一决策只推导一次:新增的 MSVC 可用性判据必须与 prepare.cppm:1284 现有的 「有 VC tools 缺 SDK」检查合并成一处,不得并排两个 if。
  • e2e # requires: 必须在脚本第 2 行,否则等同没写。
  • 单测风格:#include <gtest/gtest.h> + import std; + import mcpp.<module>;
  • 分支:feat/windows-usability,单 PR,合入用 bypass squash。

File Structure

文件 责任 本计划中的变化
src/toolchain/msvc.cppm VS/SDK 发现 + has_usable_msvc() 谓词
src/toolchain/triple.cppm triple 词表 + 版本 pin kFirstRunMacWin 拆三
src/toolchain/dialect.cppm 命令方言表 + 四个链接/语言字段
src/build/prepare.cppm 构建前置:工具链解析、依赖、计划 首跑分档、意图来源、修复门
src/build/flags.cppm 全局编译/链接 flag 组装 + include_token() 导出
src/build/ninja_backend.cppm ninja 文件生成 local_include_flags 改调 helper
src/build/build_program.cppm build.mcpp 编译/执行/指令解析 方言化、env、import std;
src/platform/process.cppm 进程执行 Windows argv[0] 引号
tests/unit/test_windows_defaults.cpp — A 组件单测 新建
tests/unit/test_build_flags.cpp flags 单测 + include_token 用例
tests/unit/test_dialect.cpp — 方言字段单测 新建
tests/e2e/179_spaced_paths.sh — 含空格路径 新建
tests/e2e/180_msvc_build_mcpp.sh — MSVC × build.mcpp 新建
tests/e2e/181_build_mcpp_import_std.sh — build.mcpp import std; 新建
tests/e2e/182_windows_no_msvc_fallback.sh — 回退 + 意图分档 新建
tests/e2e/112_build_mcpp_cross.sh 交叉 build.mcpp + import std; 断言
.github/workflows/ci-fresh-install.yml 全新安装验证 windows-fresh 拆三轴
.github/workflows/ci-windows.yml Windows CI MSVC 步骤补 180
docs/03-toolchains.md docs/07-build-mcpp.md README*.md 用户文档 同步新行为

Task 0: 分支与版本号

Files:

  • Modify: mcpp.toml:3

  • Modify: src/toolchain/fingerprint.cppm:21

  • Step 1: 建分支

git checkout -b feat/windows-usability
  • Step 2: bump 两处版本号(且仅两处)

mcpp.toml:3:version = "2026.8.1.1"version = "2026.8.2.1" src/toolchain/fingerprint.cppm:21:MCPP_VERSION = "2026.8.1.1""2026.8.2.1"

.xlings.json:3 保持 2026.8.1.1 不动

  • Step 3: 校验 pin 一致性

Run: bash .github/tools/check_version_pins.sh Expected: 退出码 0,输出 mcpp version: building=2026.8.2.1 (fingerprint=2026.8.2.1) bootstrap pin=2026.8.1.1

  • Step 4: 提交
git add mcpp.toml src/toolchain/fingerprint.cppm .agents/docs/
git commit -m "chore: bump to 2026.8.2.1 + windows usability design/plan docs"

Task 1: has_usable_msvc() 谓词

Files:

  • Modify: src/toolchain/msvc.cppm(export 区 :25-40,实现区 :239 附近)
  • Test: tests/unit/test_windows_defaults.cpp(新建)

Interfaces:

  • Produces: bool mcpp::toolchain::msvc::has_usable_msvc(); —— 当且仅当 find_std_module_source()find_windows_sdk() 都返回值时为 true。 非 Windows 平台恒为 false

  • Step 1: 写失败测试

tests/unit/test_windows_defaults.cpp:

#include <gtest/gtest.h>

import std;
import mcpp.toolchain.msvc;
import mcpp.platform;

// has_usable_msvc() 的契约:两件齐才为真。非 Windows 恒假。
// 在 Windows CI 上这两条分别覆盖有 VS / 遮蔽后无 VS 的机器。
TEST(WindowsDefaults, HasUsableMsvcIsFalseOffWindows) {
    if constexpr (!mcpp::platform::is_windows) {
        EXPECT_FALSE(mcpp::toolchain::msvc::has_usable_msvc());
    } else {
        GTEST_SKIP() << "windows-specific path covered by e2e 182";
    }
}

// 谓词必须与它的两个组成部分一致 —— 不允许出现
// 「has_usable_msvc() 为真但 find_windows_sdk() 为空」这种自相矛盾。
TEST(WindowsDefaults, HasUsableMsvcAgreesWithItsParts) {
    const bool both = mcpp::toolchain::msvc::find_std_module_source().has_value()
                   && mcpp::toolchain::msvc::find_windows_sdk().has_value();
    EXPECT_EQ(mcpp::toolchain::msvc::has_usable_msvc(), both);
}
  • Step 2: 跑测试确认失败

Run: mcpp test -- --gtest_filter='WindowsDefaults.*' Expected: 编译失败 —— has_usable_msvc 未声明。

  • Step 3: 实现

src/toolchain/msvc.cppm export 区(紧跟 find_windows_sdk 声明之后)加:

// True only when BOTH halves of a usable MSVC C++ setup are present:
// the STL's std module source AND the Windows SDK. Either alone is a
// half-installed state (VS with only the .NET workload; VC tools without
// the SDK) that fails at compile time instead of at selection time —
// which is exactly the bug this predicate exists to prevent.
bool has_usable_msvc();

实现区:

bool has_usable_msvc() {
#if defined(_WIN32)
    return find_std_module_source().has_value() && find_windows_sdk().has_value();
#else
    return false;
#endif
}
  • Step 4: 跑测试确认通过

Run: mcpp test -- --gtest_filter='WindowsDefaults.*' Expected: PASS(Linux 上第一条通过、第二条 both=false 也通过)

  • Step 5: 提交
git add src/toolchain/msvc.cppm tests/unit/test_windows_defaults.cpp
git commit -m "feat(toolchain): add msvc::has_usable_msvc() — STL and SDK both present"

Task 2: pin 拆分 + 首跑分档

Files:

  • Modify: src/toolchain/triple.cppm:146-154(pins 块)
  • Modify: src/build/prepare.cppm:1167-1183(offline 硬错误)、:1207-1232(首跑)
  • Test: tests/unit/test_windows_defaults.cpp

Interfaces:

  • Consumes: msvc::has_usable_msvc()(Task 1)

  • Produces:

    • pins::kFirstRunMac = "llvm@20.1.7"
    • pins::kFirstRunWinMsvc = "llvm@20.1.7"
    • pins::kFirstRunWinGnu = "gcc@16.1.0"
    • pins::kFirstRunWinGnuTarget = "x86_64-windows-gnu"
    • kFirstRunMacWin 删除(全仓库无残留引用)
  • Step 1: 写失败测试

追加到 tests/unit/test_windows_defaults.cpp:

import mcpp.toolchain.triple;

// 拆分后的三个 pin 必须各自可解析,且 GNU 档位的 target 必须是已登记的
// verified 目标 —— 否则回退会把用户送进一个 mcpp 拒绝构建的 target。
TEST(WindowsDefaults, FirstRunPinsParse) {
    namespace pins = mcpp::toolchain::triple::pins;
    for (auto spec : { pins::kFirstRunMac, pins::kFirstRunWinMsvc,
                       pins::kFirstRunWinGnu }) {
        auto parsed = mcpp::toolchain::parse_toolchain_spec(std::string(spec));
        ASSERT_TRUE(parsed.has_value()) << spec;
        EXPECT_FALSE(parsed->version.empty()) << spec;
    }
}

TEST(WindowsDefaults, GnuFallbackTargetIsVerified) {
    namespace triple = mcpp::toolchain::triple;
    auto t = triple::parse(std::string(triple::pins::kFirstRunWinGnuTarget));
    ASSERT_TRUE(t.has_value());
    const auto* known = triple::find_known_target(*t);
    ASSERT_NE(known, nullptr);
    EXPECT_EQ(known->tier, "verified");
    // 词表 pin 必须与回退用的工具链一致,否则两处推导会漂移。
    EXPECT_EQ(known->pin, triple::pins::kFirstRunWinGnu);
}
  • Step 2: 跑测试确认失败

Run: mcpp test -- --gtest_filter='WindowsDefaults.FirstRun*:WindowsDefaults.GnuFallback*' Expected: 编译失败 —— kFirstRunMac 等未声明。

  • Step 3: 改 pins 块

src/toolchain/triple.cppm:146-154 替换为:

namespace pins {
    // First-run auto-install defaults (prepare.cppm), per host platform/arch.
    //
    // macOS and Windows used to share ONE pin. They must not: Apple ships no
    // GCC so LLVM is the only self-contained choice there, while on Windows
    // clang targets the MSVC ABI and therefore needs Visual Studio's STL +
    // the Windows SDK — neither of which is preinstalled. A bare Windows box
    // got a default it could never build with, and no diagnostic.
    inline constexpr std::string_view kFirstRunMac          = "llvm@20.1.7";
    // Windows WITH a usable MSVC (STL + SDK): unchanged behavior — the MSVC
    // ABI is what lets a project link vcpkg / third-party .lib artifacts.
    inline constexpr std::string_view kFirstRunWinMsvc      = "llvm@20.1.7";
    // Windows WITHOUT one: winlibs GCC targeting PE/GNU. Fully self-contained
    // (static libstdc++/libgcc, its own UCRT), zero VS dependency, `import std`
    // works. Must stay equal to kKnownTargets["x86_64-windows-gnu"].pin.
    inline constexpr std::string_view kFirstRunWinGnu       = "gcc@16.1.0";
    inline constexpr std::string_view kFirstRunWinGnuTarget = "x86_64-windows-gnu";
    inline constexpr std::string_view kFirstRunLinuxX86_64  = "gcc@16.1.0";
    inline constexpr std::string_view kFirstRunLinuxOther   = "gcc@15.1.0-musl";
    // Suggested install spellings used by help / MCPP_NO_AUTO_INSTALL errors.
    inline constexpr std::string_view kSuggestLlvm          = "llvm 20.1.7";
    inline constexpr std::string_view kSuggestGccMusl       = "gcc 15.1.0-musl";
    inline constexpr std::string_view kSuggestGccMingw      = "gcc 16.1.0";
} // namespace pins
  • Step 4: 改首跑分支

src/build/prepare.cppm:1209-1215if constexpr (is_macos || is_windows) 拆开:

        namespace pins = mcpp::toolchain::triple::pins;
        std::string defaultSpec;
        std::string defaultTargetSpec;   // empty = host target
        if constexpr (mcpp::platform::is_macos) {
            defaultSpec = std::string(pins::kFirstRunMac);
        } else if constexpr (mcpp::platform::is_windows) {
            // detection-first: the MSVC ABI is only a viable default when the
            // machine actually has the MSVC STL *and* the Windows SDK. Without
            // them, fall back to the self-contained winlibs GCC — same product
            // shape (PE, static), zero Visual Studio dependency.
            if (mcpp::toolchain::msvc::has_usable_msvc()) {
                defaultSpec = std::string(pins::kFirstRunWinMsvc);
            } else {
                defaultSpec       = std::string(pins::kFirstRunWinGnu);
                defaultTargetSpec = std::string(pins::kFirstRunWinGnuTarget);
            }
        } else if (mcpp::platform::host_arch == std::string_view("x86_64")) {
            defaultSpec = std::string(pins::kFirstRunLinuxX86_64);
        } else {
            defaultSpec = std::string(pins::kFirstRunLinuxOther);
        }

首跑 info 文案(:1223-1231)同步分档:Windows-GNU 档位说明「未检测到 Visual Studio, 使用自包含的 MinGW-w64 工具链」。

持久化(:1265-1275)在 defaultTargetSpec 非空时同时写 defaultTarget,并把 overrides.target_triple 设为它,使本次构建立即生效。

  • Step 5: 改 offline 硬错误分支

prepare.cppm:1167-1174 的 macOS/Windows 合并分支拆开;Windows 且 !has_usable_msvc() 时建议 mcpp toolchain install gcc 16.1.0 + mcpp toolchain default gcc@16.1.0 --target x86_64-windows-gnu, 而不是建议一条在该机器上不可用的 llvm 命令。

  • Step 6: 全仓库确认无 kFirstRunMacWin 残留

Run: grep -rn "kFirstRunMacWin" src/ docs/ .github/ Expected: 无输出。

  • Step 7: 跑测试

Run: mcpp test -- --gtest_filter='WindowsDefaults.*' Expected: PASS

  • Step 8: 提交
git add src/toolchain/triple.cppm src/build/prepare.cppm tests/unit/test_windows_defaults.cpp
git commit -m "feat(toolchain): detection-first Windows first-run default (winlibs GCC when no MSVC)"

Task 3: 意图来源分档 + 存量修复门

Files:

  • Modify: src/build/prepare.cppm:947/:951/:1015/:1024/:1274(来源标记)
  • Modify: src/build/prepare.cppm:1280-1292(现有 MSVC 诊断 → 合并成统一门)

Interfaces:

  • Consumes: msvc::has_usable_msvc()(Task 1)、pins(Task 2)

  • Produces: 文件内的 enum class TcOrigin { ManifestToolchain, GlobalDefault, TargetSection, TargetPin, FirstRun }bool tc_origin_is_user_explicit(TcOrigin)

  • Step 1: 写失败测试

追加到 tests/unit/test_windows_defaults.cpp —— 分档策略是纯函数,可直接测:

import mcpp.build.prepare;

// 分档判据:只有用户显式写下的两种来源算「显式」。mcpp 自选的三种可以被
// 自动修复。写错这张表的后果是两个方向的:把显式判成自选 → 静默推翻用户的
// ABI 选择;把自选判成显式 → 存量用户永远撞墙。
TEST(WindowsDefaults, OriginClassification) {
    using mcpp::build::TcOrigin;
    EXPECT_TRUE (mcpp::build::tc_origin_is_user_explicit(TcOrigin::ManifestToolchain));
    EXPECT_TRUE (mcpp::build::tc_origin_is_user_explicit(TcOrigin::TargetSection));
    EXPECT_FALSE(mcpp::build::tc_origin_is_user_explicit(TcOrigin::GlobalDefault));
    EXPECT_FALSE(mcpp::build::tc_origin_is_user_explicit(TcOrigin::TargetPin));
    EXPECT_FALSE(mcpp::build::tc_origin_is_user_explicit(TcOrigin::FirstRun));
}
  • Step 2: 跑测试确认失败

Run: mcpp test -- --gtest_filter='WindowsDefaults.OriginClassification' Expected: 编译失败。

  • Step 3: 加枚举与谓词并 export

src/build/prepare.cppm export 区:

// Where the resolved toolchain spec came from. Used to decide whether an
// unusable toolchain may be auto-repaired: mcpp may revise a default it
// picked itself, but must never silently overrule a choice the user wrote
// down (a project that needs the MSVC ABI to link vcpkg .lib artifacts is
// better served by an error than by a silent ABI swap).
enum class TcOrigin {
    ManifestToolchain,  // mcpp.toml [toolchain]        — user explicit
    TargetSection,      // mcpp.toml [target.X].toolchain — user explicit
    GlobalDefault,      // config.toml [toolchain] default
    TargetPin,          // triple.cppm vocabulary convention
    FirstRun,           // written by this very invocation
};

inline bool tc_origin_is_user_explicit(TcOrigin o) {
    return o == TcOrigin::ManifestToolchain || o == TcOrigin::TargetSection;
}
  • Step 4: 在五个赋值点打标记
赋值 标记
:947 tcSpec = m->toolchain.for_platform(...) ManifestToolchain
:951 tcSpec = (*cfg)->defaultToolchain GlobalDefault
:1015 tcSpec = it->second.toolchain TargetSection
:1024 tcSpec = std::string(known->pin) TargetPin
:1274 tcSpec = defaultSpec FirstRun
  • Step 5: 把现有诊断替换成统一门

prepare.cppm:1280-1292 现在是:

    if (tc->compiler == mcpp::toolchain::CompilerId::MSVC
        && tc->envOverrides.empty()) {
        return std::unexpected(std::format("msvc {} was detected at {}, ..."));
    }

替换为一个判据、两个分支:

    // One decision, one derivation: "this build targets the MSVC ABI but the
    // machine has no usable MSVC" covers BOTH the old `msvc@system without a
    // Windows SDK` case and the (previously undiagnosed) `clang targeting the
    // MSVC ABI on a box with no Visual Studio at all` case. Splitting them
    // into two ifs is how the second one went unnoticed in the first place.
    const bool targetsMsvcAbi =
        tc->compiler == mcpp::toolchain::CompilerId::MSVC
        || mcpp::toolchain::is_msvc_target(*tc);
    if (targetsMsvcAbi && !mcpp::toolchain::msvc::has_usable_msvc()) {
        if (tc_origin_is_user_explicit(tcOrigin)
            || mcpp::platform::env::offline_mode()
            || mcpp::platform::env::no_auto_install()) {
            return std::unexpected(msvc_unavailable_guidance(*tc));
        }
        // mcpp picked this default itself and it cannot work here — revise it.
        // This gate runs on EVERY build, which is what repairs users who
        // already have `llvm@20.1.7` persisted from an older mcpp: the
        // first-run branch never fires again for them.
        ... 切到 kFirstRunWinGnu + kFirstRunWinGnuTarget,重写全局 config,
            info 一行,然后重新 detect() ...
    }

msvc_unavailable_guidance() 输出设计 §2.6 的文案,并在 envOverrides.empty() 且 探到了 VC tools 时保留原有的「装 Windows SDK 组件」措辞,不丢信息。

  • Step 6: 跑测试 + 全量单测

Run: mcpp test Expected: 全过。

  • Step 7: 提交
git add src/build/prepare.cppm tests/unit/test_windows_defaults.cpp
git commit -m "feat(build): auto-repair an unusable MSVC default, diagnose an explicit one"

Task 4: include token 引号收敛

Files:

  • Modify: src/build/flags.cppm:215-242(抽出 helper 并 export)
  • Modify: src/build/ninja_backend.cppm:108-132(改调 helper)
  • Test: tests/unit/test_build_flags.cpp

Interfaces:

  • Produces: std::string mcpp::build::include_token(const mcpp::toolchain::CommandDialect& d, const std::filesystem::path& dir, std::string_view prefixOverride = {}); 返回已加前缀、已做 ninja $ 转义、已加 shell 引号的单个 token(不含前导空格)。 prefixOverride 非空时取代 d.includePrefix(供 -idirafter / NASM -I 复用)。

  • Step 1: 写失败测试

追加到 tests/unit/test_build_flags.cpp:

import mcpp.toolchain.dialect;

// #331: 同一份 manifest include_dirs 走两条通道到命令行,只有 flags.cppm 那条
// 做了 shell 引号。含空格的路径在 per-TU 通道上裂成多个 shell 词。两条通道
// 必须调同一个 helper —— 抄一遍不算修好,下一条通道还会漏。
TEST(BuildFlags, IncludeTokenQuotesSpaces) {
    const auto& gnu = mcpp::toolchain::gnu_dialect();
    auto tok = mcpp::build::include_token(gnu, std::filesystem::path("/home/my dir/inc"));
    // 引号之后,交给 shell 时必须仍是单个词。
    EXPECT_NE(tok.find("my dir"), std::string::npos);
    EXPECT_TRUE(tok.starts_with("'-I") || tok.starts_with("\"-I") || tok.find('\'') != std::string::npos)
        << "unquoted token: " << tok;
}

TEST(BuildFlags, IncludeTokenUsesDialectPrefix) {
    const auto& msvc = mcpp::toolchain::msvc_dialect();
    auto tok = mcpp::build::include_token(msvc, std::filesystem::path("/tmp/inc"));
    EXPECT_NE(tok.find("/I"), std::string::npos);
    EXPECT_EQ(tok.find("-I"), std::string::npos);
}

TEST(BuildFlags, IncludeTokenPrefixOverride) {
    const auto& gnu = mcpp::toolchain::gnu_dialect();
    auto tok = mcpp::build::include_token(gnu, std::filesystem::path("/tmp/inc"), "-idirafter");
    EXPECT_NE(tok.find("-idirafter"), std::string::npos);
}

gnu_dialect() / msvc_dialect() 的实际取用函数名与此不同,改用 dialect.cppm 中真实的取用入口(实现时以 grep -n "dialect_for\|gnu_dialect" src/toolchain/dialect.cppm 为准)。

  • Step 2: 跑测试确认失败

Run: mcpp test -- --gtest_filter='BuildFlags.IncludeToken*' Expected: 编译失败 —— include_token 未声明。

  • Step 3: 抽 helper

src/build/flags.cppm export 区加声明,实现区把 :222-242 现有逻辑抽成:

std::string include_token(const mcpp::toolchain::CommandDialect& d,
                          const std::filesystem::path& dir,
                          std::string_view prefixOverride) {
    std::string prefix(prefixOverride.empty() ? d.includePrefix : prefixOverride);
    return shell_quote_arg(escape_path(std::filesystem::path(prefix + dir.string())));
}

:222-242 改为调用它,行为不变(回归由现有 flags 单测兜住)。

  • Step 4: ninja_backend 改调 helper

src/build/ninja_backend.cppm:108-132local_include_flags 需要拿到 dialect。 签名从 (const CompileUnit& cu, bool msvcDialect) 改为 (const CompileUnit& cu, const mcpp::toolchain::CommandDialect& d),调用点同步。 函数体:

std::string local_include_flags(const CompileUnit& cu,
                                const mcpp::toolchain::CommandDialect& d) {
    const bool nasmUnit = is_nasm_source(cu.source);
    const bool msvcDialect = d.includePrefix == std::string_view("/I");
    std::string flags;
    for (auto const& inc : cu.localIncludeDirs) {
        flags += ' ';
        flags += mcpp::build::include_token(d, inc);
    }
    // #249 的三态降级保持不变,只把 quoting 并轨。
    for (auto const& inc : cu.localIncludeDirsAfter) {
        std::string_view pfx = nasmUnit ? "-I" : (msvcDialect ? "/I" : "-idirafter");
        flags += ' ';
        flags += mcpp::build::include_token(d, inc, pfx);
    }
    return flags;
}
  • Step 5: 跑测试

Run: mcpp test -- --gtest_filter='BuildFlags.*:NinjaBackend.*' Expected: PASS

  • Step 6: 自建回归(归一化 diff build.ninja)
mcpp build -p mcpp 2>/dev/null || mcpp build
# 与 main 的 build.ninja 做归一化 diff,确认除 include token 的引号外无差异
  • Step 7: 提交
git add src/build/flags.cppm src/build/ninja_backend.cppm tests/unit/test_build_flags.cpp
git commit -m "fix(build): quote per-TU include dirs — converge both channels on one helper (#331)"

Task 5: command_from_argv 的 Windows argv[0] 引号

Files:

  • Modify: src/platform/process.cppm:234
  • Test: tests/unit/test_platform_process.cpp(若不存在则新建)

Interfaces:

  • 无新导出;command_from_argv 行为变更(Windows 分支)。

  • Step 1: 写失败测试

#include <gtest/gtest.h>
import std;
import mcpp.platform.process;

// #331 #1a: Windows 走 cmd.exe /c 时 argv[0] 未加引号,payload 装在
// "C:\Program Files\..." 或用户名含空格的机器上,命令行在第一个空格处断开。
// 这条不是 MSVC 专属 —— 是全 Windows 的路径假设。
TEST(PlatformProcess, ArgvZeroWithSpacesStaysOneToken) {
    std::vector<std::string> argv = {
        "C:\\Program Files\\mcpp\\g++.exe", "-c", "main.cpp" };
    auto cmd = mcpp::platform::process::command_from_argv(argv);
    if constexpr (mcpp::platform::is_windows) {
        EXPECT_TRUE(cmd.starts_with('"')) << cmd;
        EXPECT_NE(cmd.find("\"C:\\Program Files\\mcpp\\g++.exe\""), std::string::npos) << cmd;
    } else {
        // POSIX 侧已有引号,断言不回退
        EXPECT_NE(cmd.find("Program Files"), std::string::npos) << cmd;
    }
}
  • Step 2: 跑测试确认失败

Run: mcpp test -- --gtest_filter='PlatformProcess.*' Expected: 若 command_from_argv 未导出则编译失败 → 先 export 再跑;Windows 上 FAIL。

  • Step 3: 实现

process.cppm:234 的 Windows 分支对 argv[0] 加双引号(其余参数沿用现有规则)。

实测门:cmd.exe /c 对整条命令的引号剥壳规则与 CreateProcess 不同。 本步骤的最终形态必须由 Windows CI 的实跑结果确定,不得只凭 Linux 侧推断合入。 e2e 179(Task 10)在 Windows 上跑通即为验收。

  • Step 4: 跑测试

Run: mcpp test -- --gtest_filter='PlatformProcess.*' Expected: PASS(Linux);Windows 由 CI 验收。

  • Step 5: 提交
git add src/platform/process.cppm tests/unit/test_platform_process.cpp
git commit -m "fix(platform): quote argv[0] on Windows — payload paths with spaces (#331)"

Task 6: build.mcpp 支持 import std;

Files:

  • Modify: src/build/build_program.cppm:660-700
  • Modify: src/build/prepare.cppm(把 host_tc_for_build_program() 的结果与 m->package.standard / stdFlagAndDialect 传进 build.mcpp 调用)
  • Test: tests/e2e/181_build_mcpp_import_std.sh(新建)

Interfaces:

  • Consumes: mcpp::toolchain::stdmod::ensure_built(tc, standard, dialectFlags, macosDeploymentTarget)StdModule{ bmiPath, objectPath, compatBmiPath, compatObjectPath }(stdmod.cppm:46/63)

  • Consumes: prepare.cppm:1349host_tc_for_build_program(){frontend, Toolchain}

  • Step 1: 写失败 e2e

tests/e2e/181_build_mcpp_import_std.sh(注意 # requires: 必须在第 2 行):

#!/usr/bin/env bash
# requires: unix-shell
# 181_build_mcpp_import_std.sh — build.mcpp can `import std;`
#   mcpp 让用户全项目 import std,却要求构建脚本回退到 #include —— 这条测试
#   锁住那个缺口被补上。跨平台:import std 不是 Windows 专属问题。
set -e

TMP=$(mktemp -d); trap 'rm -rf "$TMP"' EXIT
cd "$TMP"
"$MCPP" new imp_std >/dev/null 2>&1
cd imp_std

cat > build.mcpp <<'EOF'
import std;
int main() {
    std::vector<std::string> flags{"MCPP_FROM_IMPORT_STD"};
    for (auto const& f : flags) std::println("mcpp:cfg={}", f);
    std::println("mcpp:rerun-if-changed=build.mcpp");
    return 0;
}
EOF

cat > src/main.cpp <<'EOF'
import std;
int main() {
#ifdef MCPP_FROM_IMPORT_STD
    std::println("import-std-ok");
    return 0;
#else
    std::println("define missing");
    return 1;
#endif
}
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" == *"import-std-ok"* ]] || { echo "FAIL: run output: $run_out"; exit 1; }

echo "PASS: build.mcpp import std"
  • Step 2: 跑 e2e 确认失败

Run: MCPP=$(pwd)/target/<fingerprint>/release/bin/mcpp bash tests/e2e/181_build_mcpp_import_std.sh Expected: FAIL —— build.mcpp 编译报错(找不到 std 模块)。

:target/ 下有多个指纹目录,ls | head -1 会取到旧版本的二进制。 用 mcpp build 的输出路径,或 find target -name mcpp -newer mcpp.toml

  • Step 3: 实现

build_program.cppm 检测扩展:

    bool usesModule   = srcText.find("import mcpp") != std::string::npos;
    bool usesStd      = srcText.find("import std;") != std::string::npos
                     || srcText.find("import std.compat;") != std::string::npos;
    bool usesStdCompat= srcText.find("import std.compat;") != std::string::npos;

usesStd 为真时,调 stdmod::ensure_built(hostTc, standard, stdFlagAndDialect, deploymentTarget), 然后按方言接入:

  • GCC:把 sm->bmiPath 暂存到 bdir/gcm.cache/std.gcm(std.compat.gcm 同理), 并令 compileCwd = bdir(与 usesModule 共用同一条 cwd 决策,不要写成两个 if);
  • Clang:compileArgv.push_back(std::string(traits.stdBmiUsePrefix) + sm->bmiPath.string()), 复用 flags.cppm:358 的同一前缀常量;
  • 两者都把 sm->objectPath(及 compat 的)推进 compileArgv,位置同 mcpp.o

必须喂宿主工具链:调用点传的是 host_tc_for_build_program() 返回的 htc, 不是 *tc。喂 *tc 会在交叉构建下产出跑不了的 helper。

  • Step 4: 跑 e2e

Run: bash tests/e2e/181_build_mcpp_import_std.sh Expected: PASS: build.mcpp import std

  • Step 5: 更新 kMcppModuleSource 的注释

build_program.cppm:247"no #include, no import std;" 改为陈述现状 (内置模块用 C 级原语实现,与 build.mcpp 能否 import std; 无关),不改代码。

  • Step 6: 提交
git add src/build/build_program.cppm src/build/prepare.cppm tests/e2e/181_build_mcpp_import_std.sh
git commit -m "feat(build.mcpp): support import std; via the shared stdmod cache"

Task 7: e2e 112 扩交叉 import std; 断言

Files:

  • Modify: tests/e2e/112_build_mcpp_cross.sh

  • Step 1: 追加断言

在现有交叉 build.mcpp 用例之后,追加一个 import std; 形态的 build.mcpp, 断言构建通过且 helper 在宿主上真的跑起来(产生了它该产生的 marker 文件)。 这是 §5.2 host≠target 的锁:喂错工具链时 helper 会在 exec 阶段失败。

  • Step 2: 跑

Run: bash tests/e2e/112_build_mcpp_cross.sh(需 mingw-cross 能力,本机无则 CI 验收)

  • Step 3: 提交
git add tests/e2e/112_build_mcpp_cross.sh
git commit -m "test(e2e): lock host!=target for build.mcpp import std (112)"

Task 8: CommandDialect 扩四字段

Files:

  • Modify: src/toolchain/dialect.cppm:22-60
  • Test: tests/unit/test_dialect.cpp(新建)

Interfaces:

  • Produces(CommandDialect 新成员):

    • std::string_view libFlag; GNU "-l{}" / MSVC "{}.lib"
    • std::string_view libSearchPrefix; GNU "-L" / MSVC "/LIBPATH:"
    • std::string_view forceCxxLang; GNU "-x c++" / MSVC "/TP"
    • std::string_view staticRuntime; GNU "-static"/ MSVC "/MT"
  • Produces: std::string lib_flag_for(const CommandDialect&, std::string_view name); —— 因为 MSVC 是后缀形态(foo.lib),单个前缀常量表达不了。

  • Step 1: 写失败测试

#include <gtest/gtest.h>
import std;
import mcpp.toolchain.dialect;

// 链接侧字段:GNU 是前缀形态、MSVC 是后缀形态,所以 libFlag 必须是格式串
// 而非前缀 —— 用 std::string_view prefix 表达不了 `foo.lib`。
TEST(Dialect, LibFlagBothShapes) {
    EXPECT_EQ(mcpp::toolchain::lib_flag_for(mcpp::toolchain::gnu_dialect(),  "z"), "-lz");
    EXPECT_EQ(mcpp::toolchain::lib_flag_for(mcpp::toolchain::msvc_dialect(), "z"), "z.lib");
}

TEST(Dialect, LinkAndLangFieldsArePopulated) {
    for (auto const* d : { &mcpp::toolchain::gnu_dialect(),
                           &mcpp::toolchain::msvc_dialect() }) {
        EXPECT_FALSE(d->libFlag.empty());
        EXPECT_FALSE(d->libSearchPrefix.empty());
        EXPECT_FALSE(d->forceCxxLang.empty());
        EXPECT_FALSE(d->staticRuntime.empty());
    }
}
  • Step 2: 跑测试确认失败

Run: mcpp test -- --gtest_filter='Dialect.*' Expected: 编译失败。

  • Step 3: 实现 —— 加四个字段到 CommandDialect、填两个方言表、加 lib_flag_for

  • Step 4: 跑测试

Run: mcpp test -- --gtest_filter='Dialect.*' Expected: PASS

  • Step 5: 提交
git add src/toolchain/dialect.cppm tests/unit/test_dialect.cpp
git commit -m "feat(toolchain): add link/language fields to CommandDialect"

Task 9: build.mcpp 方言化(L2)+ 环境(L3)+ MSVC 模块门

Files:

  • Modify: src/build/build_program.cppm:132-140(指令面)、:168-243(host_base_flags)、 :677-700(compileArgv + capture_exec)

  • Step 1: 指令面改为方言无关

parse_line 不再拼 -l / -L,改为产出中立字段 d.libs / d.libSearchDirs; 翻译推迟到拼 argv 处,用 lib_flag_for() / d.libSearchPrefix。 理由:mcpp: 协议是声明式的,方言属于消费端。

  • Step 2: compileArgv 方言化

-O0d.optPrefix+0;-x c++d.forceCxxLang;-staticd.staticRuntime; -o <bin> → 按方言(MSVC 用 /Fe:)。

  • Step 3: host_base_flags 加 MSVC 分支

返回空 —— MSVC 的搜索路径全部经由 INCLUDE / LIB 环境变量。

  • Step 4: capture_exec 传 env

capture_exec(compileArgv, {}, compileCwd) → 传 tc.envOverrides(model.cppm:46)。 这是三层里的最后一层:没有它,方言全对 cl.exe 依然找不到 <cstdio>

  • Step 5: MSVC × 模块化 build.mcpp 的 unsupported 门

usesModule || usesStd 且方言为 MSVC 时,返回:

build.mcpp: `import mcpp;` / `import std;` are not yet supported under MSVC.
            Use `#include` in build.mcpp, or build with a GCC/Clang toolchain.

一个门、一条诊断,不写成两处。

  • Step 6: 跑现有 build.mcpp e2e 确认无回归

Run: for t in 89 92 110 111 124 125 143 144 145 164 168; do bash tests/e2e/${t}_*.sh || echo "FAIL $t"; done Expected: 全 PASS(GNU 路径行为不变)

  • Step 7: 提交
git add src/build/build_program.cppm
git commit -m "feat(build.mcpp): dialect-aware compile/directives + MSVC env plumbing (#331)"

Task 10: e2e 179 含空格路径

Files:

  • Create: tests/e2e/179_spaced_paths.sh

  • Step 1: 写测试

第 2 行 # requires: unix-shell。在一个路径含空格的临时目录里建工程, [build] include_dirs 指向一个含空格的目录,放一个只能通过该 include 找到的头文件, 再放一个 build.mcpp,断言 build + run 全绿。跨平台。

  • Step 2: 跑,确认在修复前失败 / 修复后通过

Run: bash tests/e2e/179_spaced_paths.sh

  • Step 3: 提交

Task 11: e2e 180 MSVC × build.mcpp

Files:

  • Create: tests/e2e/180_msvc_build_mcpp.sh(第 2 行 # requires: windows msvc)

  • Step 1: 写测试 —— #include 形态的 build.mcpp,在 msvc@system 下 build + run; 并断言 import std; 形态给出的是明确的 unsupported 诊断而非裸崩。

  • Step 2: 由 Windows CI 验收(本机无 MSVC)

  • Step 3: 提交


Task 12: e2e 182 裸 Windows 回退 + 意图分档

Files:

  • Create: tests/e2e/182_windows_no_msvc_fallback.sh(第 2 行 # requires: windows)

  • Step 1: 写测试

在遮蔽了 MSVC 的环境下(由 CI job 提供,脚本自身先断言 toolchain default msvc 失败):

  1. 全新 MCPP_HOME 下 mcpp new + build + run 必须全绿;
  2. toolchain list 星在 x86_64-windows-gnu;
  3. 写一个显式 [toolchain] windows = "llvm@20.1.7" 的工程 → 必须硬失败且诊断里含 x86_64-windows-gnu 指路(验证意图分档没把显式判成自选)。
  • Step 2: 由 Windows CI 验收
  • Step 3: 提交

Task 13: CI —— fresh-install 三轴 + 遮蔽自证门

Files:

  • Modify: .github/workflows/ci-fresh-install.yml:375-440

  • Modify: .github/workflows/ci-windows.yml:267(MSVC 步骤补 180)

  • Step 1: 把 windows-fresh 的步骤抽成可复用单元,避免三处推导。

  • Step 2: 加 windows-2022-fresh / windows-2025-fresh 两轴。

  • Step 3: 加 windows-nomsvc-fresh —— 遮蔽 vswhere / VS 根 / 四个环境变量, 第一步就断言 mcpp toolchain default msvc 必须失败(自证门),再跑 182。

  • Step 4: ci-windows.yml 的 MSVC 步骤追加 tests/e2e/180_msvc_build_mcpp.sh

  • Step 5: 提交。


Task 14: 文档同步

Files:

  • Modify: docs/03-toolchains.md(Windows 默认行为、回退规则)

  • Modify: docs/07-build-mcpp.md(import std; 现已支持;MSVC 限制)

  • Modify: README.md / README.zh.md(把 winlibs 从脚注提到正文)

  • Modify: docs/zh/ 对应中文页

  • Step 1–2: 改文档、检查 [build] linkage 的三处错误措辞(设计 §7.2)。

  • Step 3: 提交。


Task 15: PR → CI 全绿 → 合入

  • Step 1: 本机全量回归
mcpp test                              # 单测
bash tests/e2e/run_all.sh              # e2e(本机能力范围内)
bash .github/tools/check_version_pins.sh
  • Step 2: 开 PR
git push -u origin feat/windows-usability
gh pr create --title "feat(windows): usable on a bare Windows box (2026.8.2.1)" --body "..."
  • Step 3: 盯 CI,红了就修,直到全绿。

关注点:Windows 三轴、MSVC 步骤、cross-build-test(112)、Linux/macOS 无回归。

  • Step 4: 合入
gh pr merge --squash --admin --delete-branch

:叠栈 PR 时 --delete-branch 会 CLOSE 子 PR 且不可 reopen。本批次是单 PR,无此风险。


Task 16: Release + 生态闭环

  • Step 1: 打 tag 触发 release.yml,产出四平台产物。
  • Step 2: 镜像到 xlings-res 双端(GitHub + GitCode)。

publish-ecosystem 的 per-file 超时会杀大件 —— 失败时本地 gtc 补传, token 走 ~/.config/gitcode-tool/config.json 而非环境变量;沙箱 wrapper 的 gtc 坏, 用 repo 里的 gtc + /usr/bin/python3

  • Step 3: 两端独立 GET + sha256 核验(不信 workflow 的绿灯; obs_callback 400 EOF 是假错,对象可能已落盘,须回探下载 URL 判定)。
  • Step 4: 更新 xim-pkgindex(PR 须用 Sunrisepeak 账号合)。
  • Step 5: clean-room 验证

用隔离的 XLINGS_HOME 真装 xlings install mcpp@2026.8.2.1,别动本地 ~/.xlings; 索引 artifact CDN 滞后可达 ~40min,not found ≠ 回归,重跑即绿。

  • Step 6: bump bootstrap pin

包上架之后才改 .xlings.json:32026.8.2.1,单独提交/PR。


实施记录(执行中发现、计划里没写的东西)

发现 影响
command_from_argv 的裸 argv[0] 是故意的 —— 注释写明引号会被 cmd /c 剥掉 真修法不在 command_from_argv 一处:要按 cmd.exe /c 的文档规则给整条命令再包一层外引号,让 cmd 吃掉它,内层引号才能抵达。run_exec 必须只包不封 stdin(mcpp run 要交互)
command_from_argv 只在非 Linux/macOS 分支编译 Linux 上测不到。把 Windows 的字符串成形抽成宿主无关的 windows_command_from_argv / windows_wrap_for_cmd_c,Linux CI 也能守住这条规则 —— 这个分支在开发平台上根本不编译,正是裸 argv[0] 活这么久的原因
local_include_flagsmsvcDialect 形参只用于 after-dirs 普通 include 硬编码 -I。收敛到 include_token 后 MSVC 方言下变成 /I,test_ninja_backend.cpp 里有一条断言编码的正是这个 bug,已更新
两层转义,顺序固定 ninja 的 $ 在内、shell 引号在外。写断言时必须知道文件里是 '-I/opt/my$ dep/include',单测和 e2e 各踩了一次
ensure_builttc 已经是宿主工具链 prepare.cppmrun_build_program(*m, *root, host->first, host->second, ...),host≠target 在调用点就闭合了,组件 D 无需自己解析
Directives 会被序列化进 build.mcpp 缓存 计划里的「产出中立字段」会改缓存格式。改为在 parse 时按方言翻译 —— 缓存键已含 compiler <hash>,换工具链自动失效,所以安全且零格式变更
mcpp toolchain default msvc 也写 config.toml 与 mcpp 自己持久化的默认同源,会被误判成可改写。收紧:tc->compiler == MSVC 一律视为用户显式 —— mcpp 从不自选 msvc@system
[build] linkage 根本不被解析 toml.cppm:995 只在 [target.<triple>] 下读。文档三处 + prepare.cppm 一处注释都在教一个静默失效的键,已改
no-msvc 需要是一个显式能力 不能由「非 msvc」推出:e2e 182 必须 REQUIRE 它,否则在有 MSVC 的机器上会走普通路径并通过,证明不了任何事

CI 才暴露的问题(本机全绿也发现不了)

发现 影响
$local_includes 进 response file,rsp 按 GNU 分词、反斜杠是转义符 收敛两条通道时连路径形态也一起收敛了,而它们必须不同:命令行保留原生分隔符,rsp 必须正斜杠。include_token 因此带 PathForm 形参;escape_ninja_chars 收字符串而非 path,否则 fs::path 往返会在 Windows 上把分隔符归一化、悄悄撤销 generic_string()
clang 22 + modules:未被调用的 split_ws 误编译了邻居函数 memory/clang-modules-unused-fn-miscompile.md。修法=方言表直接存 span<string_view> token,不再运行时切分(本来也更干净)
BMI flag 与其路径是一个 shell 词 -fmodule-file=std=<path> 只做了 ninja 转义。GCC 永远看不到(它的 BmiTraits 这几项为空),macOS/clang 上含空格路径直接裂开
deployment target 必须进 base 它同时喂内置 mcpp 模块的预编译、其 object 步骤、以及 build.mcpp 编译。只加在 std 那一路,不匹配就换个模块继续报——clang 两个方向都拒
持久化 default_target 会盖过工程显式 [toolchain] 词表 pin 挂在 target 上。回退一旦持久化了 target,后续每个工程都继承它,于是 mcpp 自己记住的默认推翻了用户写下的选择——正是回退承诺的反面。判据:记住的 target 不压过用户显式的 toolchain,请求的(--target)照旧
断言不能写死引号字符 shell_quote_arg POSIX 单引号、cmd.exe 双引号。写死 ' 在 Linux 通过、在 Windows 因为一个正确的差异而红
VS 遮蔽:同 job 里先用 clang 构建会让 VS 目录改不动 clang 读 MSVC STL 留下句柄。改为独立 job + 取 build-test 的 artifact。另两个坑:-Path 末段带通配符列的是目录内容不是目录本身;-ErrorAction SilentlyContinue 会把 rename 失败吞掉,让"遮蔽了个寂寞"看起来正常

Self-Review

Spec coverage

设计节 覆盖任务
§2 组件 A Task 1(谓词)、2(pin+首跑)、3(分档+修复门+诊断+offline 例外)
§3 组件 B Task 4(include 收敛)、5(argv[0])
§4 组件 C Task 8(方言字段)、9(L2+L3+模块门)
§5 组件 D Task 6(import std;)、7(交叉锁)
§6 组件 E Task 10–13(e2e 179/180/182 + 112 扩 + CI 三轴)
§7 拒绝项 Task 14(文档修正 [build] linkage 措辞);其余为「不做」,无任务
§9 顺序 Task 编号即顺序;1–6 可并行,8–9 依赖 5

已知遗留(设计中明确不做,此处记录以免误判为漏项): ARM64 Windows;真实客户端 SKU 行为;MSVC × 模块化 build.mcpp(Task 9 Step 5 只给诊断); 内置 mcpp 模块改用 import std;(Task 6 Step 5 只改注释)。