日期:2026-07-18 基线:mcpp 0.0.96(HEAD
42198fb,MCPP_VERSION = "0.0.96"@fingerprint.cppm:21) 范围:GitHub issue #215 及其后全部 OPEN 项(#224/#225/#226/#227/#228/#229/#230/#232/#233/#234/#235),外加"mcpp run构建过但运行前很慢"、"nasm 冷环境失败"、"mcpp.toml 相关"三条用户重点;并入 R6(index 仓测试模块包所需的默认命名空间重定向)与两条 workspace ergonomics 补充(root[indices]一次声明全员可用、mcpp run选择成员)。 所有 file:line 锚点均在 0.0.96 源码上核实(五路并行 explorer 交叉验证)。 上游语境:本批 issue 绝大多数来自 mcpplibs 把 ffmpeg-m / opencv-m 以"全源码直编"形态收录时的踩坑(参见 2026-07-17-asm-sources-and-general-build-capabilities-design.md 的下一波),是 0.0.95 声明式清单能力落地后暴露的第二层缺口。
先纠正一个直觉:用户提示"mcpp.toml 相关的问题可能是一类"——实测不是一类。它们分裂成两个不同根因:命令行装配语义(#226/#234,把 flag 当不透明串)与解析器语法表达力(#227/#228,parser 缺 array-of-tables / glob 缺花括号)。而 mcpp run 慢(#225)与 nasm(#232)各自独立成类。#230 已在 0.0.96 修复(根因是 scanner 崩溃,非 nasm/子进程,详见 §7)。
按根因所在子系统归并,收敛为 6 个可独立成 PR 的簇 + 2 个独立项:
| 簇 | issue | 根因一句话 | 触及子系统 | 量级 | 建议 PR |
|---|---|---|---|---|---|
| A flag 装配模型 | #226 #234 | flag 是不透明 vector<string>:include 重写只认 -I、含空格值不转义 |
scanner/flags/ninja_backend | 中 | PR-1 |
| B 清单语法表达力 | #227 #228 | 自研 TOML parser 全局拒 [[table]];glob 匹配器无 {a,b} |
libs/toml + scanner glob | 中 | PR-2 |
| C 构建图节点身份与输入完备 | #233 #235 | 对象路径按"父目录名+文件名"折叠必撞;扫描产出的 .dep 被丢弃、编译边根本无 depfile |
plan/ninja_backend/dyndep | 中 | PR-3 |
| D 条件源集统一求值 | #229(=#218 复发) | cfg 条件 sources 只在 root/version-dep 求值,path/git dep 漏;feature sources 已统一而 cfg 没有 | prepare 条件求值漏斗 | 小-中 | PR-4 |
| E 源发现范围 + run/build 缓存复用 | #225(#228 glob 顺带) | glob 永远从 root 全树遍历、只排 .mcpp;mcpp run 每次重扫、build 缓存不存源清单/产物路径 |
scanner/execute | 中 | PR-5 |
| F 外部工具供给统一门 | #232(#230 已修) | nasm 走 bespoke 弱机制(无索引刷新前置、失败降级为 warning、guard 吞真错),未接入工具链那条同步供给门 | prepare/xlings/fetcher | 中 | PR-6 |
| G workspace 配置与测试基建 | #224 + R6 + 2 补充 | 继承只传 version 丢 path/根锚点;相对 [indices] 按成员目录解析;默认命名空间结构上不可重定向;mcpp run 无成员选择 |
project/manifest/prepare/cli | 中 | PR-7 |
| 不做 | #215 | 上游 Clang 落地 P2996 后才加一行,现无从动手 | cppfly | 微 | 不做 |
版本建议:A/B/C/D/E 是 ffmpeg-m/opencv-m 全源码直编的硬阻塞,建议合成一个版本里程碑(0.0.97)分 5 个 PR 顺序合入;F 可并行;#224 独立;#230 只需在 windows CI 用 0.0.96 pin 复验后关闭;#215 挂 upstream。
贯穿全文的一条架构主线:mcpp 目前多处把"结构化意图"过早压平成字符串/单点特判,再在下游试图恢复——A(flag 边界丢失)、C(对象名折叠丢路径)、D(条件求值散在多个调用点)、E(glob 前缀信息被丢弃后靠遍历补)、F(工具供给不复用统一门)都是同一病理的不同切面。每个簇的"架构方案"都指向把意图保留到唯一的收敛点,而非再加一处特判。
判定"能否放进一个 PR"的标准不是"issue 表面相似",而是三个必须同时成立:
- 同根因子系统——改动落在同一组文件/同一个抽象层,一个 reviewer 能一次性把握;
- 同回归面——共享同一批测试夹具与验证路径(避免一个 PR 拆两拨 e2e);
- 无语义耦合冲突——两处改动不互相改写对方的中间表示(否则应先做基础层)。
据此,用户直觉里的"mcpp.toml 一类"被拆开:#226/#234 落在命令行装配(scanner→ninja_backend 的 flag 流),#227/#228 落在解析层(libs/toml + glob 匹配器)——两层之间只有数据流经过、无共享逻辑,强行同 PR 会让 diff 横跨"parser 改造 + 命令装配改造"两个不相干高风险区。反过来,#233/#235 表面一个是"路径撞名"一个是"改 .inc 不重编",却同根:都在 ninja_backend.cppm 的构建边生成、都因"扫描已拿到的信息没并进 ninja 依赖边"而坏——故合一簇。
- #226:
[build].cflags/cxxflags里相对-Ihdr会被绝对化(命令行可见),但-iquotehdr/-isystem/-idirafter不重写,按 ninja 构建目录解析 → 头文件静默找不到。 - #234:
defines = ["T=long long"]未加引号拼进命令行,g++ 把long当独立输入文件 →linker input file not found。
flag 在整条管线里是不透明预拼字符串,没有任何 Flag{kind,value} 结构:
- 数据模型:
GlobFlags(types.cppm:125-131)、BuildConfig.cflags/cxxflags/ldflags(types.cppm:137-156)、SourceUnit.packageC*flags(graph.cppm:15-29)全是vector<string>;全局 flag 更早在flags.cppm:312-331就压平成单个std::stringblob。 - 有损融合点:
apply_glob_flags(scanner.cppm:571-586)把 define 拼成"-D"+d——-DT=long long在此刻成为一个含内部空格的 vector 元素,参数边界意图就此丢失。 - include 重写只认
-I:absolutize_include_flags(scanner.cppm:338-350)用字面f.starts_with("-I")(342)+ 硬编码substr(2)(343)+ 硬编码"-I"重建(347)。-iquote/-isystem/-idirafter首字母是小写-i、且有 joined/separated 两种拼写,根本不进这个判断。注意-I本已被Dialect::includePrefix(dialect.cppm:27,74)抽象,这里却绕过 dialect 硬编码。 - 发射端零转义:
join_flags(ninja_backend.cppm:99-106)就是out += ' '; out += flag;。现有escape_ninja_path/escape_flag_path(ninja_backend.cppm:60-88、flags.cppm:80-90)只做 ninja 语法转义且只用于路径,不解决 shell 参数边界。
一句话:边界与角色信息在 scanner.cppm:571-586 被抹掉,到 ninja_backend.cppm:99 想转义也已无从分辨 -DT=long long(一个参数须整体加引号)与 -isystem /a/b(两个 vector 元素、本就是两参数)。
引入结构化参数中间表示,把"边界 + 角色"保留到发射点。
- 定义
struct Arg { enum Kind { Plain, IncludeDir, Define, Separated } kind; std::string spelling; std::string value; };作为 per-glob/per-unit flag 的规范内部形式(替换裸vector<string>的语义,发射时才铺开成命令行 token)。 - 规范化 pass(唯一收敛点,放在
apply_glob_flags/scanner 附加处):- include 族——识别
{-I, -iquote, -isystem, -idirafter, -iprefix, -L}全家族(joined 与 separated 两拼写),路径部分统一相对项目根绝对化,拼写走Dialect而非硬编码。把flags.cppm:161-165那条另一路 includeDirs 绝对化也并进来,消除两套逻辑。 - define——
value原样保留(含空格),不在此处拼-D+串。
- include 族——识别
- 发射 pass(
join_flags+flags.cppm:312-331blob 装配两个 choke point):按Arg.kind生成 token,含空格/特殊字符的value施加 shell 引号 + ninja$转义双层(叠在现有escape_*之上)。 - 一致性单测:枚举
-I/-iquote/-isystem/-idirafter× 相对/绝对 × cflags/cxxflags/asmflags 三通道,断言绝对化;define 值含空格断言命令行单参数完整。
落地边界:本 PR 只碰 flag 流(scanner 附加 → CompileUnit → ninja 变量),不动 sources/依赖解析。
Arg可先以最小形态落地(保留"是否 include 族 + 是否需整体引用"两个 bit),不必一步到位做全类型系统——关键是边界不再在中途丢失。
asm 设计文档 §1.6 已把 #226 记为 G8b("相对 -I 对 .cppm 不生效,先加复现再修,随 flags PR 顺手"),此 PR 即其正式兑现,并把范围从"补一个漏"升级为"根治不透明串"。
- #227:
[[build.flags]]标准 TOML 数组表被拒(array-of-tables not supported),长条目(NASM 8 个-I+-Pconfig.asm,单行 300+ 字符)只能挤一行。 - #228:glob 不支持
{a,b},libavcodec/{aac,bsf,hevc,opus,vvc}/**触发 matched-no-source 警告,只能写 5 条重复条目。
- #227 的拒绝在词法/解析库层、全局:
libs/toml.cppm:474-476见[直接return unexpected("array-of-tables not supported"),注释("mcpp doesn't use them")说明是刻意未实现。mcpp.toml 走这个 parser(toml.cppm:7import),故[[build.flags]]在 tokenize 期就死,toml.cppm:692的消费分支根本到不了。xpkg/Lua 路径反而原生支持有序数组表(xpkg.cppm:864-919)——两条 ingest 表达力不对等。 - #228:全仓唯一 glob 匹配器
path_matches_glob(scanner.cppm:103-163)的递归match(126-161)只支持**/*/字面量,{当普通字符,故 brace glob 永不命中。
两处都是**"closed syntax, open vocabulary"下的纯语法补齐,零新语义**(符合 schema 准入规则):
- #227:教
libs/toml.cppm的解析主循环(460-499,[分支 471)把[[table]]累积成一个数组Value;再扩toml.cppm:692-730让build.flags同时接受内联表数组与数组表两形式(声明顺序=应用顺序语义不变)。这是补一个通用 TOML 能力,顺带让未来任何配置段都能用数组表。 - #228:在
expand_glob入口处把{a,b}脱糖成多个 glob(而非改match递归)——理由:脱糖与簇 E 的"前缀收窄遍历"天然复合(先脱糖再对每个分支取前缀),且不污染热路径匹配器。备选:flags 条目的glob键接受数组(更小、解析层),二者可都做。
落地边界:纯 parser + glob 入口,不碰命令装配与构建图。可与 PR-1 并行(无文件重叠除 glob 入口,而 PR-1 不碰 glob)。
- #233:
a/src/util.cpp与b/src/util.cpp都折叠成obj/repro_src/util.cpp.ddi→ninja: multiple rules generate。是 OpenCV/LLVM 式modules/<mod>/src/*.cpp标准布局的直接杀手(opencv-m 现被迫生成 457 个转发 stub)。 - #235:模块 purview 内
#include "vals.inc"改动后mcpp build判 up-to-date(0.01s),产物 stale。ffmpeg-m(978 函数)/opencv-m(~2000 名字)的gen_exports/*.inc导出面全走这个模式。
- #233:对象路径唯一赋值点
plan.cppm:450-462。折叠逻辑455:parentDir = u.path.parent_path().filename()——只取直接父目录名,丢掉相对路径其余部分;456-459拼成obj/<pkg>_<parentDir>/<fname>。basenameCount(430-433)只检测 basename 撞名,而它的"解法"(父目录名前缀)本身不唯一、且无最终唯一性断言。.ddi路径(ninja_backend.cppm:620)从cu.object.parent_path()派生,故连带撞。 - #235:信息本已产出却被丢弃。P1689 扫描命令带
-M -MM -MF $out.dep(ninja_backend.cppm:531)确实生成了含全部文本 include(purview + GMF + 普通头)的 depfile,但:①cxx_scan规则没有depfile=/deps=gcc(对比 nasm 规则:463-468是有的),$out.dep生成即弃;② 计划期扫描p1689.cppm:350-365同样带-MF {dep}却只解析.ddi,.dep从不读;③ 更根本——cxx_module/cxx_object编译规则在非 MSVC 下根本没有任何 depfile(ninja_backend.cppm:402-435,只有msvcDeps分支加deps=msvc)。即扫描的.dep是唯一曾捕获过 include 的地方,而它被丢在地上;到达 ninja 的模块依赖边只有逻辑名→BMI。
- #233:对象路径镜像源相对路径。 唯一改点
plan.cppm:450-462:把前缀从parent_path().filename()改为源文件相对(包/项目根)完整路径(sanitize 分隔符),即obj/<target>/a/src/util.cpp.o;.ddi/.dd/compile_target全部从cu.object派生故自动跟随。追加一条 post-loop 断言:所有cu.object互异(把"撞名"从运行期 ninja 错误提前成 mcpp 层可诊断错误)。基名撞名启发式(430-433)可退役。 - #235:给编译边加真正的 depfile 追踪。 首选方案 2(而非只捞扫描
.dep):给cxx_module/cxx_object加-MD -MF $out.d+depfile = $out.d+deps = gcc(镜像 nasm 规则:463-468)。这在编译期捕获所有头/purview/GMF include,一次性根治整类 stale——不止 #235 的 purview,还包括至今非 MSVC 下普通头文件改动也不重编的潜伏 bug(本次探查的额外发现)。若要更省(不重复编译期扫描),备选方案 1:让mcpp dyndep收集器(dyndep.cppm:314-340)读每个.ddi旁的.dep、把文件列表作为额外 implicit input 并进 dyndep 边——但这只覆盖模块 TU,不如方案 2 普适。
落地边界:两改都在
plan.cppm+ninja_backend.cppm构建图生成,同组文件同 reviewer。回归夹具:#233 用双src/util.cpp,#235 用 purview#include .inc改值后断言重编;顺带加"改普通头触发重编"用例锁住方案 2 的额外收益。
[target.'cfg(...)'.build].sources(0.0.95 #223 引入)在包自身 build 时正常,但该包作为 path 依赖被消费时不展开 → undefined reference。与 0.0.94 修的 #218("feature sources 在 mcpp test 下不编译")同类:条件源集只在主构建路径求值,次级路径缺失。
- 存储:
ConditionalConfig(types.cppm:252-268,含sources)、Manifest.conditionalConfigs(types.cppm:367);parser(toml.cppm:827-862、xpkg.cppm:930-986)只存不并入 base。 - 求值/合并漏斗:
merge_conditional_build(prepare.cppm:345-362)对命中谓词把cc.sourcesAPPEND 进buildConfig.sources(358)与modules.sources(359,scanner 实际走这个)。 - 关键不对称:该漏斗只在 root(
prepare.cppm:792)与 version/registry dep(prepare.cppm:1714,#223 加)被调用;path/git dep 走另一条分支(prepare.cppm:2447-2459裸manifest::load→ 直接makePackageRoot),从不调用merge_conditional_build→ path dep 的 cfg sources 存了却永不并入 → scanner 看不到 → undefined reference。这就是 #229。 - 对照 feature sources:
applylambda(prepare.cppm:2564-2668)的 drop+add,#218 后 ADD 被移出!includeDevDeps门,且apply已对每个包在每种模式调用(root2689+ 每个 dep2710)——feature 维度已统一,cfg 维度没有。
把 cfg 源求值折进 feature sources 那个已经存在的 per-package 循环(prepare.cppm:2669-2711),使每个包(packages[0..n])在每种模式(build/test/workspace)走同一段"解析有效源集 = base + cfg + feature、按已解析 target 求值"——正是 #218 统一 feature sources 的同款做法。具体:
- 在 per-package 循环内、modgraph 扫描前,对
pkg.manifest调用merge_conditional_build(此处 manifest 已可原地改,且在 feature 激活后、扫描前的正确时点); - 退役现有两个散点调用(root
792的 sources/flags 半、version-dep1714)以免双重合并; - 保留 root-only 的条件依赖合并(
800-802,必须在依赖解析前),这是合法的 root 专属。
这条修复的价值不止 #229:它把"条件源集必须在所有构建路径统一求值"从惯例(#218 靠 code review 记住"add 要在所有模式做")升级成结构不变量(只有一个漏斗,想漏都难)。任何未来新增的条件源维度自动被覆盖。 落地边界:纯
prepare.cppm漏斗重排,回归夹具用 #229 的cfg(linux) sourcespath dep +mcpp test双路径(呼应记忆里"诊断 feature 必须 build/test 双路径对比")。
mcpp run --version 每次 real≈9.5s(其中 build 只 0.03s),strace 见 ~143 万次文件操作,几乎全在无关子模块(compat/node 37 万、compat/bun 13 万路径引用)。同一二进制直接跑 0.00s,mcpp build 缓存命中 0.04s。即延迟全在 run 的解析/发现路径。
- 无界遍历:
expand_glob(scanner.cppm:233-287)永远从root起recursive_directory_iterator(251-252),glob 只在遍历后逐文件path_matches_glob(276)做词法过滤。src/**/*仍访问整棵树。从不从 glob 的固定前缀src/起走。 - 排除面太窄:df985df 只加了
.mcpp剪枝(scanner.cppm:261-264);无.git/git 子模块/target/vendored 树排除;且仍follow_directory_symlink(252),只有 canonical 环检测。 - run 每次重扫、缓存复用不了:
mcpp run→build_run_target(execute.cppm:387-431)无条件prepare_build(false)(391),不像cmd_build有try_fast_build快路径(cmd_build.cppm:81-90)。原因:run 事后要用ctx->plan.linkUnits定位二进制(397-410),而 plan 只是prepare_build的产物。而target/.build_cache(BuildCacheEntry,execute.cppm:34-41)只存够重跑 ninja 的字段,不存解析后的源清单、不存二进制目标路径——run 无可复用。- 附带:
try_fast_build自己还有第二份硬编码到src/的独立遍历做新鲜度检查(execute.cppm:317-328),非 glob 感知。
- 附带:
唯一 choke point 是 expand_glob/expand_dir_glob,所有源发现都过它:
- 前缀收窄遍历:从 glob 提取首个通配前的字面前缀(
src/),recursive_directory_iterator从root/prefix起,而非root。单点改,全体调用者受益。与簇 B 的{a,b}脱糖复合(脱糖后每分支各取前缀)。 - 边界排除:在
.mcpp剪枝旁,对.git、target、.gitmodules登记的子模块路径disable_recursion_pending();子模块边界默认排除除非显式声明为 member/package。 - 共享解析缓存,让 run 跳过重扫:扩
.build_cacheschema 持久化解析后的二进制目标输出路径(+ 已有 runtime env),以既有 fingerprint(prepare.cppm:2936)为有效性键;给build_run_target加与cmd_build平行的快路径:缓存有效则直接定位并启动 exe,不prepare_build。顺手把try_fast_build那第二份src/硬编码遍历统一到前缀收窄的expand_glob。
落地边界:
scanner.cppm(遍历)+execute.cppm(缓存 schema/快路径)。回归夹具:含大无关目录/子模块的工程,断言mcpp run与缓存mcpp build耗时同量级(#225 明确要求)。这是"运行前很慢"的正解——不是加个跳过开关,而是让发现有界 + 让 run 复用 build 已解析的结果。
含 .asm 的包冷环境构建:error: NASM sources present but no usable nasm ... 先于 Bootstrap nasm into mcpp sandbox (one-time) 打出(~0.25s 后),构建已判死;两轮独立 run 时序一致;PATH 有 nasm 则不受影响。
- nasm 消费边
prepare.cppm:3019-3032:auto cfgNasm = get_cfg(); // 默认 requireBootstrap=true if (cfgNasm) nasmBin = xlings::ensure_nasm(...); // ← guard if (!nasmBin) return unexpected("...no usable nasm..."); ensure_nasm(xlings.cppm:1276-1295)链:PATH(which+版本)→ sandbox(find_sandbox_nasm)→ 打印 "Bootstrap"(1287)→install_with_progress(1289,同步 joinxlings.cppm:993)→ 再find_sandbox_nasm。它技术上是同步的,但两个架构缺陷制造了报错先行:- guard 吞真错:冷环境
get_cfg()的check_base_init失败(prepare.cppm:636-644)返回 error-expected,if(cfgNasm)为假 →ensure_nasm根本没被调(故无 "Bootstrap" 行)→ 直接抛 "no nasm";而get_cfg()的真实 bootstrap 错误被丢弃。 - 无索引刷新前置:
ensure_nasm装前不刷新 xlings 包索引(对比 fetcher 路径package_fetcher.cppm:745-746会ensure_official_package_index_fresh),冷环境xim:nasm尚不在索引 → 装失败 → 仅 warning(1290-1293)→find_sandbox_nasm返 nullopt → 抛错。失败根因再次被降级丢弃。
- guard 吞真错:冷环境
对照正确范式——工具链自动装是同步 provision-before-edge 门:Fetcher::resolve_xpkg_path(target, autoInstall=true, &progress)(prepare.cppm:851/958-959/1080,impl package_fetcher.cppm:644-762),它先刷索引(745-746)→ 阻塞 install(753)→ 校验 payload 落地(758-762)→ 有 XLINGS_HOME 传播兜底(764-779);失败是硬错不是 warning。
把 prepare.cppm:3019-3032 的 bespoke ensure_nasm + if(cfgNasm) guard 替换为工具链同款 resolve_xpkg_path("xim:nasm", autoInstall=true, ...)(或抽一个 ensure_tool(pkg) 共享 helper,工具链与 nasm 都调),使 nasm 继承:索引刷新前置、失败硬错、payload 校验、copy-from-global 兜底。并修 guard:不再静默丢 get_cfg() 的 bootstrap 错、config 加载失败时不跳过供给。xlings::ensure_nasm + install_with_progress 可退役/吸收。
更广的收益:任何未来"惰性供给的构建期工具"(未来的 protoc/bison/flex 之类)都应走这道门——工具供给不该有第二套弱机制。这与记忆里 mingw-cross/msvc 供给都收敛到 registry/fetcher 的方向一致。
探查推翻了 issue 的初步怀疑(非 #220 nasm / #222 build.mcpp 子进程)。真相:windows "exit 127" 实为 0xC0000409(__fastfail,git-bash 报成裸 127),源自 module scanner 未捕获的 std::system_error——glob 遍历经 .mcpp 符号链接逃逸进 index checkout,对 CJK 文件名做窄串转换时抛异常。正是 df985df("prune .mcpp from glob walks; never throw on unnarrowable names (#230)")修的——commit 消息直接引用 #230。修复在 scanner.cppm(~110-124 narrow guard + 261-264 剪枝)+ main.cpp:7-18(last-resort catch,返 70 不 fastfail)。行动:mcpp-index windows CI 用 0.0.96 pin 复验后关闭 #230,无需代码。
附带硬化(可选、低优):
build_program.cppm:499的build.mcpp.bin缺exe_suffix(nasm/其他工具都用platform::exe_suffix),windows 上是潜伏隐患;prepare.cppm:2395-2415的 git clone 是仅存的裸命令名 spawn(无which前置)。二者可随 F 顺手,但非 #232/#230 的成因。
这四项同根因子系统(workspace 配置继承 + [indices] 命名空间路由),且同一受益方:让 index 仓能用一条 mcpp test --workspace 声明式地验证所有包(含模块包),删掉一整套 shell 伪造补丁。故合一簇一 PR。
mcpp workspace 有 members 枚举与逐成员构建,但两个横切能力有洞:
- 继承的相对配置无根锚点——继承下来的
path/[indices]相对值按消费成员目录而非 workspace 根求值; - 命名空间路由无"默认"入口——
[indices]只能重定向有名字的命名空间,默认命名空间(namespace = "")在两处被硬编码短路到 builtin index,m->indices根本不被查。
这两个洞叠加,逼出 index 仓的 shell 补丁(R6)与逐成员重复声明(#224)。
- 根因:
- 复现 1(path dep 走
workspace.dependencies):merge_workspace_deps(project.cppm:52-75)只把spec.version从 workspace 抄给成员、丢弃path,于是本地 path 依赖被当 index/semver 解析报 "isn't cloned locally yet"。 - 复现 2(root
[indices]重基):workspace 索引仅当成员自己没声明时才继承(prepare.cppm:577-579、600-602,if (m->indices.empty())),且相对path值(如mcpp)按成员目录解析成<root>/<member>/mcpp而非<root>/mcpp;嵌套成员深度还改变含义(须../mcpp、../../mcpp)。这正是用户说的"根添加一次就 ok 不成立"——每个成员被迫重复声明各自相对路径。
- 复现 1(path dep 走
- 架构方向:解析后的 workspace 配置保留 workspace 根目录为锚点;所有继承而来的相对路径(
[indices]、path dep)一律相对根求值(而非消费成员);merge_workspace_deps传播path时连带记录"相对根"来源。
- 背景:index 仓本身是个 workspace,每个包配测试工程,用
[indices] compat = { path = "../../.." }把compat命名空间指向本仓 checkout,PR 合并前即可mcpp test --workspace真实验证描述符。但 imgui/ffmpeg/opencv 三个公开模块包走默认命名空间(描述符namespace = "",用户写[dependencies] opencv = "0.0.2"不带前缀),而[indices]没有语法能把"裸的默认命名空间"指到本地路径。 - 现状替代品(要删的补丁):三个 ~100 行
tests/smoke_*_module.sh——建临时MCPP_HOME→ 把本仓 checkout 整个拷贝覆盖到默认 index 的落盘位置(registry/data/mcpplibs)→ 造临时消费工程mcpp build && mcpp run;CI 还得为它们各开一个专属 job。能用但丑:三份近似复制的 shell(违背 index 仓 "zero-shell 自举" 立仓哲学,cf. [记忆:workspace-test-and-zero-shell-index])、易碎的 MCPP_HOME 重播/缓存链接/nasm 沙箱索引刷新细节、CI job 数随模块包线性增长。这个空洞在 imgui 时代就被 index 仓注释标记"待架构重构解决"。 - 根因(核实)—— 默认命名空间被两处硬编码短路,
[indices]从不被查:usesBuiltinIndex(prepare.cppm:1134-1145):default-ns dep 在1140if (ns == kDefaultNamespace) return true;直接短路,m->indices.find(ns)(1142)对默认命名空间永不执行;findIndexForNs(prepare.cppm:1297-1310):1300if (ns.empty() || ns == kDefaultNamespace) return nullptr;(nullptr = builtin)又一处短路,同样在查m->indices之前。- 即:默认命名空间→builtin index 是结构写死的,
[indices]只对有名字的命名空间生效。[indices]解析(toml.cppm:924起)本就迭代任意键,空引号键"" = {...}TOML 可表达——缺的不是解析,是路由未查表。
- 架构方案(非临时):让默认命名空间可被
[indices]重定向。定一个规范拼写映射到kDefaultNamespace(建议保留 tokendefault = { path = ... },比空引号键""更可读、更不易误写;二者可都接受);两处短路(1140、1300)改为先查m->indices是否有默认命名空间的重定向条目,有则用之、无则回落 builtin。这是把"默认命名空间不可寻址"这个语法空洞补上,不是给用户加功能——补完后三个模块包各建一个和opencv5一样的普通测试工程(只多一行default = { path = "../../.." })加入 workspace members:三个 smoke shell 删除、三个专属 CI job 删除、模块包验证并入mcpp test --workspace一条命令,以后每加模块包只加一个成员目录。
- 根因(核实):
cmd_run(cmd_build.cppm:99-107)只读一个位置参数(二进制目标名)+ passthrough,不解析--package、不做 workspace fan-out;对比cmd_build(读--packagecmd_build.cppm:47)、cmd_test(读--package:121+workspace_fanout_members)。resolve_member_dir(project.cppm:87-106)这条 build/test 已共享的成员选择规则,run 完全没接。 - 架构方案:
cmd_run接入与 build/test 同一--package解析 +resolve_member_dir,build_run_target增加package_filter形参、在成员目录 scope 内解析并启动该成员的二进制。语义与mcpp build -p <m>/mcpp test -p <m>对齐(run 天然单成员,不需要--workspacefan-out)。
- 触及
project.cppm(继承/根锚点)、prepare.cppm(两处命名空间短路 + 索引继承重基)、manifest/toml.cppm(default键规范化)、cli/cmd_build.cppm(run 成员选择)。与 A–F 无文件重叠(F 也碰 prepare 但在 nasm 供给段 3019-3032,与此处 577/1134/1297 不冲突),可完全并行。 - 回归夹具:虚拟 + rooted workspace、不同深度成员、root
[indices]相对路径一次声明全员可用、default = {path}重定向默认命名空间命中本地包、mcpp run -p <member>。收尾验证:在 mcpp-index 侧把一个模块包改成 workspace member 测试工程、删对应 smoke shell + CI job,mcpp test --workspace全绿——这是 R6 真正落地的判据。
上游 Clang(非 bloomberg fork)落地 P2996 后才谈得上给 cppfly.cppm 的 kReflectionRules 加一行(且版本号 + 旗标拼写须实机 -fsyntax-only 特性宏核实,不得臆测)。截至 2026-07 上游无。标记为不做:不排期、不占里程碑;真要提前,只能走"把 clang-p2996 fork 打包成 llvm-p2996@x 进生态"的独立打包路径,与本批 issue 无关。
0.0.97 里程碑(ffmpeg-m / opencv-m 全源码直编解锁):
PR-1 簇A flag 装配模型(#226 #234) ─┐ 可并行(文件不重叠)
PR-2 簇B 清单语法(#227 #228) ─┘
PR-3 簇C 构建图身份+depfile(#233 #235) ← 独立,建议先合(#233 阻塞大库直编)
PR-4 簇D 条件源集统一漏斗(#229) ← 依赖清晰,独立
PR-5 簇E 源发现范围+run缓存(#225) ← 含 #228 glob 入口,若与 PR-2 都改 expand_glob 需约定先后
并行/独立:
PR-6 簇F 工具供给统一门(#232)
PR-7 簇G workspace 配置与测试基建(#224 + R6 + run成员选择) ← 解锁 index 仓删 smoke shell
复验即关:
#230 windows CI 0.0.96 pin 复验(已由 df985df 修)
不做:
#215 upstream P2996 未落地,无从动手
PR-7 的生态收尾:合入后到 mcpp-index 侧把 imgui/ffmpeg/opencv 三个模块包各改成 workspace member 测试工程(default = {path=...} 一行)、删三份 smoke_*_module.sh + 三个专属 CI job,mcpp test --workspace 全绿即验收。这是 R6 从"mcpp 补语法"到"index 仓删补丁"的闭环。
合并顺序注意:
- PR-2(#228 glob 脱糖)与 PR-5(expand_glob 前缀收窄)都改
expand_glob入口——建议 PR-5 先(把遍历改造做扎实),PR-2 的脱糖叠在其上;或合成一个"scanner glob 引擎"PR。这是唯一的文件级耦合。 - PR-1 的
Arg规范化落在 scanner 附加处,PR-4 的条件源漏斗落在 prepare per-package 循环,PR-3 落在 plan/ninja_backend——三者无重叠,可乱序。 - 可选精简:若 reviewer 偏好"按
[build].flags特性端到端",可把 PR-1 + #227 合成一个"[build].flags 管线 PR"(parse 数组表 → normalize include/边界 → escape),把 #228 glob 归 PR-5。本文默认按子系统切,更利于隔离回归。
- 版本策略:A–E 五 PR 是否统一挂 0.0.97 一次发布(vs 分批发)?
- PR-1 粒度:
Arg结构化中间表示做到多细(最小两 bit vs 完整类型)?本文建议最小可用、以"边界不丢"为准。 - PR-2/PR-5 合并:
expand_glob改造是拆两 PR(先遍历后脱糖)还是合成一个"scanner glob 引擎"PR? - 簇 C depfile 方案:方案 2(编译边加
-MD,根治整类 stale,含额外收益)vs 方案 1(只捞扫描.dep,改动更小但仅覆盖模块 TU)?本文推荐方案 2。 - #227 归属:独立 PR-2,还是并入"[build].flags 管线"随 PR-1?
- R6 默认命名空间重定向拼写:保留 token
default = { path = ... }(推荐,可读)、空引号键"" = { path = ... },还是两者都接受? - PR-7 范围:#224 + R6 +
run -p member三者同 PR(同子系统、无重叠,推荐),还是把run -p member拆成独立小 PR 先行?