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。
- 版本号:本批次发
2026.8.2.1。规范YYYY.M.D.N,月日不补零,.0保留给稳定版。 - 版本号只改两处:
mcpp.toml:3与src/toolchain/fingerprint.cppm:21。.xlings.json:3是 bootstrap pin,是自举起点,不随本次发布走 —— 它只在 release 完成、包已上架之后单独 bump。提前改会让全部 CI 去装一个不存在的版本。 - 校验:任何版本改动后必须
bash .github/tools/check_version_pins.sh通过。 - 不引入 clang + libc++ 的任何形态(设计 §7.1)。
- 不新增配置字段:意图来源分档复用现有的两层配置(项目
mcpp.tomlvs 全局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。
| 文件 | 责任 | 本计划中的变化 |
|---|---|---|
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 |
用户文档 | 同步新行为 |
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"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"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-1215 的 if 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)"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"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-132 的 local_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)"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)"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:1349的host_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"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)"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"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 方言化
-O0 → d.optPrefix+0;-x c++ → d.forceCxxLang;-static → d.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)"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: 提交
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: 提交
Files:
-
Create:
tests/e2e/182_windows_no_msvc_fallback.sh(第 2 行# requires: windows) -
Step 1: 写测试
在遮蔽了 MSVC 的环境下(由 CI job 提供,脚本自身先断言 toolchain default msvc 失败):
- 全新 MCPP_HOME 下
mcpp new+build+run必须全绿; toolchain list星在x86_64-windows-gnu;- 写一个显式
[toolchain] windows = "llvm@20.1.7"的工程 → 必须硬失败且诊断里含x86_64-windows-gnu指路(验证意图分档没把显式判成自选)。
- Step 2: 由 Windows CI 验收
- Step 3: 提交
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: 提交。
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: 提交。
- 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,无此风险。
- 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:3 → 2026.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_flags 的 msvcDialect 形参只用于 after-dirs |
普通 include 硬编码 -I。收敛到 include_token 后 MSVC 方言下变成 /I,test_ninja_backend.cpp 里有一条断言编码的正是这个 bug,已更新 |
| 两层转义,顺序固定 | ninja 的 $ 在内、shell 引号在外。写断言时必须知道文件里是 '-I/opt/my$ dep/include',单测和 e2e 各踩了一次 |
ensure_built 的 tc 已经是宿主工具链 |
prepare.cppm 调 run_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 的机器上会走普通路径并通过,证明不了任何事 |
| 发现 | 影响 |
|---|---|
$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 失败吞掉,让"遮蔽了个寂寞"看起来正常 |
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 只改注释)。