Skip to content

Latest commit

 

History

History
475 lines (406 loc) · 34.8 KB

File metadata and controls

475 lines (406 loc) · 34.8 KB

mcpp 0.0.97 架构级修复 —— 实施设计与任务拆分(单 PR / 逐簇 commit)

给实施者:本计划配套 2026-07-18-issue-triage-215plus-architectural-remediation.md(分类与根因)。本文是可执行计划:一个大 PR(分支 feat/v0.0.97-remediation),每一类问题一个修复 commit 节点,末尾一个版本 commit。每个 commit 落地即树保持可编译 + mcpp test 绿 + 相关 e2e 绿。步骤用 - [ ] 勾选跟踪。

目标(Goal):以架构级设计(非 workaround)一次性修掉 #224/#225/#226/#227/#228/#229/#232/#233/#234/#235 + R6 + 两条 workspace ergonomics,发布 0.0.97。

架构主线(Architecture):七簇修复都遵循同一原则——把结构化意图保留到唯一收敛点,消除"过早压平成串 / 散在多处特判":源发现在 glob choke point 收窄并复用缓存;flag 作为 token 列表在规范化+发射两 pass 处理;对象路径镜像源相对路径;条件源集在唯一 per-package 漏斗求值;工具供给复用工具链同款同步门;命名空间路由查表而非硬编码短路。

技术栈:C++23 modules、自研 mcpp(self-host)、ninja backend、自研 TOML parser、gtest 单测(mcpp test)、tests/e2e/*.sh 端到端。

决策已定(2026-07-18)

  1. 簇 G 粒度 → 一个 commit(C7)。 2. R6 键拼写 → default = {path}(归一化点 toml.cppm:924-948;"" 亦归一到 kDefaultNamespace,但文档/示例只推 default)。 3. 簇 A → token-list 两 pass(取代 Arg 草案)。 4. 执行 → subagent-driven(每 commit 一个 fresh subagent + commit 间审查)。

实施进度(动态更新)

  • 环境:bootstrap mcpp 0.0.96 @ /home/speak/.xlings/data/xpkgs/xim-x-mcpp/0.0.96/bin/mcpp;基线 mcpp build 43s、mcpp test 35 binaries 全绿。分支 feat/v0.0.97-remediation(docs commit 已落)。
commit 状态 备注
C1 簇E #225 ✅ 完成 33e6d9a scanner 前缀收窄+边界排除;run 复用缓存(try_fast_run);+2 单测(含确定性 glob_literal_prefix)+e2e 114;review 通过
C2 簇B #227 #228 ✅ 完成 2230872 libs/toml AOT + expand_braces;consumer 零改(同 Value 形);+manifest 层 closed-grammar 守卫(拒非白名单 [[x]] typo,防静默丢数据)+brace 深度上限;+7 单测+e2e 115;review 通过
C3 簇A #226 #234 ✅ 完成 b71d93c token-list 两 pass:normalize_include_flags(全 include 族 joined/separated)+shell_quote_arg;修 1 Critical=MSVC include_dirs 需先绝对化再加 dialect 前缀(typed path 通道≠flag-string 通道);+5 单测+e2e 116;review 通过
C4 簇C #233 #235 ✅ 完成 3b4f18e(α) 对象路径:保留 basename 碰撞门,消歧前缀改 relPath.parent_path()(非碰撞项字节不变,4 个断言扁平路径 e2e 保绿)+唯一性断言;depfile:cxx_module/object 加 -MMD,过滤 GCC -fmodules 反向规则(awk 保留文本 include 记录,POSIX 门控);+2 e2e 117/118+1 单测;review 通过
C5 簇D #229 ✅ 完成 d6b3b6c(β) 3-site 漏斗(root+version-dep 保留,新增 path/git-dep site=真正 #229 修复);偏离 brief 有据(apply()-loop 会丢 cfg flags:makePackageRoot 早快照 privateBuild);+e2e 119;review 通过(3 风险全清)
C6 簇F #232 ✅ 完成 482b428(β) nasm 走工具链同款 resolve_xpkg_path("xim:nasm@3.02",autoInstall) 同步门 + guard 不再吞 config 错;ensure_nasm 退役→find_usable_nasm/find_sandbox_nasm;+6 单测+e2e 105 冷环境;review 通过
C7 簇G #224 R6 +run-p ✅ 完成 c044f8e(β) #224 根锚点继承(merge_workspace_deps 传 path+按 wsRoot 解析;[indices] 相对路径按根)+ R6 默认命名空间 default={path} 重定向(两处短路查表;url-form 拒绝 loud+dup-key 拒绝)+ run -p member;+7 单测+e2e 120/121/122;review 通过(1 Important 已修)
合并 β→α ✅ 完成 c7a2b61 merge --no-ff,无冲突;合并后全量 rebuild+test 35/35 绿;9 个新 e2e(114-122)全绿
C8 版本 0.0.97 ✅ 完成 964f9d0 fingerprint.cppm + mcpp.toml → 0.0.97;CHANGELOG 追加;mcpp --version=0.0.97
全量 e2e 回归 ✅ 通过(修 1 回归) 119→123 pass;抓到并修 rpath $ORIGIN 双重引号回归(d3ad223,C3 shell_quote_arg 误碰 link flags);余 22/54/62 为沙箱环境性(gcc ELF interp 缺失,0.0.96 基线同败,CI 真工具链过)
最终整支 review ✅ 通过 adc7c... opus,READY,无 Critical/Important;deferred minors 已分诊
rpath 回归修复 ✅ 完成 d3ad223 join_flags 加 shellQuote 参数,link flags 不再二次引号;+单测 LinkFlagsAreNotReQuoted;e2e 55/56/57/64 红→绿
PR / CI / 合入 / release ⏳ 进行中 gh 已认证(Sunrisepeak),origin=mcpp-community/mcpp

全局约束(Global Constraints)

  • 版本:MCPP_VERSION 唯一真源 src/toolchain/fingerprint.cppm:21;必须与 mcpp.toml:3 同步。本 PR 目标 0.0.97(仅末尾版本 commit 改,前七个修复 commit 不动版本号)。
  • 基线:HEAD 42198fb(0.0.96),分支 feat/v0.0.97-remediation 从 main 切出。
  • 每 commit 绿:任一修复 commit 落地后 mcpp build(self-host)+ mcpp test 必须通过;涉及行为的簇附带新 e2e(编号从 114 起)且并入 tests/e2e/run_all.sh 顺序。
  • 单测就近:新单测加进现有 tests/unit/test_*.cpp(gtest,由 mcpp test 发现),不新建测试工程。
  • host-aware:e2e 断言凡涉及平台差异必须 host-aware(记忆教训:自托管 e2e 会在 win/mac 上跑,勿硬编 Linux 断言)。
  • closed syntax, open vocabulary:解析层扩展(#227/#228/R6)只加固定形状机制,不加开放语义键。
  • 无 workaround:每个修复改的是根因收敛点(见各 commit 的"设计")而非加旁路开关或特判豁免。

Commit 序(DAG)与"每步绿"不变量

main(42198fb)
  └─ feat/v0.0.97-remediation
       C1 [簇E] scanner glob 收窄+边界排除 + run/build 缓存复用        (#225)   ← 基础:动 expand_glob
       C2 [簇B] TOML 数组表 + glob 花括号交替                          (#227 #228) ← 花括号脱糖挂在 C1 后的 expand_glob 入口
       C3 [簇A] flag token 列表:include 族规范化 + 发射端 shell 引号   (#226 #234)
       C4 [簇C] 对象路径镜像源相对路径 + 编译边 depfile                (#233 #235) ← depfile 依赖对象路径唯一,同 commit 内先做路径
       C5 [簇D] 条件源集统一 per-package 漏斗                          (#229)
       C6 [簇F] nasm 走工具链同款同步供给门                            (#232)
       C7 [簇G] workspace 根锚点继承 + 默认命名空间重定向 + run 选成员  (#224 R6 +run-p)
       C8 [版本] bump 0.0.97 + CHANGELOG + 发布说明

唯一的 commit 间耦合:C2 的花括号脱糖复用 C1 重构后的 expand_glob 入口 → C1 必须先于 C2。其余 C3–C7 相互无文件级冲突(C6/C7 都碰 prepare.cppm 但分别在 nasm 段 3019-3032 与命名空间/继承段 577/1134/1297,不重叠),理论可乱序;按上表顺序合入最清晰。#230 已由 df985df 修复,不在本 PR;#215 不做。


Commit 1 —— 簇 E:源发现收窄 + run/build 缓存复用(#225)

Files:

  • Modify: src/modgraph/scanner.cppm:233-287(expand_glob)、:289-333(expand_dir_glob)
  • Modify: src/build/execute.cppm:34-41(BuildCacheEntry)、:43-126(read/write cache)、:269-383(try_fast_build)、:387-431(build_run_target)
  • Modify: src/build/prepare.cppm(写缓存处 ~:3100,补新字段)
  • Test: tests/unit/test_modgraph.cpp(glob 前缀/边界)、新 tests/e2e/114_run_scan_scope.sh

Interfaces:

  • Produces:glob_literal_prefix(std::string_view glob) -> std::filesystem::path(scanner 内部);BuildCacheEntry 新增 std::vector<std::pair<std::string,std::string>> runTargets;(target 名 → 相对 outputDir 的可执行路径)。
  • Consumes:C2 复用 expand_glob 的脱糖入口(见 C2)。

设计(架构,非 workaround)

三处根因(triage §6.2)在唯一 choke point expand_glob/expand_dir_glob 与 run 快路径修:

  1. 前缀收窄遍历:expand_glob 目前恒从 rootrecursive_directory_iterator(scanner.cppm:251-252),glob 只在遍历后词法过滤。改为先从 glob 提取首个通配符前的字面目录前缀,迭代器从 root/prefix 起。
  2. 边界排除:现仅按名剪 .mcpp(scanner.cppm:261-264)。扩为一个排除判定:目录名 ∈ {.mcpp,.git,target} 或命中 <root>/.gitmodules 登记的子模块路径 → disable_recursion_pending()
  3. run 复用缓存:build_run_target(execute.cppm:391)无条件 prepare_build。给它一条与 cmd_buildtry_fast_build 平行的快路径:缓存有效(fingerprint + ninja 比源新)则跑 ninja 后从 runTargets 定位 exe,跳过 prepare_build。为此 BuildCacheEntry 持久化 runTargets(在 prepare_build 成功后连 fingerprint 一起写)。

Before(scanner.cppm:251-264 摘要):

fs::recursive_directory_iterator it(
    root, fs::directory_options::follow_directory_symlink, ec);
for (...; !ec && it != end; it.increment(ec)) {
    auto& e = *it;
    if (e.is_directory(eec) && !eec) {
        if (e.path().filename() == ".mcpp") { it.disable_recursion_pending(); continue; }
        ...

After(要点):

// 1) 前缀收窄:src/**/*.cppm → 从 root/"src" 起走
fs::path prefix = glob_literal_prefix(glob);        // 首个 * ? { [ 之前的目录段
fs::path start  = prefix.empty() ? root : root / prefix;
if (!fs::exists(start)) return out;                  // 前缀不存在 = 空集(非全树)
fs::recursive_directory_iterator it(
    start, fs::directory_options::follow_directory_symlink, ec);
...
    if (e.is_directory(eec) && !eec) {
        if (is_excluded_walk_dir(e.path(), root)) {  // .mcpp/.git/target/submodule
            it.disable_recursion_pending(); continue;
        }
        ...

注:path_matches_glob 仍对全 glob 做词法匹配(前缀只收窄起点,不改语义);! 排除 glob 与 expand_dir_glob 同款处理。glob_literal_prefix 须对花括号(C2)保守——见 C2 的脱糖先行。

Tasks

  • Step 1:test_modgraph.cpp 加失败单测 ExpandGlobStartsAtLiteralPrefix:构造 root/{src/a.cppm, junk/big/... },断言 expand_glob(root,"src/**/*.cppm") 只含 src/a.cppm不访问 junk(用一个哨兵:在 junk 放一个会让旧代码变慢/命中的文件,断言结果集不含它)。再加 ExcludesGitAndTargetAndMcpp:root/{.git/x.cppm, target/y.cppm, .mcpp/z.cppm, src/ok.cppm} → 只 src/ok.cppm
  • Step 2:mcpp test,确认新单测 FAIL(旧代码会把 junk/.git/target 命中或遍历)。
  • Step 3:实现 glob_literal_prefix + is_excluded_walk_dir(读一次 root .gitmodules 缓存子模块路径集),改 expand_globexpand_dir_glob 两处起点+排除。
  • Step 4:mcpp test 绿。
  • Step 5:BuildCacheEntryrunTargets;write_build_cache/read_build_cache 序列化该字段(向后兼容:旧缓存无此行 → 视为无效,重跑一次);prepare_build 成功后填充(从 ctx.plan.linkUnits 取每个 bin target 的名与相对 exe 路径)。
  • Step 6:build_run_target 加快路径:read_build_cache + fingerprint/ninja 新鲜度校验(复用 try_fast_build 的校验,并把其硬编码 src/ 遍历统一到 expand_glob)→ 命中则 ninja + 从 runTargets 定位 exe;未命中回落 prepare_build
  • Step 7:新增 tests/e2e/114_run_scan_scope.sh:建含大无关目录(伪 compat/blob 千文件 + src/main.cpp)的工程,mcpp build 后两次 mcpp run,断言第二次 real 与缓存 mcpp build 同量级(设阈值,如 < 2s;避免绝对时间脆弱,用"run 不重新扫描"标志:MCPP_VERBOSE 下断言无 scan 日志行)。并入 run_all.sh
  • Step 8:mcpp test + bash tests/e2e/114_run_scan_scope.sh 绿。
  • Step 9:commit
git add src/modgraph/scanner.cppm src/build/execute.cppm src/build/prepare.cppm \
        tests/unit/test_modgraph.cpp tests/e2e/114_run_scan_scope.sh tests/e2e/run_all.sh
git commit -m "perf(scanner): bound glob walk to literal prefix + exclude vcs/build dirs; reuse resolved cache in \`mcpp run\` (#225)"

Commit 2 —— 簇 B:清单语法表达力(#227 数组表 + #228 花括号)

Files:

  • Modify: src/libs/toml.cppm:460-499(解析主循环,[ 分支 471-476)——加 [[table]] 累积
  • Modify: src/manifest/toml.cppm:692-730(build.flags 消费,接受数组表形式)
  • Modify: src/modgraph/scanner.cppm(expand_glob 入口加花括号脱糖;scan_one_into glob 消费处)
  • Test: tests/unit/test_toml.cpp(AOT)、tests/unit/test_modgraph.cpp(brace)、新 tests/e2e/115_manifest_syntax_aot_brace.sh

Interfaces:

  • Consumes:C1 的 expand_glob(脱糖插在其最前)。
  • Produces:expand_braces(std::string_view glob) -> std::vector<std::string>(scanner 内部,笛卡尔展开 {a,b})。

设计

两处纯语法补齐(triage §3.3),零新语义:

  1. #227:libs/toml.cppm:474-476 现见 [[ 直接报错。改为:识别 [[path]] → 在文档里对 path 维护一个表数组,每遇一次追加一个新子表、后续键值填入该子表。这是给自研 parser 补一个通用 TOML 能力。随后 manifest/toml.cppm:692-730build.flags 同时接受内联表数组(现状)与数组表(新),声明顺序=应用顺序不变。
  2. #228:expand_bracesexpand_glob 入口把 a/{x,y}/** 展成 ["a/x/**","a/y/**"](支持嵌套/多组,笛卡尔积),每分支走既有匹配;glob_literal_prefix(C1)对含 { 的原始 glob 返回 { 前的前缀,脱糖后各分支再各自取更长前缀。

Tasks

  • Step 1:test_toml.cppParsesArrayOfTables:解析
[[build.flags]]
glob = "a/**"
cxxflags = ["-DX=1"]
[[build.flags]]
glob = "b/**"

断言得到 2 个条目、键值正确、顺序保持。

  • Step 2:mcpp test → FAIL(现 array-of-tables not supported)。
  • Step 3:实现 libs/toml.cppm 的 AOT 累积 + manifest/toml.cppm 消费两形式。
  • Step 4:mcpp test 绿。
  • Step 5:test_modgraph.cppExpandBraceAlternation:root/{p/aac/x.c, p/bsf/y.c, p/opus/z.c},断言 expand_glob(root,"p/{aac,bsf}/**") 含 aac/bsf 不含 opus。→ FAIL。
  • Step 6:实现 expand_braces 并接到 expand_glob 入口 + scan_one_into glob 消费点;glob_literal_prefix{ 保守回退。
  • Step 7:mcpp test 绿。
  • Step 8:tests/e2e/115_manifest_syntax_aot_brace.sh:一个工程用 [[build.flags]] + {a,b} glob 编译两目录,断言构建成功、define 生效。并入 run_all.sh
  • Step 9:mcpp test + e2e 绿。
  • Step 10:commit
git commit -m "feat(manifest): support [[build.flags]] array-of-tables + glob brace alternation {a,b} (#227 #228)"

Commit 3 —— 簇 A:flag token 列表规范化 + 发射端引号(#226 #234)

Files:

  • Modify: src/modgraph/scanner.cppm:338-350(absolutize_include_flags → 泛化为 include 族)
  • Modify: src/build/ninja_backend.cppm:99-106(join_flags 加逐 token 引号)、src/build/flags.cppm:312-331(全局 blob 装配加同款引号)
  • Test: tests/unit/test_build_flags.cpptests/unit/test_ninja_backend.cpp、新 tests/e2e/116_include_family_and_define_spaces.sh

Interfaces:

  • Produces:normalize_include_flags(root, std::vector<std::string>&)(取代 absolutize_include_flags);shell_quote_arg(std::string_view) -> std::string(平台感知:POSIX 单引号 / Windows 双引号,叠加 ninja $ 转义)。

设计(修正 triage §2.3 的 Arg 草案 → 更贴合源码的最小忠实设计)

关键澄清:flag vector 的每个元素本就 = 一个 argv token——apply_glob_flags(scanner.cppm:578-580)push 的 "-D"+d(即 -DT=long long)是一个元素、边界并未丢。#234 的 bug 纯在发射端 join_flags(ninja_backend.cppm:99-106)裸空格拼接、不引号。故不需要引入新 Arg 结构(那是过度设计);正确架构 = 把 vector 当 token 列表对待,补两个 pass:

  1. include 族规范化(#226):absolutize_include_flags 只认 -I(scanner.cppm:342 字面 starts_with("-I"))。泛化:一张前缀表 {-I,-iquote,-isystem,-idirafter,-iprefix,-L},每 flag:
    • joined(-iquotehdr):starts_with(prefix) && len>prefix → 重写尾部路径;
    • separated(-isystem 后跟独立元素 hdr):元素恰等于裸前缀 → 下一元素为待重写路径;
    • 重写 = 相对(!has_root_path())则 root/p-I 拼写仍走 Dialect::includePrefix。把 flags.cppm:161-165 那条另一路 includeDirs 绝对化并入同一函数,消除双实现。
  2. 发射端 shell 引号(#234):shell_quote_arg 对含空格/shell 特殊字符的 token 加引号,在 join_flags(per-unit)与 flags.cppm:312-331(全局 blob)两个 choke point 逐 token 施加,叠加既有 ninja $/: 转义(escape_flag_path)。

Before(scanner.cppm:341-348):

for (auto& f : flags) {
    if (f.size() > 2 && f.starts_with("-I")) {
        std::filesystem::path p(f.substr(2));
        if (!p.has_root_path()) f = "-I" + (root / p).string();
    }
}

After(结构):

static constexpr std::string_view kIncPrefixes[] =
    {"-I","-iquote","-isystem","-idirafter","-iprefix","-L"};
for (std::size_t i = 0; i < flags.size(); ++i) {
    for (auto pre : kIncPrefixes) {
        if (flags[i] == pre && i+1 < flags.size()) {        // separated
            rewrite_rel(flags[i+1], root); ++i; break;
        }
        if (flags[i].size() > pre.size() && flags[i].starts_with(pre)) { // joined
            std::string tail = flags[i].substr(pre.size());
            std::string abs  = rewrite_rel_copy(tail, root);
            if (abs != tail) flags[i] = std::string(pre) + abs;
            break;
        }
    }
}

Tasks

  • Step 1:test_build_flags.cppRewritesFullIncludeFamily:输入 ["-Ihdr","-iquotehdr","-isystem","hdr","-idirafterhdr"] + root /proj,断言四者路径都变 /proj/hdr(joined 拼回前缀、separated 改下一元素)。
  • Step 2:mcpp test → FAIL(现只 -I 变)。
  • Step 3:实现 normalize_include_flags,替换调用点(scanner.cppm:618-620/633-635plan.cppm:707-708),并入 flags.cppm:161-165 includeDirs 绝对化。
  • Step 4:mcpp test 绿。
  • Step 5:test_ninja_backend.cppQuotesFlagValueWithSpace:token -DT=long longjoin_flags 后在命令行是单参数(断言输出含 '-DT=long long' 或等效引号形式,不含裸 long 断裂)。
  • Step 6:mcpp test → FAIL。
  • Step 7:实现 shell_quote_arg,施加于 join_flags + flags.cppm:312-331
  • Step 8:mcpp test 绿。
  • Step 9:tests/e2e/116_include_family_and_define_spaces.sh:hdr/magic.h + -iquotehdr include 成功;defines=["T=long long"] 编译成功且 typedef T x 生效。并入 run_all.sh
  • Step 10:mcpp test + e2e 绿。
  • Step 11:commit
git commit -m "fix(flags): rewrite full -I/-iquote/-isystem include family to project root + shell-quote flag values with spaces (#226 #234)"

Commit 4 —— 簇 C:对象路径镜像 + 编译边 depfile(#233 #235)

Files:

  • Modify: src/modgraph/graph.cppm:15-29(SourceUnitrelPath)、src/modgraph/scanner.cppm(scan 时填 relPath)
  • Modify: src/build/plan.cppm:426-462(对象路径方案 + 唯一性断言)
  • Modify: src/build/ninja_backend.cppm:402-435(cxx_module/cxx_object 加 depfile)
  • Test: tests/unit/test_dyndep.cpp/test_ninja_backend.cpp、新 tests/e2e/117_object_path_collision.sh118_purview_include_rebuild.sh

Interfaces:

  • Produces:SourceUnit.relPath(源相对包根路径,scanner 填);对象路径 obj/<pkg?>/<relDir>/<name>
  • 顺序:先对象路径唯一(#233)后 depfile(#235)——$out.d 依赖 $out 唯一,否则 depfile 也撞。

设计

  1. #233 对象路径镜像源相对路径(triage §4.3):plan.cppm:450-462 现按 parent_path().filename() 折叠必撞。scanner 掌握 root,故在 SourceUnitrelPath = fs::relative(path, root);plan 镜像目录层级: Before(plan.cppm:451-462):
    if (basenameCount[fname] > 1) {
        auto parentDir = u.path.parent_path().filename().string();   // 只取父目录名 → 撞
        auto prefix = ... sanitize(pkg) + "_" + parentDir;
        cu.object = "obj" / prefix / fname;
    } else cu.object = "obj" / fname;
    After:
    // 镜像源相对路径,天然唯一;去掉 basenameCount 启发式
    auto relDir = u.relPath.parent_path();                            // a/src, b/src ...
    auto prefix = u.packageName.empty() ? fs::path{} : sanitize(u.packageName);
    cu.object = fs::path("obj") / prefix / relDir / fname;            // obj/<pkg>/a/src/util.o
    循环后加唯一性断言:std::set 收集所有 cu.object,冲突则返回可诊断错误(列出 colliders),把 ninja "multiple rules" 前移成 mcpp 层错误。
  2. #235 编译边 depfile(triage §4.3 方案 2,根治整类 stale):cxx_module/cxx_object 在非 MSVC 下无任何 depfile(ninja_backend.cppm:402-435,append_deps 仅 msvc)。仿 nasm 规则(:463-468)给二者的 $cxx 命令加 -MMD -MF $out.d,规则加 depfile = $out.d + deps = gcc(非 msvc 分支)。这捕获编译期全部 header/purview/GMF include,不止 #235 的 purview .inc,连"改普通头不重编"的潜伏 bug 一并根治。 After(cxx_object 非 msvc):
    append(std::format(
        "  command = $cxx $local_includes $cxxflags $unit_cxxflags -MMD -MF $out.d {}\n",
        compile_tail));
    append("  description = OBJ $out\n");
    if (!msvcDeps) { append("  deps = gcc\n"); append("  depfile = $out.d\n"); }
    else append_deps();
    cxx_module 同理(把 -MMD -MF $out.d 插进非 windows 的 BMI-restat $cxx ... 调用与 windows 分支命令,规则加 depfile/deps)。

Tasks

  • Step 1:tests/e2e/117_object_path_collision.sh:a/src/util.cpp+b/src/util.cpp+src/main.cpp 引用两者,mcpp build 成功(现 ninja "multiple rules generate")。
  • Step 2:bash tests/e2e/117... → FAIL。
  • Step 3:SourceUnitrelPath,scanner 填;plan.cppm 改对象路径为镜像 + 唯一性断言;确认 .ddi/.dd(派生自 cu.object)自动跟随。
  • Step 4:bash tests/e2e/117... 绿 + mcpp test 绿(注意 test_dyndep/既有 obj 路径断言若硬编旧格式需同步更新)。
  • Step 5:tests/e2e/118_purview_include_rebuild.sh:m.cppm purview #include "vals.inc";mcpp run 得 41 → 改 vals.inc 41→42 → mcpp run42(现 stale 得 41)。附加一条:改普通 .h 也重编。
  • Step 6:bash tests/e2e/118... → FAIL(stale)。
  • Step 7:给 cxx_module/cxx_object 加 -MMD -MF $out.d + depfile/deps=gcc(非 msvc)。
  • Step 8:bash tests/e2e/118... 绿 + mcpp test 绿。两 e2e 并入 run_all.sh
  • Step 9:commit
git commit -m "fix(build): mirror source relative path in object paths + track compile-edge depfiles for purview/header includes (#233 #235)"

Commit 5 —— 簇 D:条件源集统一 per-package 漏斗(#229)

Files:

  • Modify: src/build/prepare.cppm:merge_conditional_build(:345-362)拆分;调用点重排(退役 :792 的 sources/flags 半 + :1714;新增 per-package 循环 :2669-2711 内调用;保留 :800-802 条件依赖 root-only)
  • Test: 扩 tests/unit/test_manifest.cpp 或直接 e2e;新 tests/e2e/119_dep_cfg_sources.sh

Interfaces:

  • Produces:merge_conditional_sources_flags(Manifest&, ctx, triple)(每包调用,只并 cfg 的 sources/flags);条件依赖合并留在 root 专属函数。

设计(triage §5.3)

建立"每包有效源集 = base + cfg + feature、按已解析 target 求值"的唯一漏斗,消除 path/git dep 漏评(#229 根因:merge_conditional_build 只在 root :792 与 version-dep :1714 调,path/git 分支 :2447-2459 不调)。做法:把 cfg 的 sources/flags 求值折进 feature sources 已有的 per-package 循环(root :2689 + 每 dep :2710),与 apply 同点、扫描前;退役两个散点调用;条件依赖(:800-802,须在依赖解析前)留 root。

Tasks

  • Step 1:tests/e2e/119_dep_cfg_sources.sh:mylib[target.'cfg(linux)'.build] sources=["src/impl.cpp"],app 以 path dep import mylib;impl_value();mcpp build(现 undefined reference)+ mcpp test 双路径。
  • Step 2:bash tests/e2e/119... → FAIL(undefined reference)。
  • Step 3:拆 merge_conditional_buildmerge_conditional_sources_flags(per-package)+ 条件依赖 root 函数;per-package 循环内对 packages[0..n] 调用;删 :792 sources/flags 半与 :1714;保留 :800-802
  • Step 4:bash tests/e2e/119... 绿(build+test 双路径)+ 既有 108_cfg_conditional_sources.sh/85_target_cfg_build_flags.sh/100_feature_sources_test_mode.sh 回归绿(确认无双重合并)。并入 run_all.sh
  • Step 5:commit
git commit -m "fix(build): evaluate cfg-conditional sources for every package via one per-package funnel (path/git deps + test mode) (#229)"

Commit 6 —— 簇 F:nasm 走工具链同款同步供给门(#232)

Files:

  • Modify: src/build/prepare.cppm:3019-3032(nasm 消费边:换用 fetcher 门 + 修 guard 吞错)
  • Modify: src/xlings.cppm:1276-1295(ensure_nasm 退役或委托)
  • Test: 扩 tests/unit/test_xlings.cpp/test_pm_package_fetcher.cpp;e2e 复用 105_asm_sources_nasm.sh(冷环境变体)

设计(triage §7.3)

nasm 现走 bespoke 弱机制(无索引刷新前置、失败降 warning、if(cfgNasm) guard 吞真错)。改为走工具链同款同步门 Fetcher::resolve_xpkg_path("xim:nasm", autoInstall=true, &progress)(范式见 prepare.cppm:851/958-959,impl package_fetcher.cppm:644-762:先刷索引→阻塞 install→校验 payload→XLINGS_HOME 兜底),失败硬错;并修 guard:不静默丢 get_cfg() 的 bootstrap 错、config 失败时不跳过供给。xlings::ensure_nasm 保留为 PATH→sandbox 探测的薄封装或删。

Before(prepare.cppm:3019-3032):

auto cfgNasm = get_cfg();
if (cfgNasm) nasmBin = mcpp::xlings::ensure_nasm(make_xlings_env(**cfgNasm), false, {});
if (!nasmBin) return std::unexpected("...no usable nasm...");

After(结构):

auto cfgNasm = get_cfg();
if (!cfgNasm) return std::unexpected(cfgNasm.error());          // 不再吞错
if (auto sys = xlings::find_usable_nasm(**cfgNasm)) nasmBin = *sys;  // PATH/sandbox 快路
else {
    Fetcher f(...);                                             // 同工具链构造
    auto p = f.resolve_xpkg_path(nasm_pkg.target(), /*autoInstall=*/true, &progress);
    if (!p) return std::unexpected(std::format("nasm provisioning failed: {}", p.error()));
    nasmBin = xlings::find_sandbox_nasm(**cfgNasm);
}
if (!nasmBin) return std::unexpected("...no usable nasm...");

Tasks

  • Step 1:test_xlings.cpp/test_pm_package_fetcher.cpp 加断言:nasm 供给路径调用 resolve_xpkg_path(可用一个可注入的 fetcher stub 或验证"索引刷新前置被触发");并断言 get_cfg() 错误被透传而非吞掉。
  • Step 2:mcpp test → FAIL。
  • Step 3:实现 fetcher 门替换 + guard 修复;抽 find_usable_nasm(PATH+sandbox)复用。
  • Step 4:mcpp test 绿。
  • Step 5:105_asm_sources_nasm.sh 加/改冷环境断言:临时空 MCPP_HOME + PATH 无 nasm → mcpp build 一次成功(供给同步阻塞完成),错误信息若失败须指明真实根因(非"no nasm")。
  • Step 6:bash tests/e2e/105... 绿(有 nasm 沙箱的环境)。
  • Step 7:commit
git commit -m "fix(nasm): provision nasm through the toolchain's synchronous resolve_xpkg_path gate (index refresh + hard error), stop swallowing config bootstrap errors (#232)"

Commit 7 —— 簇 G:workspace 根锚点继承 + 默认命名空间重定向 + run 选成员(#224 R6 + run-p)

Files:

  • Modify: src/project.cppm:52-75(merge_workspace_deps 传播 path + 根锚点)
  • Modify: src/build/prepare.cppm:577-579/600-602(索引继承相对根)、:1134-1145 + :1297-1310(默认命名空间查表)
  • Modify: src/manifest/toml.cppm:924-948(default/"" 索引键归一化到 kDefaultNamespace)
  • Modify: src/cli/cmd_build.cppm:99-107(cmd_run--package)、src/build/execute.cppm:387-431(build_run_targetpackage_filter)
  • Test: tests/unit/test_manifest.cpp;新 tests/e2e/120_ws_root_indices.sh121_default_ns_redirect.sh122_run_member.sh

设计(triage §8)

同子系统三洞一并补(可选按 G1/G2/G3 拆三 commit,见文末):

  1. #224 根锚点继承:merge_workspace_deps(project.cppm:52-75)现只抄 version。改为传播 path(记录"相对 workspace 根");prepare.cppm:577/600 继承索引时相对workspace 根解析 path(线程 wsRoot),而非成员目录。
  2. R6 默认命名空间重定向:两处硬短路让 [indices] 对默认命名空间失效—— Before:prepare.cppm:1140 if (ns == kDefaultNamespace) return true;;:1300 if (ns.empty() || ns == kDefaultNamespace) return nullptr; After:两处在返回 builtin/nullptr 前先查 m->indices——
    // usesBuiltinIndex (1140):
    if (ns == kDefaultNamespace) {
        auto it = m->indices.find(std::string(kDefaultNamespace));
        return it == m->indices.end() || it->second.is_builtin();
    }
    // findIndexForNs (1300):
    if (ns.empty() || ns == kDefaultNamespace) {
        auto it = m->indices.find(std::string(kDefaultNamespace));
        return it == m->indices.end() ? nullptr : &it->second;
    }
    toml.cppm:924-948 把索引键 default(建议规范拼写)或 "" 归一化为 kDefaultNamespace 存入 m.indices
  3. run 选成员:cmd_run(cmd_build.cppm:99-107)接入 --package + resolve_member_dir(与 build/test 同规则);build_run_targetpackage_filter 形参,在成员目录 scope 内解析并启动该成员二进制。

Tasks

  • Step 1:test_manifest.cppDefaultNsIndexRedirect:manifest 带 [indices] default = {path="x"} + 默认命名空间 dep,断言 findIndexForNs("") 返回该 spec(非 nullptr)。→ FAIL。
  • Step 2:mcpp test → FAIL。
  • Step 3:实现 toml 键归一化 + 两处短路查表。
  • Step 4:mcpp test 绿。
  • Step 5:project.cppm 传播 path + prepare.cppm 索引继承相对 wsRoot;tests/e2e/120_ws_root_indices.sh:root [indices] x={path="mcpp"} 一次声明,成员不重复声明即可解析;path dep 经 workspace.dependencies 可用。
  • Step 6:bash tests/e2e/120... 绿。
  • Step 7:tests/e2e/121_default_ns_redirect.sh:一个"模块包"风格工程(默认命名空间)+ default={path=本地包},mcpp build && mcpp run 用本地 checkout 解析(不碰线上 index)。
  • Step 8:bash tests/e2e/121... 绿。
  • Step 9:cmd_run + build_run_target 加成员选择;tests/e2e/122_run_member.sh:workspace 两成员各一 bin,mcpp run -p memberB 跑 B 的二进制。
  • Step 10:bash tests/e2e/122... 绿 + 三 e2e 并入 run_all.sh + 90_workspace_test.sh 回归绿。
  • Step 11:commit
git commit -m "feat(workspace): root-anchored path/index inheritance + default-namespace index redirect + \`mcpp run -p <member>\` (#224, R6)"

Commit 8 —— 版本 bump 0.0.97 + CHANGELOG + 发布说明

Files:

  • Modify: src/toolchain/fingerprint.cppm:21mcpp.toml:3CHANGELOG(若有)、.agents/docs 发布说明

Tasks

  • Step 1:MCPP_VERSION 0.0.960.0.97;mcpp.toml version 同步。
  • Step 2:mcpp build(self-host)成功,mcpp --version 打 0.0.97。
  • Step 3:写 CHANGELOG:七簇一句话 + issue 号;注明 #230 已于 0.0.96 修、#215 不做。
  • Step 4:全量 mcpp test + bash tests/e2e/run_all.sh 绿(host-aware)。
  • Step 5:commit
git commit -m "chore(release): bump mcpp 0.0.96 -> 0.0.97 (scanner scope, flag model, build-graph identity, cfg funnel, nasm gate, workspace)"

发布运维(镜像/索引/pin bump)按 [记忆:release-publish-pipeline] 在 PR 合入后单独走,不在本 PR。坑复盘:publish-ecosystem 30min timeout(大件 gtc 补传)、squash --delete-branch 会 close 子 PR——本 PR 单分支单 PR 无叠栈,规避该坑。


PR 描述模板(单 PR)

title: feat: 0.0.97 架构级修复 —— scanner 范围/flag 模型/构建图身份/条件源漏斗/nasm 供给门/workspace

本 PR 以架构级设计(非 workaround)一次修掉 #224/#225/#226/#227/#228/#229/#232/#233/#234/#235 + R6,
按"每一类问题一个 commit 修复节点"组织,末尾一个版本 commit。设计见
.agents/docs/2026-07-18-v0.0.97-architectural-remediation-implementation-plan.md。

Commits(逐簇):
- 簇E #225  scanner glob 收窄 + 边界排除 + run 复用缓存
- 簇B #227 #228  TOML 数组表 + glob 花括号
- 簇A #226 #234  include 族规范化 + 发射端 shell 引号
- 簇C #233 #235  对象路径镜像 + 编译边 depfile
- 簇D #229  条件源集统一 per-package 漏斗
- 簇F #232  nasm 走工具链同款同步供给门
- 簇G #224 R6  workspace 根锚点继承 + 默认命名空间重定向 + run 选成员
- release  bump 0.0.97

Closes #224 #225 #226 #227 #228 #229 #232 #233 #234 #235
Note: #230 已于 0.0.96(df985df)修复,windows CI 0.0.97 pin 复验后一并关闭;#215 不做(挂 upstream P2996)。

自审:spec 覆盖对照

triage 簇/issue 本计划 commit 覆盖点
E #225 C1 前缀收窄 + 边界排除 + run 缓存快路径 ✅
B #227 C2 libs/toml AOT + build.flags 消费 ✅
B #228 C2 expand_braces 脱糖 ✅
A #226 C3 include 族(joined/separated)统一重写 ✅
A #234 C3 发射端逐 token shell 引号 ✅
C #233 C4 对象路径镜像相对路径 + 唯一性断言 ✅
C #235 C4 编译边 -MMD depfile(含普通头额外收益)✅
D #229 C5 per-package 条件源漏斗(build+test+path/git dep)✅
F #232 C6 fetcher 同步门 + guard 修复 ✅
G #224 C7 根锚点继承 + path 传播 ✅
G R6 C7 默认命名空间查表重定向 ✅
G run-p C7 cmd_run 成员选择 ✅
#230 已 0.0.96 修,仅复验 ✅
#215 不做 ✅

类型一致性核对:glob_literal_prefix/expand_braces(C1/C2 同在 scanner)、normalize_include_flags/shell_quote_arg(C3)、SourceUnit.relPath(C4 定义、plan 消费)、merge_conditional_sources_flags(C5)、find_usable_nasm(C6)、build_run_target(package_filter)(C7)命名前后一致。

待实施者/maintainer 决策(继承 triage §10)

  1. 簇 G 粒度:一个 commit(C7,推荐,同子系统)还是拆 G1/G2/G3 三 commit?本计划默认合一。
  2. R6 索引键拼写:default = {path}(推荐)vs "" = {path} vs 两者都接受(归一化点在 toml.cppm:924-948)。
  3. 簇 A:确认采用"token 列表 + 两 pass"的忠实设计(本计划,取代 triage 的 Arg 结构草案)。
  4. 执行方式:subagent-driven(每 commit 一个 fresh subagent + 审查)vs 本会话 inline 批执行。