From 52706c958b4bc7416ed340af8fae6d75ed662de8 Mon Sep 17 00:00:00 2001 From: speak-agent Date: Sun, 9 Aug 2026 08:29:58 +0800 Subject: [PATCH 01/44] docs: plan focused ecosystem convergence --- ...ate-runtime-graphics-aur-focused-design.md | 1034 +++++++++++++++++ ...untime-graphics-aur-implementation-plan.md | 369 ++++++ ...emplate-runtime-graphics-aur-validation.md | 124 ++ ...lings-mcpp-ecosystem-convergence-design.md | 985 ++++++++++++++++ 4 files changed, 2512 insertions(+) create mode 100644 .agents/docs/2026-08-09-mcpp-template-runtime-graphics-aur-focused-design.md create mode 100644 .agents/docs/2026-08-09-mcpp-template-runtime-graphics-aur-implementation-plan.md create mode 100644 .agents/docs/2026-08-09-mcpp-template-runtime-graphics-aur-validation.md create mode 100644 .agents/docs/2026-08-09-xlings-mcpp-ecosystem-convergence-design.md diff --git a/.agents/docs/2026-08-09-mcpp-template-runtime-graphics-aur-focused-design.md b/.agents/docs/2026-08-09-mcpp-template-runtime-graphics-aur-focused-design.md new file mode 100644 index 00000000..47f6e73c --- /dev/null +++ b/.agents/docs/2026-08-09-mcpp-template-runtime-graphics-aur-focused-design.md @@ -0,0 +1,1034 @@ +# mcpp 模板、运行时、图形栈与 AUR 聚焦设计 + +> 状态:Review Draft +> +> 日期:2026-08-09 +> +> 基线:mcpp main@80291ca01a98 +> +> 上位分析:.agents/docs/2026-08-09-xlings-mcpp-ecosystem-convergence-design.md +> +> 本文只冻结四项产品设计,不代表实现、发布或 AUR 状态已经改变。 + +## 1. 已确认的四项产品决策 + +本文将以下内容视为已确认方向,不再把旧方案并列为推荐项: + +1. **模板 selector 与 mcpp add 保持同一风格** + - 稳定形式:ns.name@version:tname。 + - ns. 可省略;省略时使用 mcpp/mcpp.toml 的默认 namespace mcpplibs。 + - @version 可省略。 + - :tname 可省略。 + - 不引入 --variant。 +2. **xlings 拥有图形栈与运行时,mcpp 构建在该生态之上** + - xlings 负责图形 runtime/provider 的解析、安装、激活和生命周期。 + - xim-pkgindex 提供 Mesa、NVIDIA、WSL、Vulkan ICD/driver 等 package/provider recipe。 + - mcpp-index 声明 C++ 图形包到 xlings/xim 图形能力的依赖关系。 + - mcpp 只通过 mcpp-index 的包描述、模板和通用构建契约完成构建。 + - mcpp 不探测 GPU,不按厂商写构建分支。 +3. **运行时分为 mcpp 默认与项目显式 SubOS** + - 不配置时使用 mcpp 管理、发布验证过的默认运行时。 + - 项目可在 mcpp.toml[xlings] 中选择命名 SubOS。 + - SubOS 是根项目本地 build/run 环境,类似选择“在哪个 OS 开发”;它不是库的传递依赖,也不要求消费者使用同一 SubOS。 + - 多个项目可以并存于不同 SubOS/不同 glibc,缓存和产物按实际 RuntimeBinding 隔离。 +4. **AUR 自动同步聚焦 mcpp-bin** + - mcpp-bin 是唯一自动对账并纳入漂移告警的 AUR 包。 + - AUR 是 GitHub Release 的最终一致投影,AUR 故障不阻塞 GitHub Release 本身。 + - mcpp-m 本阶段完全不动:不修改 package 文件、不推送、不退役、不改变其现有状态。 + - mcpp-git 不进入主发布关键路径。 + +## 2. 目标、边界与成功定义 + +### 2.1 目标 + +- 用户只学习一种包 selector 风格;namespace 可省略,省略时默认 mcpplibs。 +- 默认用户无需理解 xlings/SubOS 即可获得稳定运行时;需要固定 ABI/环境的项目能声明 SubOS。 +- 图形包的新平台、驱动和 backend 由 xlings/xim 生态演进;mcpp-index 表达 C++ 依赖关系,不要求修改 mcpp 核心。 +- SubOS 只影响当前根项目本地构建/运行,不从库依赖向消费者传递。 +- AUR 临时故障恢复后,mcpp-bin 能自动收敛到最新稳定 GitHub Release。 +- 设计保持 CLI 简洁、内部身份明确、构建热路径无网络和 GPU 探测。 + +### 2.2 非目标 + +- 不在本轮增加 --variant#template 或新的顶层 template 命令。 +- 不让项目 mcpp.toml 写本机绝对 xlings binary/home。 +- 不在 mcpp 中实现 Mesa/NVIDIA/Vulkan/WSL 专用逻辑。 +- 不要求 mcpp-m 与每次 release 同步。 +- 不修改或重新定义 mcpp-m 的维护策略。 +- 不在本文中解决 C1–C9 的全部实现;只保留与这四项直接相关的契约。 +- 本轮不实现代码、不重跑 workflow、不推送 AUR。 + +### 2.3 成功定义 + +~~~text +mcpp new app --template ocornut.imgui@1.92.8:glfw-opengl3 + -> dotted selector 显式解析为 namespace=ocornut, name=imgui + -> 解析出完整 PackageId 与 exact version + -> 从 mcpp-index 获取模板/构建描述 + -> mcpp-index 声明 xlings/xim 图形依赖 + -> xlings 解析并物化图形运行时 + -> 使用 mcpp 默认 runtime 或 [xlings].subos + -> mcpp 只执行通用 build/link/run plan +~~~ + +发布侧: + +~~~text +latest stable GitHub Release + -> immutable release manifest + -> mcpp-bin generator and validation + -> idempotent AUR reconcile + -> remote git + AUR RPC + clean install verification +~~~ + +## 3. 方案比较 + +| 主题 | 方案 A | 方案 B | 方案 C | 选择 | +|---|---|---|---|---| +| 模板名 | --variant 独立参数 | #tname 新分隔符 | [ns.]name[@version][:tname] | **C**:与现有 CLI/TOML 风格一致 | +| runtime 配置 | 跟随全局 active SubOS | 新增 [runtime].provider | 默认 mcpp runtime;已有 [xlings].subos 覆盖 | **C**:字段最少、可复现 | +| graphics | mcpp 内置 GL/Vulkan/provider 逻辑 | mcpp 直接调用 xim 图形接口 | xlings 拥有图形 runtime;mcpp-index 声明 C++ 依赖 | **C**:职责单一 | +| AUR | 双包串行同步 | 三包 matrix 同步 | 只自动对账 mcpp-bin | **C**:主用户路径优先 | + +方案 C 的共同原则是:**用户表面复用已有概念,内部不复用含糊字符串;每层只拥有自己能证明的事实。** + +## 4. 总体架构 + +~~~mermaid +flowchart LR + U[mcpp.toml / CLI] --> M[mcpp generic resolver and builder] + M --> I[mcpp-index
package, template, build contract] + I --> X[xlings
graphics stack owner, install, SubOS] + X -->|reads provider recipes| XI[xim-pkgindex
provider recipes and sentinels] + X --> RC[resolved graphics and runtime contract] + RC --> M + M --> A[artifact] + + R[GitHub Release Manifest] --> AR[AUR mcpp-bin reconciler] + AR --> AB[AUR mcpp-bin] +~~~ + +核心依赖方向: + +- mcpp 可以理解 PackageSelector、PackageId、LinkIntent、RuntimeBinding 等通用结构。 +- mcpp 不理解 mesanvidiawslvulkan icd 的选择规则。 +- mcpp-index 声明图形包如何编译、链接,以及它依赖哪些 xlings 生态 package/capability。 +- xlings 负责解析、安装、激活和导出图形 runtime contract。 +- xim-pkgindex 是 xlings 消费的 provider/recipe 数据源,实现 capability 并记录 artifact/provenance。 +- AUR reconciler 不读取工作区临时状态,只读取不可变 release manifest。 + +## 5. 模板 selector 统一设计 + +### 5.1 用户语法 + +稳定文法: + +~~~text +TemplateSpec := PackageSelector [ "@" ExactVersion ] [ ":" TemplateName ] +PackageSelector := Name | NamespacePath "." Name +NamespacePath := Segment { "." Segment } +TemplateName := NameAtom +~~~ + +NamespacePath 可省略。省略时: + +~~~text +imgui -> namespace = mcpplibs, name = imgui +~~~ + +它不是全索引 short-name 搜索,也不是“跟随当前 index 的 namespace”;默认值固定为 mcpplibs,与 mcpp.toml 默认 dependency table 对齐。 + +分隔符唯一职责: + +| 分隔符 | 含义 | +|---|---| +| . | 与 mcpp add 相同的 dotted package selector | +| @ | exact package version | +| : | package 内的 template name | + +合法示例: + +| 输入 | package selector | version | template | +|---|---|---|---| +| imgui | mcpplibs.imgui(ns 省略) | 默认稳定版 | descriptor default | +| ocornut.imgui | ocornut.imgui | 默认稳定版 | descriptor default | +| ocornut.imgui@1.92.8 | ocornut.imgui | 1.92.8 | descriptor default | +| ocornut.imgui:glfw-opengl3 | ocornut.imgui | 默认稳定版 | glfw-opengl3 | +| ocornut.imgui@1.92.8:vulkan | ocornut.imgui | 1.92.8 | vulkan | +| mcpplibs.gui.templates@2.0.0:window | mcpplibs.gui.templates | 2.0.0 | window | + +非法示例: + +| 输入 | 错误 | +|---|---| +| ocornut.imgui@ | empty version | +| ocornut.imgui: | empty template name | +| .imgui@1.0 | empty selector segment | +| ocornut..imgui | empty namespace segment | +| ocornut.imgui@1.0:vulkan:extra | more than one template delimiter | + +pkg: 不再兼任列表语法。列举模板继续使用现有显式表面: + +~~~text +mcpp new --list-templates ocornut.imgui@1.92.8 +~~~ + +这样“省略 tname”永远表示选择 default,不会同时表示 list。 + +### 5.2 与 mcpp add / mcpp.toml 的一致性 + +模板不能只复制 mcpp add 的视觉风格,两个命令最终必须复用同一条规范化链: + +~~~text +raw selector + -> shared PackageSelector parser + -> fill default namespace mcpplibs when omitted + -> one normalized PackageId + -> IndexRoute::lookup_descriptor by exact PackageId + -> resolved PackageId + descriptor provenance +~~~ + +目标规则: + +- imgui(mcpplibs, imgui)。 +- acme.widget(acme, widget)。 +- mcpplibs.capi.lua(mcpplibs.capi, lua)。 +- 多级 selector 总是以最后一个 segment 为 name,其余为 namespace。 + +对应的 mcpp.toml 语义: + +~~~toml +[dependencies] +imgui = "1.92.8" # default namespace mcpplibs + +[dependencies.acme] +widget = "1.0.0" # explicit namespace acme +~~~ + +CLI dotted form acme.widget@1.0.0 是第二种 TOML 形式的紧凑输入,不再表示“先猜 mcpplibs.acme,再猜 acme”。 + +当前 mcpp add 的 ordered dotted candidates 属于兼容实现。为了真正一致,迁移后 add/template 都使用上述单一规范化规则;旧项目清单已解析并锁定的 dependency 不被自动重写。 + +selector 是用户输入,最终身份仍是结构化: + +~~~text +PackageSelector { + namespace?: NamespacePath, + name: NameAtom +} + +PackageId { + namespace: NamespacePath, + name: NameAtom +} + +ResolvedTemplatePackage { + id: PackageId, + version: ExactVersion, + indexRoute, + descriptorDigest, + payloadDigest, + root +} + +TemplateSelection { + package: ResolvedTemplatePackage, + templateName: NameAtom +} +~~~ + +PackageSelector 规范化时填入默认 namespace,所以进入 resolver 后不再存在“namespace 未知”状态。xlings 安装 wire address 仍由 resolved PackageId 派生为 namespace:name@version。点号只属于 mcpp 用户 selector;冒号 wire address 不反向塞回 --template。 + +### 5.3 解析顺序 + +为了避免 namespace、version 与 tname 相互抢分隔符,解析固定为: + +1. 验证最多一个 :,分离右侧 TemplateName。 +2. 在左侧验证最多一个 @,分离 ExactVersion。 +3. 将剩余部分完整交给共享 PackageSelector parser。 +4. resolver 返回 PackageId 后再进入版本和模板选择。 + +禁止当前 scaffold 的“先按短名试空 namespace/compat,命中后再反推 namespace”旁路。 + +### 5.4 version 省略规则 + +- 写出 @version 时只接受 exact version;第一阶段不引入 range。 +- 省略时选择目标平台可用的最新 stable version。 +- prerelease 不自动成为默认;只有用户显式写出 exact prerelease 才选择。 +- 解析成功后后续流程只携带 exact version。 +- 成功输出、wire result 与生成的自依赖都展示 exact version。 + +版本选择必须复用包管理 resolver,不能由模板代码排序字符串。 + +### 5.5 tname 省略规则 + +省略 :tname 时: + +1. 正好一个 template 声明 default = true:选择它。 +2. 没有显式 default,但 package 只有一个 template:该单模板自动成为 default。 +3. 没有显式 default,且存在多个 templates:失败并列出所有 template,要求用户写 :tname 或 provider 选出 default。 +4. 多于一个显式 default:descriptor/index validation 失败。 +5. package 没有 templates/:明确报告它不是 template provider。 + +单模板自动默认是当次已解析 provider/version 的确定事实;同一已锁版本不会因未来 release 新增模板而改变。新版本若增加第二个模板却未声明 default,省略 tname 会明确失败,不进行目录排序猜测。 + +### 5.6 生成与依赖注入 + +模板解析后必须保留完整 PackageId,解决当前 namespace 在 fetch 后丢失的问题。 + +RenderVars: + +~~~text +project.name +project.namespace +project.qualifiedName +template.package.namespace +template.package.name +template.package.selector +template.package.version +template.name +~~~ + +自依赖注入复用 mcpp add 的 manifest editor: + +- 不做字符串搜索。 +- 使用用户风格的 dotted selector key。 +- 写 exact resolved version。 +- 保留 namespace 与 index provenance 到 lock/build resolution。 +- 模板已经声明相同 PackageId 时不重复写入。 +- 同 short name、不同 namespace 不视为同一个 dependency。 + +### 5.7 输出与诊断 + +成功的人类输出: + +~~~text +Created app +Template ocornut.imgui@1.92.8:glfw-opengl3 +Runtime mcpp-default +~~~ + +机器输出至少包含: + +~~~json +{ + "packageSelector": "ocornut.imgui", + "resolvedPackage": { + "namespace": "ocornut", + "name": "imgui", + "version": "1.92.8" + }, + "template": "glfw-opengl3", + "runtimeSelection": "mcpp-default" +} +~~~ + +诊断规则: + +- package 不存在:展示规范化后的 exact PackageId;namespace 省略时明确注明使用了默认 mcpplibs。 +- version 不存在:列出该平台可用 stable versions。 +- template 不存在:列出 provider 内 template names 和 default。 +- 用户写 ocornut:imgui 时,按文法它表示 package ocornut 的 template imgui;若解析失败,诊断额外建议 namespace 风格 ocornut.imgui。 +- 所有失败发生在创建目标目录前,或由临时目录事务回滚。 + +### 5.8 兼容迁移 + +旧的无 namespace 形式天然兼容: + +~~~text +pkg +pkg@version +pkg:tname +pkg@version:tname +~~~ + +变化只有: + +- 新增 dotted namespace selector。 +- bare selector 明确填入默认 namespace mcpplibs。 +- mcpp add 与 template 最终收敛为“dot 表示显式 namespace、无 dot 表示默认 mcpplibs”,不再各自猜候选。 +- pkg: 的 legacy list 含义先 warning 一个 release train,之后错误;用户改用 --list-templates pkg。 +- 不再新增 --variant,上一份综合设计中的该建议由本文覆盖。 +- builtin bin/gui 可暂时保留 alias,但 package template 输出统一用 TemplateSpec。 + +当前 add 的 dotted selector 具有默认 namespace 前缀候选,例如 capi.lua 会先尝试 mcpplibs.capi:lua。新规则下: + +~~~text +capi.lua -> capi:lua +mcpplibs.capi.lua -> mcpplibs.capi:lua +lua -> mcpplibs:lua +~~~ + +迁移要求: + +1. 已有 mcpp.toml 与 lockfile 不自动改写,仍按其已记录身份工作。 +2. 一个 release train 检测“旧候选结果与新 exact 结果不同”,warning 同时给出两种完整 selector。 +3. 新 mcpp addmcpp new --template 在迁移窗口后统一采用 exact dotted 规则。 +4. mcpp-index 中所有 nested mcpplibs.* 文档示例改成完整 namespace,不能依赖隐式前缀。 + +## 6. mcpp 默认 runtime 与 mcpp.toml SubOS + +### 6.1 两种模式 + +只定义两种项目运行时选择: + +~~~text +McppDefault +NamedSubos(name) +~~~ + +不增加第三种“跟随当前全局 active subos”模式。 + +| mcpp.toml | 选择 | +|---|---| +| 没有 [xlings].subos | McppDefault | +| [xlings] subos = "dev" | NamedSubos("dev") | +| [xlings] subos = "default" | 显式选择 xlings home 中的 default SubOS | + +### 6.2 默认模式 + +McppDefault 的定义: + +- 使用 mcpp 所选择的 xlings provider。 +- 使用 mcpp home 内已初始化、release 验证过的 default SubOS/RuntimeBinding。 +- 不受用户另一个 shell 中执行 xlings subos use 的 active 状态影响。 +- 缺失时走 mcpp bootstrap,不从任意目录挑第一个 glibc。 +- xlings/runtime contract schema 不兼容时明确失败或升级,不静默回退宿主。 + +默认路径的用户体验是“安装 mcpp 后直接 build/run”,不要求用户了解 SubOS。 + +### 6.3 项目显式 SubOS + +复用当前已支持的配置: + +~~~toml +[xlings] +subos = "dev" +deps = ["cmake@3.28", "python@3.13"] + +[xlings.workspace] +clang = "20.1.7" + +[xlings.envs] +OPENBLAS_NUM_THREADS = "1" +~~~ + +语义: + +- subos 只选择当前根项目 build/run/test 共用的本地命名环境。 +- deps、workspace pins 与 envs 在该环境中物化。 +- Linux 上 RuntimeBinding 同时决定 loader/libc。 +- macOS/Windows 上仍选择一致的工具与环境契约,但不伪造 Linux glibc 语义。 +- 指定 SubOS 不存在或无法回答 runtime contract 时 hard error,不退回 default。 +- 同一台机器可同时保留 el8trixiedefault 等多个环境;每个项目按自己的 mcpp.toml 选择,互不切换全局 active 状态。 + +### 6.4 xlings binary/home 属于机器配置 + +项目清单必须可移植,所以不在 mcpp.toml 接受绝对 binary/home: + +~~~toml +# ~/.mcpp/config.toml +[xlings] +binary = "bundled" # or "system" / absolute administrator path +home = "" +~~~ + +职责分离: + +- 全局 config 决定“使用哪一个 xlings 实例和 home”。 +- 项目 mcpp.toml 决定“在该实例中使用 default 还是哪个 named SubOS”。 + +这样团队可以提交 subos = "el8",而不提交某位开发者的 /home/user/.xlings。 + +### 6.5 选择优先级 + +稳定顺序: + +~~~text +project [xlings].subos exists + -> NamedSubos +otherwise + -> McppDefault +~~~ + +本设计不增加临时 --subos CLI override。runtime 是 build identity,命令行临时覆盖会让同一份 mcpp.toml 产生不同 ABI,并污染缓存解释。 + +global xlings binary/home 只决定 provider,不插入第三个 runtime selection rung。 + +### 6.6 一个 snapshot 贯穿生命周期 + +~~~text +RuntimeSelection + -> resolve exact SubOS + -> read RuntimeBinding contract + -> include contract hash in toolchain/build fingerprint + -> configure/link + -> post-link validation + -> run/test environment +~~~ + +build、run、test、post-install fixup 不得分别重新猜 runtime。 + +建议内部模型: + +~~~text +RuntimeSelection { + mode: McppDefault | NamedSubos, + subosName, + source: DefaultPolicy | Manifest +} + +RuntimeBinding { + schema, + providerId, + platform, + arch, + contractHash, + loader?, + libc?, + libraryDirs[], + environment[], + capabilities[], + provenance +} +~~~ + +### 6.7 [runtime] 与 [xlings] 不混用 + +现有 [runtime] 表示程序/包启动时需要的 library dirs、dlopen libs 和 capabilities;它不是“选择哪一个 SubOS”的配置。 + +~~~toml +[xlings] +subos = "dev" # 选择 build/run 环境 + +[runtime] +capabilities = ["opengl"] # 程序需要的通用 capability +~~~ + +选择环境属于 [xlings],声明程序需求属于 [runtime]。禁止再增加 [runtime] provider = "subos" 形成第二入口。 + +### 6.8 SubOS 是本地、根项目级、非传递环境 + +SubOS 的心智模型是: + +> mcpp 在一台机器上选择一个本地开发 OS 环境来 configure/build/run 当前项目。 + +它不是库依赖约束。规则如下: + +1. 只有本次构建的 root manifest/workspace root 能选择 SubOS。 +2. dependency manifest 中的 [xlings].subos 不合并、不继承、也不要求消费者创建同名 SubOS。 +3. 一个库作为独立根项目开发时,它自己的 [xlings].subos 生效;同一源码作为另一个项目的 dependency 时,使用消费者 root 选择的环境构建。 +4. workspace 整体构建时由 workspace root 选择一个环境;member 的 SubOS 不覆盖 root。member 独立构建时才成为自己的 root。 +5. SubOS name 不写入 dependency requirement,不从 lockfile 向下游传播,也不成为 mcpp-index package identity 的一部分。 + +典型源码分发: + +~~~text +application root selects subos=el8 + -> source dependency A builds inside el8 + -> source dependency B builds inside el8 + -> application runs inside el8 + +another application selects subos=trixie + -> the same A/B sources rebuild under trixie +~~~ + +因此可以在同一机器上用不同 glibc 环境开发同一组源码依赖,而不是要求所有库声明或传递 glibc=2.x。 + +间接关系只来自实际产物: + +- RuntimeBinding 必须进入当前项目 build fingerprint,防止跨 SubOS 复用 object/BMI。 +- 如果发布的是预构建 binary,发布流程应从最终 artifact 派生 ABI、loader、GLIBC symbol floor 等兼容元数据。 +- 这些是 artifact 的客观兼容属性,不是把开发时的 SubOS name 传播给消费者。 +- package 确实需要某项运行时能力时,通过 mcpp-index/xim package requirement 表达,不通过 [xlings].subos 表达。 + +### 6.9 当前 active SubOS 的迁移 + +当前未显式配置时可能读取 active SubOS。切换到稳定 default 的迁移: + +1. 一个 release train 输出 warning,显示当前 active 与未来 default 是否不同。 +2. 提供可复制配置:[xlings] subos = "current-name"。 +3. 下一 release 将 absence 固定为 McppDefault。 +4. 不自动修改用户 mcpp.toml。 + +冷 HOME、新安装可以直接采用新规则,不需要 legacy 过渡。 + +## 7. OpenGL/Vulkan 图形栈职责 + +### 7.1 强制边界 + +| 层 | 拥有 | 不拥有 | +|---|---|---| +| mcpp | 通用 PackageId、source/build graph、LinkIntent、RuntimeBinding 消费、产物验证 | OpenGL/Vulkan 包名、GPU vendor、ICD 选择、WSL 探测 | +| mcpp-index | ImGui/GLFW/OpenGL/Vulkan C++ 包、features、templates、平台 build/link 声明,以及这些包对 xlings 图形能力的依赖关系 | 安装宿主驱动、判断 NVIDIA/WSL 当前状态 | +| xlings | 图形栈 orchestration、依赖解析/安装/激活、SubOS、runtime contract、provider/sentinel 生命周期 | C++ GUI template 与项目源码 | +| xim-pkgindex | xlings 消费的 Mesa、Vulkan loader/ICD、NVIDIA/WSL sentinel recipe 与 provenance schema | 运行时自行做全局选择、mcpp 工程生成和 source graph | + +mcpp 源码中不新增以 openglvulkanmesanvidiawsl 为条件的 planner 分支。 + +### 7.2 数据流 + +~~~text +mcpp.toml dependency + -> mcpp resolves descriptor through mcpp-index + -> mcpp-index descriptor contributes sources, features, LinkIntent + and xlings graphics package/capability dependencies + -> xlings resolves and materializes the dependency closure + -> xlings activates providers/sentinels described by xim-pkgindex + -> xlings exports RuntimeArtifacts and provenance + -> mcpp consumes only generic resolved contract + -> link, validate and run +~~~ + +mcpp 不直接选择 xim:graphics。mcpp-index 的平台 package contract 声明 xlings 生态依赖;xlings 才是图形栈运行时 owner,负责从 xim-pkgindex recipe 中选择和物化具体 provider。 + +### 7.3 通用构建接口 + +mcpp-index 输出: + +~~~text +LinkIntent { + libraries[], + linkLibraryDirs[], + transitiveNeededDirs[], + runtimeSearchDirs[], + frameworks[], + deployFiles[] +} + +RuntimeRequirement { + kind: soname | capability | icd_manifest | display | host_service, + value, + phase: link | run, + requester: PackageId, + required +} +~~~ + +xlings 解析 xim-pkgindex provider recipe 后提供: + +~~~text +RuntimeArtifact { + role: loader | library | driver | manifest | host_bridge, + provider: PackageId, + path, + provenance: payload | subos_view | host_link | system_sdk, + abi, + digest?, + hostFingerprint? +} +~~~ + +mcpp 只检查这些通用结构是否完整、目标平台是否匹配、最终 artifact 是否满足 loader/ABI 物理约束。 + +### 7.4 OpenGL 收口 + +mcpp-index: + +- ocornut.imgui 声明 core 与 backend features。 +- glfw-opengl3 template/feature 引入 GLFW、OpenGL headers 与通用 runtime requirement。 +- Linux package contract 依赖由 xim 提供的 graphics capability。 +- macOS contract 使用 native frameworks。 +- Windows contract 使用 Win32/system SDK 与声明的 runtime DLL。 + +xlings(基于 xim-pkgindex recipes): + +- Mesa/GLVND 与软件/硬件 driver closure。 +- NVIDIA/WSL host-link sentinel。 +- runtime dirs、实际 libraries 和 provenance 的解析/导出。 +- sentinel 的 applicable/not-applicable/inconclusive 生命周期。 + +目标状态是移除 mcpp-index 中复制 SubOS view library 的长期 symlink bridge;runtime artifact 通过 contract 传递。 + +### 7.5 Vulkan 收口 + +mcpp-index: + +- Vulkan headers、loader-facing API、backend sources 和 templates。 +- vulkan template 声明 loader、ICD manifest、display/surface requirements。 +- 不从 /usr/lib* 自行收集宿主 ICD/DSOs。 + +xlings(基于 xim-pkgindex recipes): + +- Vulkan loader、ICD manifests 与对应 driver。 +- Mesa RADV/Intel 等 payload provider。 +- NVIDIA/WSL host-link provider。 +- macOS MoltenVK、Windows system/provider 策略由平台包契约表达。 + +验证不能止于 loader symbol: + +1. create instance。 +2. enumerate physical devices。 +3. 记录实际 ICD manifest 与 driver provider。 +4. 有窗口 lane 时创建 surface。 +5. 无硬件环境明确报告 NOT_EXERCISED,不能汇总为 GPU pass。 + +### 7.6 ImGui feature/template 形态 + +建议 mcpp-index 将图形入口拆成: + +~~~text +core +headless +backend-glfw-opengl3 +backend-vulkan +app +docking +viewports +~~~ + +示例: + +~~~text +mcpp new app --template ocornut.imgui:glfw-opengl3 +mcpp new app --template ocornut.imgui@1.92.8:vulkan +mcpp new app --template ocornut.imgui@1.92.8:docking +~~~ + +- core/headless 不应无条件拉入 GLFW/OpenGL。 +- app 可以组合默认 backend,但 resolved dependency/features 必须写入生成清单。 +- docking/viewports 与 renderer backend 正交。 + +### 7.7 可观察性 + +mcpp why runtime 可以展示 xlings 已解析完成的结果: + +~~~text +requirement + -> selected canonical provider + -> runtime artifact + -> provenance + -> ABI/loader verdict +~~~ + +它是通用 contract 的解释器,不进行 GPU 探测。GPU/driver 诊断与重探测由 xlings 统一入口及其 xim provider/sentinel 暴露,mcpp 只给出跳转提示。 + +图形依赖和 SubOS 选择是正交的: + +- mcpp-index/xim 依赖描述“当前项目需要哪些图形能力”。 +- root [xlings].subos 描述“当前项目在哪个本地环境 build/run”。 +- library dependency 不通过自己的 SubOS 要求消费者环境;源码会在消费者已选环境中构建。 +- 某个图形 provider 对实际 ABI/glibc 的要求,由 xlings 在当前环境解析并验证,而不是把 provider 的 SubOS name 传播出去。 + +## 8. AUR 只自动维护 mcpp-bin + +### 8.1 包策略 + +| AUR 包 | 策略 | +|---|---| +| mcpp-bin | 自动生成、验证、对账和漂移告警;不阻塞 GitHub Release | +| mcpp-m | 本阶段不更新;不修改 package 文件或 AUR remote,只从 mcpp-bin 自动对账路径隔离 | +| mcpp-git | 不进入 release workflow;如要发布,单独认领与设计 | + +用户主路径是安装 release 预构建产物,因此自动化可靠性集中投入 mcpp-bin。 + +### 8.2 当前事实 + +2026-08-09 的审计快照: + +- GitHub latest stable:v2026.8.8.4。 +- AUR mcpp-bin2026.8.1.1-1。 +- 连续 push 失败的直接响应是 AUR maintenance。 +- workflow 缺少 retry、schedule 与状态对账,所以临时故障变成长时间漂移。 +- mcpp-m 存在已知独立问题,但本文不处理、不推送,也不让它成为 mcpp-bin reconciler 的前置条件或后置条件。 + +旧失败 run 不应直接 rerun,因为它从旧 release commit 执行旧生成逻辑。 + +### 8.3 单一 desired state + +release 产出不可变 manifest: + +~~~json +{ + "schema": 1, + "version": "2026.8.8.4", + "tag": "v2026.8.8.4", + "commit": "", + "assets": [ + { + "platform": "linux", + "arch": "x86_64", + "name": "mcpp-2026.8.8.4-linux-x86_64.tar.gz", + "sha256": "" + }, + { + "platform": "linux", + "arch": "aarch64", + "name": "mcpp-2026.8.8.4-linux-aarch64.tar.gz", + "sha256": "" + } + ] +} +~~~ + +reconciler 只消费 latest complete、非 draft、非 prerelease manifest。 + +### 8.4 mcpp-bin reconciler + +触发: + +- successful release workflow_run:低延迟。 +- schedule:建议每 6 小时。 +- workflow_dispatch:人工恢复,默认仍指向 latest stable。 + +流程: + +~~~text +read latest stable release manifest + -> query AUR mcpp-bin version and remote git head + -> compare with Arch vercmp + -> download both Linux assets + -> recompute sha256 and compare manifest/sidecar + -> generate PKGBUILD + -> generate .SRCINFO from PKGBUILD in Arch container + -> makepkg --verifysource + -> dry-run diff + -> fast-forward push with bounded retry + -> verify AUR git head + -> bounded poll AUR RPC + -> clean Arch install and mcpp --version smoke +~~~ + +### 8.5 幂等和失败分类 + +| 状态 | 行为 | +|---|---| +| desired == current 且内容一致 | success no-op | +| desired > current | 生成、验证、push | +| desired < current | 默认拒绝降级 | +| asset/hash 缺失 | push 前 permanent failure | +| AUR maintenance/timeout | transient;指数退避,后续 schedule 补偿 | +| SSH auth/metadata invalid | permanent;立即告警 | +| git 已更新但 RPC 延迟 | poll,不重复 commit | + +已知 package clone 失败不能自动当作首次发布。禁止 force-push AUR 历史。 + +### 8.6 mcpp-m 完全不动的边界 + +本文对 mcpp-m 的要求只有隔离,不包含任何维护动作: + +- 不修改 scripts/aur/mcpp-m/**。 +- 不生成或推送 mcpp-m AUR commit。 +- 不改变其版本、checksum、maintainer、deprecated 状态或现有远端历史。 +- 不宣称它与 GitHub Release 同步。 +- mcpp-bin reconciler 的成功/失败只由 mcpp-bin desired/observed state 决定。 + +实现 mcpp-bin reconciler 时应建立独立 job/workflow;现有 combined workflow 唯一允许涉及 mcpp-m 的变化是停止自动调用其 publish leg,以保证“不推送 mcpp-m”。不得读取、生成或改写 mcpp-m 内容。 + +### 8.7 AUR SLO + +建议初始目标: + +- AUR 可用时,release 后 30 分钟内 mcpp-bin 收敛。 +- event path 失败后,6 小时 schedule 再尝试。 +- 超过 24 小时仍漂移,自动更新固定告警 issue 或发高优先级通知。 + +## 9. 四项设计如何协同 + +### 9.1 默认用户 + +~~~text +install mcpp-bin + -> bundled xlings/bootstrap default runtime + -> mcpp new app --template ocornut.imgui:glfw-opengl3 + -> mcpp-index supplies template/build contract + -> xlings/xim supplies graphics runtime + -> mcpp build + -> mcpp run using the same default RuntimeBinding +~~~ + +用户不需要手工选择 GPU provider,也不需要配置 SubOS。 + +### 9.2 固定运行时用户 + +~~~toml +[xlings] +subos = "el8" +~~~ + +~~~text +mcpp new/build/run + -> same template/package selector + -> same mcpp-index graph + -> named SubOS RuntimeBinding + -> cache fingerprint changes + -> artifact targets that runtime +~~~ + +### 9.3 平台差异 + +| 平台 | mcpp default runtime | graphics provider | +|---|---|---| +| Linux x86_64 | mcpp-managed default SubOS/runtime binding | xim Mesa or explicit host sentinel | +| Linux aarch64 | mcpp-managed native runtime | capability must fail clearly until graphics recipe supports it | +| macOS arm64/x86_64 | mcpp-managed tool/SDK environment | native frameworks/MoltenVK package contract | +| Windows x64 | mcpp-managed tool/runtime environment | Win32/system DLL or declared Vulkan provider | + +跨平台只共享抽象契约,不假装共享底层文件布局。 + +## 10. 迁移阶段 + +### Stage 0 — 文法与边界冻结 + +- 冻结 TemplateSpec grammar:namespace/version/tname 均可省略,namespace 默认 mcpplibs。 +- 冻结“单模板在没有显式 default 时自动成为 default”。 +- 冻结 absence → McppDefault、[xlings].subos → NamedSubos。 +- 冻结 SubOS 是 root-local、非传递 build/run 环境。 +- 冻结 xlings 是 graphics runtime owner,mcpp 只有 generic contract knowledge。 +- 冻结 AUR managed set = {mcpp-bin}。 +- 冻结 mcpp-m package/AUR remote 不动。 + +### Stage 1 — 模板统一 + +- 提取共享 PackageSelector parser/resolver。 +- 将 add/template 统一为 bare → mcpplibs、dotted → exact namespace。 +- scaffold 保存完整 PackageId/provenance。 +- manifest 注入复用 add editor。 +- dotted namespace/template E2E。 +- pkg: list deprecation。 + +### Stage 2 — runtime 选择统一 + +- 引入 RuntimeSelection。 +- default 与 named SubOS 生成同一 RuntimeBinding snapshot。 +- build/run/test 共用 snapshot 和 contract hash。 +- 只从 root/workspace root 读取 SubOS;dependency/member 不向消费者传播。 +- active SubOS legacy warning。 + +### Stage 3 — graphics 收口 + +- mcpp-index 调整 ImGui/OpenGL/Vulkan features/templates 及 xlings 生态依赖。 +- xlings 基于 xim-pkgindex recipes 收口 GL/Vulkan runtime artifacts 和 sentinels。 +- 移除 host ICD farm 与 SubOS view symlink bridge。 +- mcpp 保持 generic planner/validator。 + +### Stage 4 — mcpp-bin 对账 + +- release manifest。 +- 单包 generator、dry-run 与 Arch validation。 +- event + schedule reconciler。 +- 从旧状态恢复到 latest stable,并验证真实安装。 + +这些 stage 是设计级依赖顺序,不是 implementation plan;每个 stage 仍需在设计获批后拆成独立可评审 PR。 + +## 11. 验收矩阵 + +### 11.1 模板 + +| 场景 | 期望 | +|---|---| +| pkg | mcpplibs:pkg + latest stable + default/single template | +| pkg@1.2.0 | default namespace + exact version + default/single template | +| ns.pkg:t | exact namespace ns + named template | +| ns.pkg@1.2.0:t | 完整解析,输出 exact PackageId/version/template | +| capi.lua / mcpplibs.capi.lua | 分别解析为 capi:lua / mcpplibs.capi:lua | +| add/template 相同 selector | 规范化为同一个 PackageId | +| 同 short name 不同 namespace | 不按 short name 注入/缓存碰撞 | +| 单 template、无显式 default | 自动选择该 template | +| 多 templates、无 default | 失败并列出 templates | +| 多 default | provider validation 失败 | +| prerelease only 且省略版本 | 失败并要求显式 version | +| 失败渲染/I/O | 目标目录不存在或事务回滚 | + +### 11.2 runtime + +| 场景 | 期望 | +|---|---| +| fresh HOME,无 [xlings] | bootstrap mcpp default,build/run 同 binding | +| named SubOS 存在 | build/run/test 使用该 contract | +| named SubOS 不存在 | hard error,不退回 default | +| active SubOS 与 default 不同 | 迁移期 warning;最终仍选择 default | +| 切换 subos | build fingerprint 改变,不复用旧 ABI objects | +| dependency 声明自己的 SubOS | 作为 dependency 时不传递、不覆盖 root 环境 | +| 同一源码在 el8/trixie 两个 root 中构建 | 各自在自己的 RuntimeBinding 下生成独立缓存/产物 | +| 预构建库 | 传递实际 artifact ABI requirement,不传递 SubOS name | +| Linux loader/libc 混源 | post-link 失败或严格迁移阶段明确 warning | +| macOS/Windows | 不执行 Linux glibc 规则 | + +### 11.3 graphics + +| Gate | 必须证明 | +|---|---| +| mcpp source ownership | 无 GPU vendor/ICD selection branch,只消费 xlings generic contract | +| xlings ownership | 根据 mcpp-index 依赖解析 xim recipes、provider/sentinel 与 provenance | +| mcpp-index static | OpenGL/Vulkan features、templates、platform dependencies 可解析 | +| Linux software GL | Xvfb/Wayland headless + llvmpipe 创建窗口/帧 | +| Linux native GL | AMD/Intel/NVIDIA/WSL 分别记录实际 provider | +| Vulkan | instance、physical device、ICD manifest/driver provenance | +| macOS | native framework/MoltenVK 路径 | +| Windows | Win32 backend 与 Vulkan provider 路径 | +| unsupported arch | 明确 capability error,不以 SKIP 计 pass | + +### 11.4 AUR + +| 场景 | 期望 | +|---|---| +| 新 stable release | mcpp-bin 在 SLO 内收敛 | +| 重复事件 | no-op,无新 commit | +| 迟到旧事件 | 不降级 | +| 两架构任一资产/hash 错 | push 前失败 | +| AUR maintenance | transient retry + schedule 补偿 | +| mcpp-m | package 文件和 AUR remote 均不变,不参与 mcpp-bin verdict | +| push 后 | git head、RPC version、clean install 全部验证 | + +## 12. 性能与简洁性约束 + +- TemplateSpec parse 为 O(length),不访问网络。 +- resolver 与 mcpp add 共用缓存与 IndexRoute,不增加第二轮全索引扫描。 +- RuntimeBinding 每次 configure 解析一次,contract hash 进入 snapshot;hot no-op 不启动 xlings。 +- mcpp build 热路径不探测 GPU、不执行 Vulkan/OpenGL 工具。 +- graphics provider 探测发生在 xim install/doctor 生命周期,并缓存 host fingerprint。 +- AUR schedule 在 desired == current 时只做轻量查询与一致性检查。 +- 不因这四项新增顶层 CLI 命令。 + +## 13. 错误与安全边界 + +- selector、version、template name 任一非法,在网络/install/目录创建前失败。 +- package descriptor 命中后必须校验声明的完整身份。 +- 模板为纯数据,不执行 provider hooks/scripts。 +- 生成在 sibling temp dir 完成,验证后原子 rename。 +- manifest 不接收项目级 absolute xlings binary/home。 +- named SubOS 不存在时不 fallback。 +- mcpp 不读取 host GPU library 目录来补齐索引缺口。 +- AUR 先验证全部 mcpp-bin assets,再加载 SSH secret/push。 +- AUR 只 fast-forward,不改写历史。 + +## 14. 第二轮 review 后已冻结的细节 + +1. namespace 可以省略;省略固定为 mcpplibs,写出 dotted namespace 时按 exact namespace 解释。 +2. 只有一个 template 且没有 default = true 时,该单模板自动成为 default。 +3. SubOS 暂时只允许通过 root/workspace-root mcpp.toml 选择,不增加 CLI override。 +4. SubOS 是本地 build/run 开发环境,不作为库的传递要求;不同项目可选择不同 SubOS/glibc。 +5. xlings 是 graphics stack/runtime owner;mcpp-index 声明 C++ 图形包到 xlings 生态的依赖,mcpp 只做 generic build。 +6. AUR 自动对账只面向 mcpp-bin;mcpp-m package 与 AUR remote 本阶段不动。 + +## 15. 与上一份综合设计的关系 + +本文仅在以下四处覆盖上位文档: + +| 上位文档建议 | 本文最终方向 | +|---|---| +| --template namespace:name@version --variant name | --template [ns.]name[@version][:tname];默认 ns=mcpplibs | +| mcpp 参与更宽的 graphics runtime 规划 | xlings 负责 graphics runtime;mcpp-index 声明依赖;mcpp 只消费通用契约 | +| runtime contract 为大范围跨仓主线 | McppDefault + existing [xlings].subos,且 SubOS root-local/non-transitive | +| mcpp-bin/mcpp-m 都进入自动 reconciler | 自动 reconciler 只管理 mcpp-bin;mcpp-m 不动 | + +未被本文覆盖的 C1–C9、artifact physics、identity 类型化与机器输出分析继续保留在上位文档中。 + +## 16. 当前实现证据锚点 + +| 主题 | 证据 | +|---|---| +| mcpp add dotted/colon selector | tests/e2e/12_add_command.sh:82-106src/pm/commands.cppm:75-143 | +| shared dotted candidate rules | src/pm/dependency_selector.cppm:78-120 | +| package identity spec | docs/spec/package-identity.md:150-169 | +| current TemplateSpec | src/scaffold/template.cppm:22-47 | +| scaffold short-name loss | src/scaffold/create.cppm:30-129 | +| template E2E | tests/e2e/69_package_templates.sh:94-194 | +| existing [xlings].subos | src/manifest/types.cppm:469-485src/manifest/toml.cppm:988-996 | +| root project environment materialization | src/build/prepare.cppm:1997-2009 | +| current runtime resolution | src/build/prepare.cppm:928-974 | +| user documentation | docs/05-mcpp-toml.md:946-978 | +| AUR workflow | .github/workflows/aur-publish.yml:14-102 | + +本文是待 review 设计,不应据此宣称上述行为已落地或关闭相关 issue。 diff --git a/.agents/docs/2026-08-09-mcpp-template-runtime-graphics-aur-implementation-plan.md b/.agents/docs/2026-08-09-mcpp-template-runtime-graphics-aur-implementation-plan.md new file mode 100644 index 00000000..49a6509c --- /dev/null +++ b/.agents/docs/2026-08-09-mcpp-template-runtime-graphics-aur-implementation-plan.md @@ -0,0 +1,369 @@ +# mcpp Template, Runtime, Graphics, and AUR Convergence Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 在一张 mcpp PR 中落地已冻结的模板 selector、项目级 RuntimeSelection/RuntimeBinding、provider-neutral 图形运行时契约与仅管理 mcpp-bin 的 AUR reconciler;随后完成 squash/bypass 合入、release、mcpp-index/GitCode 对接和隔离环境下的 xlings 全生态验证。 + +**Architecture:** CLI 和 mcpp.toml 共用一个 exact dotted PackageSelector;scaffold 在任何落盘前解析出完整 PackageId/version/template,并在 sibling 临时目录事务生成。运行时只允许 McppDefault 或 root/workspace-root NamedSubos,两者解析成一次性 RuntimeBinding snapshot,进入构建指纹并由 build/run/test 复用,dependency/member 的 SubOS 不传递。mcpp-index 声明图形 RuntimeRequirement,xlings/xim 解析 provider,mcpp 仅记录 canonical requester/provider/artifact provenance 和执行平台通用链接/运行验证。GitHub Release manifest 是 AUR desired state,mcpp-bin reconciler 单调、幂等、校验实物并只做 fast-forward push。 + +**Tech Stack:** C++23 modules, mcpplibs.cmdline, nlohmann/json, TOML manifest parser, Bash/Python 3 release tooling, GitHub Actions, Arch makepkg, gh/git, xlings/xim/mcpp-index. + +## Global Constraints + +- [ ] 所有行为改动遵循 RED → GREEN → refactor;每个 RED 命令和预期失败原因写入提交/验证台账。 +- [ ] mcpp 实现只使用一张 PR,基于最新 `origin/main`;不直接 push main,不 force-push,不 amend/rebase 历史。 +- [ ] 最终按用户授权使用 admin/bypass squash merge;只有最新 PR HEAD 的 required checks 全部终态成功后执行。 +- [ ] `mcpp-m` 边界为字节级不变:不修改 `scripts/aur/mcpp-m/**`,不读取/生成/发布其内容,不访问其 AUR remote。 +- [ ] 不增加 `--variant`、`--subos` 或其他顶层 CLI;不在 mcpp 中出现 GPU vendor、Mesa、NVIDIA、WSL 或 ICD 选择分支。 +- [ ] SubOS 只来自本次构建 root/workspace root 的 `mcpp.toml`,不写入 dependency requirement/lock identity,不从 dependency/member 继承。 +- [ ] 平台物理规则放入 `src/platform/` 或已有平台模块;Linux ELF/glibc 校验在 macOS/Windows 明确 no-op,不用 Linux 推断替代原生 CI。 +- [ ] 所有 stateful 本地/生态验证使用隔离 `HOME`、`MCPP_HOME`、`XLINGS_HOME` 和 SubOS root;验证前后确认宿主状态未变化。 +- [ ] 只显式 stage 本计划列出的文件;保留主 checkout 中用户的未跟踪 issue triage 文档。 +- [ ] GitHub release 是主发布真源;AUR 暂时不可用时 GitHub release 不回滚,reconciler 留下可重试的精确失败分类。 + +--- + +## Task 1: Freeze Baseline, Issue, Branch, and Test Evidence + +**Files:** + +- Modify: `.agents/docs/2026-08-09-mcpp-template-runtime-graphics-aur-implementation-plan.md` +- Add: `.agents/docs/2026-08-09-mcpp-template-runtime-graphics-aur-validation.md` +- Preserve: `.agents/docs/2026-08-09-mcpp-template-runtime-graphics-aur-focused-design.md` +- Preserve: `.agents/docs/2026-08-09-xlings-mcpp-ecosystem-convergence-design.md` + +- [x] Create focused issue #398 referencing #397, #380, #392, and #396. +- [x] Create isolated worktree `feat/template-runtime-graphics-aur` from `origin/main@80291ca`. +- [x] Record baseline versions, latest releases, open PRs, and exact hashes for mcpp, xlings, mcpp-index, and xim-pkgindex in the validation ledger. +- [x] Run baseline `mcpp build`, unit suite, focused scaffold E2E, runtime E2E, AUR script tests (if any), and `git diff --check`; record any pre-existing failure without weakening later gates. +- [x] Snapshot hashes of `scripts/aur/mcpp-m/**` and the host xlings configuration for final boundary comparison. +- [x] Commit the approved design documents, implementation plan, and baseline validation ledger as the first explicit-files commit. + +## Task 2: Replace Candidate Guessing with One Exact PackageSelector + +**Files:** + +- Modify: `src/pm/dependency_selector.cppm` +- Modify: `src/pm/commands.cppm` +- Modify: `src/manifest/toml.cppm` +- Modify: `src/manifest/xpkg.cppm` +- Modify: `src/pm/dep_spec.cppm` +- Modify: `tests/unit/test_pm_compat.cpp` +- Modify: `tests/unit/test_pm_index_route.cpp` +- Modify: `tests/e2e/12_add_command.sh` +- Modify: `tests/e2e/27_namespace_dependencies.sh` +- Modify: `docs/spec/package-identity.md` + +**Interfaces:** + +- `PackageSelector { optional namespace; NameAtom name; string spelling; }` +- `parse_package_selector(string_view) -> expected` +- `normalize_package_selector(PackageSelector, defaultNs="mcpplibs") -> DependencyCoordinate` +- `format_package_selector(DependencyCoordinate) -> dotted selector` + +- [ ] RED: add unit rows proving `lua -> (mcpplibs,lua)`, `capi.lua -> (capi,lua)`, `mcpplibs.capi.lua -> (mcpplibs.capi,lua)`, and rejection of empty/double-dot/control segments. +- [ ] RED: update add E2E so `mcpp add capi.lua@5.4.7` probes/writes exact `capi:lua`, not `mcpplibs.capi:lua`; prove a same-short-name sibling cannot win. +- [ ] Implement the O(length), no-I/O shared parser/normalizer and return one exact coordinate after default namespace filling. +- [ ] Route `mcpp add`, dependency TOML parsing, feature dependency parsing, and xpkg dependency parsing through the same normalized coordinate. +- [ ] Preserve already-parsed/locked identities; during the migration release, emit a warning only when an old dotted candidate exists and differs from the new exact coordinate, with both copyable selectors. +- [ ] Update existence-gate and not-found diagnostics to show the normalized PackageId and explicitly mention default `mcpplibs` when namespace was omitted. +- [ ] GREEN: run the focused unit and namespace/add E2E tests, then `git diff --check`. +- [ ] Commit exact selector normalization and identity documentation. + +## Task 3: Make TemplateSpec Typed, Namespace-Aware, and Deterministic + +**Files:** + +- Modify: `src/scaffold/template.cppm` +- Modify: `src/scaffold/create.cppm` +- Modify: `src/cli/cmd_new.cppm` +- Modify: `src/cli.cppm` +- Add: `tests/unit/test_scaffold.cpp` +- Modify: `tests/e2e/69_package_templates.sh` +- Modify: `.github/workflows/ci-fresh-install.yml` + +**Interfaces:** + +- `TemplateSpec { PackageSelector package; optional version; optional templateName; bool legacyList; }` +- `parse_template_spec(string_view) -> expected` +- `ResolvedTemplatePackage { DependencyCoordinate id; string version; string indexRoute; string descriptorDigest; string payloadDigest; path root; }` +- `TemplateSelection { ResolvedTemplatePackage package; string templateName; }` + +- [ ] RED: unit-test every valid and invalid grammar row from focused design §5.1, including multiple `:`/`@`, empty components, exact dotted namespaces, and `pkg:` legacy list recognition. +- [ ] RED: add E2E packages with `mcpplibs:widget`, `acme:widget`, and `mcpplibs.capi:lua`; prove template resolution uses the exact PackageId through `IndexRoute::lookup_descriptor`. +- [ ] Implement TemplateSpec parsing in the fixed order template delimiter → version delimiter → shared PackageSelector; reject before config/network access. +- [ ] Replace scaffold short-name/compat probing with `IndexRoute` exact lookup and keep canonical namespace, version, route, descriptor digest, payload digest, and root through fetch/render/output. +- [ ] Reuse package-manager semver resolution for latest stable; omitted versions must not select prereleases, while explicit exact prerelease remains allowed. +- [ ] RED: test default selection rules: one explicit default wins; one sole non-default auto-wins; multiple without default list choices and fail; multiple defaults fail validation; no templates directory reports provider error. +- [ ] Implement the default rules and `pkg:` one-release warning pointing to `--list-templates`. +- [ ] Update human and machine output to include canonical selector, resolved namespace/name/version, template, and runtime selection. +- [ ] GREEN: run new unit tests, template E2E, and three-platform fresh-install template lanes. +- [ ] Commit typed TemplateSpec and exact template-package resolution. + +## Task 4: Make Scaffolding Safe and Transactional (#380) + +**Files:** + +- Add: `src/scaffold/project_name.cppm` +- Add: `src/platform/project_name.cppm` +- Modify: `src/scaffold/template.cppm` +- Modify: `src/scaffold/create.cppm` +- Modify: `src/manifest/toml.cppm` +- Modify: `tests/unit/test_scaffold.cpp` +- Add: `tests/e2e/202_new_transactional_scaffold.sh` + +**Interfaces:** + +- `validate_project_name(string_view) -> expected` +- `render_tokens(string_view, RenderVars) -> expected` +- `ScaffoldTransaction::begin(parent,name)`, `commit()`, destructor rollback. + +- [ ] RED: reject empty, absolute, separators, `.`, `..`, C0/DEL, Windows reserved device names, trailing dot/space, quote/tab and names containing the legacy `PROJECT` marker before target creation. +- [ ] RED: prove inserted values containing placeholder-like text are not rescanned and all RenderVars render canonical project/template identities. +- [ ] RED: inject read/write/copy/rename failures and assert neither final target nor sibling temporary directory remains. +- [ ] Implement shared portable project-name validation with platform-specific reserved-name rules isolated under `src/platform/project_name.cppm`. +- [ ] Replace repeated string substitution with a single-pass token renderer that never rescans inserted values. +- [ ] Extend RenderVars with project namespace/qualified name and template package namespace/name/selector/version/template. +- [ ] Replace substring dependency detection with structured manifest editing keyed by canonical PackageId; exact resolved version and features must be idempotent across same short names in different namespaces. +- [ ] Generate builtin and package templates in a same-parent temporary directory, check every filesystem/stream operation, fsync/close as supported, and atomically rename only after validation. +- [ ] GREEN: run scaffold unit/E2E tests, reproduce #380 cases with timeouts, and confirm no path escape or partial output. +- [ ] Commit transactional scaffold and close #380 from the final PR only after CI. + +## Task 5: Introduce Root-Local RuntimeSelection and One RuntimeBinding Snapshot + +**Files:** + +- Add: `src/xlings/runtime_selection.cppm` +- Add: `src/platform/runtime_binding.cppm` +- Modify: `src/build/prepare.cppm` +- Modify: `src/build/plan.cppm` +- Modify: `src/build/execute.cppm` +- Modify: `src/xlings/subos_info.cppm` +- Modify: `src/toolchain/model.cppm` +- Modify: `src/toolchain/detect.cppm` +- Modify: `src/toolchain/fingerprint.cppm` +- Modify: `src/project.cppm` +- Add: `tests/unit/test_runtime_selection.cpp` +- Modify: `tests/unit/test_subos_info.cpp` +- Modify: `tests/unit/test_fingerprint.cpp` +- Add: `tests/e2e/203_root_local_subos.sh` + +**Interfaces:** + +- `RuntimeSelection { enum Mode { McppDefault, NamedSubos }; string subosName; Source source; path ownerRoot; }` +- `RuntimeBinding { int schema; string providerId; string platform; string arch; string contractHash; optional loader; optional libc; vector libraryDirs; vector environment; vector capabilities; string provenance; }` +- `select_runtime(rootManifest, optional workspaceManifest, rootPath) -> expected` +- `resolve_runtime_binding(selection, compiler, GlobalConfig) -> expected` + +- [ ] RED: no `[xlings].subos` selects `McppDefault` even if active SubOS differs; an explicit `subos = "default"` is a NamedSubos selection. +- [ ] RED: missing named SubOS is a hard error and cannot fall back to default/active/compiler-baked runtime. +- [ ] RED: workspace root SubOS overrides member declaration during workspace build; a member declaration applies only when that member is built independently. +- [ ] RED: dependency manifest SubOS never merges into the consumer, lockfile, or cache identity; the same sources under el8/trixie produce distinct RuntimeBinding contract hashes. +- [ ] Implement selection before workspace member substitution and preserve its owner root; materialize project xlings config from the root/workspace-root selection only. +- [ ] Define McppDefault as the mcpp-managed default SubOS/runtime in configured xlings home, independent of xlings active/current symlink; bootstrap it when absent. +- [ ] Read one RuntimeBinding snapshot, canonicalize/sort its fields, compute contractHash, store it in BuildContext/BuildPlan/cache metadata, and feed it to toolchain detection/fingerprint. +- [ ] Reuse that snapshot for run/test environment; remove build/run fast-path re-reads of active SubOS. +- [ ] Match current xlings `op=set` presence semantics: preserve any ambient value, including an explicitly empty value, while `prepend` remains ordered and de-duplicated. +- [ ] On macOS/Windows return platform-native bindings without invented glibc/ELF fields; platform-specific derivation stays in `src/platform/runtime_binding.cppm`. +- [ ] GREEN: run unit tests, named/default/workspace/dependency SubOS E2E, and compare binding/fingerprint output. +- [ ] Commit root-local runtime selection and shared binding snapshot. + +## Task 6: Fix Runtime Payload Selection and Add Linux Artifact Physics (#392/#396) + +**Files:** + +- Modify: `src/toolchain/post_install.cppm` +- Add: `src/platform/elf_runtime.cppm` +- Add: `src/build/runtime_validation.cppm` +- Modify: `src/build/ninja_backend.cppm` +- Modify: `src/build/execute.cppm` +- Modify: `src/doctor.cppm` +- Add: `tests/unit/test_elf_runtime.cpp` +- Add: `tests/e2e/204_runtime_binding_physics.sh` + +**Interfaces:** + +- `ElfRuntimeFacts { interp; runpaths; needed; requiredGlibcVersions; resolvedLibc; resolvedObjects; }` +- `validate_runtime_artifact(path, RuntimeBinding, RuntimeResolution) -> RuntimeVerdict` +- `RuntimeVerdict { Pass | ProvenMismatch | Inconclusive; diagnostics[]; }` + +- [ ] RED: select the glibc payload named by RuntimeBinding, never the first directory entry; stale/absent payload is an explicit error. +- [ ] RED: fixture ELFs prove Rule B rejects interpreter/libc from different payloads and accepts same-payload paths. +- [ ] RED: fixture version tables prove a required GLIBC symbol floor above the selected libc exports is a hard proven mismatch; a lower/equal floor passes; unavailable closure data is inconclusive, not falsely green. +- [ ] Implement semantic exact payload lookup in post-install fixup and bump the fixup revision so existing toolchains repair against the selected binding. +- [ ] Implement internal ELF64 little-endian parsing for PT_INTERP, DT_RPATH/RUNPATH, DT_NEEDED and GNU version need/definition sections under `src/platform/elf_runtime.cppm`; no shell parsing on the build hot path. +- [ ] Validate only newly linked Linux ELF outputs; cache the verdict by artifact stat/link fingerprint so hot no-op performs zero parses. +- [ ] Emit canonical requester/provider/artifact paths and a copyable SubOS remediation; hard-fail proven Rule B/A mismatches, classify unresolvable host/hardware closure as inconclusive with an explicit diagnostic. +- [ ] Ensure macOS/Windows validators compile to a typed no-op and never apply Linux glibc rules. +- [ ] Extend doctor/runtime explanation to reuse stored verdict rather than re-probe or guess. +- [ ] GREEN: run ELF unit fixtures, form-X E2E, current #392 reproduction shape, and a safe host-DSO control. +- [ ] Commit runtime physics validation; only close #392/#396 if the final released E2E proves their exact acceptance cases. + +## Task 7: Carry Provider-Neutral Graphics Runtime Provenance + +**Files:** + +- Modify: `src/manifest/types.cppm` +- Modify: `src/manifest/toml.cppm` +- Modify: `src/manifest/xpkg.cppm` +- Modify: `src/build/plan.cppm` +- Modify: `src/build/flags.cppm` +- Modify: `src/build/prepare.cppm` +- Modify: `src/doctor.cppm` +- Add: `tests/unit/test_runtime_contract.cpp` +- Modify: `tests/unit/test_link_model_runtime_dirs.cpp` +- Modify: `tests/e2e/62_runtime_library_dirs.sh` + +**Interfaces:** + +- `RuntimeRequirement { kind; value; phase; canonical requester PackageId; required; }` +- `RuntimeArtifact { role; canonical provider PackageId; path; provenance; abi; digest; hostFingerprint; }` +- `LinkIntent { libraries; linkLibraryDirs; transitiveNeededDirs; runtimeSearchDirs; frameworks; deployFiles; }` + +- [ ] RED: same-short-name providers in two namespaces remain distinguishable in runtime resolution JSON and `mcpp why runtime`. +- [ ] RED: required capabilities and provided capabilities are separate; a requester cannot become its own provider merely because it requires a capability. +- [ ] RED: runtime search dirs do not enter `-L`; Linux transitive-needed dirs use `-Wl,-rpath-link`, macOS emits rpath/install-name semantics, and Windows uses explicit deploy files. +- [ ] Introduce structured generic requirement/artifact/provenance values while keeping legacy descriptor fields readable for one compatibility train. +- [ ] Populate requester/provider from the resolved package identity, including namespace/version/index provenance; never use bare `package.name` as provider identity. +- [ ] Write the resolved RuntimeBinding, requirements, artifacts, search mechanism, and validation verdict into `resolution.json`. +- [ ] Make `mcpp why runtime` a pure interpreter of stored generic facts; GPU/driver diagnostics point to xlings and never probe hardware. +- [ ] Add a static ownership gate that rejects new mcpp source branches containing provider-specific GPU/ICD selection vocabulary outside docs/tests. +- [ ] GREEN: run runtime-contract unit/E2E tests and prove the build hot path launches no GL/Vulkan probe. +- [ ] Commit provider-neutral runtime contract and link-intent separation. + +## Task 8: Generate an Immutable Release Manifest + +**Files:** + +- Add: `scripts/release/generate_manifest.py` +- Add: `tests/scripts/test_release_manifest.py` +- Modify: `.github/workflows/release.yml` +- Modify: `docs/10-distribution.md` + +**Interface:** + +- Release asset `mcpp-release.json` schema 1 with version, tag, release commit, and exact name/SHA256 for Linux x86_64/aarch64 plus every shipped platform asset. + +- [ ] RED: fixtures reject duplicate platform/arch rows, missing sidecars, mismatched hashes, draft/prerelease input, wrong tag/version, and non-deterministic ordering. +- [ ] Implement deterministic manifest generation from downloaded release artifacts and sidecars, recomputing every SHA256. +- [ ] Wire the release workflow so the manifest is uploaded only after all required release assets exist and validation passes. +- [ ] Add a release gate that downloads the uploaded manifest and compares it to the final GitHub release inventory. +- [ ] GREEN: run manifest tests and a local fixture generation twice with byte-identical output. +- [ ] Commit immutable release desired-state manifest. + +## Task 9: Replace Combined AUR Publishing with an mcpp-bin Reconciler + +**Files:** + +- Add: `scripts/aur/reconcile_mcpp_bin.py` +- Add: `scripts/aur/render_mcpp_bin.py` +- Add: `tests/scripts/test_aur_reconcile.py` +- Modify: `scripts/aur/update.sh` +- Modify: `scripts/aur/README.md` +- Modify: `.github/workflows/aur-publish.yml` +- Preserve byte-for-byte: `scripts/aur/mcpp-m/**` + +**Interface:** + +- `inspect -> DesiredState/ObservedState/ReconcilePlan` +- exit classification `noop | updated | transient | permanent | refused-downgrade` +- manual inputs `publish=false|true`, optional exact latest stable tag only; no downgrade override in this phase. + +- [ ] RED: fixture tests cover desired==observed no-op, upgrade, late old event refusal, missing/hash-mismatched asset, AUR maintenance retry, auth/permanent failure, RPC lag after git update, and known clone failure not becoming first publish. +- [ ] RED: assert the reconciler never opens, hashes, copies, stages, or addresses `scripts/aur/mcpp-m/**`; compare pre/post tree hashes. +- [ ] Split `update.sh` into an mcpp-bin-only compatibility wrapper around the renderer; remove all mcpp-m reads/writes without editing mcpp-m files. +- [ ] Render PKGBUILD from `mcpp-release.json`; regenerate `.SRCINFO` with non-root Arch `makepkg --printsrcinfo`, then `makepkg --verifysource`. +- [ ] Query AUR RPC and HTTPS git, compare versions with Arch `vercmp`, validate both Linux assets/sidecars, and produce a dry-run diff before secrets are loaded. +- [ ] Publish only by normal fast-forward SSH push with pinned AUR host key and bounded exponential retry for maintenance/timeouts; never force or initialize a missing known package. +- [ ] Verify remote git head, bounded-poll RPC, then install in a clean Arch container and assert `mcpp --version`. +- [ ] Change workflow triggers to successful release workflow_run + six-hour schedule + workflow_dispatch; all call the same latest-stable reconciler. Remove the mcpp-m publish leg. +- [ ] Emit Actions summary with trigger, desired/observed versions, hashes, remote commit, retry count, classification, and drift age; AUR failure must not alter GitHub release conclusion. +- [ ] GREEN: run Python tests, shell lint, dry-run against current latest release, and an Arch container source verification. +- [ ] Commit mcpp-bin-only AUR reconciliation and verify mcpp-m byte hashes are unchanged. + +## Task 10: Pin Latest xlings, Version mcpp, and Update User Documentation + +**Files:** + +- Modify: `src/xlings.cppm` +- Modify: `.github/actions/bootstrap-mcpp/action.yml` +- Modify: `.github/workflows/bootstrap-macos.yml` +- Modify: `.github/workflows/ci-linux-e2e.yml` +- Modify: all other authoritative xlings pin sites found by `tests/unit/test_xlings_version_pin.cpp` +- Modify: `mcpp.toml` +- Modify: `README.md` +- Modify: `docs/00-getting-started.md` +- Modify: `docs/zh/00-getting-started.md` +- Modify: `docs/05-mcpp-toml.md` +- Modify: `docs/zh/05-mcpp-toml.md` +- Modify: `docs/08-toolchain-internals.md` +- Modify: `docs/zh/08-toolchain-internals.md` +- Modify: `docs/spec/package-identity.md` +- Modify: `scripts/aur/README.md` + +- [ ] Re-query latest non-draft/non-prerelease xlings immediately before pinning; pin the exact version and verify every authoritative pin site matches. +- [ ] Choose the next unused calendar version (expected `2026.8.9.1` after live tag check), update `mcpp.toml`, and leave AUR snapshots to release-time generation. +- [ ] Document `[ns.]name[@version][:tname]`, default `mcpplibs`, exact dotted namespaces, sole-template default, and legacy list migration. +- [ ] Document McppDefault vs root/workspace-root `[xlings].subos`, no CLI override, non-transitive dependency semantics, coexistence across glibc bindings, and prebuilt ABI metadata boundary. +- [ ] Document xlings/xim ownership of OpenGL/Vulkan providers and that mcpp never probes GPU/driver/ICD. +- [ ] Document mcpp-bin-only eventual AUR reconciliation and explicitly state mcpp-m/mcpp-git are outside automation. +- [ ] Update English and Chinese examples together; regenerate command reference if CLI help changed. +- [ ] GREEN: run pin tests, docs example tests, generated command reference tests, `git diff --check`, and forbidden-vocabulary/boundary scans. +- [ ] Commit version, xlings pin, and documentation. + +## Task 11: Full Local Validation and PR Publication + +**Files:** + +- Modify: `.agents/docs/2026-08-09-mcpp-template-runtime-graphics-aur-validation.md` + +- [ ] Build a fresh mcpp binary with the pinned xlings in isolated homes; do not validate with a stale installed mcpp. +- [ ] Run the complete unit suite and every applicable Linux E2E, then focused no-cache workspace/self-host/release builds. +- [ ] Run scaffold/template, default/named/workspace SubOS, runtime physics, runtime provenance, release-manifest, and AUR reconcile gates independently and record commands/results. +- [ ] Run sanitizers/static checks available in repository CI, `git diff --check origin/main...HEAD`, forbidden mcpp-m diff, and explicit scope inventory. +- [ ] Measure hot no-op build and template parse against baseline; require no material regression and zero xlings/GPU subprocess on hot no-op. +- [ ] Rebase is forbidden; if origin/main advanced, merge origin/main normally, rerun all affected gates, and preserve visible history. +- [ ] Push the branch and open one non-draft PR with `Closes #398`, related issue notes, architecture decisions, RED/GREEN evidence, boundary proof, version/pin, and cross-repo follow-up plan. +- [ ] Request review using the repository review workflow and address feedback with fresh evidence. + +## Task 12: Native GitHub Actions, Squash/Bypass Merge, and Release + +**Skills required at this task:** `mcpp-release`, `superpowers:verification-before-completion`, and `superpowers:finishing-a-development-branch`. + +- [ ] Wait for all latest-HEAD checks to reach terminal success across Linux, macOS, Windows, cross/QEMU aarch64, native aarch64, fresh install, release manifest, and AUR dry-run lanes. +- [ ] Treat skipped native/runtime hardware rows as NOT_EXERCISED, not pass; create an isolated temporary CI lane if a required platform gate is absent. +- [ ] Verify remote PR HEAD equals local HEAD, branch diff is clean, no unresolved reviews, no conflict, and `scripts/aur/mcpp-m/**` is unchanged. +- [ ] Use the explicitly authorized admin/bypass squash merge; verify the squash commit is on main and PR/issue states are correct. +- [ ] Follow `mcpp-release`: verify version strings, create/push the release tag, monitor every release job, asset, checksum, `mcpp-release.json`, GitCode mirror, and GitHub release until terminal success. +- [ ] Do not block/rollback the GitHub release for an AUR transient; dispatch the fixed latest-stable mcpp-bin reconciler and record its exact terminal state. +- [ ] Close #380/#392/#396 only when their released acceptance paths are proven; otherwise comment with delivered subset and keep the residual open. + +## Task 13: mcpp-index, GitCode Resources, and xlings Ecosystem Follow-Through + +**Repositories:** + +- `/home/speak/workspace/github/mcpplibs/mcpp-index` +- `/home/speak/workspace/github/openxlings/xim-pkgindex` +- `/home/speak/workspace/github/openxlings/xlings` +- local GitCode helper/config already used by release workflow + +- [ ] In isolated repo-specific worktrees, update mcpp-index minimum/current mcpp pins and template descriptors to canonical selectors and sole/default template lint. +- [ ] Refactor ImGui features/templates to core/headless, backend-glfw-opengl3, backend-vulkan, app, docking, and viewports without making core pull graphics unconditionally. +- [ ] Express OpenGL/Vulkan runtime requirements through generic xlings/xim package/capability dependencies and canonical provider identities; remove mcpp-index host ICD/DSO collection paths. +- [ ] In xim-pkgindex, add/fix Vulkan loader/ICD and host-link provider recipes/provenance only where current recipes cannot satisfy the new generic contract; keep provider detection/lifecycle outside mcpp. +- [ ] Use the established index CI workflow, one repo PR per external repository as required by repository boundaries, and wait for every latest-head validation job before merge. +- [ ] Confirm the mcpp release job opens/merges the mcpp index version bump and local GitCode resources contain the exact new release assets plus SHA256 sidecars; verify GitCode with ranged GET and full SHA256. +- [ ] Publish/rebuild index artifacts and verify their public hashes/refs. +- [ ] From fresh isolated HOME/MCPP_HOME/XLINGS_HOME, install the released mcpp and pinned/released xlings, run `xlings update`, canonical template new/build/run/test, default runtime, two named SubOS/glibc builds, and package lifecycle smoke. +- [ ] Run software OpenGL and Vulkan instance/device lanes where hardware exists; report AMD/Intel/NVIDIA/WSL/macOS/Windows hardware rows as PASS/FAIL/NOT_EXERCISED with actual provider/ICD provenance. +- [ ] Verify mcpp does not probe GPU and the same graphics dependencies resolve through xlings/xim under each exercised environment. +- [ ] Record all PRs, commits, CI run/job IDs, release tags, asset hashes, GitCode URLs, AUR state, and public smoke results in the validation ledger. + +## Task 14: Completion Audit and Final Report + +- [ ] Verify mcpp main/release/index heads, tags, PR merge states, issue comments/states, AUR mcpp-bin desired/observed state, and public artifact hashes from live sources. +- [ ] Verify local worktrees are clean and main checkout user files remain untouched. +- [ ] Re-run `git diff --check`, version/pin consistency, mcpp-m boundary hash, and one final cold-home released-binary smoke. +- [ ] Mark every checkbox with evidence; do not convert NOT_EXERCISED hardware rows into green. +- [ ] Update the active goal to complete only when no required item remains. +- [ ] Report concise core outcomes plus links to the implementation plan, validation ledger, issue, PR, CI, release, index PRs/artifacts, GitCode, AUR and ecosystem evidence. diff --git a/.agents/docs/2026-08-09-mcpp-template-runtime-graphics-aur-validation.md b/.agents/docs/2026-08-09-mcpp-template-runtime-graphics-aur-validation.md new file mode 100644 index 00000000..8298e4cb --- /dev/null +++ b/.agents/docs/2026-08-09-mcpp-template-runtime-graphics-aur-validation.md @@ -0,0 +1,124 @@ +# mcpp Template, Runtime, Graphics, and AUR Validation Ledger + +> Started: 2026-08-09 Asia/Shanghai +> +> Implementation issue: https://github.com/mcpp-community/mcpp/issues/398 +> +> This ledger separates baseline, RED, GREEN, CI, release, and public-ecosystem evidence. A running, skipped, cancelled, or superseded job is never recorded as pass. + +## 1. Scope and repository boundaries + +The mcpp implementation is developed in one branch and one pull request: + +- worktree: `/home/speak/.config/superpowers/worktrees/mcpp/template-runtime-graphics-aur` +- branch: `feat/template-runtime-graphics-aur` +- base: `mcpp-community/mcpp main@80291ca01a982c1e8c00e43bfa97ffe68516e6d7` +- focused issue: `mcpp-community/mcpp#398` +- umbrella issue: `mcpp-community/mcpp#397` + +External repositories retain their own history and review boundaries. Their implementation, if required after the mcpp release, uses separate repository PRs; it does not create a second mcpp implementation PR. + +The main mcpp checkout contains user-owned untracked design/triage files. This worktree was created from `origin/main`; no tracked or untracked file in the main checkout is moved or deleted. + +## 2. Live baseline + +Captured after fresh `git fetch origin main --prune` on 2026-08-09: + +| Repository | local HEAD | `origin/main` | local state | latest release | +|---|---|---|---|---| +| `mcpp-community/mcpp` | `80291ca01a982c1e8c00e43bfa97ffe68516e6d7` | same | isolated feature worktree clean before docs | `v2026.8.8.4`, published 2026-08-08 10:51:31Z | +| `openxlings/xlings` | `2913a0949af3b26192a6c1d8f12b78f976bea6b8` on `feat/version-grammar` | `f203b6b9a5e3e0d3f707468ed56cdb8b50cc7acc` | dirty user checkout; must use a new worktree | `v2026.8.9.2`, published 2026-08-08 20:38:57Z | +| `mcpplibs/mcpp-index` | `b86fc7c0c80a93f4ccdf797c13ec0cca2557d6eb` | same | clean main | no GitHub release list | +| `openxlings/xim-pkgindex` | `576ef09b69becefca00de8c94c6ba17d5cdb1ee4` | `e8029381beb2e0c83c4ec4318f01bd18c523890a` | local main behind; must use a new worktree | no GitHub release list | + +Open PR inventory at capture time: + +- mcpp: #387, #372 (Draft), #353, #351 (Draft), #272. +- xlings: none. +- mcpp-index: #150 (Draft), #61. +- xim-pkgindex: none. + +Local executables visible before the isolated cold build: + +- PATH mcpp: `mcpp 2026.8.6.2`. +- latest existing source-built binary: `mcpp 2026.8.8.4` at `target/x86_64-linux-gnu/09887d532ce30543/bin/mcpp` in the main checkout. +- PATH xlings: `xlings 2026.8.9.2`. + +The release version proposed for this implementation is `2026.8.9.1`; the tag is re-queried immediately before changing `mcpp.toml`. + +## 3. Immutable boundary snapshots + +Approved design hashes: + +- focused design: `e06821b73102049e1f2184a2967e5dd81dfb6f05389a763dad326d139d0e3f25` +- parent convergence design: `ad93db80a3222f6536629a15d9b5ba430b222e179b91de01131c619ecbd3bd65` +- initial implementation plan: `aea2963f593ef00e71b2876f52a773014a28098f513f82260063dcd336acb09e` + +`scripts/aur/mcpp-m/**` baseline: + +| File | SHA256 | +|---|---| +| `.SRCINFO` | `9d8b8279aadfa6b1915850fbbfe33c2ef237456f94dd706e726e3169b60be0f8` | +| `PKGBUILD` | `545fe0de51f0cf7e5979871ba8f8d6cd3233c485338a9768f57e53c0a6f8fa7f` | +| `mcpp.sh` | `cbab68984b02c415f8ae42bf9417647b842e900fbf7bf292067e4a384b924f8f` | +| sorted aggregate | `afb8a647e04483a86985119e07086016f49d55f177ee6257094c336d226113c6` | + +Host xlings configuration snapshot: + +- scope: sorted SHA256 rows for `/home/speak/.xlings/.xlings.json` and `subos/*/.xlings.json`; contents were not copied. +- aggregate: `f218aadf3792ee815c8535ce0ca0bb53f634fecbdbc5d0d47db442052d786d1b`. + +All stateful verification uses a separate temporary root and must reproduce both aggregate hashes before completion. + +## 4. Baseline verification + +Isolated root: `/tmp/mcpp-focused-398.XVUEhz`. + +| Gate | Command shape | Status | Evidence | +|---|---|---|---| +| cold source build | isolated `MCPP_HOME`, `XLINGS_HOME`, vendored xlings 2026.8.9.2; `mcpp build --no-cache` | PASS | cold bootstrap installed `glibc@2.44` and resolved `gcc@16.1.0`; release build finished in 64.80s; binary `target/x86_64-linux-gnu/72abd390cce53924/bin/mcpp` reports `2026.8.8.4` | +| unit suite | fresh baseline binary, isolated homes; `mcpp test` | PASS | `68 passed; 0 failed`, 89.06s total | +| template E2E | isolated homes; `MCPP= bash tests/e2e/69_package_templates.sh` | PASS | exit 0, `OK` | +| runtime E2E | isolated homes; scripts 74, 166 and 201 | PASS with one fixture rerun | #74 proved payload glibc 2.44 PT_INTERP; #166 proved private glibc env inclusion/exclusion; #201 passed both sysroot/payload-first modes after using a non-`/tmp` isolated HOME | +| AUR tests | inventory + `bash -n scripts/aur/update.sh` | PASS for syntax; coverage absent | baseline has no reconciler tests; Task 9 adds fixture coverage | +| diff check | `git diff --check origin/main...HEAD` before edits | PASS | no tracked diff before documentation was added | + +### Baseline fixture diagnosis + +The first #201 run exited 1 because the isolated `MCPP_HOME` itself was `/tmp/mcpp-focused-398.XVUEhz/.mcpp`. The test intentionally rejects every RUNPATH beginning with `/tmp/` as evidence of another temporary home, so it classified the current, valid isolated home as pollution. The observed two paths were the current glibc and gcc payload directories, not a stale foreign home. + +The hypothesis was tested by hard-link cloning the disposable state into the non-`/tmp` isolated root `/home/speak/.cache/mcpp-focused-398.IE23if`, letting the normal fixup rebind paths, and running the unmodified script again. It exited 0 and proved both link modes load nothing from the host. No product or test source was changed for this baseline result. + +## 5. RED/GREEN ledger + +Each behavior entry is appended with: + +1. exact test command; +2. RED exit code and why the old code failed; +3. production change; +4. GREEN exit code and relevant assertions; +5. refactor/full-gate result. + +No RED/GREEN evidence has been claimed yet. + +## 6. Pull request and CI + +Not published yet. This section will record PR URL, local/remote HEAD, review state, all latest-head job IDs and terminal conclusions. + +## 7. Merge and release + +Not started. This section will record the authorized bypass squash commit, tag, GitHub release, release workflow/job IDs, asset inventory, checksums, `mcpp-release.json`, and GitCode mirror evidence. + +## 8. AUR mcpp-bin + +Current live snapshot from the preceding audit: + +- GitHub desired release: `v2026.8.8.4`. +- AUR observed `mcpp-bin`: `2026.8.1.1-1`. +- latest failed AUR run: `31254088758`, after version and both checksums resolved, then AUR SSH returned maintenance. + +No old failed run will be rerun. Recovery uses the reconciler from a fixed main/release state. `mcpp-m` is outside every verdict and publish path. + +## 9. Cross-repository and public ecosystem verification + +Not started. This section will record mcpp-index/xim/xlings PRs and commits, index artifact hashes, GitCode ranged/full downloads, cold-home package lifecycle, canonical template build/run, multiple SubOS/glibc bindings, and graphics PASS/FAIL/NOT_EXERCISED provenance. diff --git a/.agents/docs/2026-08-09-xlings-mcpp-ecosystem-convergence-design.md b/.agents/docs/2026-08-09-xlings-mcpp-ecosystem-convergence-design.md new file mode 100644 index 00000000..01cd8c34 --- /dev/null +++ b/.agents/docs/2026-08-09-xlings-mcpp-ecosystem-convergence-design.md @@ -0,0 +1,985 @@ +# xlings × mcpp 生态契约收敛与优化设计 + +> 状态:Review Draft +> +> 日期:2026-08-09 +> +> 基线:mcpp main@80291ca01a98;xlings 最新发布 2026.8.9.2;mcpp 发布包当前内置 xlings 2026.8.8.1。#397 的跨仓证据快照为 mcpp-index b86fc7c、xim-pkgindex c0aded29;2026-08-09 复核的远端 main 分别为 b86fc7ce802938,设计不依赖本地旧 checkout。 +> +> 范围:设计与迁移方案,不代表本文所列实现已经完成。 +> +> 关联问题:[#397(C1–C9 汇总)](https://github.com/mcpp-community/mcpp/issues/397)、[#396](https://github.com/mcpp-community/mcpp/issues/396)、[#392](https://github.com/mcpp-community/mcpp/issues/392)、[#380](https://github.com/mcpp-community/mcpp/issues/380)。 + +## 1. 结论先行 + +这些问题不是九个独立 bug,也不是“再补一轮 if”能稳定解决的问题。它们共同暴露了五类边界不清: + +1. **身份边界不清**:namespace:name 在 CLI、模板、索引、渲染器之间被拆掉或退化为裸 name。 +2. **状态边界不清**:构建缓存只记“输入看起来没变”,却没有声明一次成功必须留下哪些输出与证明。 +3. **运行时边界不清**:xlings、SubOS、索引和 mcpp 都能推断或改写运行时环境,却没有统一、可持久化、可验证的运行时契约。 +4. **策略边界不清**:用户构建时的物理可运行性、索引仓库的生态闭包策略、GPU/宿主探测混在同一层。 +5. **发布边界不清**:GitHub Release 是事件源,AUR/Homebrew 等下游却按“一次事件必成功”实现,缺少最终一致性对账。 + +建议采用 **分层、类型化、可验证契约**,而不是把 xlings 扩成一个全知守护进程: + +- **xlings** 是环境与运行时 substrate:安装、版本选择、payload provenance、SubOS、RuntimeBinding、环境操作语义。 +- **xim-pkgindex** 是系统级 payload 与宿主桥接策略:glibc、Mesa、NVIDIA/WSL sentinel、图形运行时闭包。 +- **mcpp-index** 是 C/C++ 包、模板和链接元数据:模块、头文件、link plan、GUI 模板与平台依赖声明。 +- **mcpp** 是项目规划器与产物验证器:构建图、缓存后置条件、模板事务、实际二进制物理检查、可解释诊断。 +- **发布流水线** 是投影对账器:以不可变 Release Manifest 为源,将 AUR 等渠道收敛到期望状态。 + +推荐方案的核心不是“增加更多配置”,而是减少重复推断:每个事实只有一个所有者,其他组件消费版本化契约。 + +## 2. 目标与非目标 + +### 2.1 目标 + +- 将 C1–C9、#380、#392、#396 收敛到少量可复用的根契约。 +- 让 mcpp newmcpp add、索引和生成后的清单使用同一套包身份。 +- 让构建“成功”同时意味着必要产物、元数据和运行时证明完整。 +- 让私有 glibc、宿主库、图形运行时的来源和兼容性可计算、可解释、可缓存。 +- 支持 Linux、macOS、Windows 的真实平台差异,不制造虚假对称。 +- 让发布渠道从一次性推送转成幂等对账,外部服务恢复后可自动收敛。 +- 保持无变化构建快速,不在构建热路径启动 xlings 或访问网络。 + +### 2.2 非目标 + +- 不在 mcpp 中实现 GPU 驱动管理。 +- 不要求每个 GUI 测试都在普通共享 runner 上打开真实窗口。 +- 不用 xlings daemon 替代 mcpp 的项目图规划。 +- 不把索引闭包策略变成用户本地每次构建的强制联网检查。 +- 不以一个跨四仓库的巨型 PR 交付。 +- 本文不直接关闭 issues、重跑 AUR 工作流或发布新版本。 + +## 3. 当前事实与根因 + +### 3.1 C1–C9 不是同一优先级,但可以归并 + +| 编号 | 当前现象 | 直接根因 | 应归入的长期契约 | 优先级 | +|---|---|---|---|---| +| C1 | fast path 在 compile_commands.json 等配置产物缺失时仍可直接进入 Ninja,编辑器反复请求;普通 executable 缺失通常仍由 Ninja 重建 | 缓存没有声明配置成功的后置条件 | BuildSnapshot + OutputManifest | P1 | +| C2 | 注释中的 R"(、普通字符串中的注释记号会污染模块扫描状态 | 先做脆弱的行级 strip,再解析 token | 线性词法 masker | P1 | +| C3 | TTY 下 --no-color 无效 | color 状态延迟初始化覆盖显式参数 | 一次初始化的 UiPolicy | P2 | +| C4 | namespaced 包 exports 非 strict 时漏检,strict 时会把合法 exports 误报 missing | qualified owner 与短 manifest name 做字符串比较 | PackageId 类型贯穿 | P1 | +| C5 | Windows 版本探测含 POSIX 2>/dev/null | 命令以 shell 字符串表达 | CommandSpec + direct exec | P1 | +| C6 | 部分安装形态下 release 内置 xlings 不进入候选更新链 | 候选只按位置推断,没有 provenance/capability | XlingsCandidate 选择策略 | P1/P2 | +| C7 | xlings 端已按 presence 语义修复,但 mcpp 仍把 op=set 无条件覆盖 | reader 与当前 wire 语义漂移,且缺少显式 replace | EnvOp schema v2 | P1 | +| C8 | 两段无版本安装循环在进入 fetch 前即失败并吞掉错误;当前用户可达影响尚未证明 | 多个安装 owner、错误被忽略 | 单一 solver/installer owner | P2 | +| C9 | 路径、源输出、glob、脚手架、链接、版本、canary、CI、机器输出等 11 个长期项 | 同一事实多处生产、字符串协议与 fail-open | 分别映射到下表所列基础契约 | P1–P3 | + +C9 的 11 项应明确归属,而不是留作“杂项”: + +| C9 项 | 归属 | +|---|---| +| Windows 路径与 action 规范化(#393) | typed Path + CommandSpec | +| generated source output 被静默排除(#393) | OutputManifest + action postcondition | +| 默认 source glob 漂移(#386) | 单一 SourceSet producer | +| 脚手架安全与卡死(#380) | NameAtom + 单次渲染 + 事务目录 | +| runtime link order / macOS runtime dir(#304) | LinkPlan token + 平台 capability | +| prerelease 依赖解析(#370) | 单一 VersionReq parser | +| cfg version 被忽略(#290) | 封闭条件词汇;未知条件 hard error | +| operator-template canary 只 precompile、不 import(#256) | 真实 importer + crash/unsupported/fail 三态 | +| cppfly resolver 后续候选不可达、canary 可自我跳过(#215) | 遍历全部候选 + fail-closed capability gate | +| fresh install 被缓存掩盖(#259) | 冷 HOME 独立 gate | +| machine output 不一致(#379) | UiPolicy + WireEnvelope | + +其中 #386 的“四项 fallback glob 与七项 canonical 默认不一致”是已证实的代码漂移,但正常 xpkg 装载路径已补默认并要求 sources 非空;当前用户可达性尚未建立,应作为 P3 清理而不是阻塞前两阶段。 + +### 3.2 #380:输入验证只是第一层,真正缺的是脚手架事务 + +当前 mcpp new 可接受路径穿越、控制字符和会重新引入占位符的名称;builtin renderer 对含 PROJECT 的替换值可静态证明无限循环,package-template renderer 虽推进游标,不会以同一种方式无限循环,但仍会发生占位符相互消费。两条路径都有 I/O 错误被忽略和半成品风险。修复不能只加一个正则: + +- 项目逻辑名必须是一个 NameAtom,与目标目录 --dir 分离。 +- 新项目默认执行 portable policy:拒绝绝对路径、./..、任一平台路径分隔符、NUL/控制字符、Windows 保留设备名及尾随点/空格;已有 legacy manifest 继续可读,不借此批量改写用户身份。 +- namespace 是结构化字段,不允许塞回项目名或模板字符串。 +- 渲染必须单次完成;未知占位符、重复键、非法 TOML/C++ 标识均 hard error。 +- 先写同父目录临时目录,验证生成清单与必需文件,再原子 rename。 +- 任一步失败清理临时目录;目标已存在默认不覆盖。 + +### 3.3 #392 与 #396:问题是“实际装载物理”,不是版本号猜测 + +#392 展示了私有 glibc 与宿主 libtinfo/Mesa 混装时的两个失败方向: + +- 私有 loader/libc 较旧,宿主库要求更高 GLIBC symbol version。 +- 通过全局 LD_LIBRARY_PATH 暴露私有 glibc,又让外部宿主命令加载到错误 libc/私有符号。 + +当前 main 已有隔离私有 glibc 环境的局部缓解,因此不能表述成“完全未修”。但仅靠目录过滤仍不能证明最终 ELF 的实际闭包可运行。 + +另一个独立的不确定性是 post-install 的 sandbox glibc 查找:注释声称选择 newest,代码实际返回 directory_iterator 的第一个命中;该顺序未规定,不能描述为“字典序第一”。目标设计必须按完整 PackageId/version/RuntimeBinding 选择,不从目录枚举顺序推断。 + +#396 中应保留两条物理不变量,但修正 Rule A 的计算方式: + +- **Rule B — 单一 RuntimeBinding**:实际 PT_INTERP 与最终解析到的 libc 必须来自同一个 payload/runtime binding。 +- **Rule A — symbol ceiling**:对每一个实际借用的宿主 ELF,对其 .gnu.version_r 所需的最高 GLIBC 版本逐对象检查,不用粗糙的“私有 glibc 版本 ≥ host glibc 版本”替代。 + +生态策略另行定义: + +- **Policy D — 生态闭包**:索引包的运行时依赖应由生态包闭包满足;宿主对象只允许出现在显式、类型化的 host-link sentinel 后。 +- D 在索引 CI/安装物化时强制。用户构建的最终目标是对**已证明**的 A/B 冲突 hard fail,并清晰展示 provenance;在精确闭包解析器积累真机证据前,按 observe → warning → strict opt-in → 默认 strict 分阶段,不能拿粗略的 host glibc 代理在第一天全局 hard fail。 + +这一区分很重要:A/B 是物理事实,D 是仓库治理策略。 + +### 3.4 模板 namespace:现有语法先天歧义 + +当前 --template 解析 pkg@ver:template,第一个冒号被当作 template 分隔符。因此 ocornut:imgui 会被理解为 package=ocornut、template=imgui,无法表达规范身份。scaffold resolver 又只尝试空 namespace 与 compat,没有复用依赖路径的 IndexRoute;裸 imgui 会先命中冻结的 mcpplibs:imgui,而不是新的 ocornut:imgui,也不会报告跨 namespace 候选歧义。 + +同时,模板获取阶段即使从 descriptor 得到 namespace,返回值和渲染变量仍只保留裸 name;依赖注入也用字符串搜索和短名。这会重新引入此前包索引已经修复的同名碰撞。 + +正确方向是复用 mcpp add 已有身份语法,而不是再发明第三套压缩分隔符: + +~~~text +mcpp new app --namespace acme \ + --template ocornut:imgui@1.92.8 \ + --variant glfw-opengl3 \ + --index official + +mcpp new app --template mcpplibs:templates@0.0.1 +mcpp new --list-templates ocornut:imgui@1.92.8 +~~~ + +- --template:规范 PackageRef,即 namespace:name@version。 +- --variant:该 provider 内的模板变体。 +- --namespace:新项目自身 namespace。 +- --dir:文件系统目的地。 +- --index:路由来源;index alias/path/URL 不属于 PackageId。 +- 旧 pkg@ver:template 只作为 deprecated ingress,解析后立即规范化并告警。 +- 对 pkg:word,新语义“namespace:name”与旧语义“pkg 的 variant”可能同时成立:只在一侧候选唯一时兼容;两侧都存在或无法排除时返回稳定的 ambiguity error,并给出可复制的显式 --template/--variant 写法。 +- builtin bin 可保留兼容 alias,但内部也使用明确的 builtin provider identity。 + +可选的紧凑糖 namespace:name@version#variant 没有冒号歧义,但建议在显式 flags 与 wire model 稳定后再决定;内部始终是 TemplateSelection,不让紧凑语法成为第三种身份。 + +### 3.5 图形程序:当前已有正确积木,但责任仍有重复 + +当前生态已经包含: + +- mcpp-index 的 ocornut:imguicompat.glfwcompat.glx-runtime。 +- xim-pkgindex 的 xim:graphics、Mesa,以及 NVIDIA/WSL host-link sentinels。 +- Linux Mesa 的软件和多类硬件 backend;macOS native frameworks;Windows Win32/system SDK 路径。 + +问题在于 compat.glx-runtime 仍需要从 SubOS view 选择并 symlink GL 库到自己的 runtime 目录。这是过渡桥,而不是最终模型。最终应让 xlings 持久化可传递 runtime exports,mcpp 直接消费 resolved runtime contract;mcpp-index 只声明依赖,不复制 xlings 的视图布局。 + +Vulkan 还存在更明显的旁路:compat.vulkan-runtime 仍从 host 的 /lib*//usr/lib* 收集 ICD 和 transitive DSOs,测试主要覆盖 loader API,没有证明真实 ICD/device、来源或 GLIBC 闭包。GL 的收敛方案必须同时覆盖 Vulkan loader、ICD manifest 与 driver;否则只是把同类风险移到另一个 API。 + +### 3.6 AUR:外部维护是触发原因,缺少对账才是长期漂移原因 + +2026-08-09 的只读审计显示: + +- GitHub 最新 release 是 [v2026.8.8.4](https://github.com/mcpp-community/mcpp/releases/tag/v2026.8.8.4),AUR RPC 中 mcpp-binmcpp-m 均停在 2026.8.1.1-1,相差 20 个正式 release。 +- 最后成功的 aur-publish run 是 [30649165652](https://github.com/mcpp-community/mcpp/actions/runs/30649165652);从 [30718043364](https://github.com/mcpp-community/mcpp/actions/runs/30718043364) 起到 2026-08-08,共核对到 18 个同类失败、4 个因 release gate skipped;最新失败是 [31254088758](https://github.com/mcpp-community/mcpp/actions/runs/31254088758)。 +- 失败日志均已完成版本与 sha 刷新,随后在 AUR SSH 阶段收到 The AUR is down due to maintenance 并退出 128。 +- 当前 workflow 只有 release 完成事件与手工触发;没有 retry、schedule/reconcile、推送后验证。 + +还存在一个必须先修的独立 P0: + +- scripts/aur/update.sh:65 只匹配单行 sha256sums=('...')。 +- scripts/aur/mcpp-m/PKGBUILD:34-35 是两行数组,所以 source checksum 没有被更新。 +- workflow 生成的 .SRCINFO 使用新 checksum,而 PKGBUILD 仍保留旧 checksum,形成同一提交内部不一致。 +- 当前顺序先推 mcpp-bin 再推 mcpp-m;AUR 恢复后直接重跑可能先产生部分成功,再尝试推送坏的 mcpp-m 元数据。 + +因此不能把“重跑工作流”作为修复。应先增加本地结构化更新与一致性 gate,再恢复发布。 + +### 3.7 多视角评估 + +| 视角 | 当前风险 | 评价 | 目标变化 | +|---|---|---|---| +| 架构 | 高 | xlings、probe、post-install、build plan、run env 多次推导 runtime truth;scaffold 又绕开 canonical resolver | 单一 owner + versioned contract + typed snapshot | +| 稳定性 | 高 | fail-open、忽略 I/O/error_code、配置副作用未入 manifest;外部发布一次失败即永久漂移 | transaction/postcondition/reconciler | +| 兼容性 | 高 | GLIBC 风险取决于实际 DSO;namespace、EnvOp、runtime dirs 在读写端语义漂移 | exact artifact proof + schema negotiation | +| 优雅简洁 | 中高 | CLI 表面不算大,但内部靠短名、目录顺序、字符串命令和目录复制造成隐性复杂度 | 保持少量 CLI,内部用 PackageRef/CommandSpec/LinkIntent | +| 用户体验 | 高 | 成功可能留下半状态;错误只报 GLIBC/包不存在,无法说明来源;机器输出不稳定 | 原子操作、canonical ref、why provenance、稳定 envelope | +| 性能 | 中 | 现有 fast path 很快,但把完整性排除在“命中”之外;runtime/版本探测可能重复 | 本地 hash snapshot、按产物增量验证、无热路径 xlings/network | +| 多平台 | 高 | POSIX shell 片段泄漏到 Windows;Mach-O runtime dirs 与 Linux ELF 语义混用;硬件结论常由 headless/cross build 代替 | LinkIntent 平台 lowering + native cold-home/hardware evidence | + +这里的目标不是用“更严格”换“更慢”。类型化契约让昂贵解析只发生在安装、configure 或 changed artifact 上,hot no-op 只校验 fingerprint 与 required outputs。 + +## 4. 三种架构选择 + +| 方案 | 架构 | 稳定性 | 兼容性 | 简洁性 | 性能 | 多平台 | 结论 | +|---|---|---|---|---|---|---|---| +| A. 按 issue 打补丁 | 局部修改快,但同一事实继续多处推断 | 短期变绿,回归概率高 | 表面影响小,长期漂移大 | 初看简单,维护复杂 | 可维持当前热路径 | 平台分支继续散落 | 只用于 P0 止血 | +| B. xlings 全知 daemon | 所有解析/探测集中 | daemon 生命周期与状态成为新故障域 | 强耦合 xlings 版本 | 接口表面少,系统更重 | 构建热路径多进程/IPC | Windows/macOS 服务语义复杂 | 不采用 | +| C. 分层 typed contracts | 每个事实单一 owner,边界清楚 | 可校验、可缓存、可回滚 | 支持 schema/legacy ingress | 数据模型略增,重复逻辑显著减少 | 构建只读本地契约 | 原生 provider 表达差异 | **推荐** | + +方案 C 可以允许 P0 局部修复先落地,但所有 P0 修复都应朝目标契约收敛,不能制造第二套临时协议。 + +## 5. 目标架构 + +~~~mermaid +flowchart LR + XI[xlings
install / SubOS / RuntimeBinding] --> RC[Runtime & Install Contract] + XP[xim-pkgindex
system payload / host sentinel] --> XI + MP[mcpp-index
C++ package / template / LinkPlan] --> MC[mcpp
planner / builder / inspector] + RC --> MC + MC --> OM[BuildSnapshot / OutputManifest] + MC --> ELF[Artifact Physics Verdict] + GH[Immutable GitHub Release Manifest] --> REC[Channel Reconciler] + REC --> AUR[AUR mcpp-bin / mcpp-m] + REC --> OTHER[other package channels] +~~~ + +### 5.1 单一事实所有者 + +| 事实 | 唯一 owner | 消费者 | +|---|---|---| +| 包 canonical identity、版本与安装 payload | xlings/libxpkg | mcpp、索引工具、模板 | +| runtime binding、loader/libc、exports、env op | xlings + xim package descriptor | mcpp | +| C/C++ source/module/link/template metadata | mcpp-index | mcpp | +| 项目 build graph 与 required outputs | mcpp | IDE、CI、用户 | +| 实际 ELF 闭包与兼容 verdict | mcpp post-link inspector | 用户、CI、缓存 | +| GitHub release 资产与 checksum | Release Manifest | AUR/Homebrew/其他渠道 | +| AUR 当前版本 | AUR | reconciler,仅作为 observed state | + +### 5.2 不可违反的不变量 + +1. **Identity**:内部永远用结构体,不用拼接字符串承担身份。 +2. **Success**:返回成功前,声明的 required outputs 必须存在并通过验证。 +3. **Runtime**:loader、libc 与解析到的共享库必须能追溯到 provider。 +4. **No hot-path orchestration**:构建热路径不启动 xlings、不访问索引网络。 +5. **Atomicity**:cache snapshot、模板目录、投影元数据和渠道更新必须原子提交或明确部分失败。 +6. **Fail closed on ambiguity**:同名候选、多 payload、未知 cfg/placeholder 不按目录顺序或短名猜。 +7. **Native evidence**:Windows/macOS/aarch64 行为只由对应 runner 证明。 + +## 6. 核心数据契约 + +### 6.1 Identity types + +建议共享概念定义,语言实现可独立: + +~~~text +NameAtom = one validated identifier atom +NamespacePath = one or more NameAtom segments +PackageId = { namespace: NamespacePath, name: NameAtom } +PackageRef = { id: PackageId, versionReq?: VersionReq } +ResolvedPackageId= { id: PackageId, version, indexRoute, descriptorDigest, payloadDigest } +ProjectIdentity = { namespace: NamespacePath, name: NameAtom, version } +TemplateSelection= { provider: PackageRef, variant?: NameAtom } +~~~ + +规则: + +- wire/display 规范形式是 namespace:name。 +- bare name 只能在候选唯一时作为便捷输入;歧义必须列出候选并失败。 +- legacy FQN 只在 ingress 解析一次;之后不再拆字符串。 +- map/cache key 使用完整 PackageId。 +- qualified display 由类型派生,不在各层自行加前缀。 +- index route 和 transport provenance 随 ResolvedPackageId 保留用于诊断/lock,但不混进 PackageId 本身。 + +### 6.2 xlings Runtime & Install Contract v2 + +xlings 在安装/物化时写入 versioned、只含相对路径的契约。可以演进现有安装描述文件,也可以使用专用文件;名称不是本文的关键决策,schema 是。 + +建议字段: + +~~~json +{ + "schema": 2, + "package": {"namespace": "xim", "name": "glibc", "version": "2.44"}, + "payload": { + "id": "content-addressed-id", + "digest": "sha256:...", + "root": "." + }, + "runtimeBinding": { + "id": "linux-glibc-2.44-x86_64", + "platform": "linux", + "arch": "x86_64", + "loader": "lib/ld-linux-x86-64.so.2", + "libc": "lib/libc.so.6", + "glibcProvidedCeiling": "2.44" + }, + "exports": { + "includeDirs": [], + "libraryDirs": ["lib"], + "runtimeDirs": ["lib"], + "crtObjects": [] + }, + "runtimeDependencies": [ + {"namespace": "xim", "name": "ncurses", "version": "..."} + ], + "environment": [ + {"name": "PATH", "op": "prepend", "value": "bin"} + ], + "capabilities": ["runtime.opengl", "runtime.egl"], + "provenance": "ecosystem", + "hostObservation": { + "glibc": "2.43", + "fingerprint": "optional-host-fingerprint" + } +} +~~~ + +约束: + +- 所有路径相对 payload root;移动安装根不使契约失效。 +- 契约文件有 content hash;mcpp cache key 使用 hash,不使用 mtime 猜测。 +- SubOS 只引用 active provider contract id,不复制或重新解释 payload 目录。 +- provenance 是封闭枚举:ecosystemsystem-sdkhost-link。 +- 复用 xlings 现有 host_glibc 观测并带入诊断/host-link fingerprint;mcpp 不再忽略该字段,但它不能替代逐 DSO 的 symbol requirement 检查。 +- 旧 schema 可读时告警并转成内存 v2;多 payload/歧义时 fail closed,不再按字典序挑目录。 + +### 6.3 EnvOp schema v2 + +废弃歧义的单一 set: + +事实边界:xlings 自 2026.8.8.2 起,POSIX/fish/PowerShell/进程内路径已经统一为“变量存在即不覆盖”,包括显式空字符串;剩余缺陷是 mcpp reader 仍把 Set 解释成 replace。schema v2 是为了把这一区别永久写入协议,而不是声称 xlings writer 仍未修。 + +| op | 精确定义 | +|---|---| +| default | 变量不存在时设置;变量存在但为空也视为已存在 | +| prepend | 按平台路径分隔符前置,去重并保序 | +| replace | 无条件覆盖,必须显式使用 | + +兼容规则: + +- schema 1 的 set 按 xlings 历史行为映射为 default。 +- xlings writer 与 mcpp reader 同一发布窗口支持 v2。 +- POSIX shell、fish、PowerShell、xlings 进程内应用和 mcpp 读取共享同一组 golden fixtures。 + +### 6.4 BuildSnapshot 与 OutputManifest + +一次配置成功写入原子、版本化 snapshot: + +~~~text +BuildSnapshot + schema + inputFingerprint + graphFile + sourceSetDigest + toolchainContractHash + runtimeContractHashes[] + outputManifest + postLinkVerdicts[] + +OutputManifest + ninjaOwnedArtifacts[] + metadataProjections[] + generatedSources[] + requiredPostconditions[] +~~~ + +fast path 决策: + +- Ninja-owned artifact 缺失:交给 Ninja 依据图重建。 +- compile_commands.json 等 metadata projection 缺失:从 output-dir 中 fingerprint 对应的 canonical copy 原子重投影,不必完整 prepare。 +- graph/snapshot/runtime contract 缺失或 schema 不支持:完整 prepare。 +- 任一 required postcondition 缺失:不得输出 “Finished”。 +- compile_commands.json 加入新项目默认 ignore;已有项目即使未 ignore 也应正确修复。 + +### 6.5 UiPolicy 与 WireEnvelope + +进程启动时仅初始化一次: + +~~~text +UiPolicy { + color: Auto | Always | Never, + quiet: bool, + output: Human | Wire +} +~~~ + +- --no-color 直接得到 Never,后续 TTY 探测不能覆盖。 +- human/status/progress 写 stderr。 +- machine stdout 只允许 versioned envelope;无彩色、无 spinner、无说明文字。 +- pseudo-TTY、Windows console、管道和重定向分别测试。 + +### 6.6 CommandSpec + +所有子进程用结构化 argv,不拼 shell: + +~~~text +CommandSpec { + executable, + argv[], + cwd, + envDelta, + stdinPolicy, + stdoutPolicy, + stderrPolicy +} +~~~ + +2>/dev/null、引号、重定向不再进入参数字符串。POSIX 与 Windows 使用各自 native spawn backend,共享上层语义测试。 + +## 7. 详细设计 + +### 7.1 xlings 候选发现与升级 + +当前 release-bundled xlings 被排除在候选更新链之外。改为显式候选: + +1. CLI/config 显式覆盖。 +2. mcpp release-bundled sibling。 +3. distro/AUR 提供的环境候选。 +4. mcpp writable sandbox 已安装候选。 +5. system PATH。 + +每个候选携带: + +~~~text +XlingsCandidate { + path, + version, + provenance, + writable, + contractCapabilities[], + digest? +} +~~~ + +选择原则: + +- 先满足 required contract capability/schema。 +- 默认不降级。 +- 在可信候选中选择最高兼容版本,而不是只按目录位置。 +- 需要复制到 sandbox 时原子替换并验证 digest/version。 +- mcpp self env 显示候选、被选原因、版本和 provenance。 + +release pin 同时表达两件事: + +- 本次 release 原生 CI 验证过的 exact bundled version。 +- 运行时允许的 minimum contract capability/schema。 + +这样可接受未来兼容版本,又不丢失发布可复现性。 + +### 7.2 构建完整性、C1 和 generated source + +将构建分成三个明确阶段: + +1. **Plan**:解析 identity、source set、toolchain/runtime contract,生成 graph 与 snapshot。 +2. **Execute**:Ninja/runner 生产 artifacts 和 generated sources。 +3. **Verify/Project**:检查 OutputManifest、运行 post-link inspector、原子投影 CDB/机器元数据。 + +generated action 必须声明 outputs;其输出并入下一轮 SourceSet 或明确标为最终产物。没有声明或声明后缺失均失败,不能静默排除。 + +默认 source glob 只由一个 SourceSet producer 展开;清单、scanner、Ninja graph 不再各自 glob。 + +性能要求: + +- snapshot 校验按 digest/stat 快速路径完成。 +- CDB 重投影是文件复制/rename,不重新求解全部依赖。 +- no-change build 目标维持 < 0.5 s,或相对当前基线不回退超过 10%;两者取更严格、但先在 CI 固定硬件建立基线。 + +### 7.3 C++ scanner + +用 O(bytes) streaming lexical masker 代替启发式 strip,状态至少包括: + +- Normal +- line comment +- block comment +- string / char / escape +- raw string delimiter / raw body +- line splice 与 CRLF 处理 + +masker 保留换行与列宽,将注释/字符串内容替换为空白;module/import parser 只读取 masked code。注释只可在 Normal 状态开始。 + +回归 corpus 必含: + +- 行注释和块注释中的 R"(。 +- 普通字符串中的 /*//。 +- raw string 中的假 import。 +- char literal、encoding prefix、自定义 raw delimiter。 +- CRLF、行拼接、文件末尾未闭合状态。 + +scanner 保持热路径无编译器子进程;若未来接 P1689,编译器扫描作为权威慢路径/不确定输入 fallback,不与 masker 产生第三套模块身份。 + +### 7.4 exports 与 namespace + +SourceUnit.owner、manifest owner、dependency owner 全部改为 PackageId。校验使用结构体相等;诊断需要字符串时调用一个 formatter。 + +这个改动同时解决: + +- C4 namespaced exports 被跳过。 +- namespace 被重复前缀化。 +- 模板依赖注入回退短名。 +- cache/graph 在同短名包之间碰撞。 + +### 7.5 脚手架与模板 + +#### 7.5.1 CLI + +推荐稳定表面: + +~~~text +mcpp new + [--namespace ] + [--dir ] + [--template ] + [--variant ] +~~~ + +交互输出必须同时展示 provider 的 qualified identity 和 variant;--list-templates 的 wire 模式返回结构化数组。 + +#### 7.5.2 renderer + +RenderVars 至少包含: + +~~~text +project.name +project.namespace +project.qualifiedName +project.version +template.provider.namespace +template.provider.name +template.provider.qualifiedName +template.variant +~~~ + +实现要求: + +- parse template 成 token stream,一次替换,不扫描插入值。 +- 未知 token hard error;需要 literal token 使用明确 escape。 +- 文件路径渲染也走同一验证器,禁止绝对路径、.. 和目录逃逸。 +- 依赖注入操作 TOML AST,不用字符串查找;写出完整 namespace 分组。 +- 模板 descriptor 声明兼容的 mcpp contract/version、必需 capability 与平台。 + +#### 7.5.3 transaction + +流程: + +~~~text +parse and validate input + -> resolve exact template provider + -> materialize to sibling temp dir + -> render once + -> parse generated mcpp.toml + -> validate required files and no path escape + -> optional offline configure smoke + -> atomic rename to destination +~~~ + +任何错误返回非零并删除 temp;诊断给出字段、非法值和允许形式,不留下半项目。 + +#### 7.5.4 compatibility + +- 旧 pkg@ver:variant 支持两个 release train,仅限能唯一解析的无 namespace provider。 +- 第一个 release train warning;第二个可通过 compatibility flag 使用;之后删除。 +- 模板索引新增 canonical identity,不原地改变旧模板含义。 + +### 7.6 Artifact Physics Inspector + +当前 pre-link driver -### 检查只看到“编译器计划”,不能证明最终 ELF。新增 post-link inspector: + +1. 读取实际 PT_INTERPDT_NEEDED、RPATH/RUNPATH。 +2. 根据 artifact 路径、Runtime Contract 和平台规则递归解析动态闭包。 +3. 给每个对象标注 payload/provider/provenance。 +4. 从 .gnu.version_r 计算每个 host-borrowed object 的 GLIBC requirements。 +5. 验证 Rule B 与 Rule A。 +6. 生成 versioned verdict,按 artifact digest + Runtime Contract hash 缓存。 + +可复用 vendored patchelf 获取 interp/rpath/needed;GLIBC version requirement 建议实现最小只读 ELF parser,避免假设用户安装 readelf,也避免仅用系统 glibc 版本近似。 + +诊断示例信息应包含: + +~~~text +artifact +selected loader and provider +selected libc and provider +offending object +required GLIBC symbol ceiling +provided runtime ceiling +resolution path and remediation candidates +~~~ + +build fast path 必须把 verdict 当作 required output;artifact 或 Runtime Contract hash 变化后失效。 + +allow_host_libs 可以调整 Policy D 的告警/允许范围,但不能关闭物理 A/B:配置项不能让不可装载的 ELF 变得可运行。 + +上线分四步: + +1. **observe**:只记录闭包与 provider,对照 linker trace/map、readelf/patchelf fixtures。 +2. **warning**:对 proven mismatch 报高质量诊断,但保留显式 strict opt-in。 +3. **strict opt-in**:在 mcpp/xim-index native matrix 中积累低误报数据。 +4. **default strict**:只对精确解析得到的 mismatch/unresolved required object hard fail;解析器自身无法证明的情况返回“inconclusive”,不得假装兼容,也不得用 our_glibc ≥ host_glibc 代理误拒绝。 + +### 7.7 GUI / graphics 分层 + +当前 [runtime] 的 soname、capability、provider 和目录大多是扁平字符串:provider 只记 short name,library_dirs 又同时影响 -L 与 RPATH,capability 还可能同时表示 requires 与 provides。目标模型先拆开“需求”和“已解析实物”: + +~~~text +RuntimeRequirement { + kind: soname | capability | icd_manifest | display | host_service, + value, + phase: link | run, + required, + target, + requester: PackageId +} + +RuntimeArtifact { + role: loader | library | driver | manifest | host_bridge, + relativePath, + provider: ResolvedPackageId, + provenance: payload | subos_view | host_link | system_sdk, + abi, + requiredGlibcCeiling?, + digest?, + hostFingerprint? +} +~~~ + +链接和运行目录也必须分义: + +~~~text +link.libraryDirs +link.transitiveNeededDirs +runtime.rpathDirs +runtime.dlopenDirs +runtime.environment +deploy.files +~~~ + +runtime 目录不得隐式进入 -L。ELF lowering 分别生成 link search、-rpath-link 和 RPATH/RUNPATH;Mach-O 使用 @rpath/install name;PE 使用 app-local DLL 与 system DLL contract。resolution.json 输出 requirement → canonical provider → artifact → search mechanism → provenance → ABI verdict 的完整链。 + +#### 7.7.1 层次 + +| 层 | 职责 | 禁止事项 | +|---|---|---| +| xlings | payload、SubOS view、RuntimeBinding、runtime exports、host sentinel 激活 | 不理解 ImGui/GLFW 项目语义 | +| xim-pkgindex | Mesa/系统运行时闭包、NVIDIA/WSL host-link sentinel、平台系统包 | 不生成 mcpp 工程 | +| mcpp-index | ImGui/GLFW 等 C++ 包、LinkPlan、模板、平台依赖选择 | 不复制/symlink xlings 内部 view 作为长期 ABI | +| mcpp | 根据 capability 规划、链接、artifact physics、why 诊断 | 不探测 GPU 型号或选择驱动 | + +#### 7.7.2 平台策略 + +- **Linux**:xim:graphics 作为图形运行时入口;Mesa 默认闭包;NVIDIA/WSL 通过 sentinel 表达显式 host-link。 +- **macOS**:使用系统 SDK/framework capability;不伪装成 Linux Mesa 布局。 +- **Windows**:使用 Win32/system SDK 与明确 DLL runtime contract;命令执行和路径由 native backend。 + +模板声明平台依赖,不在源码中散落“如果 Linux 就手工找 libGL”。 + +ImGui 包本身也应按 feature 解耦: + +- core/headless 不拉窗口系统。 +- backend-glfw-opengl3 显式引入 GLFW、OpenGL headers 与 runtime。 +- backend-vulkan 显式引入 Vulkan loader + ICD requirement。 +- app 可组合默认 backend;docking/viewports 是正交 feature。 + +当前 ocornut:imgui 的 app facade/后端依赖仍容易让只用 core 的项目拉入 graphics 栈,这一拆分应由 mcpp-index 完成,不放进 xlings。 + +#### 7.7.3 过渡 + +compat.glx-runtime 的 symlink bridge 暂时保留: + +1. xlings contract v2 能表达 resolved transitive runtime exports。 +2. mcpp 能直接消费并生成正确 LinkPlan/runtime dirs。 +3. mcpp-index native tests 证明不再需要桥。 + +然后在一个有 warning 的 release train 中弃用,避免双份 runtime dir 漂移。 + +#### 7.7.4 测试矩阵 + +| 层级 | Linux | macOS | Windows | +|---|---|---|---| +| 常规 PR | headless llvmpipe configure/build/run;artifact A/B | native framework build + minimal smoke | Win32 backend build + minimal smoke | +| 原生/计划任务 | X11/Wayland;AMD/Intel/NVIDIA;WSL2 | arm64 + x86_64 native window | x64/arm64 native window | +| side-effect | 安装 graphics 前后无关 ELF 的 interpreter 与 GLIBC ceiling 不变 | SDK selection 不污染全局 env | DLL/path contract 不污染宿主 shell | + +Vulkan native gate 至少创建 instance、枚举 physical device,并验证实际加载的 ICD manifest/driver provenance;只测试 loader symbol 不算通过。真实 GPU 行为只能由对应 native runner 证明;Linux headless 通过不外推成所有硬件通过。当前 xim:graphics recipe 明确仅支持 Linux x86_64,所以 aarch64 在支持落地前应返回清晰的 capability unavailable,而不是把 cross-build 或 skipped runtime test 汇总成绿色。 + +### 7.8 安装 owner 与错误传播 + +删除 C8 的两段 versionless 预安装循环。唯一流程: + +~~~text +parse PackageRef + -> solve exact graph + -> install/materialize exact nodes through xlings + -> verify install contracts + -> expose graph to planner +~~~ + +- 任何节点失败立即带完整 PackageId/version/provenance 返回。 +- 不允许 catch (...) {} 或忽略 error_code 后继续。 +- 离线模式只使用已验证 materialization;缺失时明确报错。 + +### 7.9 Release Manifest 与 AUR reconciler + +#### 7.9.1 Release Manifest + +release 成功后发布不可变 manifest: + +~~~json +{ + "schema": 1, + "version": "2026.8.8.4", + "tag": "v2026.8.8.4", + "commit": "...", + "assets": [ + {"name": "...", "sha256": "...", "platform": "...", "arch": "..."} + ], + "bundled": {"xlings": "2026.8.8.1"} +} +~~~ + +AUR 生成器只消费 manifest,不重新从日志/文件名猜版本和资产。 + +mcpp-m 应消费 release 自己发布且带 sidecar 的版本化 source asset,不再依赖可能重生成的 tag archive;生成器下载实物重算 hash,并与 manifest/sidecar 交叉验证。 + +#### 7.9.2 先修 P0 生成器 + +- 不再用行级 sed 更新 Bash array;使用小型、确定性生成器从 model 完整生成 PKGBUILD 与 .SRCINFO。 +- 在推送前下载/校验 source archive 与二进制资产。 +- 比较 PKGBUILD 解析结果与 .SRCINFO:version、source URL、每个 checksum 必须一致。 +- 在 Arch container 中运行 makepkg --printsrcinfo 并与提交文件 diff。 +- 在 Arch container 中运行 makepkg --verifysource;PKGBUILD 是源,.SRCINFO 只由 makepkg 生成。 +- 分别构建/安装 mcpp-binmcpp-m 的最小 smoke。 +- 所有 package 预检全部通过后才允许任何 push,避免 mcpp-bin 先成功、mcpp-m 后失败。 +- dry-run 默认不加载 SSH secret、不 push,只输出 desired/current、生成文件、diff 和验证报告。 + +#### 7.9.3 对账工作流 + +触发: + +- release workflow 成功事件:低延迟路径。 +- schedule:建议每 6 小时。 +- workflow_dispatch:恢复/指定版本,但默认仍以 latest stable manifest 为准。 + +算法: + +~~~text +read latest immutable Release Manifest + -> query AUR RPC and git heads for every managed package + -> compare desired/current with Arch vercmp; reject implicit downgrade + -> generate all expected repositories in temp dirs + -> validate all package postconditions + -> for each drifting package, push idempotently with bounded retry/backoff + -> query AUR RPC/git again + -> succeed only if observed state matches expected state +~~~ + +行为: + +- AUR maintenance、连接超时等 retryable failure 使用指数退避与抖动。 +- auth、invalid metadata、checksum mismatch 属于 permanent failure,立即失败并告警。 +- 每个 package 的 push 可独立重试,但总体状态明确显示 partial convergence。 +- GitHub Release 是 desired state,AUR 是 observed projection;无需再维护一份可漂移的 mutable ledger。 +- 错过中间 release 时允许直接收敛 latest stable,不要求重放全部历史版本。 +- workflow_run 只负责唤醒;每次都重新读取最新完整、非 draft、非 prerelease manifest,迟到事件不得降级 AUR。 +- 已知 AUR package clone 失败时不得自动当作“首次发布”初始化空仓;首次认领必须是独立、显式流程。 +- 读取可走 HTTPS,SSH 只用于 push;固定官方 host key,不在发布时盲信动态 ssh-keyscan。 +- 使用专用发布身份/Ed25519 key;普通 PR 与不受信任代码永远拿不到 secret。 +- AUR git head 是立即验证源,RPC 允许 bounded poll;RPC 延迟不得触发重复提交。 +- 使用普通 fast-forward push,禁止 force-push/历史改写。 + +建议 SLO(待 review): + +- AUR 可用时,release 后 30 分钟内收敛。 +- 事件失败后,scheduled backstop 最迟 6 小时再次尝试。 +- 连续 24 小时未收敛触发高优先级告警。 + +mcpp-git 是否发布是独立产品决策;若保留,必须加入 managed package matrix 和相同 postcondition,不能只在仓库里放模板。 + +## 8. 兼容与迁移策略 + +### 8.1 双读单写 + +- xlings/mcpp 在迁移窗内读取 schema 1 和 2,只写 schema 2。 +- build cache schema 改变视为 cache miss,不尝试就地猜测迁移。 +- 唯一可转换的 legacy identity 在 ingress 转换;歧义立即失败。 +- 旧模板语法有明确两个 release train 的弃用窗口。 + +### 8.2 Fail-open 与 fail-closed 边界 + +| 情况 | 行为 | +|---|---| +| 缺少可再生成的 CDB projection | 本地重投影 | +| 旧但唯一可转换的 contract | 转换 + warning | +| 多 payload、同短名多候选 | fail closed | +| Rule A/B 已证明的物理冲突 | 经过 observe/warning 迁移后 hard fail;解析不确定单独报告 inconclusive | +| Policy D 仅在本地用户项目违反且 A/B 安全 | 默认清晰 warning;索引 CI hard fail | +| 未知 cfg、placeholder、output | hard fail | +| AUR 外部维护 | retry + scheduled reconcile,不回滚 GitHub release | + +### 8.3 可回滚性 + +- contract/snapshot 都有 schema 与原子文件;回滚二进制时旧 reader 忽略不支持的新文件并重新 materialize。 +- symlink bridge 只在新 runtime export 经过至少一个 release train 后移除。 +- 渠道 reconciler 生成提交前保存 expected diff;不 force-push AUR 历史。 +- 不通过修改全局 host env 回滚 runtime;切换 active RuntimeBinding。 + +## 9. 分阶段交付 + +### Phase 0 — 止血,不等待完整架构 + +1. mcpp:C1 输出完整性检查/CDB repair;C2 masker;C3 color;C4 PackageId 比较;C5 CommandSpec;删除 C8 循环。 +2. scaffold:#380 NameAtom、路径约束、单次渲染、临时目录事务。 +3. AUR:修复 mcpp-m checksum 生成;增加 PKGBUILD/.SRCINFO 一致性 gate;全部预检后再 push;再人工恢复一次对账。 +4. release:先完成 C5/C6 的候选/版本探测闭环,再将 bundled xlings 升到已验证的 2026.8.9.x,并跑冷 HOME 原生 gate;不能只改 pin。 + +每项独立小 PR,避免 P0 被 contract v2 设计阻塞。 + +### Phase 1 — Identity 与本地契约 + +1. PackageId/PackageRef/ProjectIdentity 全链路。 +2. 新 --template PackageRef --variant 表面和 legacy ingress。 +3. BuildSnapshot/OutputManifest。 +4. EnvOp schema v2 golden fixtures。 +5. xlings Runtime & Install Contract v2 writer;mcpp 双 reader。 +6. XlingsCandidate capability/provenance 选择。 + +### Phase 2 — Runtime physics 与 graphics + +1. post-link ELF parser/verdict 与 mcpp why runtime。 +2. xim-pkgindex Policy D CI。 +3. mcpp 读取 transitive runtime exports。 +4. Linux llvmpipe/native 图形矩阵。 +5. 弃用 compat.glx-runtime symlink bridge。 + +### Phase 3 — 脚手架 UX 与原生 GUI vertical slice + +1. TOML AST 依赖注入与模板 contract。 +2. ImGui/GLFW canonical template。 +3. Linux、macOS、Windows 各自一条从 mcpp new 到真实运行的冷 HOME vertical slice。 +4. 机器输出与 IDE 消费统一。 + +### Phase 4 — 发布渠道最终一致性 + +1. Release Manifest。 +2. AUR event + schedule reconciler、retry 分类、推后验证。 +3. 将 Homebrew/其他渠道逐步迁到同一 manifest 模型。 +4. 发布 dashboard 展示 GitHub release、索引、AUR 和 bundled xlings 的期望/实际版本。 + +## 10. 跨仓库 PR 切分建议 + +| 顺序 | 仓库 | 小 PR 主题 | 依赖 | +|---|---|---|---| +| 0A | mcpp | AUR generator/checksum consistency + dry-run validation | 无 | +| 0B | mcpp | C1/C3/C5 快速修复与回归测试 | 无 | +| 0C | mcpp | scanner masker(C2) | 无 | +| 0D | mcpp | scaffold transaction(#380) | 无 | +| 1A | xlings | install/runtime contract v2 + EnvOp v2 writer | 设计字段冻结 | +| 1B | mcpp | contract v1/v2 reader + candidate selection | 1A fixtures | +| 1C | mcpp/libxpkg consumer | PackageId 全链路与模板 CLI | identity spec | +| 1D | mcpp | BuildSnapshot/OutputManifest | 0B | +| 2A | mcpp | ELF actual-artifact inspector | 1B | +| 2B | xim-pkgindex | Policy D closure CI + sentinel schema | 1A | +| 2C | mcpp-index | graphics runtime exports 消费,移除桥的准备 | 1A/1B/2B | +| 3A | 三平台仓库矩阵 | GUI cold-home vertical slices | 1C/2A/2C | +| 4A | mcpp release | Release Manifest + AUR reconciler | 0A | + +协调者负责 schema fixtures、跨仓集成顺序和最终 native gate;各 PR 保持可独立回滚,不改写历史。 + +## 11. 验收标准 + +### 11.1 功能与回归 + +- #380 的 hang、路径逃逸、控制字符、部分目录均有回归测试。 +- 同短名不同 namespace 的 package/template 可同时存在,bare ambiguity 明确失败。 +- 删除 artifact、CDB、graph、verdict 任一项后,下一次 build 能正确重建/重投影或完整 prepare。 +- C2 最小 corpus 及现有真实项目 corpus 零 pass-to-fail。 +- --no-color 在 TTY、pipe、Windows console 均无 ANSI。 +- EnvOp golden fixtures 在 xlings 与 mcpp 结果字节一致。 +- A/B 测试含:同 binding 成功、loader/libc 混源失败、host object ceiling 高于 runtime 失败、低于等于成功。 +- 安装 graphics 不改变无关 ELF 的 interpreter/provider/GLIBC ceiling。 + +### 11.2 用户路径 + +每个平台都从隔离 HOME/MCPP_HOME/XLINGS_HOME 开始: + +~~~text +install released mcpp + -> verify selected bundled/upgraded xlings + -> mcpp new namespaced GUI project + -> resolve/install dependencies + -> configure/build + -> run platform-appropriate smoke + -> delete CDB/artifact and verify repair + -> inspect mcpp why runtime / wire output +~~~ + +不得用已缓存开发机状态替代。 + +### 11.3 性能 + +- scanner O(bytes),以大 translation-unit corpus 防止超线性回退。 +- no-change build 不启动 xlings、不访问网络。 +- artifact physics 仅在新 artifact 或 contract hash 变化时运行。 +- 固定硬件建立 median/p95 基线后,hot no-op build 回退不超过 max(5%, 10 ms);缺 CDB 只允许一次轻量 reconfigure/project。 +- 增量链接的 artifact inspector 额外成本目标不超过 max(5%, 50 ms/产物);hot no-op parse 次数必须为 0。 +- contract 解析与 snapshot 校验有单独 benchmark,避免 JSON/磁盘布局成为热路径瓶颈。 + +### 11.4 多平台与发布 + +- Linux x86_64/aarch64、macOS arm64/x86_64、Windows x64 至少有原生 cold-home gate。 +- 不从 Linux source review 推断 Windows spawn/console 正确。 +- release 完成后验证 remote tag/commit、assets、checksums、bundled xlings、索引版本。 +- AUR push 后同时验证 AUR git head 与 RPC version;只看到 workflow green 不算渠道收敛。 + +## 12. 可观测性与用户体验 + +新增统一解释命令,优先扩展现有 mcpp self/mcpp why,不创建大量顶层命令: + +~~~text +mcpp self env + selected xlings, all candidates, provenance, contract schema + +mcpp why package namespace:name + normalized PackageRef, selected version, source index, dependency path + +mcpp why runtime + loader/libc provider, dynamic closure, host-link leaves, A/B verdict + +mcpp build --output wire + versioned event/result envelope only +~~~ + +human 模式先给解决动作,再给细节。例如 loader/libc 混源时直接指出是哪个对象、来自哪里、需要哪个 runtime contract,而不是只输出 “GLIBC not found”。 + +## 13. 风险与缓解 + +| 风险 | 缓解 | +|---|---| +| schema v2 同时改 xlings/mcpp,发布错位 | 双读单写、共享 fixtures、capability negotiation | +| typed identity 改动面大 | 先在 ingress/graph 边界引入,禁止新增裸字符串 key | +| ELF parser 容易遗漏格式 | 最小只读范围、fixture 与 readelf/patchelf 对照、fuzz | +| Policy D 过严伤害用户自定义宿主库 | 只在 index CI hard fail;用户项目区分 A/B 与 D | +| GUI native CI 不稳定 | headless PR gate + 有标签的 native scheduled gate,分别报告 | +| AUR 部分发布 | 全包预检、独立状态、post-push reconcile;不 force-push | +| contract 使 fast path 变慢 | content hash 缓存、只读本地文件、按 artifact digest 复用 verdict | + +## 14. Review 需要确认的决策 + +1. 是否接受 **方案 C:分层 typed contracts**,并明确拒绝 xlings daemon 化? +2. 是否接受模板 CLI 将 provider 与 variant 分离:--template namespace:name@version --variant name?是否还需要后续提供 #variant 紧凑糖? +3. 是否接受项目逻辑 namespace、项目名和输出目录分别由 --namespace、位置参数、--dir 表达? +4. 是否接受 Rule A 按实际 host object 的 GLIBC symbol requirement 计算,而不是比较两端 glibc 发行版本? +5. 是否接受 A/B 在用户构建 hard fail、Policy D 只在索引 CI hard fail的分层? +6. 是否接受 mcpp 不做 GPU 探测,GPU/宿主驱动选择只由 xim sentinel/provider 负责? +7. 是否接受 Runtime & Install Contract v2 使用相对路径、content hash、双读单写迁移? +8. 是否接受 AUR 先修 generator consistency,再启用 6 小时 scheduled reconcile;30 分钟/6 小时/24 小时作为初始 SLO? +9. mcpp-git 是正式维护的第三个 AUR package,还是从自动发布范围明确移除? +10. Phase 0 的顺序是否同意:AUR 元数据安全、C1–C5/C8、#380 事务、bundled xlings 升级并行止血? + +## 15. 证据锚点与验证边界 + +下列行号对应本文基线,后续代码移动时以符号为准: + +| 主题 | 当前实现证据 | +|---|---| +| C1 fast path / CDB | src/build/execute.cppm:705-774src/build/ninja_backend.cppm:1547-1549src/build/compile_commands.cppm:282-307 | +| C2 scanner | src/modgraph/scanner.cppm:137-189,579-590 | +| C3 color | src/cli.cppm:97-108src/ui.cppm:233-240,266-331 | +| C4 exports | src/modgraph/scanner.cppm:786-792src/modgraph/validate.cppm:90-118 | +| C5 process | src/fallback/xlings_binary.cppm:156-183src/platform/process.cppm:191-226,335-350 | +| C7 EnvOp | src/xlings/subos_info.cppm:250-300tests/unit/test_subos_info.cpp:290-310 | +| C8 dead loops | src/toolchain/lifecycle.cppm:551-565src/build/prepare.cppm:1664-1670src/pm/package_fetcher.cppm:923-936 | +| #380 | src/scaffold/create.cppm:183-298src/scaffold/template.cppm:134-182 | +| template identity | src/scaffold/template.cppm:22-47,125-228src/scaffold/create.cppm:30-141 | +| #392 selection | src/toolchain/post_install.cppm:378-388src/xlings.cppm:35-48 | +| #396 pre-link only | src/build/hermetic.cppm:103-211src/build/ninja_backend.cppm:1575-1582 | +| runtime flattening | src/manifest/types.cppm:455-467src/build/plan.cppm:631-712src/build/flags.cppm:709-724,860-885 | +| AUR trigger/push | .github/workflows/aur-publish.yml:14-102 | +| AUR checksum defect | scripts/aur/update.sh:61-66scripts/aur/mcpp-m/PKGBUILD:32-35 | + +本次只读交叉审计没有执行会安装 graphics 或创建工程的命令,也没有在 WSL2、AMD/Intel/NVIDIA、macOS MoltenVK、Windows Vulkan 或 Linux aarch64 graphics 上实跑。mcpp 基线已有的 7 个被触发 Actions workflow 全绿,只能证明现有覆盖集;它们没有针对性覆盖上述大部分缺陷,native aarch64 在该提交也未触发。因此本文不建议仅凭现有 CI 关闭 #397/#396/#392/#380。 + +## 16. 相关既有设计 + +本文是跨问题的收敛层,不替代以下文档的细节: + +- .agents/docs/2026-08-07-xlings-as-runtime-substrate-design.md +- .agents/docs/2026-08-08-payload-version-and-contract-drift-design.md +- .agents/docs/2026-08-08-machine-readable-output-protocol-design.md +- .agents/docs/2026-08-05-build-mcpp-extensibility-architecture.md +- xlings 的 .agents/docs/2026-08-09-ecosystem-closure-design.md + +如果本文获批,下一步不是直接开启一个大实现,而是把第 14 节决策写成冻结的 ADR/schema fixtures,再按第 10 节拆分小 PR。 From 527b26e71301e0a19dd341907085fcf9f31b4cfe Mon Sep 17 00:00:00 2001 From: speak-agent Date: Sun, 9 Aug 2026 09:23:39 +0800 Subject: [PATCH 02/44] feat: make package selectors exact --- ...untime-graphics-aur-implementation-plan.md | 38 ++- ...emplate-runtime-graphics-aur-validation.md | 20 +- docs/05-mcpp-toml.md | 66 ++-- docs/06-workspace.md | 2 +- docs/spec/package-identity.md | 92 +++--- docs/zh/05-mcpp-toml.md | 47 +-- docs/zh/06-workspace.md | 2 +- mcpp.toml | 3 +- src/build/prepare.cppm | 114 +++++-- src/cli.cppm | 12 +- src/manifest/toml.cppm | 127 ++++---- src/manifest/xpkg.cppm | 76 +++-- src/pm/commands.cppm | 291 ++++++++++++------ src/pm/dep_spec.cppm | 21 +- src/pm/dependency_selector.cppm | 146 ++++++--- src/pm/index_route.cppm | 6 +- tests/e2e/12_add_command.sh | 167 ++++++++-- tests/e2e/162_bare_name_namespace_scope.sh | 28 +- ..._bare_name_cross_namespace_wire_address.sh | 24 +- .../e2e/203_exact_selector_lock_migration.sh | 109 +++++++ .../62_dotted_dependency_selector_priority.sh | 9 +- .../63_bare_dependency_peer_root_priority.sh | 12 +- tests/e2e/78_test_main_combinations.sh | 2 +- .../e2e/79_gtest_regular_dep_feature_main.sh | 12 +- tests/unit/test_manifest.cpp | 139 ++++++--- tests/unit/test_pm_compat.cpp | 95 +++++- tests/unit/test_pm_index_route.cpp | 10 +- tests/unit/test_pm_package_fetcher.cpp | 13 +- 28 files changed, 1148 insertions(+), 535 deletions(-) create mode 100755 tests/e2e/203_exact_selector_lock_migration.sh diff --git a/.agents/docs/2026-08-09-mcpp-template-runtime-graphics-aur-implementation-plan.md b/.agents/docs/2026-08-09-mcpp-template-runtime-graphics-aur-implementation-plan.md index 49a6509c..57840594 100644 --- a/.agents/docs/2026-08-09-mcpp-template-runtime-graphics-aur-implementation-plan.md +++ b/.agents/docs/2026-08-09-mcpp-template-runtime-graphics-aur-implementation-plan.md @@ -48,10 +48,26 @@ - Modify: `src/manifest/toml.cppm` - Modify: `src/manifest/xpkg.cppm` - Modify: `src/pm/dep_spec.cppm` +- Modify: `src/pm/index_route.cppm` +- Modify: `src/build/prepare.cppm` +- Modify: `src/cli.cppm` - Modify: `tests/unit/test_pm_compat.cpp` - Modify: `tests/unit/test_pm_index_route.cpp` +- Modify: `tests/unit/test_manifest.cpp` +- Modify: `tests/unit/test_pm_package_fetcher.cpp` - Modify: `tests/e2e/12_add_command.sh` - Modify: `tests/e2e/27_namespace_dependencies.sh` +- Modify: `tests/e2e/62_dotted_dependency_selector_priority.sh` +- Modify: `tests/e2e/63_bare_dependency_peer_root_priority.sh` +- Modify: `tests/e2e/78_test_main_combinations.sh` +- Modify: `tests/e2e/79_gtest_regular_dep_feature_main.sh` +- Modify: `tests/e2e/162_bare_name_namespace_scope.sh` +- Modify: `tests/e2e/165_bare_name_cross_namespace_wire_address.sh` +- Add: `tests/e2e/203_exact_selector_lock_migration.sh` +- Modify: `docs/05-mcpp-toml.md` +- Modify: `docs/zh/05-mcpp-toml.md` +- Modify: `docs/06-workspace.md` +- Modify: `docs/zh/06-workspace.md` - Modify: `docs/spec/package-identity.md` **Interfaces:** @@ -61,14 +77,14 @@ - `normalize_package_selector(PackageSelector, defaultNs="mcpplibs") -> DependencyCoordinate` - `format_package_selector(DependencyCoordinate) -> dotted selector` -- [ ] RED: add unit rows proving `lua -> (mcpplibs,lua)`, `capi.lua -> (capi,lua)`, `mcpplibs.capi.lua -> (mcpplibs.capi,lua)`, and rejection of empty/double-dot/control segments. -- [ ] RED: update add E2E so `mcpp add capi.lua@5.4.7` probes/writes exact `capi:lua`, not `mcpplibs.capi:lua`; prove a same-short-name sibling cannot win. -- [ ] Implement the O(length), no-I/O shared parser/normalizer and return one exact coordinate after default namespace filling. -- [ ] Route `mcpp add`, dependency TOML parsing, feature dependency parsing, and xpkg dependency parsing through the same normalized coordinate. -- [ ] Preserve already-parsed/locked identities; during the migration release, emit a warning only when an old dotted candidate exists and differs from the new exact coordinate, with both copyable selectors. -- [ ] Update existence-gate and not-found diagnostics to show the normalized PackageId and explicitly mention default `mcpplibs` when namespace was omitted. -- [ ] GREEN: run the focused unit and namespace/add E2E tests, then `git diff --check`. -- [ ] Commit exact selector normalization and identity documentation. +- [x] RED: add unit rows proving `lua -> (mcpplibs,lua)`, `capi.lua -> (capi,lua)`, `mcpplibs.capi.lua -> (mcpplibs.capi,lua)`, and rejection of empty/double-dot/control segments. +- [x] RED: update add E2E so `mcpp add capi.lua@5.4.7` probes/writes exact `capi:lua`, not `mcpplibs.capi:lua`; prove a same-short-name sibling cannot win. +- [x] Implement the O(length), no-I/O shared parser/normalizer and return one exact coordinate after default namespace filling. +- [x] Route `mcpp add`, dependency TOML parsing, feature dependency parsing, and xpkg dependency parsing through the same normalized coordinate. +- [x] Preserve already-parsed/locked identities; during the migration release, emit a warning only when an old dotted candidate exists and differs from the new exact coordinate, with both copyable selectors. +- [x] Update existence-gate and not-found diagnostics to show the normalized PackageId and explicitly mention default `mcpplibs` when namespace was omitted. +- [x] GREEN: run the focused unit and namespace/add E2E tests, then `git diff --check`. +- [x] Commit exact selector normalization and identity documentation. ## Task 3: Make TemplateSpec Typed, Namespace-Aware, and Deterministic @@ -110,7 +126,7 @@ - Modify: `src/scaffold/create.cppm` - Modify: `src/manifest/toml.cppm` - Modify: `tests/unit/test_scaffold.cpp` -- Add: `tests/e2e/202_new_transactional_scaffold.sh` +- Add: `tests/e2e/204_new_transactional_scaffold.sh` **Interfaces:** @@ -146,7 +162,7 @@ - Add: `tests/unit/test_runtime_selection.cpp` - Modify: `tests/unit/test_subos_info.cpp` - Modify: `tests/unit/test_fingerprint.cpp` -- Add: `tests/e2e/203_root_local_subos.sh` +- Add: `tests/e2e/205_root_local_subos.sh` **Interfaces:** @@ -179,7 +195,7 @@ - Modify: `src/build/execute.cppm` - Modify: `src/doctor.cppm` - Add: `tests/unit/test_elf_runtime.cpp` -- Add: `tests/e2e/204_runtime_binding_physics.sh` +- Add: `tests/e2e/206_runtime_binding_physics.sh` **Interfaces:** diff --git a/.agents/docs/2026-08-09-mcpp-template-runtime-graphics-aur-validation.md b/.agents/docs/2026-08-09-mcpp-template-runtime-graphics-aur-validation.md index 8298e4cb..a273459f 100644 --- a/.agents/docs/2026-08-09-mcpp-template-runtime-graphics-aur-validation.md +++ b/.agents/docs/2026-08-09-mcpp-template-runtime-graphics-aur-validation.md @@ -99,7 +99,25 @@ Each behavior entry is appended with: 4. GREEN exit code and relevant assertions; 5. refactor/full-gate result. -No RED/GREEN evidence has been claimed yet. +### 5.1 Exact PackageSelector and dependency identity (Task 2) + +| Gate | RED evidence | Production change | GREEN evidence | +|---|---|---|---| +| shared parser | Selector tests initially could not compile because `PackageSelector` / `parse_package_selector` did not exist; unsafe-segment rows then exposed missing character validation | Added one O(length), no-I/O parser, default `mcpplibs` normalization, dotted formatter, and diagnostic-only legacy coordinate helper | `test_pm_compat`: 17 tests passed, including bare/dotted/nested/invalid rows | +| manifest/xpkg parsing | Updated manifest expectations produced 3 failures under ordered-candidate behavior; invalid TOML/xpkg selectors were previously not hard parse failures | Routed direct, nested, feature, target and xpkg dependency inputs through the shared exact parser | `test_manifest`: 148 passed; `test_pm_index_route`: 7 passed; `test_pm_package_fetcher`: 10 passed | +| add/exact sibling | `12_add_command.sh` first showed `capi.lua` written/resolved with the old default-prefix candidate and no migration diagnostic; a later RED left both legacy `"acme.util"` and canonical `[dependencies.acme] util` rows | `mcpp add` now accepts canonical `[ns.]name@version`, retains `ns:name` as a warned one-release alias, probes only one PackageId, removes an equivalent legacy flat row, and writes/upserts the canonical table | `12_add_command.sh`: `capi.lua` cannot be stolen by `(mcpplibs.capi,lua)`; warning names both exact selectors; legacy source shape becomes one canonical row; malformed input leaves TOML byte-stable | +| scoped remove | Added same-name `[dev-dependencies]` before `[dependencies]`; RED removed the dev row and left the regular row | Limited flat removal to the exact `[dependencies]` body and kept nested namespace removal exact | Rebuilt source binary; the same E2E preserves the dev row and removes only the regular dependency | +| exact build miss | `162_bare_name_namespace_scope.sh` initially passed an exact miss into install-time compatibility retries | Every version dependency is identity-validated before install; miss reports the exact coordinate and did-you-mean remains diagnostic-only | `162`: bare gtest stays `(mcpplibs,gtest)` while explicit `(compat,gtest)` resolves; `165` proves the descriptor supplies the exact wire namespace | +| lock migration | New `203_exact_selector_lock_migration.sh` first failed because Form-B synthesis received the ambiguous manifest map key instead of the resolved short name | Existing v2 lock identity anchors an old dotted selection for one release train; unlocked selection never falls back; Form-B synthesis uses canonical short name | `203`: warning names old/new selectors, build/run use the locked old identity, rewritten lock retains its namespace | + +Task 2 refactor/full gates on the isolated source binary +`target/x86_64-linux-gnu/aee81584bf66d3f8/bin/mcpp`: + +- `mcpp build --no-cache`: PASS, release build completed in 64.12s. +- `mcpp test --no-cache`: PASS, **68 passed; 0 failed**, 86.37s. +- focused E2E set `12, 27, 62, 63, 78, 79, 162, 165, 203`: all PASS/OK. +- changed shell scripts `bash -n`: PASS. +- `git diff --check`: PASS. ## 6. Pull request and CI diff --git a/docs/05-mcpp-toml.md b/docs/05-mcpp-toml.md index 999ce66c..585eee4b 100644 --- a/docs/05-mcpp-toml.md +++ b/docs/05-mcpp-toml.md @@ -375,18 +375,18 @@ Default convention: `src/.cppm` (e.g. package name ```toml # Packages under the default package namespace (mcpplibs) [dependencies] -gtest = "1.15.2" # Exact version -mbedtls = "3.6.1" -ftxui = "6.1.9" +cmdline = "0.0.2" # Exact version +templates = "0.0.1" -# Dotted selector: try mcpplibs. first, then fall back to the sibling peer root. -# For example, imgui.core is tried in order as mcpplibs.imgui/core, then imgui/core. -[dependencies] -capi.lua = "0.0.3" +# Dotted selector: one exact identity. Everything before the final dot is the +# namespace; the final segment is the package name. compat.gtest = "1.15.2" imgui.core = "0.0.1" imgui.backend.glfw_opengl3 = "0.0.1" +mcpplibs.capi.lua = "0.0.3" +``` +```toml # Namespace sub-table form [dependencies.mcpplibs] cmdline = "0.0.2" @@ -394,17 +394,23 @@ tinyhttps = "0.2.2" llmapi = "0.2.5" [dependencies.compat] -glfw = "3.4" # Explicit namespace, skips the mcpplibs-first candidate +glfw = "3.4" # Explicit namespace; no fallback search +``` +```toml # Path dependency (local development) [dependencies] mylib = { path = "../mylib" } +``` +```toml # Git dependency — pick exactly one of tag / branch / rev [dependencies] mylib = { git = "https://github.com/user/mylib.git", tag = "v1.0.0" } applib = { git = "https://github.com/user/applib.git", branch = "develop" } +``` +```toml # Long-form dep spec: features and backend knobs [dependencies] imgui = { version = "0.0.3", features = ["docking"] } # Request a feature of this dependency @@ -449,24 +455,20 @@ qux = ">=1.0, <2.0" # Range combination #### Namespace resolution rules -Every package has a two-part identity: a **namespace** and a **name**. How you write -a dependency key decides which namespaces mcpp will look in. +Every package has a two-part identity: a **namespace** and a **name**. Every +selector normalizes to exactly one identity: -**A bare name resolves in exactly three places**, in order: - -| # | Namespace | Example | -|---|---|---| -| 1 | `mcpplibs` — the default namespace | `cmdline = "0.0.2"` | -| 2 | `compat` — the wrapper namespace for third-party C/C++ libraries | `gtest = "1.15.2"` → `compat.gtest` | -| 3 | upstream packages that declare no namespace at all | `opencv = "4.10.0"` | +- `cmdline` → `(mcpplibs, cmdline)`; omitting the namespace means the + `mcpplibs` default, and nothing else. +- `compat.gtest` → `(compat, gtest)`. +- `mcpplibs.capi.lua` → `(mcpplibs.capi, lua)`. -**Any other namespace must be written out in full.** There is no fuzzy, index-wide -search by short name: +There is no ordered fallback or fuzzy, index-wide search by short name: ```toml # ✅ Correct — dotted selector [dependencies] -"chriskohlhoff.asio" = "1.38.1" +chriskohlhoff.asio = "1.38.1" # ✅ Correct — namespace sub-table (preferred when you have several from one org) [dependencies.chriskohlhoff] @@ -477,14 +479,13 @@ asio = "1.38.1" asio = "1.38.1" ``` -The third form fails with an error that lists the namespaces that were searched and, -when a package with that short name exists elsewhere, the exact line to write instead. +The third form fails with an error that names the exact `(mcpplibs, asio)` +identity that was tried and, when the short name exists elsewhere, gives a +copyable explicit selector. -**Why not resolve bare names across every namespace?** Because dependency resolution -has to be reproducible. A global short-name search would mean that (a) two namespaces -owning the same short name are settled by index ordering, and (b) **adding an index -could silently change which package an existing dependency resolves to**. Requiring -the namespace keeps a `mcpp.toml` resolving to the same packages on every machine. +**Why one identity?** Dependency resolution has to be reproducible. Candidate +search would let two namespaces with the same short name be settled by index +state, and adding an index could silently retarget an existing dependency. **For xpkg authors:** in an index descriptor, identity is the pair `(package.namespace, package.name)`. The namespace is the dotted path; **`name` is @@ -509,8 +510,9 @@ path) but not required. The older fully-qualified spelling (`name = "chriskohlhoff.asio"`) is still accepted, so already-published descriptors keep working. `mcpp xpkg parse` -enforces the rule — run it in your index CI. Requires mcpp >= 0.0.106 and -xlings >= 0.4.69; full normative text in `docs/spec/package-identity.md`. +enforces the descriptor rule — run it in your index CI. Descriptor identity +requires mcpp >= 0.0.106; exact selectors require mcpp >= 2026.8.9.1; both use +xlings >= 0.4.69. Full normative text is in `docs/spec/package-identity.md`. #### When mcpp refreshes the package index @@ -547,7 +549,7 @@ Run any command with `-v` to see the decision for each dependency and why. ### 2.6 `[dev-dependencies]` — Test Dependencies ```toml -[dev-dependencies] +[dev-dependencies.compat] gtest = "1.15.2" ``` @@ -1309,7 +1311,7 @@ version = "1.0.0" [targets.mymath] kind = "lib" -[dev-dependencies] +[dev-dependencies.compat] gtest = "1.15.2" ``` @@ -1417,7 +1419,7 @@ mcpp build --target x86_64-linux-musl | Static stdlib | `true` | Portable binary | | Headers | `include/` (if present) | Added to `-I` automatically | | Tests | `tests/**/*.cpp` | Discovered automatically by `mcpp test` | -| Dependency namespace | `mcpp` (default) | The flat form uses the default ns | +| Dependency namespace | `mcpplibs` (default) | A bare selector means only this exact namespace | ### 4.1 Legacy `[language]` Compatibility Layer diff --git a/docs/06-workspace.md b/docs/06-workspace.md index 7b917719..8f1fd3b4 100644 --- a/docs/06-workspace.md +++ b/docs/06-workspace.md @@ -100,7 +100,7 @@ Declare dependency versions centrally under `[workspace.dependencies]`; members # root mcpp.toml [workspace.dependencies] cmdline = "0.0.2" -capi.lua = "0.0.3" # dotted selector: mcpplibs.capi/lua, then capi/lua +mcpplibs.capi.lua = "0.0.3" # exact selector: (mcpplibs.capi, lua) [workspace.dependencies.compat] mbedtls = "3.6.1" diff --git a/docs/spec/package-identity.md b/docs/spec/package-identity.md index 46261995..3c3cc40e 100644 --- a/docs/spec/package-identity.md +++ b/docs/spec/package-identity.md @@ -5,9 +5,9 @@ | **规范编号** | SPEC-001 | | **标题** | 包身份(`package.namespace` / `package.name`)、`[dependencies]` 选择器与匹配机制 | | **状态** | **评审中(Review)** —— 已实现 | -| **版本** | 1.1 | -| **最后修改** | 2026-08-03 | -| **最低实现版本** | mcpp **0.0.106**(xlings >= 0.4.69) | +| **版本** | 1.2 | +| **最后修改** | 2026-08-09 | +| **最低实现版本** | 描述符身份:mcpp **0.0.106**;精确 selector:mcpp **2026.8.9.1**(xlings >= 0.4.69) | | **作者/维护** | mcpp-community | | **相关设计文档** | `.agents/docs/2026-06-20-package-resolution-architecture.md` §4
`.agents/docs/2026-06-26-identity-first-resolution-no-filename.md`
`.agents/docs/2026-07-25-issue278-descriptor-name-form-canonicalization-design.md`
`.agents/docs/2026-07-25-name-namespace-bidirectional-verification-report.md`
`.agents/docs/2026-07-25-name-namespace-canonical-implementation-spec.md` | | **相关 issue** | [mcpp#278](https://github.com/mcpp-community/mcpp/issues/278)
[xlings#381](https://github.com/openxlings/xlings/issues/381) —— 索引键缺命名空间维度(§3.3) | @@ -26,12 +26,12 @@ | 标记 | 含义 | |---|---| -| ✅ **已实现** | 自 mcpp 0.0.106 起的行为与本规范一致 | +| ✅ **已实现** | 自对应“最低实现版本”起的行为与本规范一致 | | ⚠️ **部分实现** | 已有实现,但语义或覆盖面与本规范有差异(差异已注明) | | ❌ **未实现** | 本规范要求但尚未支持;当前行为已注明 | -> **本规范所需行为自 mcpp 0.0.106 起已全部实现,当前实现继续符合。** 索引作者按 §3 -> 书写即可。0.0.105 及更早版本要求的过渡形态(`name` 必须写成 +> 描述符身份规则自 mcpp 0.0.106 起实现;唯一精确 selector 自 2026.8.9.1 +> 起实现。0.0.105 及更早版本要求的过渡形态(`name` 必须写成 > `.`)仍被接受为**兼容写法**,见 §8。 --- @@ -42,7 +42,7 @@ - 索引描述符(`.lua`)中 `package.namespace` / `package.name` 的语义与形态 - `mcpp.toml` 中 `[dependencies]`(及 `[dev-dependencies]` / `[build-dependencies]` / `[feature-deps.*]`)的书写文法 -- 从用户书写 → 候选身份 → 描述符发现 → 身份校验 → 安装目标的完整匹配机制 +- 从用户书写 → 唯一身份 → 描述符发现 → 身份校验 → 安装目标的完整匹配机制 ### 1.2 边界:mcpp 不改动 xlings 规范 @@ -119,7 +119,7 @@ package = { namespace = "compat", name = "compat.zlib" } -- legacy,短名 = "z ✅ **规范如此**。 -✅ **已实现**。xlings **0.4.69** 起索引按 `(effectiveNamespace, name)` 建键([#381](https://github.com/openxlings/xlings/issues/381)),同一索引内两个同短名不同命名空间的包各自可寻址;裸名请求多候选时报 ambiguity 并列出候选。 +✅ **已实现**。xlings **0.4.69** 起索引按 `(effectiveNamespace, name)` 建键([#381](https://github.com/openxlings/xlings/issues/381)),同一索引内两个同短名不同命名空间的包各自可寻址;mcpp 始终传入唯一精确身份。 e2e `163_identity_first_resolution.sh` 端到端锁住:同一 path 索引内 `(alpha, widget)` 与 `(beta, widget)` 各自安装到 `alpha-x-widget` / `beta-x-widget`。 @@ -149,30 +149,30 @@ e2e `163_identity_first_resolution.sh` 锁住:身份为 `(acme, widget)` 的描 ## 4. 消费侧规范:`[dependencies]` 书写文法 -用户写的**不是身份,而是选择器(selector)** —— 它展开为一个**有序候选身份列表**,按序尝试,首个满足者胜出。 +用户写的是 selector;它必须在 O(输入长度)、无 I/O 的解析阶段规范化成**唯一一个** `(namespace, name)`。索引状态、候选顺序或已安装内容不得参与身份决定。 ### 4.1 四种书写形式 -| # | 写法 | 展开为候选(按序) | 语义 | +| # | 写法 | 规范化身份 | 语义 | |---|---|---|---| -| 1 | `[dependencies]`
`gtest = "1.15.2"` | ① `(mcpplibs, gtest)`
② `(∅, gtest)` | **裸名**。只解析默认命名空间搜索路径(§5.2) | -| 2 | `[dependencies]`
`acme.widget = "1.0"` | ① `(mcpplibs.acme, widget)`
② `(acme, widget)` | **点式选择器**。先试默认命名空间下的同名子空间,再试同级 peer root | -| 3 | `[dependencies.acme]`
`widget = "1.0"` | ① `(acme, widget)` | **命名空间子表**。权威、单候选、无猜测 —— **推荐用于第三方命名空间** | -| 4 | `[dependencies]`
`"acme.widget" = "1.0"` | ① `(acme, widget)` | **引号点式键**(legacy)。单候选,等价于 #3 | +| 1 | `[dependencies]`
`cmdline = "0.0.2"` | `(mcpplibs, cmdline)` | **裸名**。省略 namespace 只表示默认 `mcpplibs` | +| 2 | `[dependencies]`
`acme.widget = "1.0"` | `(acme, widget)` | **点式选择器**。最后一段是 name,之前所有段是 namespace | +| 3 | `[dependencies.acme]`
`widget = "1.0"` | `(acme, widget)` | **命名空间子表**。与 #2 完全等价,多个同 namespace 包时更清晰 | +| 4 | `[dependencies]`
`"acme.widget" = "1.0"` | `(acme, widget)` | **引号点式键**(legacy source shape)。身份仍与 #2/#3 相同 | -✅ **已实现**(`resolve_dependency_selector` / `make_direct_dependency_selector`)。 +✅ **已实现**(`parse_package_selector` / `normalize_package_selector` / `make_direct_dependency_selector`)。 -**#2 与 #3 的区别很重要**:`[dependencies.acme]` 是**显式 TOML 表**,mcpp 视其为命名空间根,直接产出单候选;而 `[dependencies]` 里的点式键是**有序猜测**,会先试 `mcpplibs.` 前缀。 - -判定「是否命名空间根」的条件:该路径是 TOML **显式表**(`[dependencies.acme]` 被真实书写过),**或**键名恰为默认命名空间 `mcpplibs`。 +**#2 与 #3 只有 TOML 版式差异,没有解析差异。** 新增索引或同短名 sibling 不能改变其身份。 多级命名空间同理,`ns` 逐层累积:`[dependencies.mcpplibs.capi]` + `lua = "0.0.3"` → 单候选 `(mcpplibs.capi, lua)`。 -### 4.2 裸名的解析域是封闭的 +### 4.2 裸名就是默认 mcpplibs + +> **裸名(#1)只能表示 `(mcpplibs, name)`。`compat`、第三方 namespace 与无 namespace 包都不会成为隐式候选。** -> **裸名(#1)只解析三类包:`mcpplibs`(默认命名空间)、`compat`(包装命名空间)、无 `namespace` 声明的上游包。任何声明了第三方命名空间的包,禁止用裸名请求。** +例如 gtest 必须写成 `compat.gtest` 或 `[dependencies.compat] gtest = ...`;裸 `gtest` 请求的是不同身份 `(mcpplibs, gtest)`。 -✅ **已实现**(0.0.105)。裸名请求命中一个声明了非空第三方 `namespace` 的描述符时,该候选被拒绝。 +✅ **已实现**(2026.8.9.1)。默认 namespace 的依赖身份门禁不再接纳 `compat` 或无 namespace descriptor。 **设计理由**:全域按短名搜索会让解析结果取决于「本机装了哪些索引」—— 1. 两个命名空间拥有同名包时,胜负由索引顺序决定,而用户 `[indices]` 添加的索引之间**没有全序**; @@ -183,16 +183,16 @@ e2e `163_identity_first_resolution.sh` 锁住:身份为 `(acme, widget)` 的描 ### 4.3 解析失败时的诊断 -候选全部落空时,mcpp **必须**明确失败,并列出尝试过的身份;若该短名存在于其他命名空间,**应当**给出可直接抄写的正确写法。 +唯一身份落空时,mcpp **必须**明确失败并列出该身份;若同短名存在于其他 namespace,**应当**只在诊断中给出可复制的显式 selector,禁止把提示结果回灌解析。 -✅ **已实现**(0.0.105): +✅ **已实现**(2026.8.9.1): ``` -error: dependency 'asio': no package found under the namespaces mcpp searched - tried: (mcpplibs, asio), (no namespace, asio) +error: dependency 'asio': no package found + tried: (mcpplibs, asio) a package with this name exists under another namespace: chriskohlhoff.asio - bare names only resolve to the `mcpplibs` / `compat` namespaces. write it out: + namespace omission means `mcpplibs`; write the exact package: [dependencies] "chriskohlhoff.asio" = "1.38.1" or: @@ -200,7 +200,9 @@ error: dependency 'asio': no package found under the namespaces mcpp searched asio = "1.38.1" ``` -该 did-you-mean 扫描**仅**在已失败路径触发,结果**只进错误文案**,禁止回灌解析、lockfile 或安装层 —— 否则就退化成 §4.2 否决的全域模糊匹配。 +该 did-you-mean 扫描**仅**在已失败路径触发,结果**只进错误文案**,禁止回灌解析、lockfile 或安装层。 + +迁移 release 对旧的 compact dotted 搜索做两件事:已有 lock 继续固定已记录身份;无 lock 时若旧的 `mcpplibs.` primary 确实存在,warning 同时显示旧/新完整 selector,但仍不回退。 --- @@ -212,34 +214,33 @@ error: dependency 'asio': no package found under the namespaces mcpp searched 用户书写(selector) │ §4.1 展开 ▼ -有序候选身份列表 [(ns₁,n₁), (ns₂,n₂), …] - │ 逐个尝试 +唯一身份 (ns,name) + │ 精确查询 ▼ ① 发现:先探测推荐文件名,落空后按声明身份扫描描述符(§3.4) ② 校验:xpkg_lua_identity_matches 复核声明身份 ← §5.2 -③ 收敛:INV-RESOLVE 拒绝裸名命中第三方 ns ← §4.2 -④ 回填:用描述符**声明的** namespace 定身份 ← §5.3 - │ 首个通过者胜出;全部落空 → §4.3 报错 +③ 验证:descriptor 身份必须精确相等 ← §5.2 + │ 落空 → §4.3 报错 ▼ 选定身份 (ns, name) → 派生 wire key / store dir(§6) ``` ### 5.2 身份校验规则 -给定候选 `(ns, shortName)` 与一个描述符,`xpkg_lua_identity_matches` 的判定: +给定请求 `(ns, shortName)` 与一个描述符,`xpkg_lua_identity_matches` 的判定: -| 候选 `ns` | 判定 | +| 请求 `ns` | 判定 | |---|---| | 描述符无 `name` | **接受**(无从校验,宽松) | -| `ns` 为空(discovery) | 短名相等即可 | -| `ns == "mcpplibs"`(默认命名空间) | 描述符身份的 ns ∈ **{`mcpplibs`, `compat`}**,或(旧式)无 ns | +| `ns` 为空(仅内部 legacy discovery) | 短名相等即可;不得进入依赖 selector 主路径 | +| `ns == "mcpplibs"`(默认命名空间) | 依赖 selector 主路径要求描述符身份必须为 `mcpplibs`;底层 legacy API 的 `allowLegacyBareDefault` 只供非 selector 兼容调用 | | 其他具体 ns | **精确相等** | -✅ **已实现**。第三行即**默认命名空间搜索路径**:裸 `gtest` 能命中 `compat.gtest`,正是这条。 +✅ **已实现**。`compat.gtest` 与 `mcpplibs.gtest` 是两个不同身份。 -### 5.3 空命名空间的回填(P3) +### 5.3 空命名空间的兼容边界(P3) -候选 `(∅, name)` 命中后,**必须**用描述符**声明的** `namespace` 作为最终身份,而不是候选的空值。 +旧 discovery 调用 `(∅, name)` 命中后,**必须**用描述符**声明的** `namespace` 作为最终身份,而不是候选的空值。新的依赖 selector 已先填充默认 namespace,不使用 discovery;模板 selector 在切换到同一共享解析链后也必须遵守该规则。 若描述符本身未声明 `namespace`(上游裸包如 `opencv`),则**空命名空间就是它的合法身份**,不得强行填充。 @@ -347,18 +348,16 @@ asio = "1.38.1" **错误写法**:`[dependencies]` + `asio = "1.38.1"` —— 裸名不解析第三方命名空间(§4.2),报错并给出上述两种正确写法。 -### 9.2 裸名经 compat 搜索路径 +### 9.2 compat 必须显式选择 ```toml -[dependencies] +[dependencies.compat] gtest = "1.15.2" ``` ``` -选择器 gtest → 候选 ① (mcpplibs, gtest) ② (∅, gtest) -候选①:探测 gtest.lua / mcpplibs.gtest.lua / compat.gtest.lua - 命中 compat.gtest.lua,声明 (compat, compat.gtest) → 归一 (compat, gtest) - 校验:候选 ns 为默认命名空间 → 允许 ns ∈ {mcpplibs, compat} → ✓ +选择器 compat.gtest → 唯一身份 (compat, gtest) +探测 compat.gtest.lua,声明 (compat, compat.gtest) → 归一 (compat, gtest) → ✓ 身份 (compat, gtest) → store dir compat-x-compat.gtest ``` @@ -381,6 +380,7 @@ lua = "0.0.3" | 版本 | 日期 | 变更 | |---|---|---| +| 1.2 | 2026-08-09 | selector 收敛为唯一精确 PackageId:裸名只表示默认 mcpplibs,dotted 以最后一段为 name;移除 compat/空 namespace 隐式候选,加入 lock 保持与一个 release train 的双 selector 迁移 warning | | 1.1 | 2026-08-03 | 按当前实现复核:澄清文件名发现是快路径加身份回退扫描,修正 legacy `package.name` 的 wire key 示例,并将 0.0.106 明确为最低实现版本 | | 0.1 | 2026-07-25 | 首版草案。整合 #278 的双向验证结论:确立「身份 = `(namespace, name)`、层级归 `namespace`、`name` 为原子段」为规范形态,并如实标注 0.0.105 的过渡形态(强制 FQN)与全部待实现项 | | 1.0 | 2026-07-25 | **mcpp 0.0.106 全部实现**:身份归一化去 split-on-last-dot、target 用字面 `name`、store 目录、文件名自由(快路径+身份扫描)、`name` 形态校验反转。xlings 0.4.69 修好 #381 后 §3.3 的 `(namespace, name)` 唯一自然成立。状态 草案 → 评审中 | @@ -392,5 +392,5 @@ lua = "0.0.3" --- -> **本规范所需行为自 mcpp 0.0.106 起已全部实现,当前实现仍符合**,状态为「评审中」。 +> 描述符身份规则自 mcpp 0.0.106 起实现;精确 selector 自 2026.8.9.1 起实现。当前实现符合本规范,状态为「评审中」。 > 英文版待补(`docs/spec/` 顶层按仓库惯例为英文,本文档先以中文成稿)。 diff --git a/docs/zh/05-mcpp-toml.md b/docs/zh/05-mcpp-toml.md index c8c109b0..3d92db50 100644 --- a/docs/zh/05-mcpp-toml.md +++ b/docs/zh/05-mcpp-toml.md @@ -340,18 +340,17 @@ path = "src/capi/lua.cppm" # 覆盖默认的 lib-root 位置 ```toml # 默认包空间(mcpplibs)下的包 [dependencies] -gtest = "1.15.2" # 精确版本 -mbedtls = "3.6.1" -ftxui = "6.1.9" +cmdline = "0.0.2" # 精确版本 +templates = "0.0.1" -# dotted selector: 先匹配 mcpplibs., 找不到再匹配同级 peer root。 -# 例如 imgui.core 会按顺序尝试 mcpplibs.imgui/core, imgui/core。 -[dependencies] -capi.lua = "0.0.3" +# dotted selector 是单一精确身份:最后一段是包名,之前所有段都是 namespace。 compat.gtest = "1.15.2" imgui.core = "0.0.1" imgui.backend.glfw_opengl3 = "0.0.1" +mcpplibs.capi.lua = "0.0.3" +``` +```toml # 命名空间子表写法 [dependencies.mcpplibs] cmdline = "0.0.2" @@ -359,17 +358,23 @@ tinyhttps = "0.2.2" llmapi = "0.2.5" [dependencies.compat] -glfw = "3.4" # 显式 namespace, 不走 mcpplibs 优先候选 +glfw = "3.4" # 显式 namespace,无回退搜索 +``` +```toml # 路径依赖(本地开发) [dependencies] mylib = { path = "../mylib" } +``` +```toml # Git 依赖 —— tag / branch / rev 三选一 [dependencies] mylib = { git = "https://github.com/user/mylib.git", tag = "v1.0.0" } applib = { git = "https://github.com/user/applib.git", branch = "develop" } +``` +```toml # 长式 dep spec:features 与 backend 旋钮 [dependencies] imgui = { version = "0.0.3", features = ["docking"] } # 请求该依赖的 feature @@ -408,22 +413,18 @@ qux = ">=1.0, <2.0" # 范围组合 #### 命名空间解析规则 -每个包的身份是**命名空间 + 名字**二元组。依赖 key 的写法决定 mcpp 到哪些命名空间里找。 +每个包的身份是**命名空间 + 名字**二元组。每个 selector 都只规范化成一个身份: -**裸名只在三个地方解析**,按序: - -| # | 命名空间 | 示例 | -|---|---|---| -| 1 | `mcpplibs` — 默认命名空间 | `cmdline = "0.0.2"` | -| 2 | `compat` — 第三方 C/C++ 库的包装命名空间 | `gtest = "1.15.2"` → `compat.gtest` | -| 3 | 完全没有声明命名空间的上游包 | `opencv = "4.10.0"` | +- `cmdline` → `(mcpplibs, cmdline)`;省略 namespace 只表示默认 `mcpplibs`。 +- `compat.gtest` → `(compat, gtest)`。 +- `mcpplibs.capi.lua` → `(mcpplibs.capi, lua)`。 -**其他命名空间一律必须写全。** 不存在按短名的全索引模糊搜索: +不存在有序回退或按短名的全索引模糊搜索: ```toml # ✅ 正确 —— 点式选择器 [dependencies] -"chriskohlhoff.asio" = "1.38.1" +chriskohlhoff.asio = "1.38.1" # ✅ 正确 —— 命名空间子表(同一组织有多个包时更推荐) [dependencies.chriskohlhoff] @@ -434,9 +435,9 @@ asio = "1.38.1" asio = "1.38.1" ``` -第三种写法会明确报错,并列出搜索过的命名空间;若该短名的包存在于别处,错误信息会直接给出应当改写成的那一行。 +第三种写法会明确报错,指出实际尝试的 `(mcpplibs, asio)`;若该短名存在于别处,错误信息会给出可直接复制的显式 selector。 -**为什么不让裸名跨所有命名空间去找?** 因为依赖解析必须可复现。全域短名搜索意味着:(a) 两个命名空间拥有同名包时,胜负由索引顺序决定;(b) **新增一个索引可能悄悄改变某个既有依赖解析到的包**。要求写出命名空间,才能让同一份 `mcpp.toml` 在每台机器上解析到相同的包。 +**为什么只允许一个身份?** 因为依赖解析必须可复现。候选搜索会让同短名包受索引状态影响,新增索引还可能悄悄重定向既有依赖。 **给 xpkg 作者:** 索引描述符里,身份是 `(package.namespace, package.name)` 二元组。命名空间是点分路径,**`name` 是单一原子段**: @@ -454,7 +455,7 @@ package = { 文件名只是提示 —— 描述符按声明的身份被发现,所以 `pkgs/c/chriskohlhoff.asio.lua` 与 `pkgs/z/anything.lua` 解析结果完全相同。推荐 `.lua` 或 `..lua`(命中 mcpp 的快路径),但不强制。 -旧的完全限定拼写(`name = "chriskohlhoff.asio"`)仍被接受,已发布的描述符无需改动。`mcpp xpkg parse` 会校验该规则,请在索引 CI 里跑它。需要 mcpp >= 0.0.106 与 xlings >= 0.4.69;规范全文见 `docs/spec/package-identity.md`。 +旧的完全限定拼写(`name = "chriskohlhoff.asio"`)仍被接受,已发布的描述符无需改动。`mcpp xpkg parse` 会校验该规则,请在索引 CI 里跑它。描述符身份需要 mcpp >= 0.0.106,精确 selector 需要 mcpp >= 2026.8.9.1,两者使用 xlings >= 0.4.69;规范全文见 `docs/spec/package-identity.md`。 #### 表形式 —— 让 feature 贡献的不止是隐含 feature `[features]` 的条目除了写成数组,还可写成**表**,从而让该 feature 在隐含 feature @@ -1022,7 +1023,7 @@ version = "1.0.0" [targets.mymath] kind = "lib" -[dev-dependencies] +[dev-dependencies.compat] gtest = "1.15.2" ``` @@ -1130,7 +1131,7 @@ mcpp build --target x86_64-linux-musl | 静态 stdlib | `true` | 便携二进制 | | 头文件 | `include/`(如果存在) | 自动加到 `-I` | | 测试 | `tests/**/*.cpp` | `mcpp test` 自动发现 | -| 依赖命名空间 | `mcpp`(默认) | 平铺写法走默认 ns | +| 依赖命名空间 | `mcpplibs`(默认) | 裸 selector 只表示该精确 ns | ### 4.1 旧 `[language]` 兼容层 diff --git a/docs/zh/06-workspace.md b/docs/zh/06-workspace.md index fb05a4bf..c8ac3fca 100644 --- a/docs/zh/06-workspace.md +++ b/docs/zh/06-workspace.md @@ -100,7 +100,7 @@ mbedtls.workspace = true # 根 mcpp.toml [workspace.dependencies] cmdline = "0.0.2" -capi.lua = "0.0.3" # dotted selector: mcpplibs.capi/lua, then capi/lua +mcpplibs.capi.lua = "0.0.3" # 精确 selector:(mcpplibs.capi, lua) [workspace.dependencies.compat] mbedtls = "3.6.1" diff --git a/mcpp.toml b/mcpp.toml index 7555910c..b39633c4 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -47,7 +47,8 @@ linkage = "static" mcpplibs.cmdline = "0.0.1" # `mcpp build` ignores [dev-dependencies]; only `mcpp test` resolves them. -[dev-dependencies] +# Namespace omission means exactly mcpplibs, so the compat wrapper is explicit. +[dev-dependencies.compat] gtest = "1.15.2" # Everything else uses M5.0 conventions: diff --git a/src/build/prepare.cppm b/src/build/prepare.cppm index 3a7d2649..58dfef3c 100644 --- a/src/build/prepare.cppm +++ b/src/build/prepare.cppm @@ -57,6 +57,7 @@ import mcpp.pm.index_refresh; import mcpp.pm.mangle; import mcpp.pm.compat; import mcpp.pm.dep_spec; +import mcpp.pm.dependency_selector; import mcpp.pm.lock_io; import mcpp.version_req; import mcpp.ui; @@ -1174,13 +1175,18 @@ prepare_build(bool print_fingerprint, // fetched. Keyed by package name — the same key the writer at the end of // this function emits, both taken from the root manifest's [dependencies]. std::map gitLockAnchors; + std::map packageIdentityLockAnchors; { auto lockPath = workRoot / "mcpp.lock"; if (std::filesystem::exists(lockPath)) { if (auto lock = mcpp::pm::load(lockPath); lock) { - for (auto const& p : lock->packages) + for (auto const& p : lock->packages) { + if (!p.namespace_.empty()) + packageIdentityLockAnchors.emplace( + p.name, p.namespace_); if (auto parsed = mcpp::pm::parse_git_source(p.source); parsed) gitLockAnchors.emplace(p.name, std::move(*parsed)); + } } else { // Degraded, not a plain warning: the engine silently does less // than asked — every git branch dep falls back to `ls-remote` @@ -2210,6 +2216,8 @@ prepare_build(bool print_fingerprint, return out; }; + std::set selectorMigrationWarnings; + auto selectDependencyCandidate = [&](mcpp::manifest::DependencySpec& spec, const std::string& depName) -> std::expected @@ -2221,9 +2229,65 @@ prepare_build(bool print_fingerprint, "dependency '{}' has no lookup candidates", depName))); } + // One release train of migration support for the former dotted + // candidate search. A lockfile records the identity an existing + // project already selected, so keep that identity stable until the + // user rewrites the selector explicitly. Without a lock anchor, never + // fall back: only diagnose a valid old-primary package and continue + // with the new exact coordinate. + if (spec.legacyCandidateSearch) { + const auto exact = candidates.front(); + bool lockExpressesIntent = false; + if (auto locked = packageIdentityLockAnchors.find(depName); + locked != packageIdentityLockAnchors.end()) { + lockExpressesIntent = true; + if (locked->second != exact.namespace_) { + mcpp::pm::DependencyCoordinate lockedCoordinate{ + .namespace_ = locked->second, + .shortName = exact.shortName, + }; + if (selectorMigrationWarnings.insert(depName).second) { + mcpp::ui::warning(std::format( + "dependency selector '{}' now means exact package " + "'{}', but mcpp.lock records '{}'; keeping the " + "locked identity for this migration release. " + "Write '{}' to keep it explicitly, or remove the " + "lock and keep '{}' to migrate", + depName, + mcpp::pm::format_package_selector(exact), + mcpp::pm::format_package_selector(lockedCoordinate), + mcpp::pm::format_package_selector(lockedCoordinate), + mcpp::pm::format_package_selector(exact))); + } + candidates.assign(1, std::move(lockedCoordinate)); + } + } + + if (!lockExpressesIntent && spec.isVersion()) { + if (auto old = mcpp::pm::legacy_prefixed_coordinate(exact)) { + auto oldLua = readStrictLuaForCandidate(*old); + if (oldLua && xpkgLuaMatchesCandidate( + *old, *oldLua, + /*allowLegacyBareDefault=*/false) + && selectorMigrationWarnings.insert(depName).second) { + mcpp::ui::warning(std::format( + "dependency selector '{}' now resolves exactly to " + "'{}'; an older mcpp would select the existing " + "package '{}'. Write '{}' to keep the old identity " + "or keep '{}' for the new exact identity", + depName, + mcpp::pm::format_package_selector(exact), + mcpp::pm::format_package_selector(*old), + mcpp::pm::format_package_selector(*old), + mcpp::pm::format_package_selector(exact))); + } + } + } + } + auto selected = candidates.front(); bool matched = false; - if (spec.isVersion() && candidates.size() > 1) { + if (spec.isVersion()) { for (auto& candidate : candidates) { auto lua = readStrictLuaForCandidate(candidate); if (!lua || !xpkgLuaMatchesCandidate( @@ -2279,11 +2343,9 @@ prepare_build(bool print_fingerprint, return index_route().lazy_git(c.namespace_); }); - // T9 (#278) — no candidate resolved. This used to fall through to - // `candidates.front()` SILENTLY, so mcpp carried on with a namespace - // it had invented, and the user met the failure much later (during - // download/install) wrapped around that invented name. Fail here, - // and say exactly which identities were tried. + // An exact coordinate that a readable index cannot serve fails at + // resolution. Never carry it into install-time compatibility + // retries, which would reintroduce cross-namespace guessing. if (!matched && !anyLazyGitIndex) { std::string tried; for (auto& c : candidates) { @@ -2304,21 +2366,19 @@ prepare_build(bool print_fingerprint, hint += "\n a package with this name exists under " "another namespace:"; for (auto& fqn : fqns) hint += "\n " + fqn; - hint += std::format( - "\n bare names only resolve to the `{}` / `{}` " - "namespaces. write it out:" - "\n [dependencies]" - "\n \"{}\" = \"{}\"" - "\n or:" - "\n [dependencies.{}]" - "\n {} = \"{}\"", - mcpp::pm::kDefaultNamespace, - mcpp::pm::kCompatNamespace, - fqns.front(), spec.version.empty() ? "" - : spec.version, - fqns.front().substr(0, fqns.front().rfind('.')), - fqns.front().substr(fqns.front().rfind('.') + 1), - spec.version.empty() ? "" : spec.version); + if (auto suggested = mcpp::pm::parse_package_selector( + fqns.front()); suggested + && suggested->namespace_) { + hint += std::format( + "\n namespace omission means `{}`. write the " + "exact package:" + "\n [dependencies.{}]" + "\n {} = \"{}\"", + mcpp::pm::kDefaultNamespace, + *suggested->namespace_, suggested->name, + spec.version.empty() ? "" + : spec.version); + } } } @@ -2334,8 +2394,8 @@ prepare_build(bool print_fingerprint, mcpp::config::make_xlings_env(**cfgA))); } return std::unexpected(std::format( - "dependency '{}': no package found under the namespaces " - "mcpp searched\n tried: {}{}", + "dependency '{}': no package found for exact selector" + "\n tried: {}{}", depName, tried, hint)); } } @@ -2413,7 +2473,7 @@ prepare_build(bool print_fingerprint, } if (field.kind == mcpp::manifest::McppField::TableBody) { auto dm = mcpp::manifest::synthesize_from_xpkg_lua( - *luaContent, depName, version, targetPlatform); + *luaContent, shortName, version, targetPlatform); if (!dm) return false; for (auto const& [generatedPath, _] : dm->buildConfig.generatedFiles) { if (!generatedPath.empty()) return true; @@ -2477,7 +2537,7 @@ prepare_build(bool print_fingerprint, auto field = mcpp::manifest::extract_mcpp_field(*luaContent); if (field.kind == mcpp::manifest::McppField::TableBody) { auto depManifest = mcpp::manifest::synthesize_from_xpkg_lua( - *luaContent, depName, version, targetPlatform); + *luaContent, shortName, version, targetPlatform); if (!depManifest) { return std::unexpected(std::format( "dependency '{}': {}", depName, depManifest.error().format())); @@ -2713,7 +2773,7 @@ prepare_build(bool print_fingerprint, if (auto r = loadFrom(matches.front()); !r) return std::unexpected(r.error()); } else if (field.kind == mcpp::manifest::McppField::TableBody) { auto dm = mcpp::manifest::synthesize_from_xpkg_lua( - *luaContent, depName, version, targetPlatform); + *luaContent, shortName, version, targetPlatform); if (!dm) return std::unexpected(std::format( "dependency '{}': {}", depName, dm.error().format())); warn_unknown_xpkg_keys(*dm, depName); diff --git a/src/cli.cppm b/src/cli.cppm index 2290fb5c..201dea48 100644 --- a/src/cli.cppm +++ b/src/cli.cppm @@ -57,8 +57,8 @@ void print_usage() { std::println(" mcpp run [target] [-- args...] Build + run a binary target"); std::println(" mcpp test [pattern] [-- args...] Build + run tests/**/*.cpp (--list, --timeout, --build-timeout, --message-format json)"); std::println(" mcpp clean [--bmi-cache] Remove target/ (and optionally the build cache)"); - std::println(" mcpp add [@] Add a dependency to mcpp.toml"); - std::println(" mcpp remove Remove a dependency from mcpp.toml"); + std::println(" mcpp add [ns.]pkg@ver Add an exact dependency to mcpp.toml"); + std::println(" mcpp remove [ns.]pkg Remove an exact dependency from mcpp.toml"); std::println(" mcpp update [pkg] Re-resolve deps and rewrite mcpp.lock"); std::println(" mcpp search Search packages in registries"); std::println(" mcpp publish [--dry-run] Publish package to default registry"); @@ -321,13 +321,15 @@ int run(int argc, char** argv) { .action(wrap_rc(cmd_why))) .subcommand(cl::App("add") .description("Add a dependency to mcpp.toml") - .arg(cl::Arg("pkg").help("Package spec, e.g. foo@1.0.0").required()) + .arg(cl::Arg("pkg").help( + "Exact package spec, e.g. foo@1.0.0 or compat.gtest@1.15.2") + .required()) .option(cl::Option("dev").help( - "Add to [dev-dependencies] (test-only, e.g. gtest)")) + "Add to [dev-dependencies] (test-only, e.g. compat.gtest)")) .action(wrap_rc(mcpp::pm::commands::cmd_add))) .subcommand(cl::App("remove") .description("Remove a dependency from mcpp.toml") - .arg(cl::Arg("pkg").help("Package name").required()) + .arg(cl::Arg("pkg").help("Exact package selector [ns.]name").required()) .action(wrap_rc(mcpp::pm::commands::cmd_remove))) .subcommand(cl::App("update") .description("Re-resolve dependencies and rewrite mcpp.lock") diff --git a/src/manifest/toml.cppm b/src/manifest/toml.cppm index 6958a8af..24b3f0c0 100644 --- a/src/manifest/toml.cppm +++ b/src/manifest/toml.cppm @@ -6,7 +6,6 @@ import mcpp.manifest.types; import std; import mcpp.libs.toml; import mcpp.pm.dep_spec; -import mcpp.pm.compat; import mcpp.pm.dependency_selector; import mcpp.pm.index_spec; import mcpp.platform; @@ -664,11 +663,27 @@ std::expected parse_string(std::string_view content, return {}; }; + auto parse_dep_selector = [&](std::string_view section, + std::string_view selectorText, + std::string_view stableMapKey) + -> std::expected + { + auto parsed = mcpp::pm::parse_package_selector(selectorText); + if (!parsed) { + return std::unexpected(error(origin, std::format( + "[{}] {}", section, parsed.error().message))); + } + auto coordinate = mcpp::pm::normalize_package_selector(*parsed); + return mcpp::pm::make_direct_dependency_selector( + coordinate.namespace_, coordinate.shortName, stableMapKey); + }; + auto assign_dep = [&](std::string_view section, std::map& out, const mcpp::pm::DependencySelector& selector, const t::Value& value, - bool legacyDottedKey) + bool legacyDottedKey, + bool legacyCandidateSearch) -> std::expected { if (selector.candidates.empty()) { @@ -682,6 +697,7 @@ std::expected parse_string(std::string_view content, spec.shortName = selector.candidates.front().shortName; spec.candidates = selector.candidates; spec.legacyDottedKey = legacyDottedKey; + spec.legacyCandidateSearch = legacyCandidateSearch; auto key = selector.stableMapKey; if (value.is_string()) { @@ -734,9 +750,12 @@ std::expected parse_string(std::string_view content, auto mapKey = mapPrefix.empty() ? k : std::format("{}.{}", mapPrefix, k); - auto selector = mcpp::pm::make_direct_dependency_selector( - ns, k, mapKey); - if (auto r = assign_dep(section, out, selector, v, false); !r) + auto selectorText = std::format("{}.{}", ns, k); + auto selector = parse_dep_selector( + section, selectorText, mapKey); + if (!selector) return std::unexpected(selector.error()); + if (auto r = assign_dep( + section, out, *selector, v, false, false); !r) return r; continue; } @@ -774,10 +793,11 @@ std::expected parse_string(std::string_view content, : std::format("{}.{}", selectorPrefix, k); if (v.is_string() || (v.is_table() && looks_like_inline_dep_spec(v.as_table()))) { - auto selector = mcpp::pm::resolve_dependency_selector( - selectorText, - mcpp::pm::DependencySelectorMode::OmittedMcpplibsPriority); - if (auto r = assign_dep(section, out, selector, v, false); !r) + auto selector = parse_dep_selector( + section, selectorText, selectorText); + if (!selector) return std::unexpected(selector.error()); + if (auto r = assign_dep( + section, out, *selector, v, false, true); !r) return r; continue; } @@ -803,21 +823,15 @@ std::expected parse_string(std::string_view content, -> std::expected { for (auto& [k, v] : tt) { - // (1) string value → flat default-ns short version, or - // (3) legacy "ns.name" = "ver" (dotted key). + // A string value is either a bare default-namespace selector or a + // quoted dotted selector. Both go through the same exact parser; + // quoted dotted syntax is retained only as a source-shape marker. if (v.is_string()) { - if (k.find('.') != std::string::npos) { - auto legacyKey = mcpp::pm::compat::split_legacy_dependency_key(k); - auto selector = mcpp::pm::make_direct_dependency_selector( - legacyKey.namespace_, legacyKey.shortName, k); - if (auto r = assign_dep(section, out, selector, v, - legacyKey.legacyDottedKey); !r) - return r; - continue; - } - auto selector = mcpp::pm::resolve_dependency_selector( - k, mcpp::pm::DependencySelectorMode::OmittedMcpplibsPriority); - if (auto r = assign_dep(section, out, selector, v, false); !r) + auto selector = parse_dep_selector(section, k, k); + if (!selector) return std::unexpected(selector.error()); + const bool quotedDotted = k.find('.') != std::string::npos; + if (auto r = assign_dep( + section, out, *selector, v, quotedDotted, false); !r) return r; continue; } @@ -832,20 +846,14 @@ std::expected parse_string(std::string_view content, // (1') inline dep spec under the default namespace, e.g. // frob = { path = "..." } or // "mcpplibs.cmdline" = { version = "0.0.2" } - // The latter is the legacy dotted-key form; same treatment as (3). + // The latter is the legacy quoted source shape, but carries the + // same exact PackageId as every other selector surface. if (looks_like_inline_dep_spec(sub)) { - if (k.find('.') != std::string::npos) { - auto legacyKey = mcpp::pm::compat::split_legacy_dependency_key(k); - auto selector = mcpp::pm::make_direct_dependency_selector( - legacyKey.namespace_, legacyKey.shortName, k); - if (auto r = assign_dep(section, out, selector, v, - legacyKey.legacyDottedKey); !r) - return r; - continue; - } - auto selector = mcpp::pm::resolve_dependency_selector( - k, mcpp::pm::DependencySelectorMode::OmittedMcpplibsPriority); - if (auto r = assign_dep(section, out, selector, v, false); !r) + auto selector = parse_dep_selector(section, k, k); + if (!selector) return std::unexpected(selector.error()); + const bool quotedDotted = k.find('.') != std::string::npos; + if (auto r = assign_dep( + section, out, *selector, v, quotedDotted, false); !r) return r; continue; } @@ -853,9 +861,8 @@ std::expected parse_string(std::string_view content, // (2) namespaced or nested subtable. // // Explicit tables such as `[dependencies.acme]` are namespace - // roots. Dotted keys written inside the single dependency table, - // such as `[dependencies] capi.lua = "0.0.3"`, are ordered - // selectors: mcpplibs.capi/lua first, then capi/lua. + // roots. Dotted keys inside the single dependency table are exact + // selectors too: `capi.lua` means only `(capi, lua)`. if (is_namespace_table(section, k)) { if (auto r = load_nested_dep_table(section, out, k, k, sub); !r) return r; @@ -1306,23 +1313,14 @@ std::expected parse_string(std::string_view content, if (auto* wdeps = doc->get_table("workspace.dependencies")) { for (auto& [k, v] : *wdeps) { if (v.is_string()) { - if (k.find('.') != std::string::npos) { - auto depKey = mcpp::pm::compat::split_legacy_dependency_key(k); - auto selector = mcpp::pm::make_direct_dependency_selector( - depKey.namespace_, depKey.shortName, k); - if (auto r = assign_dep("workspace.dependencies", - m.workspace.dependencies, - selector, v, - depKey.legacyDottedKey); !r) { - return std::unexpected(r.error()); - } - continue; - } - auto selector = mcpp::pm::resolve_dependency_selector( - k, mcpp::pm::DependencySelectorMode::OmittedMcpplibsPriority); + auto selector = parse_dep_selector( + "workspace.dependencies", k, k); + if (!selector) return std::unexpected(selector.error()); if (auto r = assign_dep("workspace.dependencies", m.workspace.dependencies, - selector, v, false); !r) { + *selector, v, + k.find('.') != std::string::npos, + false); !r) { return std::unexpected(r.error()); } continue; @@ -1338,23 +1336,14 @@ std::expected parse_string(std::string_view content, // selector path components and silently mis-files the entry // under a key like "ylib.path" instead of "ylib". if (looks_like_inline_dep_spec(sub)) { - if (k.find('.') != std::string::npos) { - auto depKey = mcpp::pm::compat::split_legacy_dependency_key(k); - auto selector = mcpp::pm::make_direct_dependency_selector( - depKey.namespace_, depKey.shortName, k); - if (auto r = assign_dep("workspace.dependencies", - m.workspace.dependencies, - selector, v, - depKey.legacyDottedKey); !r) { - return std::unexpected(r.error()); - } - continue; - } - auto selector = mcpp::pm::resolve_dependency_selector( - k, mcpp::pm::DependencySelectorMode::OmittedMcpplibsPriority); + auto selector = parse_dep_selector( + "workspace.dependencies", k, k); + if (!selector) return std::unexpected(selector.error()); if (auto r = assign_dep("workspace.dependencies", m.workspace.dependencies, - selector, v, false); !r) { + *selector, v, + k.find('.') != std::string::npos, + false); !r) { return std::unexpected(r.error()); } continue; diff --git a/src/manifest/xpkg.cppm b/src/manifest/xpkg.cppm index 25019339..b18521ea 100644 --- a/src/manifest/xpkg.cppm +++ b/src/manifest/xpkg.cppm @@ -146,11 +146,9 @@ xpkg_name_form_violation_from_lua(std::string_view luaContent); // WHY THIS IS ONE FUNCTION — the rule it enforces is "the descriptor the // identity gate accepted supplies BOTH halves of the address". Deriving the // name from the descriptor while taking the namespace from the request is what -// made every bare-name dependency served by a `compat` descriptor -// uninstallable the moment the index migrated to short names: the gate -// accepted `(compat, gtest)` (a bare request resolves against the default -// namespace AND `compat`, see `xpkg_lua_identity_matches`), but the wire target -// went out as `mcpplibs:gtest`, which no index is keyed by. The old spelling +// made an explicitly selected `compat` descriptor uninstallable when request +// and descriptor identity drifted: the wire target went out as +// `mcpplibs:gtest`, which no index is keyed by. The old spelling // only ever worked because the literal `name` happened to read `compat.gtest`, // so a hardcoded `compat.` retry caught it. // @@ -291,6 +289,20 @@ std::string closest_known_xpkg_key(std::string_view unknownKey) { namespace { +std::expected +parse_xpkg_dependency_selector(std::string_view spelling, + const std::filesystem::path& sourcePath) +{ + auto parsed = mcpp::pm::parse_package_selector(spelling); + if (!parsed) { + return std::unexpected(ManifestError{ + parsed.error().message, sourcePath, 0, 0}); + } + auto coordinate = mcpp::pm::normalize_package_selector(*parsed); + return mcpp::pm::make_direct_dependency_selector( + coordinate.namespace_, coordinate.shortName, spelling); +} + struct LuaCursor { std::string_view text; std::size_t pos = 0; @@ -803,13 +815,12 @@ bool xpkg_lua_identity_matches(std::string_view luaContent, // derives the namespace from the descriptor, so a name match is enough. if (ns.empty()) return true; - // Unqualified / default-namespace request: resolve the name against the - // default namespace search path — the default namespace itself, then the - // `compat` wrapper namespace (`kCompatNamespace`, shared with the candidate - // generator). A legacy no-namespace descriptor is admitted under the flag. + // Namespace omission has already normalized to the one default namespace. + // A `compat` package is a different identity and must be selected + // explicitly. A legacy no-namespace descriptor is admitted only under the + // compatibility flag because old default-index descriptors omitted ns. if (ns == kDefaultNamespace) { return id.ns == kDefaultNamespace - || id.ns == kCompatNamespace || (allowLegacyBareDefault && id.ns.empty()); } @@ -1508,15 +1519,21 @@ synthesize_from_xpkg_lua(std::string_view luaContent, auto dver = cur.read_string(); DependencySpec spec; spec.version = dver; - auto selector = mcpp::pm::resolve_dependency_selector( - dname, - mcpp::pm::DependencySelectorMode::OmittedMcpplibsPriority); - if (!selector.candidates.empty()) { - spec.namespace_ = selector.candidates.front().namespace_; - spec.shortName = selector.candidates.front().shortName; - spec.candidates = std::move(selector.candidates); - m.featureDeps[fname][selector.stableMapKey] = std::move(spec); - } + auto selector = parse_xpkg_dependency_selector( + dname, m.sourcePath); + if (!selector) + return std::unexpected(selector.error()); + spec.namespace_ = + selector->candidates.front().namespace_; + spec.shortName = + selector->candidates.front().shortName; + spec.candidates = std::move(selector->candidates); + spec.legacyCandidateSearch = + dname.find('.') != std::string::npos + && !dname.starts_with(std::string( + mcpp::pm::kDefaultNamespace) + "."); + m.featureDeps[fname][selector->stableMapKey] = + std::move(spec); cur.skip_ws_and_comments(); } cur.consume('}'); @@ -1668,15 +1685,18 @@ synthesize_from_xpkg_lua(std::string_view luaContent, spec.tools = std::move(dtools); spec.hostModule = dhostModule; spec.reexport = dreexport; - auto selector = mcpp::pm::resolve_dependency_selector( - dname, - mcpp::pm::DependencySelectorMode::OmittedMcpplibsPriority); - if (!selector.candidates.empty()) { - spec.namespace_ = selector.candidates.front().namespace_; - spec.shortName = selector.candidates.front().shortName; - spec.candidates = std::move(selector.candidates); - m.dependencies[selector.stableMapKey] = std::move(spec); - } + auto selector = parse_xpkg_dependency_selector( + dname, m.sourcePath); + if (!selector) + return std::unexpected(selector.error()); + spec.namespace_ = selector->candidates.front().namespace_; + spec.shortName = selector->candidates.front().shortName; + spec.candidates = std::move(selector->candidates); + spec.legacyCandidateSearch = + dname.find('.') != std::string::npos + && !dname.starts_with(std::string( + mcpp::pm::kDefaultNamespace) + "."); + m.dependencies[selector->stableMapKey] = std::move(spec); } cur.skip_ws_and_comments(); } diff --git a/src/pm/commands.cppm b/src/pm/commands.cppm index f0ff07c7..a45cdb73 100644 --- a/src/pm/commands.cppm +++ b/src/pm/commands.cppm @@ -75,60 +75,88 @@ export namespace mcpp::pm::commands { inline int cmd_add(const mcpplibs::cmdline::ParsedArgs& parsed) { std::string spec = parsed.positional(0); if (spec.empty()) { - mcpp::ui::error("usage: mcpp add [:][@]"); + mcpp::ui::error("usage: mcpp add [.]@"); return 2; } - // Split @ tail. - std::string nameSpec, version; - if (auto at = spec.find('@'); at == std::string::npos) { - nameSpec = spec; + // Split exactly one optional @ tail before doing config or index + // I/O. Multiple delimiters are never reinterpreted as part of a package + // identity or version. + std::string rawNameSpec, version; + auto at = spec.find('@'); + if (at == std::string::npos) { + rawNameSpec = spec; } else { - nameSpec = spec.substr(0, at); - version = spec.substr(at + 1); + if (spec.find('@', at + 1) != std::string::npos) { + mcpp::ui::error(std::format( + "invalid package spec '{}': multiple '@' delimiters", spec)); + return 2; + } + rawNameSpec = spec.substr(0, at); + version = spec.substr(at + 1); } - // Split :. The colon form is explicit namespace syntax and is - // written as [dependencies.]. Without a colon, keep the user's - // selector spelling in the single [dependencies] table; dotted selectors - // are resolved later by the manifest parser's candidate rules. - std::string ns, shortName; - bool explicitNamespace = false; - if (auto col = nameSpec.find(':'); col != std::string::npos) { - explicitNamespace = true; - ns = nameSpec.substr(0, col); - shortName = nameSpec.substr(col + 1); - } else { - ns = std::string{mcpp::manifest::kDefaultNamespace}; - shortName = nameSpec; + if (version.empty()) { + mcpp::ui::error(std::format( + "package version required: `mcpp add {}@` (M2 supports exact-version only)", + rawNameSpec)); + return 2; + } + for (unsigned char ch : version) { + if (ch < 0x20 || ch == 0x7f || ch == '"' || ch == '\\') { + mcpp::ui::error(std::format( + "invalid package version in '{}': unsafe TOML string character", + spec)); + return 2; + } + } + + // Canonical CLI syntax is `[ns.]name@version`. Keep the historical + // `ns:name@version` form for one migration release, but normalize it + // before validation, lookup, diagnostics, or manifest mutation. + bool legacyColon = false; + std::string selectorSpelling = rawNameSpec; + if (auto col = rawNameSpec.find(':'); col != std::string::npos) { + if (rawNameSpec.find(':', col + 1) != std::string::npos + || col == 0 || col + 1 == rawNameSpec.size()) { + mcpp::ui::error(std::format( + "invalid package spec '{}': expected :", spec)); + return 2; + } + legacyColon = true; + selectorSpelling = std::format("{}.{}", + rawNameSpec.substr(0, col), rawNameSpec.substr(col + 1)); } - if (shortName.empty()) { - mcpp::ui::error(std::format("invalid spec '{}': empty package name", spec)); + + auto parsedSelector = mcpp::pm::parse_package_selector(selectorSpelling); + if (!parsedSelector) { + mcpp::ui::error(parsedSelector.error().message); return 2; } + auto coordinate = mcpp::pm::normalize_package_selector(*parsedSelector); + const std::string canonicalSelector = + mcpp::pm::format_package_selector(coordinate); + const std::string& ns = coordinate.namespace_; + const std::string& shortName = coordinate.shortName; + + if (legacyColon) { + mcpp::ui::warning(std::format( + "package selector '{}' is deprecated; use `mcpp add {}@{}`", + rawNameSpec, canonicalSelector, version)); + } auto root = mcpp::project::find_manifest_root(std::filesystem::current_path()); if (!root) { mcpp::ui::error("no mcpp.toml in current dir or parents"); return 2; } auto manifestPath = *root / "mcpp.toml"; - if (version.empty()) { - mcpp::ui::error(std::format( - "package version required: `mcpp add {}@` (M2 supports exact-version only)", - spec)); - return 2; - } - // ── Existence gate (#305) ────────────────────────────────────────── // Refuse to write a dependency no index can serve, so a typo fails here // instead of halfway into the next `mcpp build`. Two rules keep the gate // from refusing packages that are perfectly real: // - // • It probes the SAME candidates the manifest parser will derive from - // the key about to be written. A dotted selector is a namespace path, - // not a name: `capi.lua` means `(mcpplibs.capi, lua)` then - // `(capi, lua)`, and a literal `(mcpplibs, "capi.lua")` probe can - // never match one — `package.name` is a single atomic segment - // (SPEC-001 §3.2), so nothing in any index is named "capi.lua". + // • It probes the SAME exact coordinate the manifest parser will derive + // from the key about to be written. A dotted selector is a namespace + // path, not an ordered search: `capi.lua` means only `(capi, lua)`. // • It reads through mcpp.pm.index_route, the same routing // `mcpp.build.prepare` resolves dependencies with. A package served by // a project `[indices]` entry therefore counts as present, and a @@ -136,11 +164,8 @@ inline int cmd_add(const mcpplibs::cmdline::ParsedArgs& parsed) { // rather than rejected: refusing an add is only correct when absence // was actually proven. { - auto selector = explicitNamespace - ? mcpp::pm::make_direct_dependency_selector(ns, shortName, nameSpec) - : mcpp::pm::resolve_dependency_selector( - nameSpec, - mcpp::pm::DependencySelectorMode::OmittedMcpplibsPriority); + auto selector = mcpp::pm::make_direct_dependency_selector( + ns, shortName, canonicalSelector); auto cfg = mcpp::config::load_or_init( /*quiet=*/false, mcpp::fetcher::make_bootstrap_progress_callback()); @@ -151,6 +176,29 @@ inline int cmd_add(const mcpplibs::cmdline::ParsedArgs& parsed) { auto indices = mcpp::pm::effective_indices(*root); mcpp::pm::IndexRoute route{ &indices, *root, &*cfg }; + + // One release train of diagnostics for the former dotted-candidate + // rule. The old primary is never appended to the real lookup: its + // presence only earns a warning with two copyable exact selectors. + if (auto old = mcpp::pm::legacy_prefixed_coordinate(coordinate)) { + auto oldSelector = mcpp::pm::make_direct_dependency_selector( + old->namespace_, old->shortName, + mcpp::pm::format_package_selector(*old)); + auto oldFound = mcpp::pm::lookup_descriptor( + route, oldSelector.candidates); + if (oldFound.hit) { + mcpp::ui::warning(std::format( + "package selector '{}' now resolves exactly to '{}'; an " + "older mcpp would select the existing package '{}'. Use " + "'{}' to keep the old identity or keep '{}' for the new " + "exact identity", + selectorSpelling, canonicalSelector, + mcpp::pm::format_package_selector(*old), + mcpp::pm::format_package_selector(*old), + canonicalSelector)); + } + } + auto found = mcpp::pm::lookup_descriptor(route, selector.candidates); // Does the shared registry answer for any identity we tried? It is the @@ -170,7 +218,7 @@ inline int cmd_add(const mcpplibs::cmdline::ParsedArgs& parsed) { if (!found.hit && found.conclusive && registryInvolved) { auto xlEnv = mcpp::config::make_xlings_env(*cfg); auto d = mcpp::pm::decide_for_miss( - mcpp::pm::policy_for(*cfg), xlEnv, nameSpec); + mcpp::pm::policy_for(*cfg), xlEnv, canonicalSelector); if (auto r = mcpp::pm::apply(d, xlEnv); !r) mcpp::ui::warning(r.error()); if (d.shouldRefresh) @@ -195,15 +243,17 @@ inline int cmd_add(const mcpplibs::cmdline::ParsedArgs& parsed) { } mcpp::ui::error(std::format( "package '{}' not found in any configured index\n tried: {}{}", - nameSpec, detail::format_tried(selector.candidates), hint)); + canonicalSelector, detail::format_tried(selector.candidates), + hint)); return 2; } if (!found.hit) { mcpp::ui::warning(std::format( "'{}' could not be verified — no readable index covers that " - "namespace yet; adding it unchecked", nameSpec)); + "namespace yet; adding it unchecked", canonicalSelector)); } else { - detail::warn_unpublished_version(found.hit->lua, nameSpec, version); + detail::warn_unpublished_version( + found.hit->lua, canonicalSelector, version); } } @@ -219,30 +269,76 @@ inline int cmd_add(const mcpplibs::cmdline::ParsedArgs& parsed) { // `mcpp test`, never linked into `mcpp build` app binaries). const bool dev = parsed.is_flag_set("dev"); const std::string table = dev ? "dev-dependencies" : "dependencies"; - const bool isDefaultNs = !explicitNamespace - || ns == mcpp::manifest::kDefaultNamespace; + const bool isDefaultNs = ns == mcpp::manifest::kDefaultNamespace; const std::string section = isDefaultNs ? std::format("[{}]", table) : std::format("[{}.{}]", table, ns); - const std::string key = explicitNamespace ? shortName : nameSpec; + const std::string key = shortName; + + // Before writing the canonical namespace-subtable form, remove an + // equivalent retained flat spelling from the exact base table. Leaving + // both would make one PackageId appear twice in source and let TOML table + // traversal order decide which version wins. A same-short bare key is a + // different default-namespace package and is deliberately untouched. + if (!isDefaultNs) { + const auto baseSection = std::format("[{}]", table); + if (auto basePos = text.find(baseSection); + basePos != std::string::npos) { + auto bodyStart = text.find('\n', basePos); + if (bodyStart == std::string::npos) bodyStart = text.size(); + auto sectionEnd = text.find("\n[", bodyStart); + if (sectionEnd == std::string::npos) sectionEnd = text.size(); + for (const auto& needle : { + std::format("\n{} = ", canonicalSelector), + std::format("\n\"{}\" = ", canonicalSelector), + }) { + auto entry = text.find(needle, bodyStart); + if (entry == std::string::npos || entry >= sectionEnd) continue; + auto lineEnd = text.find('\n', entry + 1); + if (lineEnd == std::string::npos) lineEnd = text.size(); + text.erase(entry, lineEnd - entry); + break; + } + } + } + auto pos = text.find(section); if (pos == std::string::npos) { if (!text.empty() && text.back() != '\n') text += "\n"; text += std::format("\n{}\n{} = \"{}\"\n", section, key, version); } else { - auto nl = text.find('\n', pos); - if (nl == std::string::npos) nl = text.size(); - text.insert(nl, std::format("\n{} = \"{}\"", key, version)); + auto bodyStart = text.find('\n', pos); + if (bodyStart == std::string::npos) bodyStart = text.size(); + auto sectionEnd = text.find("\n[", bodyStart); + if (sectionEnd == std::string::npos) sectionEnd = text.size(); + + bool replaced = false; + for (const auto& needle : { + std::format("\n{} = ", key), + std::format("\n\"{}\" = ", key), + }) { + auto entry = text.find(needle, bodyStart); + if (entry == std::string::npos || entry >= sectionEnd) continue; + auto lineStart = entry + 1; + auto lineEnd = text.find('\n', lineStart); + if (lineEnd == std::string::npos) lineEnd = text.size(); + text.replace(lineStart, lineEnd - lineStart, + std::format("{} = \"{}\"", key, version)); + replaced = true; + break; + } + if (!replaced) { + text.insert(bodyStart, + std::format("\n{} = \"{}\"", key, version)); + } } { std::ofstream os(manifestPath); os << text; } - std::string display = explicitNamespace - ? (isDefaultNs ? shortName : std::format("{}:{}", ns, shortName)) - : nameSpec; - mcpp::ui::status("Adding", std::format("{} v{} to {}", display, version, table)); + mcpp::ui::status("Adding", std::format( + "{} v{} to {}", canonicalSelector, version, table)); std::println(""); std::println("Run `mcpp build` to fetch and build with the new dependency."); return 0; @@ -263,20 +359,40 @@ inline int cmd_remove(const mcpplibs::cmdline::ParsedArgs& parsed) { std::stringstream ss; ss << in.rdbuf(); std::string text = ss.str(); - // Accept the same forms as `mcpp add`: bare/dotted selector in the single - // [dependencies] table, or explicit `:` subtable syntax. - std::string ns, shortName; - bool explicitNamespace = false; + // Accept the same canonical selector as `mcpp add`; retain `ns:name` as a + // one-release migration alias. + bool legacyColon = false; + std::string selectorSpelling = name; if (auto col = name.find(':'); col != std::string::npos) { - explicitNamespace = true; - ns = name.substr(0, col); shortName = name.substr(col + 1); - } else { - ns = std::string{mcpp::manifest::kDefaultNamespace}; - shortName = name; + if (name.find(':', col + 1) != std::string::npos + || col == 0 || col + 1 == name.size()) { + mcpp::ui::error(std::format( + "invalid package selector '{}': expected :", + name)); + return 2; + } + legacyColon = true; + selectorSpelling = std::format( + "{}.{}", name.substr(0, col), name.substr(col + 1)); + } + auto parsedSelector = mcpp::pm::parse_package_selector(selectorSpelling); + if (!parsedSelector) { + mcpp::ui::error(parsedSelector.error().message); + return 2; + } + auto coordinate = mcpp::pm::normalize_package_selector(*parsedSelector); + const auto canonicalSelector = + mcpp::pm::format_package_selector(coordinate); + const auto& ns = coordinate.namespace_; + const auto& shortName = coordinate.shortName; + const bool isDefaultNs = ns == mcpp::manifest::kDefaultNamespace; + const std::string singleTableKey = isDefaultNs + ? shortName : canonicalSelector; + if (legacyColon) { + mcpp::ui::warning(std::format( + "package selector '{}' is deprecated; use `mcpp remove {}`", + name, canonicalSelector)); } - const bool isDefaultNs = !explicitNamespace - || ns == mcpp::manifest::kDefaultNamespace; - const std::string singleTableKey = explicitNamespace ? shortName : name; bool changed = false; auto erase_line_at = [&](std::size_t p) { @@ -288,16 +404,24 @@ inline int cmd_remove(const mcpplibs::cmdline::ParsedArgs& parsed) { changed = true; }; - // Try bare ` = ` and quoted `"" = ` (default-ns flat form). - if (isDefaultNs) { + // Try the flat form first: a bare default key or a retained quoted dotted + // key from an older manifest. Search only the exact [dependencies] body; + // a global line match can otherwise delete a same-name dev/build dep that + // happens to appear earlier in the file. + if (auto headerPos = text.find("[dependencies]"); + headerPos != std::string::npos) { + auto bodyStart = text.find('\n', headerPos); + if (bodyStart == std::string::npos) bodyStart = text.size(); + auto sectionEnd = text.find("\n[", bodyStart); + if (sectionEnd == std::string::npos) sectionEnd = text.size(); for (const auto& needle : { - std::format("\n{} = ", singleTableKey), - std::format("\n\"{}\" = ", singleTableKey), - }) { - if (auto p = text.find(needle); p != std::string::npos) { - erase_line_at(p + 1); - break; - } + std::format("\n{} = ", singleTableKey), + std::format("\n\"{}\" = ", singleTableKey), + }) { + auto p = text.find(needle, bodyStart); + if (p == std::string::npos || p >= sectionEnd) continue; + erase_line_at(p + 1); + break; } } @@ -358,15 +482,6 @@ inline int cmd_remove(const mcpplibs::cmdline::ParsedArgs& parsed) { erase_from_subtable(ns, shortName); } - // Backward-compatible removal: before dotted selectors were preserved in - // the single table, `mcpp add acme.util` wrote `[dependencies.acme] util`. - // Keep `mcpp remove acme.util` able to clean that old shape. - if (!changed && !explicitNamespace) { - if (auto dot = name.find('.'); dot != std::string::npos) { - erase_from_subtable(name.substr(0, dot), name.substr(dot + 1)); - } - } - // Legacy: `[dependencies.] ...` — pre-namespace inline-spec subtable // shape (e.g. when path/git deps were authored as their own subtable). We // only honour this for the default-ns input form to avoid colliding with @@ -385,18 +500,20 @@ inline int cmd_remove(const mcpplibs::cmdline::ParsedArgs& parsed) { } if (!changed) { - mcpp::ui::error(std::format("no dependency '{}' in mcpp.toml", name)); + mcpp::ui::error(std::format( + "no dependency '{}' in mcpp.toml", canonicalSelector)); return 1; } std::ofstream os(manifestPath); os << text; - mcpp::ui::status("Removing", std::format("{} from dependencies", name)); + mcpp::ui::status("Removing", std::format( + "{} from dependencies", canonicalSelector)); // Also clean lockfile entry if present auto lockPath = *root / "mcpp.lock"; if (std::filesystem::exists(lockPath)) { if (auto lock = mcpp::lockfile::load(lockPath); lock) { std::erase_if(lock->packages, - [&](const auto& p) { return p.name == name; }); + [&](const auto& p) { return p.name == canonicalSelector; }); (void)mcpp::lockfile::write(*lock, lockPath); } } diff --git a/src/pm/dep_spec.cppm b/src/pm/dep_spec.cppm index 88abc594..c84ddbf0 100644 --- a/src/pm/dep_spec.cppm +++ b/src/pm/dep_spec.cppm @@ -90,27 +90,30 @@ struct DependencySpec { bool defaultFeatures = true; // consumer opt-out: `default-features = false` // suppresses the dep's own [features].default seed // (Cargo parity). Explicit `features = [...]` still apply. - std::vector candidates; // ordered lookup candidates + // Canonical dependency parsing writes exactly one coordinate. Kept as a + // vector for lock/source compatibility with already-materialized legacy + // manifests during the migration release. + std::vector candidates; bool inheritWorkspace = false; // .workspace = true bool legacyDottedKey = false; // parsed from legacy "ns.name" flat key + // One-release migration marker for the former unquoted dotted-selector + // search (`capi.lua` tried mcpplibs.capi before capi). New exact selectors + // written as namespace subtables never set this bit. + bool legacyCandidateSearch = false; bool isPath() const { return !path.empty(); } bool isGit() const { return !git.empty(); } bool isVersion() const { return !isPath() && !isGit() && !version.empty(); } }; -// Default namespace for packages declared without an explicit one — the -// mcpplibs "root". Bare `gtest = "1.15.2"` becomes `(mcpp, gtest)`. +// Default namespace for selectors written without an explicit one. Bare +// `cmdline = "0.0.2"` becomes exactly `(mcpplibs, cmdline)`. inline constexpr std::string_view kDefaultNamespace = "mcpplibs"; // The `compat` namespace holds upstream-library wrappers (compat.zlib, -// compat.gtest, …). It is the one non-default namespace that an unqualified -// (default-namespace) dependency name reaches: bare `gtest` → `compat.gtest`. -// Centralized here so the candidate generator (xpkg_lua_candidates) and the -// identity gate (xpkg_lua_identity_matches) share one source of truth instead -// of each hard-coding the literal "compat". See the design doc's §4.1 for the -// fuller "unqualified namespace search path" direction this is a seed of. +// compat.gtest, …). It must be selected explicitly; the constant remains +// centralized for filename/store compatibility helpers. inline constexpr std::string_view kCompatNamespace = "compat"; } // namespace mcpp::pm diff --git a/src/pm/dependency_selector.cppm b/src/pm/dependency_selector.cppm index 36b8091a..90c484cb 100644 --- a/src/pm/dependency_selector.cppm +++ b/src/pm/dependency_selector.cppm @@ -1,5 +1,5 @@ -// mcpp.pm.dependency_selector — parse user dependency selectors into -// ordered package-coordinate candidates. +// mcpp.pm.dependency_selector — parse user dependency selectors into one +// canonical package coordinate. export module mcpp.pm.dependency_selector; @@ -8,15 +8,21 @@ import mcpp.pm.dep_spec; export namespace mcpp::pm { -enum class DependencySelectorMode { - OmittedMcpplibsPriority, -}; - struct DependencySelector { std::vector candidates; std::string stableMapKey; }; +struct PackageSelector { + std::optional namespace_; + std::string name; + std::string spelling; +}; + +struct SelectorError { + std::string message; +}; + inline std::vector split_dependency_selector(std::string_view selector) { std::vector segments; @@ -56,6 +62,92 @@ inline DependencySelector make_direct_dependency_selector( return out; } +inline std::expected +parse_package_selector(std::string_view spelling) +{ + if (spelling.empty()) { + return std::unexpected(SelectorError{ + .message = "package selector is empty", + }); + } + + auto segments = split_dependency_selector(spelling); + for (auto const& segment : segments) { + if (segment.empty()) { + return std::unexpected(SelectorError{ + .message = std::format( + "invalid package selector '{}': empty dotted segment", + spelling), + }); + } + for (unsigned char ch : segment) { + const bool bareKeyChar = (ch >= 'a' && ch <= 'z') + || (ch >= 'A' && ch <= 'Z') + || (ch >= '0' && ch <= '9') || ch == '_' || ch == '-'; + if (!bareKeyChar) { + return std::unexpected(SelectorError{ + .message = std::format( + "invalid package selector '{}': segment '{}' may only " + "contain ASCII letters, digits, '-' and '_'", + spelling, segment), + }); + } + } + } + + PackageSelector out{ + .name = segments.back(), + .spelling = std::string(spelling), + }; + if (segments.size() > 1) { + out.namespace_ = join_dependency_segments( + segments, 0, segments.size() - 1); + } + return out; +} + +inline DependencyCoordinate normalize_package_selector( + const PackageSelector& selector, + std::string_view defaultNamespace = kDefaultNamespace) +{ + return DependencyCoordinate{ + .namespace_ = selector.namespace_.value_or( + std::string(defaultNamespace)), + .shortName = selector.name, + }; +} + +inline std::string +format_package_selector(const DependencyCoordinate& coordinate) +{ + if (coordinate.namespace_.empty() + || coordinate.namespace_ == kDefaultNamespace) { + return coordinate.shortName; + } + return std::format("{}.{}", coordinate.namespace_, + coordinate.shortName); +} + +// During the one-release exact-selector migration, this is the coordinate an +// older mcpp would have tried first for a compact dotted selector. It is +// diagnostic/lock-compatibility data only; normal resolution never appends it +// to the exact candidate list. +inline std::optional +legacy_prefixed_coordinate(const DependencyCoordinate& exact) +{ + if (exact.namespace_.empty() + || exact.namespace_ == kDefaultNamespace + || exact.namespace_.starts_with( + std::string(kDefaultNamespace) + ".")) { + return std::nullopt; + } + return DependencyCoordinate{ + .namespace_ = std::format("{}.{}", kDefaultNamespace, + exact.namespace_), + .shortName = exact.shortName, + }; +} + // #243 Cargo dep/feat forwarding. Split a `[features]` implied-feature token: // a token containing '/' means "when this feature is active, request // from dependency " (Cargo `[features] F = ["dep/feat"]`). @@ -75,48 +167,14 @@ split_feature_forward_token(std::string_view token) return std::pair{std::string(depKey), std::string(depFeat)}; } -inline DependencySelector resolve_dependency_selector( - std::string_view selector, - DependencySelectorMode) +inline DependencySelector resolve_dependency_selector(std::string_view selector) { DependencySelector out; out.stableMapKey = std::string(selector); - auto segments = split_dependency_selector(selector); - if (segments.empty()) return out; - - if (segments.size() == 1) { - out.candidates.push_back(DependencyCoordinate{ - .namespace_ = std::string(kDefaultNamespace), - .shortName = segments.front(), - }); - out.candidates.push_back(DependencyCoordinate{ - .namespace_ = {}, - .shortName = segments.front(), - }); - return out; - } - - const auto shortName = segments.back(); - const auto nsWithoutShort = join_dependency_segments( - segments, 0, segments.size() - 1); - - if (segments.front() == kDefaultNamespace) { - out.candidates.push_back(DependencyCoordinate{ - .namespace_ = nsWithoutShort, - .shortName = shortName, - }); - return out; - } - - out.candidates.push_back(DependencyCoordinate{ - .namespace_ = std::format("{}.{}", kDefaultNamespace, nsWithoutShort), - .shortName = shortName, - }); - out.candidates.push_back(DependencyCoordinate{ - .namespace_ = nsWithoutShort, - .shortName = shortName, - }); + auto parsed = parse_package_selector(selector); + if (!parsed) return out; + out.candidates.push_back(normalize_package_selector(*parsed)); return out; } diff --git a/src/pm/index_route.cppm b/src/pm/index_route.cppm index 8594c43f..328b8383 100644 --- a/src/pm/index_route.cppm +++ b/src/pm/index_route.cppm @@ -69,9 +69,9 @@ struct Lookup { bool conclusive = true; }; -// Walk ordered candidates and return the first descriptor that DECLARES the -// requested identity, applying the same two rules prepare's dependency -// disambiguation applies. +// Resolve the supplied coordinate list and return the first descriptor that +// DECLARES the requested identity. Canonical selectors supply exactly one; +// the vector shape remains for migration callers carrying an older lock. Lookup lookup_descriptor(const IndexRoute& route, const std::vector& candidates); diff --git a/tests/e2e/12_add_command.sh b/tests/e2e/12_add_command.sh index 34ddf32b..112981ae 100755 --- a/tests/e2e/12_add_command.sh +++ b/tests/e2e/12_add_command.sh @@ -1,8 +1,9 @@ #!/usr/bin/env bash # requires: # `mcpp add` modifies mcpp.toml [dependencies]. Default-namespace packages land -# as bare keys under [dependencies]; `ns:name` (non-default ns) uses a subtable; -# dotted selectors keep their spelling. A package no readable index can serve is +# as bare keys under [dependencies]; canonical `ns.name` inputs use a subtable; +# the deprecated `ns:name` spelling is accepted only as a migration alias. A +# package no readable index can serve is # refused before mcpp.toml is touched (#305) — but only where absence is # provable, so the gate resolves the same candidates the manifest parser derives # and reads through the same index routing `mcpp build` resolves dependencies @@ -22,11 +23,12 @@ cd "$TMP" "$MCPP" new myapp > /dev/null cd myapp -# A local index carrying three packages: two in the DEFAULT namespace (so bare -# and `mcpplibs:`-qualified adds resolve against it) and one under `acme`. +# A local index carrying five packages: two in the DEFAULT namespace, one under +# `acme`, and a same-short-name pair under `capi` / `mcpplibs.capi` that proves +# exact dotted selection cannot be stolen by the old prefixed candidate. # Descriptors are filed under the first letter of their FILENAME, and publish # every OS so the version assertions below hold on all three CI platforms. -mkdir -p index/pkgs/w index/pkgs/g index/pkgs/a +mkdir -p index/pkgs/w index/pkgs/g index/pkgs/a index/pkgs/c index/pkgs/m cat > index/pkgs/w/widget.lua <<'EOF' package = { spec = "1", @@ -72,11 +74,42 @@ package = { }, } EOF +cat > index/pkgs/c/capi.lua.lua <<'EOF' +package = { + spec = "1", + namespace = "capi", + name = "lua", + description = "new exact dotted identity", + licenses = {"MIT"}, + type = "package", + xpm = { + linux = { ["5.4.7"] = { url = "https://example.invalid/lua.tar.gz" } }, + macosx = { ["5.4.7"] = { url = "https://example.invalid/lua.tar.gz" } }, + windows = { ["5.4.7"] = { url = "https://example.invalid/lua.zip" } }, + }, +} +EOF +cat > index/pkgs/m/mcpplibs.capi.lua.lua <<'EOF' +package = { + spec = "1", + namespace = "mcpplibs.capi", + name = "lua", + description = "old prefixed dotted candidate", + licenses = {"MIT"}, + type = "package", + xpm = { + linux = { ["9.9.9"] = { url = "https://example.invalid/old.tar.gz" } }, + macosx = { ["9.9.9"] = { url = "https://example.invalid/old.tar.gz" } }, + windows = { ["9.9.9"] = { url = "https://example.invalid/old.zip" } }, + }, +} +EOF cat >> mcpp.toml <<'EOF' [indices] default = { path = "index" } acme = { path = "index" } +capi = { path = "index" } EOF # (1) Default-namespace dep: bare name → unquoted key under [dependencies]. @@ -85,27 +118,46 @@ grep -qE '^\[dependencies\]' mcpp.toml || { cat mcpp.toml; echo "no [dependen grep -qE '^widget = "0\.1\.0"$' mcpp.toml || { cat mcpp.toml; echo "widget entry missing or quoted"; exit 1; } grep -qE '^"widget"' mcpp.toml && { cat mcpp.toml; echo "default-ns key should not be quoted"; exit 1; } -# (2) `:` where ns IS the default (mcpplibs) — still a bare key under +# (2) `.` where ns IS the default (mcpplibs) — still a bare key under # [dependencies], NOT [dependencies.mcpplibs]. Appends without duplicating the # section header. -"$MCPP" add mcpplibs:gadget@0.2.0 > /dev/null +"$MCPP" add mcpplibs.gadget@0.2.0 > /dev/null header_count=$(grep -cE '^\[dependencies\]$' mcpp.toml) [[ "$header_count" == "1" ]] || { cat mcpp.toml; echo "[dependencies] header duplicated"; exit 1; } grep -qE '^gadget = "0\.2\.0"$' mcpp.toml || { cat mcpp.toml; echo "gadget not set"; exit 1; } -# (3) Dotted selector input is preserved under the single [dependencies] table. -# `acme.util` is a NAMESPACE PATH, not a name: it resolves as (acme, util). A -# gate probing the literal short name "acme.util" would refuse this — no -# package.name ever contains a dot (SPEC-001 §3.2). +# (3) A dotted selector is one exact identity and is stored canonically as a +# namespace subtable. It must never become an ordered fallback search. "$MCPP" add acme.util@2.0.0 > /dev/null -grep -qE '^acme\.util = "2\.0\.0"$' mcpp.toml || { cat mcpp.toml; echo "acme.util selector entry missing"; exit 1; } - -# (4) Colon form remains explicit namespace syntax and uses a subtable. -"$MCPP" add acme:util@2.0.0 > /dev/null grep -qE '^\[dependencies\.acme\]$' mcpp.toml || { cat mcpp.toml; echo "missing [dependencies.acme] section"; exit 1; } grep -qE '^util = "2\.0\.0"$' mcpp.toml || { cat mcpp.toml; echo "util entry missing"; exit 1; } +! grep -qE '^acme\.util = ' mcpp.toml || { cat mcpp.toml; echo "dotted selector must not be stored as an ambiguous flat key"; exit 1; } + +# (4) The old colon spelling remains a one-release migration alias, gives a +# copyable canonical replacement, and writes the same canonical shape. +alias_out=$("$MCPP" add acme:util@2.0.0 2>&1) +[[ "$alias_out" == *"deprecated"* ]] || { echo "missing migration warning: $alias_out"; exit 1; } +[[ "$alias_out" == *"acme.util@2.0.0"* ]] || { echo "warning lacks canonical replacement: $alias_out"; exit 1; } + +# (5) `capi.lua` is exactly (capi,lua), even though the old first candidate +# (mcpplibs.capi,lua) exists with the requested 9.9.9 version. The version +# warning must come from capi:lua and the migration warning shows both complete +# selectors without silently selecting the sibling. +exact_out=$("$MCPP" add capi.lua@9.9.9 2>&1) +[[ "$exact_out" == *"available: 5.4.7"* ]] || { echo "sibling stole exact selector: $exact_out"; exit 1; } +[[ "$exact_out" == *"mcpplibs.capi.lua"* ]] || { echo "migration warning lacks old selector: $exact_out"; exit 1; } +[[ "$exact_out" == *"capi.lua"* ]] || { echo "migration warning lacks new selector: $exact_out"; exit 1; } +"$MCPP" add capi.lua@5.4.7 > /dev/null +grep -qE '^\[dependencies\.capi\]$' mcpp.toml || { cat mcpp.toml; echo "missing [dependencies.capi] section"; exit 1; } +grep -qE '^lua = "5\.4\.7"$' mcpp.toml || { cat mcpp.toml; echo "exact capi.lua entry missing"; exit 1; } -# (5) Dotted remove can still clean the old subtable shape for compatibility. +# Nested namespaces use the same parser for add and remove. +"$MCPP" add mcpplibs.capi.lua@9.9.9 > /dev/null +grep -qE '^\[dependencies\.mcpplibs\.capi\]$' mcpp.toml || { cat mcpp.toml; echo "nested namespace section missing"; exit 1; } +"$MCPP" remove mcpplibs.capi.lua > /dev/null +! grep -qE '^lua = "9\.9\.9"$' mcpp.toml || { cat mcpp.toml; echo "nested exact remove failed"; exit 1; } + +# (6) Dotted remove can still clean the old subtable shape for compatibility. cat >> mcpp.toml <<'EOF' [dependencies.legacy] @@ -114,14 +166,23 @@ EOF "$MCPP" remove legacy.old > /dev/null ! grep -qE '^old = "0\.1\.0"$' mcpp.toml || { cat mcpp.toml; echo "legacy.old was not removed"; exit 1; } -# (6) Reject missing version. +# (7) Reject missing version. err=$("$MCPP" add bareword 2>&1) && { echo "expected error for missing version"; exit 1; } [[ "$err" == *"version required"* ]] || { echo "wrong error: $err"; exit 1; } -# (7) Reject empty package name (e.g. `mcpp add :foo@1.0`). +# (8) Reject empty package name (e.g. `mcpp add :@1.0`). err=$("$MCPP" add ":@1.0" 2>&1) && { echo "expected error for empty package name"; exit 1; } -# (8) A package the index does not carry is refused, and mcpp.toml is left +# (9) Reject malformed/unsafe selectors before any index lookup or manifest +# mutation. These spellings must not be normalized into a different identity. +cp mcpp.toml "$TMP/before-invalid" +for invalid in 'ocornut..imgui@1.0.0' 'acme/util@1.0.0' 'acme\util@1.0.0' 'acme.util@' 'acme.util@1.0@2.0' 'widget@1.0"bad'; do + err=$("$MCPP" add "$invalid" 2>&1) && { echo "expected error for invalid selector: $invalid"; exit 1; } + [[ "$err" == *"invalid"* || "$err" == *"version required"* ]] || { echo "wrong error for $invalid: $err"; exit 1; } + diff -q "$TMP/before-invalid" mcpp.toml || { cat mcpp.toml; echo "mcpp.toml mutated for invalid selector: $invalid"; exit 1; } +done + +# (10) A package the index does not carry is refused, and mcpp.toml is left # exactly as it was. The error names the identities that were tried. cp mcpp.toml "$TMP/before" err=$("$MCPP" add definitely-not-a-real-package@9.9.9 2>&1) && { echo "expected error for missing package"; exit 1; } @@ -129,24 +190,24 @@ err=$("$MCPP" add definitely-not-a-real-package@9.9.9 2>&1) && { echo "expected [[ "$err" == *"tried:"* ]] || { echo "error should list the identities tried: $err"; exit 1; } diff -q "$TMP/before" mcpp.toml || { cat mcpp.toml; echo "mcpp.toml mutated for missing package"; exit 1; } -# (9) Same for an explicitly-namespaced miss in a readable index. -err=$("$MCPP" add acme:nope@1.0.0 2>&1) && { echo "expected error for missing acme package"; exit 1; } +# (11) Same for an explicitly-namespaced miss in a readable index. +err=$("$MCPP" add acme.nope@1.0.0 2>&1) && { echo "expected error for missing acme package"; exit 1; } [[ "$err" == *"(acme, nope)"* ]] || { echo "wrong error: $err"; exit 1; } diff -q "$TMP/before" mcpp.toml || { cat mcpp.toml; echo "mcpp.toml mutated for missing package"; exit 1; } -# (10) A namespace no readable index covers cannot be refuted, so the add goes +# (12) A namespace no readable index covers cannot be refuted, so the add goes # through unverified rather than failing. Refusing here would be worse than the # bug being fixed — `mcpp build` resolves such dependencies fine. -"$MCPP" add unknownidx:thing@1.0.0 > /dev/null +"$MCPP" add unknownidx.thing@1.0.0 > /dev/null grep -qE '^\[dependencies\.unknownidx\]$' mcpp.toml || { cat mcpp.toml; echo "unverifiable namespace should still be added"; exit 1; } -# (11) A real package asked for at a version it does not publish is a warning, +# (13) A real package asked for at a version it does not publish is a warning, # not a refusal — version tables are per-OS, so "absent here" is not "absent". out=$("$MCPP" add widget@9.9.9 2>&1) [[ "$out" == *"available: 0.1.0"* ]] || { echo "expected available-versions warning: $out"; exit 1; } grep -qE '^widget = "9\.9\.9"$' mcpp.toml || { cat mcpp.toml; echo "widget@9.9.9 should still be written"; exit 1; } -# (12) A workspace MEMBER inherits the root's [indices], so adding inside it +# (14) A workspace MEMBER inherits the root's [indices], so adding inside it # sees the same packages `mcpp build` would resolve from there (#224). mkdir -p "$TMP/ws" cd "$TMP/ws" @@ -160,9 +221,61 @@ acme = { path = "index" } EOF "$MCPP" new m1 > /dev/null cd m1 -"$MCPP" add acme:util@2.0.0 > /dev/null +"$MCPP" add acme.util@2.0.0 > /dev/null grep -qE '^util = "2\.0\.0"$' mcpp.toml || { cat mcpp.toml; echo "member should inherit workspace [indices]"; exit 1; } -err=$("$MCPP" add acme:nope@1.0.0 2>&1) && { echo "expected error inside workspace member"; exit 1; } +err=$("$MCPP" add acme.nope@1.0.0 2>&1) && { echo "expected error inside workspace member"; exit 1; } [[ "$err" == *"(acme, nope)"* ]] || { echo "wrong error: $err"; exit 1; } +# (15) Adding an exact selector upserts an equivalent legacy flat spelling +# instead of leaving two source forms for one PackageId. +mkdir -p "$TMP/upsert" +cat > "$TMP/upsert/mcpp.toml" < /dev/null +! grep -qE '^"acme\.util" = ' mcpp.toml || { + cat mcpp.toml + echo "legacy flat selector was duplicated instead of migrated" + exit 1 +} +[[ $(grep -cE '^util = "2\.0\.0"$' mcpp.toml) == 1 ]] || { + cat mcpp.toml + echo "canonical exact selector was not upserted once" + exit 1 +} + +# (16) Exact removal is scoped to [dependencies]. A same-name dev dependency +# that appears earlier in the file must not be removed by a global text match. +cat > mcpp.toml <<'EOF' +[package] +name = "remove-scope" +version = "0.1.0" + +[dev-dependencies] +widget = "0.2.0" + +[dependencies] +widget = "0.1.0" +EOF +"$MCPP" remove widget > /dev/null +grep -qE '^widget = "0\.2\.0"$' mcpp.toml || { + cat mcpp.toml + echo "remove escaped [dependencies] and deleted the dev dependency" + exit 1 +} +! grep -qE '^widget = "0\.1\.0"$' mcpp.toml || { + cat mcpp.toml + echo "regular dependency was not removed" + exit 1 +} + echo "OK" diff --git a/tests/e2e/162_bare_name_namespace_scope.sh b/tests/e2e/162_bare_name_namespace_scope.sh index 742ec377..8c0132c3 100755 --- a/tests/e2e/162_bare_name_namespace_scope.sh +++ b/tests/e2e/162_bare_name_namespace_scope.sh @@ -1,8 +1,8 @@ #!/usr/bin/env bash # requires: # 162_bare_name_namespace_scope.sh — INV-RESOLVE (#278): a bare dependency name -# resolves in exactly three places (mcpplibs, compat, no-namespace upstream); -# every other namespace must be written out. +# resolves in exactly one place (mcpplibs); every other namespace must be +# written out. # # Before this, a bare name that matched nothing fell through to the FIRST # candidate SILENTLY, so mcpp carried on with a namespace it had invented and @@ -56,7 +56,7 @@ widget = "1.0.0"' if "$MCPP" build > bare.out 2>&1; then echo "FAIL: a bare name must not reach the acme namespace"; cat bare.out; exit 1 fi -grep -q "no package found under the namespaces mcpp searched" bare.out || { +grep -q "no package found for exact selector" bare.out || { echo "FAIL: expected the explicit not-found error"; cat bare.out; exit 1; } # The identities actually attempted must be listed — the old silent fallback # reported a namespace the user never wrote. @@ -74,7 +74,7 @@ write_manifest '[dependencies] "$MCPP" build > dotted.out 2>&1 || true # The asset URL is a sentinel, so the fetch fails — but resolution must have # gotten far enough to ADDRESS the package, which is what this asserts. -if grep -q "no package found under the namespaces" dotted.out; then +if grep -q "no package found for exact selector" dotted.out; then echo "FAIL: dotted selector must resolve to (acme, widget)"; cat dotted.out; exit 1 fi @@ -82,14 +82,13 @@ fi write_manifest '[dependencies.acme] widget = "1.0.0"' "$MCPP" build > subtable.out 2>&1 || true -if grep -q "no package found under the namespaces" subtable.out; then +if grep -q "no package found for exact selector" subtable.out; then echo "FAIL: sub-table form must resolve to (acme, widget)"; cat subtable.out; exit 1 fi -# ── 5. the default-namespace search path still works ──────────────── -# `gtest` is a bare request served by the `compat.gtest` descriptor. This is -# the regression lock for the compat alias: narrowing the discovery rung must -# not touch the mcpplibs/compat search path. +# ── 5. namespace omission is exactly mcpplibs ─────────────────────── +# A bare gtest request must not cross into compat. The explicit dotted selector +# then proves the package remains reachable without ambiguity. # # Asserted on RESOLUTION, not on a successful build: whether the asset actually # downloads depends on network/cache state, but "did the bare name reach the @@ -98,9 +97,14 @@ fi write_manifest '[dependencies] gtest = "1.15.2"' "$MCPP" build > compat.out 2>&1 || true -if grep -q "no package found under the namespaces" compat.out; then - echo "FAIL: bare gtest must still resolve via the compat search path" - cat compat.out; exit 1 +grep -q "no package found for exact selector" compat.out || { + echo "FAIL: bare gtest must remain inside mcpplibs"; cat compat.out; exit 1; } + +write_manifest '[dependencies.compat] +gtest = "1.15.2"' +"$MCPP" build > compat-explicit.out 2>&1 || true +if grep -q "no package found for exact selector" compat-explicit.out; then + echo "FAIL: explicit compat.gtest must resolve"; cat compat-explicit.out; exit 1 fi echo "PASS 162_bare_name_namespace_scope" diff --git a/tests/e2e/165_bare_name_cross_namespace_wire_address.sh b/tests/e2e/165_bare_name_cross_namespace_wire_address.sh index c70bca1b..dc5e91cf 100755 --- a/tests/e2e/165_bare_name_cross_namespace_wire_address.sh +++ b/tests/e2e/165_bare_name_cross_namespace_wire_address.sh @@ -5,12 +5,9 @@ # # THE INCIDENT THIS LOCKS DOWN (2026-07-25, 6 of 7 workflows red on main): # -# A bare dependency (`gtest = "1.15.2"`) resolves to the DEFAULT namespace -# (`mcpplibs`), but the identity gate deliberately also accepts a `compat` -# descriptor for it — that is how every `compat.*` package is consumable -# without writing the namespace out. mcpp then took the wire NAME from that -# descriptor and the wire NAMESPACE from the request, emitting -# `mcpplibs:gtest`, which no index is keyed by. +# An explicitly selected compat dependency must carry the descriptor's +# namespace into the xlings wire address. The historical bug mixed the request +# and descriptor halves and emitted `mcpplibs:gtest`. # # It passed for months by coincidence: the literal `package.name` used to read # `compat.gtest`, so the hardcoded `compat.` retry caught it. When @@ -38,9 +35,9 @@ if [ ! -f "$SRC" ]; then exit 0 fi -# A `compat`-namespaced package named by its SPEC-001 short name. Filed under -# the `compat..lua` filename a bare request probes as a fallback -# candidate — exactly the live index's layout for all 34 compat packages. +# A `compat`-namespaced package named by its SPEC-001 short name. The +# `compat..lua` filename mirrors the live index, but selection is by the +# descriptor's exact declared identity, never by a bare-name fallback. mkdir -p idx/pkgs/c sed -e 's/^\( *namespace *= *\)"[^"]*"/\1"compat"/' \ -e 's/^\( *name *= *\)"[^"]*"/\1"widget"/' \ @@ -50,9 +47,8 @@ grep -qE '^ *namespace *= *"compat"' idx/pkgs/c/compat.widget.lua || { grep -qE '^ *name *= *"widget"' idx/pkgs/c/compat.widget.lua || { echo "FAIL: fixture name rewrite missed (upstream descriptor shape changed?)"; exit 1; } -# The consumer writes the dependency BARE — no namespace. `[indices] mcpplibs` -# points the default namespace at the fixture index (findIndexForNs routes the -# default namespace through this entry), so the whole thing stays offline. +# The consumer writes the compat namespace explicitly and routes it to the +# fixture index, so the whole thing stays offline. mkdir -p app/src cat > app/mcpp.toml <<'EOF' [package] @@ -60,9 +56,9 @@ name = "app" version = "0.1.0" [indices] -mcpplibs = { path = "../idx" } +compat = { path = "../idx" } -[dependencies] +[dependencies.compat] widget = "1.38.1" EOF echo 'int main() { return 0; }' > app/src/main.cpp diff --git a/tests/e2e/203_exact_selector_lock_migration.sh b/tests/e2e/203_exact_selector_lock_migration.sh new file mode 100755 index 00000000..48d51801 --- /dev/null +++ b/tests/e2e/203_exact_selector_lock_migration.sh @@ -0,0 +1,109 @@ +#!/usr/bin/env bash +# requires: gcc fresh-sandbox +# A compact dotted selector used to try mcpplibs. first. During the one +# migration release, an existing lock keeps that already-selected identity; +# without the lock, the selector is exact. Both identities exist here so index +# order cannot accidentally make the assertion pass. +set -e + +TMP=$(mktemp -d) +trap "rm -rf $TMP" EXIT + +export MCPP_HOME="$TMP/mcpp-home" +source "$(dirname "$0")/_inherit_toolchain.sh" + +INDEX_DIR="$TMP/index" +APP="$TMP/app" +mkdir -p "$INDEX_DIR/pkgs/c" "$INDEX_DIR/pkgs/m" "$APP/src" + +descriptor() { + local ns=$1 + local desc=$2 + cat < "$INDEX_DIR/pkgs/c/capi.lua.lua" +descriptor mcpplibs.capi "old prefixed identity" \ + > "$INDEX_DIR/pkgs/m/mcpplibs.capi.lua.lua" + +OLD_ROOT="$APP/.mcpp/.xlings/data/xpkgs/mcpplibs.capi-x-lua/1.0.0" +NEW_ROOT="$APP/.mcpp/.xlings/data/xpkgs/capi-x-lua/1.0.0" +mkdir -p "$OLD_ROOT/src" "$NEW_ROOT/src" +cat > "$OLD_ROOT/src/lua.cppm" <<'EOF' +export module selected.lua; +export int selected_value() { return 41; } +EOF +cat > "$NEW_ROOT/src/lua.cppm" <<'EOF' +export module selected.lua; +export int selected_value() { return 42; } +EOF +printf 'ok\n' > "$OLD_ROOT/.mcpp_ok" +printf 'ok\n' > "$NEW_ROOT/.mcpp_ok" + +cat > "$APP/src/main.cpp" <<'EOF' +import selected.lua; +int main() { return selected_value() == 41 ? 0 : 1; } +EOF +cat > "$APP/mcpp.toml" < "$APP/mcpp.lock" <<'EOF' +# Existing pre-exact-selector resolution anchor. +version = 2 + +[package."capi.lua"] +namespace = "mcpplibs.capi" +version = "1.0.0" +source = "index+mcpplibs.capi@1.0.0" +hash = "fnv1a:migration-fixture" +EOF + +cd "$APP" +"$MCPP" build > build.log 2>&1 || { cat build.log; exit 1; } +grep -q "keeping the locked identity" build.log || { + cat build.log + echo "expected selector migration warning" + exit 1 +} +grep -q "mcpplibs.capi.lua" build.log || { + cat build.log + echo "warning must include the locked selector" + exit 1 +} +"$MCPP" run > run.log 2>&1 || { cat run.log; exit 1; } +grep -q 'namespace = "mcpplibs.capi"' mcpp.lock || { + cat mcpp.lock + echo "lock identity changed during migration" + exit 1 +} + +echo "OK" diff --git a/tests/e2e/62_dotted_dependency_selector_priority.sh b/tests/e2e/62_dotted_dependency_selector_priority.sh index d7b74d70..bfbc72f9 100644 --- a/tests/e2e/62_dotted_dependency_selector_priority.sh +++ b/tests/e2e/62_dotted_dependency_selector_priority.sh @@ -1,9 +1,8 @@ #!/usr/bin/env bash # requires: gcc fresh-sandbox -# Dotted selectors in a single [dependencies] table use ordered candidates: -# imgui.core -> mcpplibs.imgui/core, then imgui/core. -# This test provides only the peer-root imgui/core package and verifies the -# build resolves through that fallback without network access. +# Dotted selectors in a single [dependencies] table are exact: +# imgui.core -> (imgui, core), and never mcpplibs.imgui/core. +# This test provides that exact package and verifies the build stays offline. set -e TMP=$(mktemp -d) @@ -19,7 +18,7 @@ package = { spec = "1", namespace = "imgui", name = "imgui.core", - description = "Dotted selector fallback test package", + description = "Exact dotted selector test package", licenses = {"MIT"}, type = "package", xpm = { diff --git a/tests/e2e/63_bare_dependency_peer_root_priority.sh b/tests/e2e/63_bare_dependency_peer_root_priority.sh index 61ccf31d..f132ceba 100755 --- a/tests/e2e/63_bare_dependency_peer_root_priority.sh +++ b/tests/e2e/63_bare_dependency_peer_root_priority.sh @@ -1,8 +1,7 @@ #!/usr/bin/env bash # requires: gcc fresh-sandbox -# Bare selectors try mcpplibs first, then an independent peer-root package. -# This test provides only an independent root `imgui` package in the default -# index and verifies `imgui = "1.0.0"` does not resolve as mcpplibs.imgui. +# Bare selectors mean exactly the default mcpplibs namespace. This test +# provides mcpplibs:imgui and verifies `imgui = "1.0.0"` records that identity. set -e TMP=$(mktemp -d) @@ -19,8 +18,9 @@ printf 'ok\n' > "$INDEX_DIR/.mcpp-index-updated" cat > "$INDEX_DIR/pkgs/i/imgui.lua" <<'EOF' package = { spec = "1", + namespace = "mcpplibs", name = "imgui", - description = "Independent bare selector fallback test package", + description = "Default-namespace exact selector test package", licenses = {"MIT"}, type = "package", xpm = { @@ -90,9 +90,9 @@ grep -q '\[package."imgui"\]' mcpp.lock || { exit 1 } -if grep -q 'namespace = "mcpplibs"' mcpp.lock; then +if ! grep -q 'namespace = "mcpplibs"' mcpp.lock; then cat mcpp.lock - echo "bare independent package should not lock as mcpplibs" + echo "bare package must lock as the default mcpplibs namespace" exit 1 fi diff --git a/tests/e2e/78_test_main_combinations.sh b/tests/e2e/78_test_main_combinations.sh index 7f6e9dc3..74115fed 100755 --- a/tests/e2e/78_test_main_combinations.sh +++ b/tests/e2e/78_test_main_combinations.sh @@ -23,7 +23,7 @@ cd combo cat >> mcpp.toml <<'EOF' -[dev-dependencies] +[dev-dependencies.compat] gtest = "1.15.2" EOF diff --git a/tests/e2e/79_gtest_regular_dep_feature_main.sh b/tests/e2e/79_gtest_regular_dep_feature_main.sh index f8c9c1e4..5a78fea8 100755 --- a/tests/e2e/79_gtest_regular_dep_feature_main.sh +++ b/tests/e2e/79_gtest_regular_dep_feature_main.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # requires: # 79_gtest_regular_dep_feature_main.sh — gtest as a REGULAR dependency -# (`[dependencies]`, via `mcpp add gtest`) must NOT inject gtest_main into a +# (`[dependencies.compat]`, via `mcpp add compat.gtest`) must NOT inject gtest_main into a # `mcpp build` app that has its own main. Regression for issue #168 # (`gtest_main.o : error LNK2005: main already defined in main.o`). # @@ -22,8 +22,8 @@ cd app # (1) #168: gtest in [dependencies] + app's own main → build must succeed, and # gtest_main must NOT be linked. -"$MCPP" add gtest@1.15.2 > /dev/null -grep -q '^\[dependencies\]' mcpp.toml || { echo "FAIL: add did not write [dependencies]"; cat mcpp.toml; exit 1; } +"$MCPP" add compat.gtest@1.15.2 > /dev/null +grep -q '^\[dependencies\.compat\]' mcpp.toml || { echo "FAIL: add did not write [dependencies.compat]"; cat mcpp.toml; exit 1; } "$MCPP" build > /dev/null || { echo "FAIL: #168 — build with regular-dep gtest failed"; exit 1; } nj=$(find target -name build.ninja | xargs ls -t 2>/dev/null | head -1) if grep -q 'gtest_main' "$nj"; then @@ -38,7 +38,7 @@ cat > mcpp.toml <<'EOF' name = "app" version = "0.1.0" -[dependencies] +[dependencies.compat] gtest = { version = "1.15.2", features = ["main"] } EOF cat > src/main.cpp <<'EOF' @@ -53,7 +53,7 @@ grep -q 'gtest_main' "$nj" || { echo "FAIL: features=[main] did not link gtest_m cd "$TMP" "$MCPP" new libapp > /dev/null cd libapp -"$MCPP" add --dev gtest@1.15.2 > /dev/null -grep -q '^\[dev-dependencies\]' mcpp.toml || { echo "FAIL: add --dev did not write [dev-dependencies]"; cat mcpp.toml; exit 1; } +"$MCPP" add --dev compat.gtest@1.15.2 > /dev/null +grep -q '^\[dev-dependencies\.compat\]' mcpp.toml || { echo "FAIL: add --dev did not write [dev-dependencies.compat]"; cat mcpp.toml; exit 1; } echo "OK" diff --git a/tests/unit/test_manifest.cpp b/tests/unit/test_manifest.cpp index 866d72fe..4a4c9caf 100644 --- a/tests/unit/test_manifest.cpp +++ b/tests/unit/test_manifest.cpp @@ -1534,7 +1534,7 @@ version = "0.1.0" EXPECT_TRUE(s.legacyDottedKey); } -TEST(Manifest, DependenciesDottedSelectorPreservesUserKeyAndCandidates) { +TEST(Manifest, DependenciesDottedSelectorIsOneExactIdentity) { constexpr auto src = R"( [package] name = "x" @@ -1548,15 +1548,34 @@ capi.lua = "0.0.3" ASSERT_EQ(m->dependencies.size(), 1u); auto& s = m->dependencies.at("capi.lua"); - EXPECT_EQ(s.namespace_, "mcpplibs.capi"); + EXPECT_EQ(s.namespace_, "capi"); EXPECT_EQ(s.shortName, "lua"); EXPECT_EQ(s.version, "0.0.3"); EXPECT_FALSE(s.legacyDottedKey); - ASSERT_EQ(s.candidates.size(), 2u); - EXPECT_EQ(s.candidates[0].namespace_, "mcpplibs.capi"); + EXPECT_TRUE(s.legacyCandidateSearch); + ASSERT_EQ(s.candidates.size(), 1u); + EXPECT_EQ(s.candidates[0].namespace_, "capi"); EXPECT_EQ(s.candidates[0].shortName, "lua"); - EXPECT_EQ(s.candidates[1].namespace_, "capi"); - EXPECT_EQ(s.candidates[1].shortName, "lua"); +} + +TEST(Manifest, DependencySelectorsRejectUnsafeOrAmbiguousSegments) { + const std::vector invalid{ + "acme/widget", "acme widget", "acme[widget", + "acme=widget", "acme#widget", "acme..widget", + }; + for (auto const& selector : invalid) { + auto src = std::format(R"( +[package] +name = "x" +version = "0.1.0" +[dependencies] +"{}" = "1.0.0" +)", selector); + auto m = mcpp::manifest::parse_string(src); + EXPECT_FALSE(m.has_value()) << selector; + if (!m) EXPECT_NE(m.error().message.find("invalid package selector"), + std::string::npos) << m.error().format(); + } } TEST(Manifest, DependenciesNamespacedSubtableNestedDottedKeyIsCanonical) { @@ -1577,6 +1596,7 @@ capi.lua = "0.0.3" EXPECT_EQ(s.shortName, "lua"); EXPECT_EQ(s.version, "0.0.3"); EXPECT_FALSE(s.legacyDottedKey); + EXPECT_FALSE(s.legacyCandidateSearch); ASSERT_EQ(s.candidates.size(), 1u); EXPECT_EQ(s.candidates[0].namespace_, "mcpplibs.capi"); EXPECT_EQ(s.candidates[0].shortName, "lua"); @@ -1644,7 +1664,7 @@ package = { EXPECT_EQ(b.version, "0.0.2"); } -TEST(SynthesizeFromXpkgLua, DepsDottedSelectorsUseManifestRules) { +TEST(SynthesizeFromXpkgLua, DepsDottedSelectorsAreExact) { constexpr auto src = R"( package = { spec = "1", @@ -1666,28 +1686,50 @@ package = { ASSERT_EQ(m->dependencies.size(), 3u); auto& lua = m->dependencies.at("capi.lua"); - EXPECT_EQ(lua.namespace_, "mcpplibs.capi"); + EXPECT_EQ(lua.namespace_, "capi"); EXPECT_EQ(lua.shortName, "lua"); EXPECT_EQ(lua.version, "0.0.3"); - ASSERT_EQ(lua.candidates.size(), 2u); - EXPECT_EQ(lua.candidates[1].namespace_, "capi"); - EXPECT_EQ(lua.candidates[1].shortName, "lua"); + EXPECT_TRUE(lua.legacyCandidateSearch); + ASSERT_EQ(lua.candidates.size(), 1u); + EXPECT_EQ(lua.candidates[0].namespace_, "capi"); + EXPECT_EQ(lua.candidates[0].shortName, "lua"); auto& imgui = m->dependencies.at("imgui.core"); - EXPECT_EQ(imgui.namespace_, "mcpplibs.imgui"); + EXPECT_EQ(imgui.namespace_, "imgui"); EXPECT_EQ(imgui.shortName, "core"); EXPECT_EQ(imgui.version, "0.0.1"); - ASSERT_EQ(imgui.candidates.size(), 2u); - EXPECT_EQ(imgui.candidates[1].namespace_, "imgui"); - EXPECT_EQ(imgui.candidates[1].shortName, "core"); + EXPECT_TRUE(imgui.legacyCandidateSearch); + ASSERT_EQ(imgui.candidates.size(), 1u); + EXPECT_EQ(imgui.candidates[0].namespace_, "imgui"); + EXPECT_EQ(imgui.candidates[0].shortName, "core"); auto& gtest = m->dependencies.at("compat.gtest"); - EXPECT_EQ(gtest.namespace_, "mcpplibs.compat"); + EXPECT_EQ(gtest.namespace_, "compat"); EXPECT_EQ(gtest.shortName, "gtest"); EXPECT_EQ(gtest.version, "1.15.2"); - ASSERT_EQ(gtest.candidates.size(), 2u); - EXPECT_EQ(gtest.candidates[1].namespace_, "compat"); - EXPECT_EQ(gtest.candidates[1].shortName, "gtest"); + EXPECT_TRUE(gtest.legacyCandidateSearch); + ASSERT_EQ(gtest.candidates.size(), 1u); + EXPECT_EQ(gtest.candidates[0].namespace_, "compat"); + EXPECT_EQ(gtest.candidates[0].shortName, "gtest"); +} + +TEST(SynthesizeFromXpkgLua, InvalidDependencySelectorIsAnError) { + constexpr auto src = R"( +package = { + spec = "1", + name = "consumer", + xpm = { linux = { ["1.0.0"] = { url = "u", sha256 = "h" } } }, + mcpp = { + sources = { "*/src/*.cppm" }, + deps = { ["acme/widget"] = "1.0.0" }, + }, +} +)"; + auto m = mcpp::manifest::synthesize_from_xpkg_lua( + src, "consumer", "1.0.0", mcpp::platform::HostPlatform::current()); + ASSERT_FALSE(m.has_value()); + EXPECT_NE(m.error().message.find("invalid package selector"), + std::string::npos) << m.error().format(); } TEST(Manifest, WorkspaceSectionParsed) { @@ -1718,7 +1760,7 @@ mbedtls = "3.6.1" EXPECT_EQ(gt.namespace_, "compat"); } -TEST(Manifest, WorkspaceDependenciesUseDottedSelectorRules) { +TEST(Manifest, WorkspaceDependenciesUseExactDottedSelectors) { constexpr auto src = R"( [workspace] members = ["libs/core"] @@ -1734,33 +1776,37 @@ mcpplibs.templates = "0.0.1" ASSERT_EQ(m->workspace.dependencies.size(), 4u); auto& lua = m->workspace.dependencies.at("capi.lua"); - EXPECT_EQ(lua.namespace_, "mcpplibs.capi"); + EXPECT_EQ(lua.namespace_, "capi"); EXPECT_EQ(lua.shortName, "lua"); EXPECT_EQ(lua.version, "0.0.3"); - ASSERT_EQ(lua.candidates.size(), 2u); - EXPECT_EQ(lua.candidates[1].namespace_, "capi"); - EXPECT_EQ(lua.candidates[1].shortName, "lua"); + EXPECT_TRUE(lua.legacyCandidateSearch); + ASSERT_EQ(lua.candidates.size(), 1u); + EXPECT_EQ(lua.candidates[0].namespace_, "capi"); + EXPECT_EQ(lua.candidates[0].shortName, "lua"); auto& imgui = m->workspace.dependencies.at("imgui.core"); - EXPECT_EQ(imgui.namespace_, "mcpplibs.imgui"); + EXPECT_EQ(imgui.namespace_, "imgui"); EXPECT_EQ(imgui.shortName, "core"); EXPECT_EQ(imgui.version, "0.0.1"); - ASSERT_EQ(imgui.candidates.size(), 2u); - EXPECT_EQ(imgui.candidates[1].namespace_, "imgui"); - EXPECT_EQ(imgui.candidates[1].shortName, "core"); + EXPECT_TRUE(imgui.legacyCandidateSearch); + ASSERT_EQ(imgui.candidates.size(), 1u); + EXPECT_EQ(imgui.candidates[0].namespace_, "imgui"); + EXPECT_EQ(imgui.candidates[0].shortName, "core"); auto& gt = m->workspace.dependencies.at("compat.gtest"); - EXPECT_EQ(gt.namespace_, "mcpplibs.compat"); + EXPECT_EQ(gt.namespace_, "compat"); EXPECT_EQ(gt.shortName, "gtest"); EXPECT_EQ(gt.version, "1.15.2"); - ASSERT_EQ(gt.candidates.size(), 2u); - EXPECT_EQ(gt.candidates[1].namespace_, "compat"); - EXPECT_EQ(gt.candidates[1].shortName, "gtest"); + EXPECT_TRUE(gt.legacyCandidateSearch); + ASSERT_EQ(gt.candidates.size(), 1u); + EXPECT_EQ(gt.candidates[0].namespace_, "compat"); + EXPECT_EQ(gt.candidates[0].shortName, "gtest"); auto& tmpl = m->workspace.dependencies.at("mcpplibs.templates"); EXPECT_EQ(tmpl.namespace_, "mcpplibs"); EXPECT_EQ(tmpl.shortName, "templates"); EXPECT_EQ(tmpl.version, "0.0.1"); + EXPECT_FALSE(tmpl.legacyCandidateSearch); ASSERT_EQ(tmpl.candidates.size(), 1u); EXPECT_EQ(tmpl.candidates[0].namespace_, "mcpplibs"); EXPECT_EQ(tmpl.candidates[0].shortName, "templates"); @@ -2162,15 +2208,14 @@ package = { namespace = "mcpplibs", name = "mcpplibs.llmapi", version = "0.2.8" EXPECT_FALSE(mcpp::manifest::xpkg_lua_identity_matches(llmapi, "", "zlib")); } -TEST(XpkgIdentity, DefaultNamespaceRequestMatchesCompatAlias) { - // Regression for the CI break: the dev-dep `gtest` is a bare/default-namespace - // request, but the descriptor is `compat.gtest` (namespace="compat"). A - // default-namespace request must accept its compat alias. +TEST(XpkgIdentity, DefaultNamespaceRequestDoesNotMatchCompatPackage) { + // Namespace omission now means exactly mcpplibs. A compat package must be + // selected as `compat.gtest`, never smuggled through an identity alias. constexpr std::string_view compatGtest = R"(package = { namespace = "compat", name = "compat.gtest", version = "1.15.2" })"; - EXPECT_TRUE(mcpp::manifest::xpkg_lua_identity_matches( + EXPECT_FALSE(mcpp::manifest::xpkg_lua_identity_matches( compatGtest, "mcpplibs", "gtest")); - EXPECT_TRUE(mcpp::manifest::xpkg_lua_identity_matches( + EXPECT_FALSE(mcpp::manifest::xpkg_lua_identity_matches( compatGtest, "mcpplibs", "gtest", /*allowLegacyBareDefault=*/false)); // But a different default-ns name does not match it. EXPECT_FALSE(mcpp::manifest::xpkg_lua_identity_matches( @@ -2180,19 +2225,17 @@ TEST(XpkgIdentity, DefaultNamespaceRequestMatchesCompatAlias) { // ─── xpkg_wire_address — the address xlings is asked to install ───── // // REGRESSION LOCK for the 2026-07-25 CI break (6 of 7 workflows red on main). -// The identity gate accepts a `compat` descriptor for a bare/default-namespace -// request (`gtest = "1.15.2"` must resolve to compat.gtest). The wire address -// then has to come from THAT descriptor — both halves. Taking the name from the +// Once a descriptor is selected by an explicit `compat.gtest` request, the +// wire address has to come from THAT descriptor — both halves. Taking the name from the // descriptor and the namespace from the request emitted `mcpplibs:gtest`, which // no index is keyed by; it only ever worked because the literal name used to // read `compat.gtest`, so a hardcoded `compat.` retry caught it. The // index's SPEC-001 short-name migration removed that coincidence. -TEST(XpkgWireAddress, CompatDescriptorUnderBareRequestAddressesCompat) { - // THE BUG. Bare `gtest` request, served by a short-name compat descriptor. +TEST(XpkgWireAddress, CompatDescriptorAddressesCompat) { constexpr std::string_view compatGtest = R"(package = { namespace = "compat", name = "gtest", version = "1.15.2" })"; - auto addr = mcpp::manifest::xpkg_wire_address(compatGtest, "mcpplibs", "gtest"); + auto addr = mcpp::manifest::xpkg_wire_address(compatGtest, "compat", "gtest"); EXPECT_EQ(addr.ns, "compat"); EXPECT_EQ(addr.name, "gtest"); EXPECT_EQ(addr.target, "compat:gtest"); // NOT "mcpplibs:gtest" @@ -2304,11 +2347,9 @@ TEST(XpkgNameForm, NoNameIsClean) { } TEST(XpkgNameForm, CompatAliasIsClean) { - // REGRESSION LOCK. The predicate must stay narrow ("declared ns non-empty - // AND name lacks the `.` prefix"). Written as the general comparison - // "literal name != derived fqname" it would flag this descriptor — a bare - // `gtest` request derives fqname `gtest` while the literal is `compat.gtest` - // — and break the working compat-alias path for every bare dependency. + // Legacy descriptors repeat the declared namespace in package.name. That + // source shape remains valid even though consumers now select compat + // explicitly. EXPECT_FALSE(mcpp::manifest::xpkg_name_form_violation("compat", "compat.gtest") .has_value()); } diff --git a/tests/unit/test_pm_compat.cpp b/tests/unit/test_pm_compat.cpp index 2ab17028..7e6b172b 100644 --- a/tests/unit/test_pm_compat.cpp +++ b/tests/unit/test_pm_compat.cpp @@ -47,36 +47,28 @@ TEST(PmCompat, NormalizeNestedNamespaceSkipsCanonicalNamespacedDeps) { EXPECT_EQ(shortName, "lua.extra"); } -TEST(DependencySelector, DottedSelectorBuildsOmittedMcpplibsPriorityCandidates) { +TEST(DependencySelector, DottedSelectorIsOneExactNamespace) { auto selector = mcpp::pm::resolve_dependency_selector( - "imgui.backend.glfw_opengl3", - mcpp::pm::DependencySelectorMode::OmittedMcpplibsPriority); + "imgui.backend.glfw_opengl3"); EXPECT_EQ(selector.stableMapKey, "imgui.backend.glfw_opengl3"); - ASSERT_EQ(selector.candidates.size(), 2u); - EXPECT_EQ(selector.candidates[0].namespace_, "mcpplibs.imgui.backend"); + ASSERT_EQ(selector.candidates.size(), 1u); + EXPECT_EQ(selector.candidates[0].namespace_, "imgui.backend"); EXPECT_EQ(selector.candidates[0].shortName, "glfw_opengl3"); - EXPECT_EQ(selector.candidates[1].namespace_, "imgui.backend"); - EXPECT_EQ(selector.candidates[1].shortName, "glfw_opengl3"); } -TEST(DependencySelector, BareSelectorBuildsOmittedMcpplibsThenPeerRootCandidates) { - auto selector = mcpp::pm::resolve_dependency_selector( - "imgui", - mcpp::pm::DependencySelectorMode::OmittedMcpplibsPriority); +TEST(DependencySelector, BareSelectorUsesOnlyDefaultNamespace) { + auto selector = mcpp::pm::resolve_dependency_selector("imgui"); EXPECT_EQ(selector.stableMapKey, "imgui"); - ASSERT_EQ(selector.candidates.size(), 2u); + ASSERT_EQ(selector.candidates.size(), 1u); EXPECT_EQ(selector.candidates[0].namespace_, "mcpplibs"); EXPECT_EQ(selector.candidates[0].shortName, "imgui"); - EXPECT_EQ(selector.candidates[1].namespace_, ""); - EXPECT_EQ(selector.candidates[1].shortName, "imgui"); } TEST(DependencySelector, ExplicitMcpplibsPrefixDoesNotAddPeerFallback) { auto selector = mcpp::pm::resolve_dependency_selector( - "mcpplibs.capi.lua", - mcpp::pm::DependencySelectorMode::OmittedMcpplibsPriority); + "mcpplibs.capi.lua"); EXPECT_EQ(selector.stableMapKey, "mcpplibs.capi.lua"); ASSERT_EQ(selector.candidates.size(), 1u); @@ -94,6 +86,77 @@ TEST(DependencySelector, ExplicitRootSelectorHasOnlyThatRoot) { EXPECT_EQ(selector.candidates[0].shortName, "gtest"); } +TEST(DependencySelector, SharedParserNormalizesBareAndDottedSelectors) { + auto bare = mcpp::pm::parse_package_selector("lua"); + ASSERT_TRUE(bare.has_value()); + EXPECT_FALSE(bare->namespace_.has_value()); + EXPECT_EQ(bare->name, "lua"); + EXPECT_EQ(bare->spelling, "lua"); + + auto bareCoord = mcpp::pm::normalize_package_selector(*bare); + EXPECT_EQ(bareCoord.namespace_, "mcpplibs"); + EXPECT_EQ(bareCoord.shortName, "lua"); + EXPECT_EQ(mcpp::pm::format_package_selector(bareCoord), "lua"); + + auto nested = mcpp::pm::parse_package_selector("mcpplibs.capi.lua"); + ASSERT_TRUE(nested.has_value()); + ASSERT_TRUE(nested->namespace_.has_value()); + EXPECT_EQ(*nested->namespace_, "mcpplibs.capi"); + EXPECT_EQ(nested->name, "lua"); + + auto nestedCoord = mcpp::pm::normalize_package_selector(*nested); + EXPECT_EQ(nestedCoord.namespace_, "mcpplibs.capi"); + EXPECT_EQ(nestedCoord.shortName, "lua"); + EXPECT_EQ(mcpp::pm::format_package_selector(nestedCoord), + "mcpplibs.capi.lua"); +} + +TEST(DependencySelector, SharedParserRejectsAmbiguousOrUnsafeSegments) { + const std::vector invalid{ + "", + ".imgui", + "ocornut..imgui", + "imgui.", + "acme/widget", + "acme\\widget", + "acme widget", + "acme[widget", + "acme=widget", + "acme\"widget", + "acme#widget", + "acme@widget", + "acme:widget", + std::string("acme\x1fwidget", 11), + }; + for (auto const& spelling : invalid) { + auto parsed = mcpp::pm::parse_package_selector(spelling); + EXPECT_FALSE(parsed.has_value()) << spelling; + if (!parsed) + EXPECT_FALSE(parsed.error().message.empty()) << spelling; + } +} + +TEST(DependencySelector, LegacyDottedPrimaryIsExplicitAndNeverImplicitlyUsed) { + auto capi = mcpp::pm::parse_package_selector("capi.lua"); + ASSERT_TRUE(capi.has_value()); + auto legacy = mcpp::pm::legacy_prefixed_coordinate( + mcpp::pm::normalize_package_selector(*capi)); + ASSERT_TRUE(legacy.has_value()); + EXPECT_EQ(legacy->namespace_, "mcpplibs.capi"); + EXPECT_EQ(legacy->shortName, "lua"); + + auto explicitDefault = mcpp::pm::parse_package_selector( + "mcpplibs.capi.lua"); + ASSERT_TRUE(explicitDefault.has_value()); + EXPECT_FALSE(mcpp::pm::legacy_prefixed_coordinate( + mcpp::pm::normalize_package_selector(*explicitDefault)).has_value()); + + auto bare = mcpp::pm::parse_package_selector("lua"); + ASSERT_TRUE(bare.has_value()); + EXPECT_FALSE(mcpp::pm::legacy_prefixed_coordinate( + mcpp::pm::normalize_package_selector(*bare)).has_value()); +} + // ─── descriptor_coordinates (package-template fetch) ──────────────── TEST(PmCompat, DescriptorCoordinatesLegacyEmbeddedNamespace) { diff --git a/tests/unit/test_pm_index_route.cpp b/tests/unit/test_pm_index_route.cpp index fb5b773b..5a73f880 100644 --- a/tests/unit/test_pm_index_route.cpp +++ b/tests/unit/test_pm_index_route.cpp @@ -64,17 +64,17 @@ TEST(PmIndexRoute, LocalPathIndexIsAuthoritative) { EXPECT_TRUE(route.authoritative_for("acme")); } -// The regression behind #305/#307: a dotted selector is a NAMESPACE PATH, so it -// only resolves through the candidates the manifest parser derives. Probing the +// The regression behind #305/#307: a dotted selector is one exact NAMESPACE +// PATH, so it resolves through the coordinate the manifest parser derives. Probing the // literal short name can never match — `package.name` is a single atomic // segment, so nothing in any index is named "acme.util". -TEST(PmIndexRoute, DottedSelectorResolvesThroughItsCandidates) { +TEST(PmIndexRoute, DottedSelectorResolvesThroughItsExactCoordinate) { LocalIndex idx("dotted"); auto indices = local_map(idx); mcpp::pm::IndexRoute route{ &indices, "/nowhere", nullptr }; - auto selector = mcpp::pm::resolve_dependency_selector( - "acme.util", mcpp::pm::DependencySelectorMode::OmittedMcpplibsPriority); + auto selector = mcpp::pm::resolve_dependency_selector("acme.util"); + ASSERT_EQ(selector.candidates.size(), 1u); auto found = mcpp::pm::lookup_descriptor(route, selector.candidates); ASSERT_TRUE(found.hit.has_value()); diff --git a/tests/unit/test_pm_package_fetcher.cpp b/tests/unit/test_pm_package_fetcher.cpp index d5421057..38f7d1c0 100644 --- a/tests/unit/test_pm_package_fetcher.cpp +++ b/tests/unit/test_pm_package_fetcher.cpp @@ -88,9 +88,7 @@ TEST(PmPackageFetcher, ForeignBareZlibAloneDoesNotSatisfyCompatRequest) { std::filesystem::remove_all(project); } -TEST(PmPackageFetcher, DefaultNamespaceRequestResolvesCompatAliasDescriptor) { - // The CI break: dev-dep `gtest` (bare → default namespace "mcpplibs") must - // resolve to the `compat.gtest` descriptor that actually lives in the index. +TEST(PmPackageFetcher, DefaultNamespaceCannotResolveCompatDescriptor) { auto project = make_tempdir("mcpp-fetcher-compat-alias"); auto dataRoot = project / ".mcpp" / "data"; @@ -105,11 +103,14 @@ package = { write_file(dataRoot / "mcpplibs" / "pkgs" / "c" / "compat.gtest.lua", compatGtest); - auto lua = mcpp::pm::Fetcher::read_xpkg_lua_from_project_data( + auto defaultLua = mcpp::pm::Fetcher::read_xpkg_lua_from_project_data( project, "mcpplibs", "gtest"); + EXPECT_FALSE(defaultLua.has_value()); - ASSERT_TRUE(lua.has_value()) << "default-ns request must find its compat alias"; - EXPECT_NE(lua->find("compat.gtest"), std::string::npos); + auto explicitLua = mcpp::pm::Fetcher::read_xpkg_lua_from_project_data( + project, "compat", "gtest"); + ASSERT_TRUE(explicitLua.has_value()); + EXPECT_NE(explicitLua->find("compat.gtest"), std::string::npos); std::filesystem::remove_all(project); } From 93db3005b0304b2c216ed99b5806dc1ec9ac93e7 Mon Sep 17 00:00:00 2001 From: speak-agent Date: Sun, 9 Aug 2026 09:40:50 +0800 Subject: [PATCH 03/44] feat: make template selection exact --- ...untime-graphics-aur-implementation-plan.md | 16 +- ...emplate-runtime-graphics-aur-validation.md | 19 ++ .github/workflows/ci-fresh-install.yml | 16 +- src/cli.cppm | 4 +- src/cli/cmd_new.cppm | 36 +++- src/manifest/xpkg.cppm | 3 + src/scaffold/create.cppm | 198 +++++++++++------- src/scaffold/template.cppm | 162 ++++++++++++-- tests/e2e/69_package_templates.sh | 194 ++++++++++++++++- tests/unit/test_manifest.cpp | 3 + tests/unit/test_scaffold.cpp | 136 ++++++++++++ 11 files changed, 658 insertions(+), 129 deletions(-) create mode 100644 tests/unit/test_scaffold.cpp diff --git a/.agents/docs/2026-08-09-mcpp-template-runtime-graphics-aur-implementation-plan.md b/.agents/docs/2026-08-09-mcpp-template-runtime-graphics-aur-implementation-plan.md index 57840594..e0279da4 100644 --- a/.agents/docs/2026-08-09-mcpp-template-runtime-graphics-aur-implementation-plan.md +++ b/.agents/docs/2026-08-09-mcpp-template-runtime-graphics-aur-implementation-plan.md @@ -105,16 +105,16 @@ - `ResolvedTemplatePackage { DependencyCoordinate id; string version; string indexRoute; string descriptorDigest; string payloadDigest; path root; }` - `TemplateSelection { ResolvedTemplatePackage package; string templateName; }` -- [ ] RED: unit-test every valid and invalid grammar row from focused design §5.1, including multiple `:`/`@`, empty components, exact dotted namespaces, and `pkg:` legacy list recognition. -- [ ] RED: add E2E packages with `mcpplibs:widget`, `acme:widget`, and `mcpplibs.capi:lua`; prove template resolution uses the exact PackageId through `IndexRoute::lookup_descriptor`. -- [ ] Implement TemplateSpec parsing in the fixed order template delimiter → version delimiter → shared PackageSelector; reject before config/network access. -- [ ] Replace scaffold short-name/compat probing with `IndexRoute` exact lookup and keep canonical namespace, version, route, descriptor digest, payload digest, and root through fetch/render/output. -- [ ] Reuse package-manager semver resolution for latest stable; omitted versions must not select prereleases, while explicit exact prerelease remains allowed. -- [ ] RED: test default selection rules: one explicit default wins; one sole non-default auto-wins; multiple without default list choices and fail; multiple defaults fail validation; no templates directory reports provider error. -- [ ] Implement the default rules and `pkg:` one-release warning pointing to `--list-templates`. +- [x] RED: unit-test every valid and invalid grammar row from focused design §5.1, including multiple `:`/`@`, empty components, exact dotted namespaces, and `pkg:` legacy list recognition. +- [x] RED: add E2E packages with `mcpplibs:widget`, `acme:widget`, and `mcpplibs.capi:lua`; prove template resolution uses the exact PackageId through `IndexRoute::lookup_descriptor`. +- [x] Implement TemplateSpec parsing in the fixed order template delimiter → version delimiter → shared PackageSelector; reject before config/network access. +- [x] Replace scaffold short-name/compat probing with `IndexRoute` exact lookup and keep canonical namespace, version, route, descriptor digest, payload digest, and root through fetch/render/output. +- [x] Reuse package-manager semver resolution for latest stable; omitted versions must not select prereleases, while explicit exact prerelease remains allowed. +- [x] RED: test default selection rules: one explicit default wins; one sole non-default auto-wins; multiple without default list choices and fail; multiple defaults fail validation; no templates directory reports provider error. +- [x] Implement the default rules and `pkg:` one-release warning pointing to `--list-templates`. - [ ] Update human and machine output to include canonical selector, resolved namespace/name/version, template, and runtime selection. - [ ] GREEN: run new unit tests, template E2E, and three-platform fresh-install template lanes. -- [ ] Commit typed TemplateSpec and exact template-package resolution. +- [x] Commit typed TemplateSpec and exact template-package resolution. ## Task 4: Make Scaffolding Safe and Transactional (#380) diff --git a/.agents/docs/2026-08-09-mcpp-template-runtime-graphics-aur-validation.md b/.agents/docs/2026-08-09-mcpp-template-runtime-graphics-aur-validation.md index a273459f..6ba549a5 100644 --- a/.agents/docs/2026-08-09-mcpp-template-runtime-graphics-aur-validation.md +++ b/.agents/docs/2026-08-09-mcpp-template-runtime-graphics-aur-validation.md @@ -119,6 +119,25 @@ Task 2 refactor/full gates on the isolated source binary - changed shell scripts `bash -n`: PASS. - `git diff --check`: PASS. +### 5.2 Typed exact TemplateSpec and deterministic provider selection (Task 3) + +| Gate | RED evidence | Production change | GREEN evidence | +|---|---|---|---| +| grammar | New `test_scaffold` could not build because typed `parse_template_spec` / `select_template` APIs did not exist | Added fixed-order `:` then `@` parsing over the shared PackageSelector; exact safe version keys; atomic tname validation; one-release `pkg:` list marker | 9 scaffold tests pass for every valid design row and empty/double-delimiter/range/unsafe failures | +| exact provider identity | Updated E2E is red on the baseline binary at the first new exact-identity diagnostic; baseline implementation probes empty/compat short names and cannot retain an explicit foreign/nested PackageId | Registry template fetch now uses one normalized coordinate through `IndexRoute::lookup_descriptor`; no short-name or compat probe remains | E2E same-short `(mcpplibs,tpl-demo)` / `(acme,tpl-demo)` selects acme exactly; nested `(mcpplibs.capi,lua)` survives output and injected selector | +| deterministic version | Baseline accepted only its ad-hoc version flow and exposed no payload provenance | Shared SemVer resolver chooses latest stable, explicit prerelease is validated, indirect aliases are refused, and xpkg version entries retain declared SHA256 | E2E omits `2.0.0-rc.1` for default selection, accepts its exact pin, rejects `latest`, rejects unpublished `9.9.9`, and prints descriptor/payload provenance | +| default template | Old code failed whenever no `default=true` existed, including a package with exactly one template | Shared selection implements sole-template auto-default, unique explicit default, explicit tname, and deterministic provider errors | Unit/E2E cover sole auto-default, multiple ambiguous choices, duplicate defaults, missing templates directory, and explicit disambiguation | +| migration/CI surfaces | Legacy `pkg:` silently meant listing and native fresh-install only exercised a bare short name | `pkg:` warns with copyable `--list-templates`; fresh-install Linux/macOS/Windows lanes now use explicit `mcpplibs.imgui` and assert resolved identity | Local legacy-list E2E passes; native lanes are configured and remain pending PR/latest-head CI evidence | + +Task 3 local gates on source binary +`target/x86_64-linux-gnu/94da92f90aedbe7f/bin/mcpp`: + +- source build: PASS, full release rebuild completed in 72.92s. +- `mcpp test --no-cache`: PASS, **69 passed; 0 failed**, 33.07s. +- `test_scaffold`: 9/9 behavior rows PASS; `test_manifest`: 148/148 PASS. +- `69_package_templates.sh`: PASS with exact/default/nested/alias/provider/wire assertions. +- `bash -n tests/e2e/69_package_templates.sh` and `git diff --check`: PASS. + ## 6. Pull request and CI Not published yet. This section will record PR URL, local/remote HEAD, review state, all latest-head job IDs and terminal conclusions. diff --git a/.github/workflows/ci-fresh-install.yml b/.github/workflows/ci-fresh-install.yml index d363080c..a3b75abb 100644 --- a/.github/workflows/ci-fresh-install.yml +++ b/.github/workflows/ci-fresh-install.yml @@ -179,11 +179,12 @@ jobs: # Template packages exercise the sha256-pinned mcpp-index fetch # path (user report: `mcpp new ... --template imgui` failed with # fetch 'imgui@0.0.6' exit 1 on hosts without a sha256sum binary). - - name: "Template: mcpp new --template imgui (fetch path)" + - name: "Template: exact mcpplibs.imgui selector (fetch path)" run: | cd "$(mktemp -d)" - mcpp new abc1 --template imgui + mcpp new abc1 --template mcpplibs.imgui 2>&1 | tee template.log test -f abc1/mcpp.toml + grep -F 'namespace=mcpplibs name=imgui' template.log - name: "Default: build mcpp" run: | @@ -391,11 +392,12 @@ jobs: # path — this is what broke on hosts without a sha256sum binary # (stock macOS / bare Windows) before xlings 0.4.51 hashed # in-process. - - name: "Template: mcpp new --template imgui (fetch path)" + - name: "Template: exact mcpplibs.imgui selector (fetch path)" run: | cd "$(mktemp -d)" - mcpp new abc1 --template imgui + mcpp new abc1 --template mcpplibs.imgui 2>&1 | tee template.log test -f abc1/mcpp.toml + grep -F 'namespace=mcpplibs name=imgui' template.log - name: "LLVM: build mcpp" run: | @@ -466,13 +468,15 @@ jobs: # path (user report: `mcpp new abc1 --template imgui` failed with # fetch 'imgui@0.0.6' exit 1 on bare Windows — no sha256sum binary # outside git-bash; fixed by xlings 0.4.51 in-process hashing). - - name: "Template: mcpp new --template imgui (fetch path)" + - name: "Template: exact mcpplibs.imgui selector (fetch path)" shell: pwsh run: | $tmp = New-TemporaryFile | ForEach-Object { Remove-Item $_; New-Item -ItemType Directory -Path $_ } Set-Location $tmp - mcpp new abc1 --template imgui + mcpp new abc1 --template mcpplibs.imgui 2>&1 | Tee-Object -Variable templateOutput + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } if (!(Test-Path abc1/mcpp.toml)) { exit 1 } + if (-not (($templateOutput -join "`n") -match 'namespace=mcpplibs name=imgui')) { exit 1 } - name: "LLVM: build mcpp" shell: pwsh diff --git a/src/cli.cppm b/src/cli.cppm index 201dea48..41f3d4d2 100644 --- a/src/cli.cppm +++ b/src/cli.cppm @@ -229,9 +229,9 @@ int run(int argc, char** argv) { // (cmd_new validates presence for project creation itself). .arg(cl::Arg("name").help("Package directory name")) .option(cl::Option("template").short_name('t').takes_value().value_name("SPEC") - .help("bin (default) | [@ver][: