给实施者:本计划配套 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端到端。
- 簇 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 build43s、mcpp test35 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 |
- 版本:
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 的"设计")而非加旁路开关或特判豁免。
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 不做。
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)。
三处根因(triage §6.2)在唯一 choke point expand_glob/expand_dir_glob 与 run 快路径修:
- 前缀收窄遍历:
expand_glob目前恒从root起recursive_directory_iterator(scanner.cppm:251-252),glob 只在遍历后词法过滤。改为先从 glob 提取首个通配符前的字面目录前缀,迭代器从root/prefix起。 - 边界排除:现仅按名剪
.mcpp(scanner.cppm:261-264)。扩为一个排除判定:目录名 ∈ {.mcpp,.git,target} 或命中<root>/.gitmodules登记的子模块路径 →disable_recursion_pending()。 - run 复用缓存:
build_run_target(execute.cppm:391)无条件prepare_build。给它一条与cmd_build的try_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 的脱糖先行。
- 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_glob与expand_dir_glob两处起点+排除。 - Step 4:
mcpp test绿。 - Step 5:
BuildCacheEntry加runTargets;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)"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_intoglob 消费处) - 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),零新语义:
- #227:
libs/toml.cppm:474-476现见[[直接报错。改为:识别[[path]]→ 在文档里对path维护一个表数组,每遇一次追加一个新子表、后续键值填入该子表。这是给自研 parser 补一个通用 TOML 能力。随后manifest/toml.cppm:692-730让build.flags同时接受内联表数组(现状)与数组表(新),声明顺序=应用顺序不变。 - #228:
expand_braces在expand_glob入口把a/{x,y}/**展成["a/x/**","a/y/**"](支持嵌套/多组,笛卡尔积),每分支走既有匹配;glob_literal_prefix(C1)对含{的原始 glob 返回{前的前缀,脱糖后各分支再各自取更长前缀。
- Step 1:
test_toml.cpp加ParsesArrayOfTables:解析
[[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.cpp加ExpandBraceAlternation: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_intoglob 消费点;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)"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.cpp、tests/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$转义)。
关键澄清: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:
- 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绝对化并入同一函数,消除双实现。
- joined(
- 发射端 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;
}
}
}- Step 1:
test_build_flags.cpp加RewritesFullIncludeFamily:输入["-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-635、plan.cppm:707-708),并入flags.cppm:161-165includeDirs 绝对化。 - Step 4:
mcpp test绿。 - Step 5:
test_ninja_backend.cpp加QuotesFlagValueWithSpace:token-DT=long long经join_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+-iquotehdrinclude 成功;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)"Files:
- Modify:
src/modgraph/graph.cppm:15-29(SourceUnit加relPath)、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.sh、118_purview_include_rebuild.sh
Interfaces:
- Produces:
SourceUnit.relPath(源相对包根路径,scanner 填);对象路径obj/<pkg?>/<relDir>/<name>。 - 顺序:先对象路径唯一(#233)后 depfile(#235)——
$out.d依赖$out唯一,否则 depfile 也撞。
- #233 对象路径镜像源相对路径(triage §4.3):
plan.cppm:450-462现按parent_path().filename()折叠必撞。scanner 掌握 root,故在SourceUnit存relPath = fs::relative(path, root);plan 镜像目录层级: Before(plan.cppm:451-462):After: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;
循环后加唯一性断言:// 镜像源相对路径,天然唯一;去掉 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 层错误。 - #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)。
- 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:
SourceUnit加relPath,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.cppmpurview#include "vals.inc";mcpp run得 41 → 改 vals.inc 41→42 →mcpp run得 42(现 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)"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 专属函数。
建立"每包有效源集 = 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。
- Step 1:
tests/e2e/119_dep_cfg_sources.sh:mylib带[target.'cfg(linux)'.build] sources=["src/impl.cpp"],app以 path depimport mylib;调impl_value();mcpp build(现 undefined reference)+mcpp test双路径。 - Step 2:
bash tests/e2e/119...→ FAIL(undefined reference)。 - Step 3:拆
merge_conditional_build→merge_conditional_sources_flags(per-package)+ 条件依赖 root 函数;per-package 循环内对packages[0..n]调用;删:792sources/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)"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(冷环境变体)
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...");- 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)"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_target加package_filter) - Test:
tests/unit/test_manifest.cpp;新tests/e2e/120_ws_root_indices.sh、121_default_ns_redirect.sh、122_run_member.sh
同子系统三洞一并补(可选按 G1/G2/G3 拆三 commit,见文末):
- #224 根锚点继承:
merge_workspace_deps(project.cppm:52-75)现只抄version。改为传播path(记录"相对 workspace 根");prepare.cppm:577/600继承索引时相对workspace 根解析path(线程 wsRoot),而非成员目录。 - R6 默认命名空间重定向:两处硬短路让
[indices]对默认命名空间失效—— Before:prepare.cppm:1140if (ns == kDefaultNamespace) return true;;:1300if (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。 - run 选成员:
cmd_run(cmd_build.cppm:99-107)接入--package+resolve_member_dir(与 build/test 同规则);build_run_target加package_filter形参,在成员目录 scope 内解析并启动该成员二进制。
- Step 1:
test_manifest.cpp加DefaultNsIndexRedirect: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)"Files:
- Modify:
src/toolchain/fingerprint.cppm:21、mcpp.toml:3、CHANGELOG(若有)、.agents/docs发布说明
- Step 1:
MCPP_VERSION0.0.96→0.0.97;mcpp.tomlversion 同步。 - 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 无叠栈,规避该坑。
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)。
| 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)命名前后一致。
- 簇 G 粒度:一个 commit(C7,推荐,同子系统)还是拆 G1/G2/G3 三 commit?本计划默认合一。
- R6 索引键拼写:
default = {path}(推荐)vs"" = {path}vs 两者都接受(归一化点在toml.cppm:924-948)。 - 簇 A:确认采用"token 列表 + 两 pass"的忠实设计(本计划,取代 triage 的
Arg结构草案)。 - 执行方式:subagent-driven(每 commit 一个 fresh subagent + 审查)vs 本会话 inline 批执行。