From 20da48c45950802cffac384674ffd8a3beab1aa2 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Thu, 5 Nov 2026 06:59:14 +0900 Subject: [PATCH 01/11] docs: record heterogeneous build decisions, experiments and implementation dependencies --- ...heterogeneous-build-implementation-plan.md | 88 ++ ...ogeneous-build-research-and-experiments.md | 369 +++++++++ ...antics-and-plugin-boundaries-supplement.md | 757 ++++++++++++++++++ 3 files changed, 1214 insertions(+) create mode 100644 .agents/docs/2026-11-05-heterogeneous-build-implementation-plan.md create mode 100644 .agents/docs/2026-11-05-heterogeneous-build-research-and-experiments.md create mode 100644 .agents/docs/2026-11-05-heterogeneous-build-semantics-and-plugin-boundaries-supplement.md diff --git a/.agents/docs/2026-11-05-heterogeneous-build-implementation-plan.md b/.agents/docs/2026-11-05-heterogeneous-build-implementation-plan.md new file mode 100644 index 000000000..38be4d877 --- /dev/null +++ b/.agents/docs/2026-11-05-heterogeneous-build-implementation-plan.md @@ -0,0 +1,88 @@ +--- +subject: heterogeneous-build +status: implementing +--- + +# 生态 885 与异构构建补充方案的实现依赖 + +**读者:** 推进 mcpp、插件、平台及语言服务发布的维护者。 + +**本文回答的问题:** 已确认的设计分别如何实现、验证和发布,哪些工作确实互为前置。 + +**范围之外:** 把局部实验等同完整验收,以及未经验证的全部 HPC 覆盖承诺。 + +日期:2026-11-05。相关规范:[构建插件](../../docs/specs/build-plugins.md)、 +[平台](../../docs/specs/platforms.md)、[目标侧](../../docs/specs/target-side.md)。 +依据:[补充方案](2026-11-05-heterogeneous-build-semantics-and-plugin-boundaries-supplement.md)、 +[第二轮实验](2026-11-05-heterogeneous-build-research-and-experiments.md)、 +[mcpp#885](https://github.com/mcpp-community/mcpp/issues/885)、 +[mcpp#887](https://github.com/mcpp-community/mcpp/issues/887)。 + +状态:用户已要求继续实现 #885 与补充子方案。按工作主题分别提交 PR,不以其中 +一个主题通过代表其他主题完成。以下为启动时状态;后续按具名检查点追加实证。 + +## 1. 当前交付与剩余范围 + +mcpp 2026.11.3.1 已发布,内置平台 0.23.3;公开平台已到 0.23.5,消费者可以显式 +选择。IDE 0.2.0 已交付。原 #885 的引擎、服务器及最终验收仍需闭环,不能只凭新 +方案实现完成就关闭原交付。 + +复核到引擎候选 `39f8589` 的完整 CI `41901404153` 已终止失败;两个 Linux 映像 +均因原生 TLS 段和 TLS 重定位被 seal 拒绝,没有 bundle。旧观察器已停止,未从 +失败构建触发诊断发布或资格提升。服务器对应候选也不能据此获准发布。 + +## 2. 实现批次 + +| 编号 | 改动主题 | 真正前置 | 验证与退出条件 | +|---|---|---|---| +| I0 | 保存设计、实验及依赖记录 | 已有用户实现指令 | 独立文档 PR,引用可定位、状态不夸大 | +| I1 | 核心完整请求验证、重复实现政策与集合规范化 | 无其他实现前置 | 新二进制的非法输入、次序及有效输入 E2E;比较单测;旧合法元数据保持兼容 | +| I2 | 插件 L2 统一读取;SYCL 归属目标及按需工具;相邻规则复核 | 通用语义已定;可与 I1 同时实现 | 单实现、混合目标、负向请求、工具可见性及多阶段实际产物 | +| I3 | requirement 消费上下文与 exclusive 链接闭包 | 无 I1/I2 前置 | 独立程序、应用供给、同程序冲突、共享库及身份控制 | +| I4 | 核心可选 action 进度 | 现有 action/执行模型 | 各角色、并发、关闭、失败、取消、普通及机器输出;真实产物不作为 stamp | +| I5 | 插件进度写入辅助与外部构建适配 | I4 的协议和运行通道 | 新核心集成,旧核心有明确可选降级;未知总量与限频 | +| I6 | 平台默认版本跟进与核心补丁发布 | 对应平台公开可安装,核心改动通过各自门槛 | 默认/显式/锁、std BMI、宿主与目标图、launcher 自举及完整发布资产 | +| L1 | 引擎 ThinLTO 的 TLS 正确性 | 小型双架构控制定位 | 平台约定覆盖最终 LTO codegen;seal 与执行控制,不移除检查 | +| L2 | 原 #885 引擎资格与发布 | L1,平台及依赖已公开 | 完整 CI、原始工作负载和性能门槛、台账、双源产物校验 | +| L3 | 原 #885 服务器资格与发布 | L2 的合格引擎 | 更新候选/lock,最终 CI 与已发布引擎一致,再发布 | +| L4 | 原 #885 最终审查、洁净室及清理 | L2、L3 及其余原门槛 | 最终数字与真实公开安装,证据保存,相关项目仅保留最近构建 | + +I1、I2、I3、I4 可分别推进;发布时只声明实际完成范围。I2 的普通修复不等待新核心 +进度接口,也不等待完整多节点测试。I4 的核心与 I5 的辅助保持版本边界。 +多目标归档的独立 SDK 失败继续在工具链/插件流程中定位,不作为新增核心模式的 +理由。安装目录污染按不可变发行包契约处理,不把全量哈希所有包放到每次构建中。 + +## 3. 依赖图 + +```mermaid +flowchart TD + I0[设计与实施记录] --> I1[核心请求正确性] + I0 --> I2[插件目标与工具请求] + I0 --> I3[provider 上下文] + I0 --> I4[核心可选进度] + I4 --> I5[插件进度辅助与适配] + I1 --> I6[核心补丁及默认平台验收发布] + I3 --> I6 + I4 --> I6 + L1[TLS 与平台正确性] --> L2[引擎完整资格及发布] + L1 --> I6 + L2 --> L3[服务器最终验收及发布] + L2 --> L4[原885最终审查与洁净室] + L3 --> L4 +``` + +图中的文档前置是交付记录,不要求等待文档 CI 才开始本地实现。没有画出 I2 到 +L2 的边,因为语言服务引擎不依赖 SYCL 目标修复。I6 的合并与发布次序按最终 +实际兼容性安排,不从图推出可绕过某项 required check。 + +## 4. 启动控制:ThinLTO 丢失最终 TLS codegen 约定 + +LLVM 23.1.3 上将 `__thread` 变量用 `-femulated-tls -flto=thin -fPIC` 编译为 bitcode, +再以 ld.lld 链接。x86_64 默认 LTO 输出含 TLS 段及 DTPMOD/DTPOFF 重定位, +aarch64 含 TLS 段及 TLSDESC。最终链接增加 `-mllvm -emulated-tls` 后,两者均无 +TLS 段,转为 `__emutls_get_address` 与 emulated storage。 + +该双架构小型复现与真实 CI 拒绝一致,证据位于 +`/tmp/mcpp-885-thinlto-tls-fix/results.json`。它不等于 clangd 完整资格通过。 +该约定属于映像平台的链接/codegen 契约,优先在平台数据中补齐,使其他 LTO +消费者同样受益;不在通用 mcpp 核心增加 openkal TLS 分支。 diff --git a/.agents/docs/2026-11-05-heterogeneous-build-research-and-experiments.md b/.agents/docs/2026-11-05-heterogeneous-build-research-and-experiments.md new file mode 100644 index 000000000..a1f468c23 --- /dev/null +++ b/.agents/docs/2026-11-05-heterogeneous-build-research-and-experiments.md @@ -0,0 +1,369 @@ +--- +subject: heterogeneous-build +status: proposed +--- + +# 异构构建补充方案的第二轮调研与实验 + +**读者:** review mcpp 通用机制、平台包及异构插件的维护者。 + +**本文回答的问题:** 首轮同意的方向中,哪些具体接口和责任可由实际控制收敛。 + +**范围之外:** 实现补丁、发布操作及未执行的 GPU、跨系统和集群验收。 + +日期:2026-11-05。相关规范:[构建插件](../../docs/specs/build-plugins.md)、 +[平台](../../docs/specs/platforms.md)、[目标侧](../../docs/specs/target-side.md)。 +前文:[异构构建语义与插件边界补充方案](2026-11-05-heterogeneous-build-semantics-and-plugin-boundaries-supplement.md)。 + +状态:首轮 D1–D10 的方向已获用户同意;D10 增加了“可在 build.mcpp 中按需配置” +的要求。本记录给出实验、收敛建议和仍需 review 的具体取舍,不表示实现已落地。 + +## 1. 基线与证据范围 + +| 对象 | 身份 | 用途 | +|---|---|---| +| 发布程序 | mcpp 2026.11.3.1,SHA-256 `e1fb3ece147c7511cc447c4b4e2499cc6f51a15c276cf771772f27e4539c1799` | 实际构建、工作空间及协议控制 | +| mcpp 源码 | `e0aee5cb7dfa5b13d1a5248d447947534d3cfa8b` | origin/main;API、解析、兼容比较与作用域 | +| mcpp-plugins 源码 | `86c76ce7dcf148e8333a69fcecf7bd97f076f415` | origin/main;已发布规则与工具声明 | +| DPC++ | 7.1.0,clang 22.1.0,Intel LLVM `880399f47228ac92f403e3b2b865cf1d6f0b1382` | 实际编译、设备链接和镜像提取 | +| CUDA 工具 | cuda-nvcc 12.9.86 | ptxas、设备库及 fatbin | +| 实验系统 | Linux x86_64 | 不代表 macOS/Windows 或其他架构 | + +独立的 DPC++ 控制使用系统 GCC 13 头文件与库;mcpp 构建控制使用其解析到的 +GCC 16.1.0。两个工具环境分开记录,不把独立驱动实验写成插件集成通过。 +原始脚本、输入与日志位于 `/tmp/mcpp-887-research-round2/`;本文保留关键事实及 +复现结构,临时文件不作为未来版本的交付保证。 + +## 2. E1:通用请求解析与集合行为 + +从准确源码提取未修改的 `AccelSet`、`major_of`、`parse_accel` 和 +`accel_arch_level/covers/accepts`,使用系统 GCC 13 以 C++23 独立编译。 +再对实际发布程序执行不含 GPU 源码的控制,分清函数行为与真实命令行为。 + +### 2.1 实际发布程序的结果 + +| 请求或控制 | 11.3.1 实际结果 | 结论 | +|---|---|---| +| `--accel 'sycl+{sm_89'` | 构建成功 | 缺少右括号仍被接受,需核心结构验证 | +| `--accel 'sycl+{sm_89}garbage'` | 构建成功 | 未消费的尾部文字被忽略,需核心结构验证 | +| `sycl+{spir64,nvptx64-sm_89,nvptx64-sm_90}` | 通用请求可进入构建 | 既有核心形状可承载带归属的混合目标;不证明现有 SYCL 规则已消费 | +| 源约束为 `sycl+{sm_90}`,请求 `sycl+{sm_89},sycl+{sm_90}` | 拒绝,不满足约束 | 比较只取第一个同名成员 | +| 同一源约束,将上述两个成员顺序交换 | 构建成功 | 重复成员导致顺序相关,需明确拒绝或合并策略 | +| 同一源约束,请求 `sycl+{sm_89,sm_90}` | 构建成功 | 单成员目标集合是正常对照 | +| 同一目标集合交换集合内顺序 | 指纹从 `afb8577403ee8e10` 变为 `95ecca2e6fe9ed71` | 有冗余重编;没有证明错误命中 | + +这些均为同一普通 C++ 程序上的构建控制,不需要 GPU。前两项不是特定 SYCL +参数的值域问题,而是核心未完整验证通用结构。重复成员的最终政策尚需 review, +但不能继续无诊断地让成员次序决定是否满足同一集合约束。 + +### 2.2 推荐收敛 + +核心首先完整验证请求结构、配对括号、空元素及未消费尾部,并给出位置与请求来源。 +L2 读取核心已接受的通用形状,L3 检查模型的目标值域;未知厂商目标不由核心枚举。 +暂不新增公开的结构化 API,以现有 `mcpp::accel()` 为传递通道。 + +推荐每种实现只出现一个成员,重复实现直接拒绝并提示合并目标集合;集合内相同 +目标可去重,等价集合规范化,命令 argv 保留原有顺序语义。规范化进入新版本的 +身份规则,说明一次缓存迁移,不静默改写用户 manifest 或锁记录。 + +SYCL 推荐评审的规范表达为: + +```toml +[build] +accel = "sycl+{spir64,nvptx64-sm_89,nvptx64-sm_90}" +``` + +该表达基于已有通用结构,**尚不是已发布 SYCL 规则的使用承诺**。不写目标集合的 +`sycl` 保留既有缺省 SPIR-V 意义;显式集合只生成所列形式,不偷偷追加回退。 +`sm_89` 可作为仅指 NVIDIA 的兼容简写评估,诊断与报告优先显示有域的目标名。 +其他后端的版本修饰保留已有含义,不在本轮统一解释为编译器包版本。 + +## 3. E2:可选 action 子进度 + +### 3.1 当前 API 与机制 + +`src/build/hostprogram.cppm::action` 具有 `description`、`env`、`cwd`、输入输出和 +角色,**没有子进度配置字段**。核心通过 `MCPP_ACTION_STARTS` 记录 check/prepare +开始;`__action` 会隔离父构建的 Ninja 进度环境,子构建不能直接报告到父开始文件。 + +设置 `NINJA_STATUS` 只改变子工具输出,不解决外层 Ninja 缓冲。Ninja 的 console +pool 可以直连标准流,但深度为一,并改变并发输出行为,因此不把所有长 action +切入该 pool 作为默认解决方案。[Ninja 官方手册](https://ninja-build.org/manual.html#_the_literal_console_literal_pool) + +### 3.2 实验结构与结果 + +真实外层 Ninja → 发布版 `mcpp __action` → 研究用 adapter → 真实内层 Ninja。 +内层串行执行三个各等待 0.4 秒的命令;adapter 同时转发普通日志,并原子替换各 +action 自己的 JSON 快照。独立观察器读取快照,模拟未来核心读取通道。 + +| 控制 | 首个快照 | 首次外层日志 | 结果 | +|---|---|---|---| +| 普通模式 | 0.469 s | 1.347 s | action 成功、stamp 存在 | +| verbose | 0.487 s | 1.373 s | verbose 未解除外层缓冲 | +| 两个 action 并行 | 两者约 0.478 s | 1.350 s | 各自快照推进,均成功 | +| 子计数 3/3 后 adapter 返回失败 | 仍可读到 3/3 | 约 1.379 s | 外层失败,stamp 不存在 | +| 关闭报告 | 无快照 | 1.328 s | 构建仍成功 | +| 首个快照后向进程组发送 SIGINT | 0.476 s | 0.479 s | 外层退出 2,stamp 不存在 | + +这些结果验证 Linux 上可绕过日志缓冲的传输形态,**没有修改 mcpp 的进度 UI**, +没有验证 Windows 的替换、取消和打开文件行为,也不证明 GPU 编译持续活跃。 +另一个快照校验原型拒绝残缺 JSON、过期序号、负数、布尔计数、越界计数和超长数据; +一万次更新只保留最后一份状态。它同样不是当前核心实现。 + +### 3.3 R1:配置与运行时报告的最小接口 + +推荐新增一个可选字段,沿用 action 现有字段风格,默认关闭: + +```cpp +// 拟议接口;11.3.1 没有 progress 字段。 +mcpp::action a; +a.id = "component-build"; +a.role = mcpp::roles::prepare; +a.description = "Build the external component"; +a.progress = true; +// 声明 inputs、outputs、output_dir 和 argv,随后 submit。 +``` + +`build.mcpp` 只声明该 action 愿意提供进度,不在规划期施工,也不静态声明总任务数。 +工具本身或插件 adapter 在执行期报告。核心不解析 LLVM、CMake 或 SDK 的业务日志。 + +| 层 | 提议职责 | +|---|---| +| 核心 | 为被观察的运行分配独立目录与 action 文件,注入预留环境变量,读取、关联和呈现状态 | +| 插件通用库 | 提供不依赖具体工具的快照写入辅助;没有通道时直接无操作 | +| 工具适配器 | 解释自己的阶段、已完成项及已知总量,作为该 action 的单一报告者 | +| 用户 | 在 build.mcpp 或规则 options 中按需启用;普通无报告 action 继续有效 | + +候选运行变量名为 `MCPP_ACTION_PROGRESS_FILE`;文件由引擎分配,不由工程固定路径。 +一份快照示意为: + +```json +{"v":1,"seq":12,"phase":"compile","done":80,"total":200} +``` + +核心确定 action 身份;快照不提交任意 action id,不声明成功,不改变图或产物。 +阶段名由报告者定义。未知总量省略 `total`;阶段变化可重新计数,`seq` 在该次运行内 +递增。快照按同目录临时文件替换,每 action 单写者;旧运行路径不复用。 + +建议初始边界为每份最多 8 KiB,读取在解析前截断并拒绝超限输入,只保留活动 action +的最新状态,写入限频至每秒五次。 +TTY 按 UI 节奏采样;非 TTY 合并输出阶段变化及间隔状态;JSON 使用有版本的事件, +不把普通子 stdout 直接混入 NDJSON。残缺或不可读的进度丢弃并限次诊断,命令和 +产物的失败仍正常报告。关闭显示且没有机器观察者时不需要分配实时通道。 + +普通日志和快照彼此独立。action 仅在进程成功及既有产物后置条件通过后完成;计数 +到 100%、心跳和“阶段完成”都没有该权限。取消后停止接收本次状态,保留既有取消 +和 stamp 规则;遥测文件不进入安装包、发布包或构建输入。 + +可选报告适用于各 action 角色。source/object 的真实输出不能借用 `--stamp` 传给 +包装器,否则成功后写 stamp 会覆盖源码或对象。观测身份与 stamp 分开,已有真实 +产物由原命令产生。发布版的无 stamp 包装控制已实际生成 ELF 对象,字节与直接编译 +一致;新增观测身份、开始记录和 UI 归属仍需在核心实现,不能只复用 check/prepare。 + +**兼容性边界:** 旧 action 不设置字段时继续省略它;新核心读取旧 payload 时缺省 +关闭。新 API 需相应核心版本;插件若兼容旧核心,只能通过依赖式模板探测可选字段 +并降级报告,或明确提高最低版本,不能在普通运行时 `if` 中访问不存在的成员。 +已用未修改的旧 action 结构与研究用新结构独立编译依赖式 `requires` 探测:旧结构 +返回不支持,新结构成功启用。该控制没有为旧核心增加实际进度能力。 +实时快照和随机路径不进身份;修改 build.mcpp 启停该字段仍可能按现有规则重规划或 +重建,不承诺零失效,也不因遥测而重新设计全部缓存。 + +## 4. E3:provider 绑定与互斥的作用域 + +### 4.1 真实工作空间反例 + +虚拟工作空间包含 app-a、app-b。app-a 只依赖 provider-a,app-b 只依赖 provider-b。 +两 provider 都提供 `probe-interface`,均声明它为 exclusive,各自定义同名入口, +分别返回一和二。两个应用分别断言对应返回值。 + +| 控制 | 结果 | +|---|---| +| 单独构建并运行 app-a | 成功 | +| 单独构建并运行 app-b | 成功 | +| 同一工作空间 `build --workspace` | 被全局 exclusive 检查拒绝 | +| 去掉 exclusive,构建工作空间并直接运行两个产物 | 均成功;生成图确认每个程序只链接自己的 provider 对象 | +| 一个应用同时依赖两个 exclusive provider | 正确拒绝,保留为负向控制 | +| 去掉 exclusive,但两个应用各自的默认 feature requires 同一 capability | 各自构建通过,工作空间构建被全局多 provider 歧义检查拒绝 | +| 共享 helper 声明 requirement,两个应用分别在根供给 provider | 各自构建通过,工作空间仍因候选被全局混合而拒绝 | + +按 `step6_capability_binding` 的实现,候选和互斥声明按准备图累计,requirement 还 +按 capability 名全局去重。实际不同链接闭包的程序因此被当作同一组合。这是已复现 +的 mcpp 通用作用域问题,不是 GPU 插件特例,也不由取消 exclusive 声明解决。 + +### 4.2 R2:两个检查范围 + +推荐 requirement 按**最终消费上下文中对要求者可见的已声明候选**选 provider, +不能从同工作空间其他独立程序的闭包借候选。候选包括应用在本消费图中供给的 +provider,不限于要求者自身的子依赖:共享 helper 声明需求、应用分别供给的控制 +实际通过,不能因修正全局作用域而破坏这种注入。 +exclusive 则按**实际组合的链接闭包**检查,防止同一程序或共享产物组合冲突。 +独立程序、独立工具图不能因一次批量构建就相互排斥。 + +绑定记录至少区分消费上下文、要求者和 capability,不再按 capability 名全局去重。 +同一 helper 可在两个应用中绑定不同 provider;若答案影响编译输入,则分别形成 +相应变体身份,若只影响最终链接则不无理由复制无关编译。该身份与闭包变化仍需 +实现验证,不能仅把全局检查搬到另一处。 + +暂不新增用户可选的“全局/局部互斥模式”。已有包声明继续表达同一接口的互斥, +核心修正检查上下文。pin 在其适用上下文内选择候选,不能凭 pin 删除已经链接的 +另一个定义。需要检查静态库、共享库、测试程序及显式运行依赖;未声明的 dlopen +不由构建图推断。错误诊断展示要求者或链接产物及两条依赖路径。 + +## 5. E4:实际多目标、跨 TU 和跨静态库 + +### 5.1 真实设备代码的控制 + +`kernel.cpp` 提交一个 SYCL single_task,调用由另一个 TU 定义的 +`SYCL_EXTERNAL int add_seven(int)`;设备结果预期为 12。 +实际驱动采用明确的 target triple 与逐目标后端参数: + +```text +-fsycl-targets=nvptx64-nvidia-cuda +-Xsycl-target-backend=nvptx64-nvidia-cuda --cuda-gpu-arch=sm_89 +-Xsycl-target-backend=nvptx64-nvidia-cuda --cuda-gpu-arch=sm_90 +``` + +混合形式使用 `-fsycl-targets=spir64,nvptx64-nvidia-cuda`,仅请求 sm_89。 +这与[驱动手册](https://intel.github.io/llvm/UsersManual.html)的目标和后端参数分工一致; +这里的最终判断来自锁定驱动的实际编译与产物,不只来自文档或 `-###`。 + +| 场景 | 实际结果 | 覆盖层级 | +|---|---|---| +| 两 TU、sm_89 与 sm_90 | 成功,约 6.68 s;从最终程序提取四份 NVPTX 镜像 | 实际编译、设备链接、最终产物 | +| 两 TU、SPIR-V 与 sm_89 | 成功,约 6.46 s;提取一份 SPIR64 与两份 NVPTX 镜像 | 实际编译、设备链接、最终产物 | +| 移除设备函数定义 TU | ptxas 拒绝未解析的 `_Z9add_seveni` | 负向设备链接 | +| 单目标 sm_89,将 util.o 放入 libdevice.a | 编译、归档与最终设备链接成功,提取两份镜像 | 单目标跨静态库 | +| sm_89 与 sm_90,二者都封装在 libdevice.a | 独立驱动链接报同一设备符号重复定义 | 多目标与归档的组合未通过 | +| NVIDIA 目标别名,无 save-temps | 实际编译通过 | 不把别名整体归为不可用 | +| 目标别名与 `-save-temps=obj` 组合 | 报 `OpenMP target is invalid`;显式 triple 同类控制通过 | 该驱动组合限制,非核心证明 | + +CUDA ELF 的 `e_machine=190`,两组 flags 分别为 `0x590559`、`0x5a055a`;对应 +镜像字节可在最终程序找到,另以 clang-offload-extract 验证。混合程序中提取的 +SPIR64 镜像具有 SPIR-V 魔数。不能用四份镜像推断四种硬件目标:多个设备模块可以 +属于同一目标,目标覆盖需按实际元数据及内容检查。 + +独立驱动成功时还有 libdl、libpthread、librt 搜索路径警告;它不是发行环境验收。 +使用 `ONEAPI_DEVICE_SELECTOR=cuda:gpu` 尝试两个程序,均返回 +`CUDA_ERROR_SYSTEM_DRIVER_MISMATCH`(803),未执行到数值断言。因此没有 GPU +执行通过、CPU 回退通过或端到端插件通过的声明。 + +### 5.2 R3:规则的阶段与组合验收 + +推荐编译和设备链接从同一个已解析目标集合派生。用户的有域目标由插件映射到 +明确 triple 和后端参数;驱动别名属于插件内部策略,不要求核心理解它们。 + +多个 TU 和单目标归档均能真实完成,不足以宣布“多目标 × 归档”也能组合。 +该失败发生在独立 SDK 调用中,优先在工具链及插件阶段设计中调查;正确调用或 +逐目标分区方案尚未验证前,不宣布上游 bug 已定根,也不增加 mcpp 专属链接模式。 +跨包还需证明产物和使用需求正确传播,本轮没有完成该集成。 + +规则不能只让单目标 fixture 通过。验收增加目标集合、TU 数量、归档边界的交叉控制; +可用组合实际检查镜像,不可用组合应给出明确工具链和阶段诊断,不能静默少生成目标。 + +## 6. E5:组件边界与平台通用性 + +普通 C++ 组件放在 `component/`,主图源码列表只含 `src/main.cpp`。build.mcpp +为两个 TU 分别声明 object action,显式命名工具、源码、对象及 depfile;组件入口为 +C ABI,内部一个 TU 调用另一个 TU。所有对象由同一个主程序消费。 + +首次运行输出 11;无变化构建约 0.029 s;只修改组件头文件后输出 21;移除内部函数 +定义则在链接阶段明确失败。说明现有 object actions 和 depfile 可承载这一 CPU +组件形态,无需核心接管另一编译器的源码。它不覆盖 C++ 模块跨编译器、OpenMP +offload、stdpar、GPU 运行或完整外部工程输入扫描。 + +另实际运行现有 e2e 952,通过非 openkal 平台的编译参数、名称、seal、runner、 +配置覆盖、关闭平台及错误平台包控制。程序重新链接后只 seal 一次。脚本有一条 +命令替换忽略 NUL 的 shell 警告;用例仍通过,不隐去该限制。 + +还构造了三个单程序打包元数据夹具,均声明 `runs_on.self=[linux-x86_64]`。 +native 与 openkal 的 emitter 控制通过,wasm leg 加同一声明被 `program_host` 拒绝。 +这是使用原生占位程序的**合成元数据实验**,只能验证名字相关分类,不是一个真实 +wasm carrier 的生成和执行证据。 + +**R4 推荐:** 平台默认升级继续按前文 PL1–PL9 验收;launcher 自举组合独立复核。 +载体宿主分类优先评估已有 `runs_on` 与角色能否表达,暂不增加第二套宿主字段。 +没有真实非 openkal carrier 交付场景前,不把上述夹具拒绝扩大为整个打包系统错误。 + +## 7. E6:兼容性与身份的收敛边界 + +精确源码比较器实际接受了 `custom1.0+{90a}` 对 `custom1.0+{90b}`,以及同一 opaque +目标上的 custom 1.0 对 1.9;重复同名成员只比较第一项。这确认数字和版本关系 +应用到整个开放词汇,并不只应用到 CUDA。 + +但现有公开文档已描述同主版本和数字架构比较,不能把这一既有契约全部称为新发现 +的 bug。新 SYCL 目标采用 `nvptx64-sm_89` 等 opaque 名字时,现有架构比较走精确 +相等路径;不需要为了 #887 先增加兼容性表达式语言。 + +**R5 推荐:** 本轮修结构验证、重复成员和作用域缺陷;保留旧描述符的既有匹配契约。 +新模型优先使用有域的 opaque 目标,不把新数字规则继续引入核心。新的通用覆盖 +元数据和版本化匹配协议,等两个实际模型证明精确路径不足时再独立设计。现阶段 +不新增 `accel_match`、覆盖 DSL 或需要运行插件的预构建筛选。 + +安装来源方面,继续采用用户已认可的不可变发行包与可变 path 边界。进度不要求扫描 +所有安装源码;安装后污染的校验和恢复作为独立维护能力,不伪装成已有自动保证。 +目标集合换序造成的新指纹单列为规范化优化;不能用减少冗余重编的工作替代正确性 +修复,也不在没有验证的情况下重用旧 BMI。 + +## 8. 后续验收与推进依赖 + +| 编号 | 待实现或待验证的断言 | 责任 | +|---|---|---| +| V1 | 缺括号、尾部垃圾、空元素在核心入口明确拒绝 | mcpp | +| V2 | 重复实现政策确定;交换成员顺序不再无诊断地改变覆盖结论 | mcpp,L2 共享读取控制 | +| V3 | 独立程序在工作空间可分别绑定,保留共享库需求由应用供给;有效输入变化不误复用 | mcpp | +| V4 | 同一组合闭包内的 exclusive 冲突继续拒绝,pin 不绕过 | mcpp | +| V5 | 各角色可选进度覆盖并发、关闭、失败、取消及机器输出;不覆盖真实输出 | mcpp 与插件通用辅助 | +| V6 | macOS/Windows 的快照替换、并发读取及取消控制 | 核心平台层及 CI | +| V7 | 多目标与跨静态库的组合形成正确设备链接与镜像 | 工具链与规则插件 | +| V8 | 实际跨包设备产物、使用需求、失效和 ABI 闭环 | 插件与包;有契约反例再扩核心 | +| V9 | GPU 设备及多节点实际执行,报告确切实现和设备 | 应用、运行时和环境 | + +确定性修复可并行:核心 V1–V2 与插件读取修复先行;V3–V4 独立修作用域;可选 +进度 V5–V6 独立开发;默认平台补丁继续走 PL 矩阵。插件混合目标支持与工具请求 +闭环后,推进 V7–V9。新匹配协议和额外核心构建模式不作为这些工作的前置。 + +on-request 继续复用现有协议。SYCL 的 CUDA 载荷声明应由“sycl 与 cuda 同时选中” +改为 SYCL 规则可见的 on-request 声明,再由自己的 NVIDIA 目标请求;不新增厂商 cfg。 +本轮重跑 e2e 874 的范围是“不请求不安装、请求到达安装器、未声明请求拒绝及规划 +延后”,不能据此宣布实际 SDK 的首次下载与完整集成已经通过。 + +## 9. 第二遍自审的结论 + +| 视角 | 修订或保留的边界 | 结论 | +|---|---|---| +| 用户表达 | 有域目标只写一次;混合实现与混合设备分开 | 提案可 review,语法未宣布发布 | +| 核心职责 | 结构验证和闭包作用域为通用问题,SDK 链接流程不进核心 | 已按实际证据重新分类 | +| 接口复杂度 | 进度只增加一个可选声明,不新增静态任务总数、核心工具解析或运行策略 | 推荐接受 R1,生产实现尚缺 | +| 进度可信度 | 3/3 后失败控制无 stamp;心跳和快照无成功权限 | Linux 原型通过,跨系统待 V6 | +| action 角色 | 真实输出不能被当作 stamp;无 stamp 包装与直接生成的 ELF 字节相同 | 观测与产物分开,新增归属仍待实现 | +| 性能与缓存 | 实时文件不进身份;修改构建程序仍可能重建 | 未承诺零开销或零失效 | +| 并发与取消 | 文件按 action 隔离,Linux 进程组取消不产 stamp | 原型通过,不外推全部进程生命周期 | +| 程序组合 | 只看要求者自身子依赖会破坏应用供给;补充 helper 控制后改为消费上下文 | 反例成立,保留注入语义,闭包及身份修正仍需实现 | +| 工具链兼容 | 别名本身成功,失败限定为与 save-temps 的组合 | 修正过宽归因 | +| 设备链接 | 多 TU、多目标分别成功,不自动推出归档交叉组合成功 | 保留实际失败与 V7 | +| 产物核验 | 从最终程序提取镜像;镜像数量不等于目标数量 | 未以命令文字替代产物 | +| 运行环境 | 驱动 mismatch 导致运行失败,未冒充 CPU 或 GPU 验证 | GPU 数值验收未通过 | +| 平台抽象 | 真实非 openkal 平台通过;carrier 夹具仍是合成输入 | 不夸大真实载体覆盖 | +| 历史兼容 | 当前数字规则已有公开契约,不直接全部移除 | 后续迁移独立设计 | +| 发布范围 | 原 #885 验收与新设计分开,已同意方向不等于实现完成 | 保留 review 与交付边界 | + +## 10. 实验记录入口 + +以下均位于 `/tmp/mcpp-887-research-round2/`: + +| 记录 | 主要证据 | +|---|---| +| `exact-accel.cpp`、`exact-accel-results.json` | 未修改核心函数及实际输出 | +| `build_probe.py`、`build-results.json` | 发布版请求、工作空间与组件控制 | +| `grammar-duplicate-controls.json`、`set-order-control.json` | 次序相关覆盖与集合指纹 | +| `exclusive-controls.json`、`capability-requirement-controls.json`、`root-supplied-provider-controls.json` | 独立程序、同程序负向控制、requirement 反例及应用供给 | +| `progress_probe.py`、`progress-results.json`、`progress-validation-results.json` | 普通/verbose/并行/失败/关闭/取消及有界快照原型 | +| `progress-api-compat.cpp`、`progress-api-compat-results.json` | 旧 action 与研究用新结构的可选字段探测 | +| `nonstamped-wrapper-results.json` | 无 stamp 包装保持真实对象字节 | +| `sycl_compile_probe.py`、`sycl-real/results.json` | 真实 TU、目标组合与负向设备链接 | +| `sycl-real/artifact-inspection.json`、`sycl-real/extraction-results.json` | 镜像字节、ELF 和最终程序提取 | +| `sycl-real/runtime-alias-results.json`、`sycl-real/alias-save-temps-results.json` | 运行失败与别名对照 | +| `sycl_archive_probe.py`、`sycl-real/archive-results.json`、`sycl-real/archive-single-results.json` | 归档组合的正负控制 | +| `platform952.log`、`on-request874.log`、`carrier-metadata-results.json` | 平台、现有工具请求及合成元数据控制 | + +后续代码提交应将确认的最小反例转为仓库内回归测试;研究脚本不是已发布测试套件。 diff --git a/.agents/docs/2026-11-05-heterogeneous-build-semantics-and-plugin-boundaries-supplement.md b/.agents/docs/2026-11-05-heterogeneous-build-semantics-and-plugin-boundaries-supplement.md new file mode 100644 index 000000000..0c3f7b0da --- /dev/null +++ b/.agents/docs/2026-11-05-heterogeneous-build-semantics-and-plugin-boundaries-supplement.md @@ -0,0 +1,757 @@ +--- +subject: heterogeneous-build +status: proposed +--- + +# 异构构建语义与插件边界补充方案(mcpp#885、mcpp#887) + +**读者:** review 本轮生态设计及实现 mcpp、规则插件和计算库的维护者。 + +**本文回答的问题:** 通用构建机制如何承载异构与多后端需求,并据此划分缺陷、优化和责任。 + +**范围之外:** GPU 算法、运行时调度策略、厂商工具参数的完整设计,以及尚未实现能力的使用手册。 + +**状态:** D1–D10 的方向已获用户同意;实验细化待评审,见 §16 与 +[第二轮调研记录](2026-11-05-heterogeneous-build-research-and-experiments.md)。 +本文不是实现完成记录,不直接采纳 #887 的补丁。 + +日期:2026-11-05。发布基线:mcpp **2026.11.3.1**。 +相关文档:[SPEC-007 构建插件](../../docs/specs/build-plugins.md)、 +[SPEC-004 清单语义](../../docs/specs/manifest-semantics.md)、 +[SPEC-002 目标侧](../../docs/specs/target-side.md)。后续规范修订由本方案的 review 决定。 +前文:[生态第 4 部分](2026-11-03-ecosystem-part4-beyond-native-design.md)。 +跟踪:[mcpp#885](https://github.com/mcpp-community/mcpp/issues/885)、 +[mcpp#887](https://github.com/mcpp-community/mcpp/issues/887)。 + +## 1. 提议结论与交付边界 + +1. mcpp 核心继续持有通用构建图、依赖与作用域、产物身份、兼容性和执行契约。 + 计算模型、设备目标值域、工具调用和设备链接流程由插件与相应包承担。 +2. island 继续作为优先接入范式。它的施工范围可以是一组单元或一个组件, + 不预设为一个源文件或一个 kernel。扩大施工范围的可行性与代价需要验证。 +3. 不因缺少某种现成插件就新增核心构建模式。核心扩展需要一个现有通用契约 + 无法表达的具体反例,并说明插件侧组合不能满足需求的原因。 +4. 多后端的源码选择、同产物共存、同实现多设备目标和运行时协作分别建模。 + 使用一个厂商的工具或设备,不隐式启用以该厂商命名的另一套源码实现。 +5. #887 先拆分为规则解析缺陷、目标与工具请求的语义缺口,以及独立的缓存调查。 + 最终语法由这些关系和现有兼容性共同决定,不能由临时补丁反推。 +6. 当前不能宣称全部 HPC 场景已被覆盖。本方案给出覆盖判据与实施顺序, + 不把架构可表达、驱动展开或局部控制等同于实际 GPU、多节点运行验证。 + +本文作为 #885 的补充设计接受 review,不自动改变其已有发布验收条件。 +本轮没有因本文新增核心字段、插件 API、发布版本或 #887 的修复完成声明。 +是否将相应实现纳入 #885 当前交付,由 review 明确,避免设计讨论隐式扩大发布范围。 + +## 2. 设计依据与不变量 + +现行 SPEC-007 区分配置、施工和校验。插件规划动作,施工通过具有输入输出的 +action 执行;本方案要求新增工具参数和厂商知识由插件承担。核心现有设备兼容性 +比较仍含模型知识,见 §14.5,不能将目标边界误写为当前实现已经完全满足。 +SPEC-004 分别定义字段作用域、 +来源和条件,条件不能改变字段本身的作用域。SPEC-002 约束目标侧与运行时兼容性。 + +本方案保留以下不变量: + +| 编号 | 不变量 | 实现后果 | +|---|---|---| +| I1 | 一个源码单元具有明确的编译所有者,一个输出具有唯一生产者 | 插件不能因 glob 匹配次序夺取核心已经分类的 `.cpp` 或 `.cppm` | +| I2 | 选择一种实现不隐式选择另一种实现 | SYCL 的 NVIDIA 目标不能独立导致 `.cu` 源码进入构建 | +| I3 | 工具依赖来自声明及请求 | store 中碰巧存在某工具不使它自动成为本次构建的输入 | +| I4 | 配置只依赖声明的输入 | 不根据未声明的 GPU、工具路径或施工结果改变图 | +| I5 | 请求、编译计划和实际产物分别可判定 | 声明两个目标不等于已经生成两个镜像 | +| I6 | 影响构建的有效输入进入相应身份与失效链 | 修改目标、规则源码、工具或已声明输入不能错误复用旧结果 | +| I7 | ABI 与运行库约束在实际组合范围内成立 | C 函数入口不能自动证明两套 C++ 运行时安全共存 | +| I8 | 运行策略保持独立 | 节点数、设备选择与回退策略不隐式改变无关编译输入 | + +I8 的独立性取决于实际用途。节点数若参与代码生成就是构建输入;仅作为启动参数时, +不应因此使同一程序重新编译。配置分类依据是影响,而不是参数名字。 + +## 3. 语义关系与最小表达 + +### 3.1 五种关系 + +| 关系 | 含义 | 现有承载点 | 责任 | +|---|---|---|---| +| 实现选择 | 本次包含工程中的哪些实现 | feature、源码条件、现有 accelerator 成员选择 | 工程声明;核心按契约选择 | +| 设备代码要求 | 某一实现需要生成的目标与可移植形式 | accelerator 的目标描述及插件选项 | 目标分工为核心传递结构、插件解释值域;当前比较器的遗留见 §14.5 | +| 构建供给 | 哪个编译器、设备库和工具满足计划 | 依赖、toolchain、payload、按需 provision | 包与插件提供;工程可覆盖策略 | +| 产物事实 | 实际包含的镜像、ABI、资源和运行依赖 | 产物核验、兼容性及打包元数据 | 插件或工具测量;核心保存和核对通用契约 | +| 执行策略 | 设备选择、回退、进程及节点分配 | 应用、runner、run_under、执行配置 | 应用或执行适配层 | + +这些关系是分析模型,不是五个拟新增的 manifest 字段。先使用现有通道, +表达不了再提出最小的通用扩展,不为每个计算框架增加一套配置语言。 + +### 3.2 请求具有归属 + +以 #887 为例,语义上的请求是“SYCL 实现生成 NVIDIA 目标的设备代码”。 +NVIDIA 是这条请求的设备目标,CUDA 工具包是完成施工所需的供给, +二者都不等于工程中另一个 CUDA 源码实现被选择。 + +推荐继续评估既有的“实现名称与其目标集合”的结构。核心按名称精确识别实现成员, +保留目标集合的归属;插件只解释自己负责的目标值。通用解析和模型知识分开。 + +`sycl+{sm_89}`、混合 SPIR-V 的写法和厂商别名均属于待评审候选, +本文不把它们宣布为已实现的规则接口。目标值域、别名、缺省形式、非法输入诊断 +与既有版本字段的含义需一起定稿。编译器和工具包的具体版本继续由工具链、依赖 +与锁记录约束,不借设备目标字符串增加第二套包版本选择机制。 + +### 3.3 简洁性的判据 + +同一个事实只声明一次,由它派生需要的配置。选择目标后,插件据此请求工具并生成 +编译和设备链接参数;工程无需再重复声明“为了这个目标安装哪些规则内部工具”。 +项目主动选择的库、运行时和部署策略仍由工程声明,不能被规则的工具默认值覆盖。 + +输入的便利写法不削弱内部结构。集合应有明确的等价、重复和顺序规则; +命令 argv 仍保持顺序,不能把所有列表都按集合排序。 +归一化变化需要兼容性与缓存迁移说明,不在本补充设计中直接改变既有构建身份。 + +### 3.4 平台与计算实现的正交关系 + +平台描述宿主对象及程序的构建、运行约定,计算实现描述特定设备施工。一个项目 +可以在同一平台上使用多个实现,同一种实现也可以有适用于不同平台的工具与运行库。 +openkal-platform 不是替 CUDA、SYCL 新增的一种互斥计算后端。 + +正交不等于任意组合均可运行。插件与包仍需声明支持的宿主 ABI、驱动接口和运行 +依赖;已有原生设备驱动不能未经适配就视作 openkal 内部可用。核心核对通用目标侧 +及产物契约,平台包和计算插件分别承担适配。本文没有 openkal 加任意 GPU 后端的 +端到端验证,不由多后端源码示例推导出这一支持结论。 + +## 4. island 的覆盖判据 + +### 4.1 三个独立层次 + +| 层次 | 判据 | 不能据此推出的结论 | +|---|---|---| +| 图的表达能力 | 能声明所需输入、动作、依赖、输出与消费关系 | 任意源码无需修改即可接入 | +| 程序语义 | ABI、内存、异常、设备链接及资源生命周期保持 | 性能、调试和增量构建代价相同 | +| 开发与工程成本 | 源码迁移、接口数量、调试、IDE、增量和跨包复用成本可接受 | 全部 HPC 工作负载已获验证 | + +当前实现的设备单元路径、架构上的 island 边界与 `mcpp.tools.island` 辅助工具 +分别描述不同范围。辅助工具的限制不能自动升级为核心构建图的限制。 + +### 4.2 施工范围 + +优先让插件在一个组件内组织多个翻译单元、设备链接、宿主对象和资源封装, +以明确的接口和产物接入主图。不要为保持单文件规则的形态而把每个 kernel +都切成独立接口,也不要为扩大施工范围而允许多个编译器无声明地拥有同一源码。 + +对于普通源码中的 OpenMP offload、stdpar 或既有大型工程,可评估由插件或外部 +构建适配器拥有整个组件。它是否保留原源码、模块协作、宿主 ABI、设备符号与调试 +体验,需要真实项目控制;当前缺少现成路径不能证明该方式必然不可行。 + +可执行外部构建命令只是接入的起点。要形成受支持路径,还需声明完整输入、 +产物与使用需求、正确失效与运行闭包,且不在规划期通过扫描施工结果决定图。 +组件中的普通 `.cpp` 也需要明确的所有权声明与消费边界;本节没有批准插件按 +扩展名或导入顺序接管主图源码,也没有证明当前辅助工具已经提供完整的组件接口。 + +### 4.3 核心扩展的准入 + +提出核心新机制时提供以下材料: + +1. 一个具有明确交付目标和约束的最小真实场景。 +2. 现有 actions、产物、依赖、使用需求与执行接口的表达尝试。 +3. 无法表达或只能破坏 I1–I8 才能表达的具体关系。 +4. 不携带厂商或计算框架知识的最小通用契约。 +5. 至少两个用途的覆盖判据,或只有一个用途时对其通用性的具体论证。 + +“没有现成插件”“不能原样编译旧工程”与“现有图无法表达”分开记录。 +源码兼容性可以是独立需求,但不能省略第 2、3 项论证。 + +## 5. 多后端的组合方式 + +| 编号 | 场景 | 接口与构建要求 | 主要责任 | +|---|---|---|---| +| M1 | 仓库保留多套实现,本次选一个 | 同一逻辑入口的替代实现,仅所选实现进入链接 | 工程选择;核心条件选择;插件编译 | +| M2 | 一个产物同时包含多套实现 | 每套实现有可同时存在的内部入口,外层提供统一接口或注册表 | 计算库或工程组织接口和分派;插件交付产物 | +| M3 | 一套实现包含多个设备代码目标 | 编译与设备链接使用同一目标集合,核验镜像覆盖 | 对应规则插件 | +| M4 | 同一进程使用多个后端或设备协作 | 运行时对象、内存、上下文、同步和错误处理有明确协议 | 应用与运行时库 | +| M5 | 不同程序或节点采用不同组合 | 每个产物有自身构建闭包,执行配置选择相应程序和资源 | 工程、依赖与执行适配层 | + +M1–M5 可以组合。例如一个库以 M2 同时提供 CUDA 和 SYCL 实现,其中 SYCL 又以 M3 +包含多个设备目标;应用以 M4 选择设备。这不要求核心增加五种构建模式。 + +### 5.1 替代与叠加的符号边界 + +当前 `tools-island` 的多 roots 描述同一入口的替代定义,约定一次只链接一个。 +将这些定义同时链接不是 M2 的正确实现。C 链接名称不因 C++ namespace 不同而隔离。 + +M2 使用独立内部入口和外层分派。当前 `multi-backend` 示例已按此方式组织 +CUDA、Vulkan 和 CPU。它证明这一图形态在源码中可表达,不构成 CUDA、SYCL、HIP +任意组合的运行验证。本轮未重新执行该示例的 GPU 验收。 + +若项目反复手写入口组织、注册与接口校验,可在插件辅助库减少机械工作。 +分派优先级、可用性判断和运行时回退仍由计算库或应用决定,不由核心推断。 + +### 5.2 进程级约束 + +同时链接的实现遵守统一的宿主 ABI 与运行库约束。一个 `extern "C"` 入口不自动 +隔离异常、分配器、全局 ABI 符号或运行时。继续使用 SPEC-002 的统一、孤岛和宿主 +决定模式及其条件,不通过追加链接参数绕过兼容性检查。 + +## 6. mcpp#887 的分类与责任 + +分类依据是当前发布版和准确源码控制;Issue 的成功补丁属于报告者证据, +不自动证明每一项根因推断或当前版本中的行为。 + +| 编号 | 现象 | 分类 | 归属与处置 | +|---|---|---|---| +| B1 | SYCL 解析按普通逗号切分,架构集合变空 | 已确认解析 bug | mcpp-plugins 修正;HIP 同类代码一并核对 | +| B2 | SYCL chunk 的目标集合未被规则消费 | 目标表达缺口;已接受目标静默退化属于正确性问题 | 插件明确支持值域与诊断,不能以缺省 SPIR-V 掩盖不支持的请求 | +| B3 | 建议启用 `cuda` 来表达 SYCL 的 NVIDIA 目标 | 插件对既有选择语义的误用 | 修正规则的目标归属和建议,不改变核心对显式 CUDA 成员的正常选择 | +| B4 | 所需 CUDA payload 只在 SYCL、CUDA 同时选中时声明 | 工具请求与目标需求不一致 | 插件按实际目标请求已声明的工具;复用按需 provision | +| B5 | 被选中的 `.cu` 没有任何动作编译,核心拒绝 | 正确的覆盖检查 | 保留检查;可优化诊断以展示选择来源和规则归属 | +| B6 | 同一后端的版本或架构约束不满足,核心拒绝 | 正确的约束检查 | 不改为静默过滤;未选中的其他实现仍按既有语义排除 | +| B7 | store 中工具已安装,但本次未声明,规则读不到 | 声明范围的预期行为 | 修复声明或请求;不增加任意搜索 store 的回退 | +| B8 | 修改安装目录内的插件版本包后继续复用 | 不可变源码假设与诊断问题 | mcpp 单独明确契约;不是 SYCL 专属缺陷 | +| B9 | `sycl` 与带架构的请求共用构建指纹 | 本次 11.3.1 通用路径未复现 | 保留调查项;取得准确入口、工具链、有效请求与缓存证据后再归因 | +| B10 | 没有 NVIDIA 时要求自动改用 CPU | 需要代码覆盖、运行环境与应用策略共同满足的需求 | 构建规则生成被请求的形式;应用或运行时决定回退 | +| B11 | 请求缺少右括号或附带未消费尾部,11.3.1 仍构建成功 | 第二轮已复现的通用输入验证问题 | mcpp 完整验证通用结构;插件继续验证模型值域 | +| B12 | 同名实现出现两次,交换次序改变源约束是否通过 | 第二轮已复现的顺序相关行为 | mcpp 明确重复成员政策,不再按第一个成员无诊断判断 | + +B2 的具体拼写支持属于接口设计,不能仅因核心能够传递一个字符串就断言规则已经 +支持其全部值。输入的非法形式、不支持的目标、环境缺失和待安装工具应分别诊断。 + +## 7. 推荐的插件侧设计 + +### 7.1 通用读取与模型专属解释 + +在插件通用库收拢线格式的读取与括号感知切分,形成一个内部结构供各规则读取。 +实现名称使用精确匹配;目标列表、版本和可移植形式修饰保留归属。 +不要继续在各规则中用独立的 `split` 与 `starts_with` 推导成员关系。 + +核心继续定义通用线格式与归一化契约。L2 承担统一读取;L3 解释自身目标值及 +调用工具的方式。本节未批准新的公开模块名或 API;若需要核心新增结构化读取接口, +先说明现有通道的不足,并保持接口不含厂商枚举。 + +### 7.2 工具请求与阶段 + +规则声明自己的编译工具及允许工程覆盖的默认值。只有部分目标需要的工具采用 +现行 `provision = "on-request"`,在目标解析后请求。项目明确自备工具时,遵守 +现行选择优先级,不重复下载。工具的选择和来源通过已有 decision 通道记录。 + +pending 表示引擎需要安装后重跑:程序正常返回,不提交依赖该工具的配置,也不报成 +工具缺失。环境尚不完整时遵守 SPEC-007 的规划义务,尽可能输出确定配置和警告; +施工及环境校验经对应 action 完成。错误的请求形式与环境缺失不是同一类问题。 + +本方案不引入“目标厂商包含某值”的新 cfg,也不把 CUDA 工具包知识放入核心。 +已有条件和按需请求不能覆盖的实际反例,才进入核心通道扩展评估。 + +### 7.3 多阶段编译 + +同一已解析目标集合同时驱动设备编译与设备链接。规则显式声明中间产物与依赖边, +不能让编译阶段和链接阶段各自解析一次并得到不同目标。 + +跨翻译单元、跨静态库的设备链接单列验收。若现有 actions 能表达,责任在插件; +若跨包产物及其使用需求无法传递,先提供最小反例,再设计通用产物传递或依赖契约。 +具体设备链接命令继续属于插件,不能把 `nvlink` 或特定 SYCL 驱动流程嵌入核心。 + +### 7.4 兼容性 + +既有显式多成员请求继续表示同时选择这些成员,不能根据源码是否存在或某个规则是否 +被导入,把 `sycl, cuda...` 静默改判为只选 SYCL。需要迁移的旧建议应提供明确诊断与 +替代路径。本文不决定弃用日期,也不删除现有拼写。 + +## 8. 身份、产物与缓存 + +### 8.1 三类记录 + +| 记录 | 内容 | 校验目标 | +|---|---|---| +| 请求 | 工程选择与约束 | 明确用户意图及选择来源 | +| 有效计划 | 已解析目标、工具与声明输入、动作和依赖 | 不发生错误命中,不遗漏编译或链接阶段 | +| 产物事实 | 实际代码镜像、ABI、资源与运行依赖 | 元数据与发布产物一致 | + +构建身份由影响对应工作的有效输入决定,不只哈希未经解析的字符串, +也不把所有执行参数无区别纳入全图指纹。插件的配置、动作命令与输入应形成完整 +失效链。集合顺序的等价性与命令参数的顺序敏感性分别处理。 + +产物事实可以先通过 check action 与报告核验。是否新增可供跨包消费的机器元数据, +由具体需求决定。本文不宣称现有打包已经自动核验并发布全部设备覆盖信息。 + +### 8.2 源码身份 + +11.3.1 当前将索引版本包和固定 Git 源按不可变身份处理,path 源码按可变树摘要处理。 +索引安装目录原地修改与 path 开发的契约不能混同。 + +推荐维持显式来源的边界:开发使用 path 或明确的替换来源,发行补丁进入可追踪 +源码身份。若正式支持安装目录原地修改,应按可变来源跟踪;若坚持不可变,应在相应 +输入校验中给出污染诊断或明确恢复方式。二者需要单独 review,不以每次全量哈希 +所有已安装包作为未经测量的默认方案。 + +声明安装时的内容摘要不能独立证明安装后未被修改;诊断污染还需要有效检测机制。 +同样,只在 `entry.json` 中没有看到源码摘要,不能证明整条构建失效链没有源码跟踪。 + +## 9. HPC 场景的覆盖账本 + +覆盖的对象是具体场景和约束,不是框架名称的数量。 + +| 场景组 | 当前证据与边界 | 继续验证的内容 | +|---|---|---| +| 原生 C/C++ 与模块 | 核心已有构建和目标侧契约 | 与外来编译组件的接口、使用需求和 ABI 一致性 | +| CUDA、SYCL 等设备单元 | 已有规则与 fixture;#887 暴露组合缺陷 | 单目标、多目标、多实现组合及错误输入 | +| HIP 的 AMD 路径 | 当前规则明确拒绝缺失的 ROCm 载荷 | 包供给、插件计划、实际运行分开验收 | +| CPU OpenMP | 索引有运行库与消费示例 | 不以此代替 GPU offload 验证 | +| 整组件 offload 与 stdpar | 当前没有完整的现成路径 | 组件所有权、原源码接入、ABI、增量和调试成本 | +| 跨库设备代码 | 单文件编译路径不足以证明覆盖 | 设备符号闭包、链接时机、库复用和失效 | +| MPI 与多节点 | 依赖、capability 和执行通道可作为接入基础 | 实现及 ABI 一致性、站点工具身份、实际多节点测试 | +| BLAS 等数值库 | 已有 capability/provider 机制及部分包 | 整数宽度、线程运行库和链接组合的具体兼容性 | +| 外部 HPC 工程及其他语言 | 可评估外部构建适配 | 完整输入、产物、使用需求、闭包与复现能力 | + +MPI 启动、站点环境、Slurm 等知识留在执行适配和相关包中。核心提供通用执行与 +结果契约;应用的通信、设备调度、内存互操作和故障恢复不由构建系统自动保证。 + +## 10. 实施顺序与责任 + +| 阶段 | 工作 | 归属 | 前置与退出条件 | +|---|---|---|---| +| P0 | 固化 #887 最小复现、确认解析缺陷与选择来源 | 插件为主,核心提供诊断证据 | 当前错误可观测,避免测试依赖清缓存才成立 | +| P1 | 定稿目标归属和值域、统一读取、按需工具请求及迁移建议 | mcpp-plugins;必要的通用接口另行提出 | 通过单实现和负向控制,不误启用另一实现 | +| P2 | 多实现共存、目标组合、接口与分派示例 | 插件、计算库与工程 | 通过 M1–M3 和 ABI 控制;运行分派策略明确 | +| P3 | 跨库设备链接、整组件及站点环境试验 | 插件与包 | 形成真实工作负载和可测成本;记录具体契约缺口 | +| P4 | 仅对 P3 证实的缺口增加通用核心机制 | mcpp | 满足 §4.3;不引入厂商分支 | +| 独立项 | 指纹报告复现、安装源码不可变策略与污染诊断 | mcpp | 不把未复现问题标记为已修复,也不与插件解析修复绑定 | + +P0 的确定性解析修复可以独立验证;P1 的目标接口需要本方案 review。 +不要求等待 P3、P4 才修复确定的 bug,也不让 P0 的局部通过替代多后端验收。 +发布最低版本依据实际使用的核心协议确定,不因文档基线较新而无理由抬高插件门槛。 + +### 10.1 最短推进路径 + +先完成 P0 的确定性解析修复;P1 的 review 与核心独立项调查可以同时进行。 +P1 定稿后验证工具请求和单实现目标,继而验证 P2 的共存,再推进 P3 的真实工程。 +只有 P3 的反例证明通用契约不足时,才进入 P4,不能把未证明需要的核心改动设为 +插件修复的前置。 + +默认平台升级 D7、兼容性审计 D9、进度可见性 D10 相互独立,也不是 #887 解析修复 +的技术前置。D7 若纳入后续 mcpp 发布,仅等待 PL 矩阵中与默认组变化有关的检查; +不能只凭单机显式消费通过就发布。#885 原有引擎、服务器和性能验收按其既定依赖 +推进,保留独立失败归因,不以新增设计项延长其发布链。 + +## 11. 验收矩阵 + +| 编号 | 场景 | 正向断言 | 反向或对照断言 | 层级 | +|---|---|---|---|---| +| A1 | 工程保留 CUDA、SYCL 两组源码,仅选择 SYCL | SYCL 源进入其动作 | CUDA 源和 CUDA 专属构建流程不因设备厂商而误启用 | 插件与核心集成 | +| A2 | 同工程仅选择 CUDA | CUDA 正常生成 | SYCL 源与 SYCL 专属工具不参与 | 插件与核心集成 | +| A3 | 同时选择两个实现 | 各自入口与产物共存 | 无重复输出、重复符号或全局参数互相污染 | 同产物集成 | +| A4 | 一个实现的多个设备目标 | 每个目标实际生成 | 不能只检查 argv 中出现过目标文字 | 驱动与产物 | +| A5 | 通用形式与 AOT 形式组合 | 两类代码均存在 | 只含 AOT 的产物不宣称通用回退覆盖 | 产物与运行 | +| A6 | 非法集合或未知目标 | 有归属明确的诊断 | 不能静默清空目标后改用缺省形式 | 解析与规则 | +| A7 | 按需工具首次未安装 | 发生声明内请求,安装后重跑 | 不需要该工具时不请求;pending 不报成最终缺失 | provision 集成 | +| A8 | 目标切换、再切回 | 动作与产物始终对应有效请求 | 不错误复用其他目标结果;等价请求的规则明确 | 身份与失效 | +| A9 | path 规则接口或其包含头文件变化 | 自动重编和重新规划 | 无变化时复用;无需用户清缓存 | 核心失效 | +| A10 | 安装源码污染 | 按选定契约检测或跟踪 | 不以固定版本名掩盖未记录修改 | 核心来源契约 | +| A11 | 跨 TU、跨静态库设备调用 | 正确设备链接并运行 | 缺少所需依赖或符号时在相应阶段失败 | 插件与跨包图 | +| A12 | 无 GPU 的构建机器 | 能按声明目标规划和构建适用场景 | 不根据本机无 GPU 隐式生成 CPU 变体 | 规划与交叉构建 | +| A13 | 不兼容的 C++ 运行库组合 | 满足要求后同场景通过 | 不兼容组合按目标侧契约拒绝 | ABI | +| A14 | 运行时选择和回退 | 报告实际执行的实现和设备 | 数值正确但实际运行于 CPU 不能冒充 GPU 验证 | 应用与运行 | +| A15 | 多节点与运行参数变化 | 实际完成分布式测试 | 仅启动参数变化不引发无关重编 | 执行集成 | +| A16 | 缺少 SDK 或只进行规划 | 保留可确定配置及明确诊断 | 规划不进行编译施工,不依赖首次施工结果 | SPEC-007 | + +A4、A5 的驱动展开仅作为中间检查,最终读取实际产物。 +A11、A14、A15 在对应设备或集群运行;环境不足明确记为未运行,不能据跳过宣称覆盖。 +性能目标另行给出数值预算,不能用架构可表达性证明性能相同。 + +## 12. 已完成的分析证据 + +### 12.1 基线身份 + +| 对象 | 身份 | 范围 | +|---|---|---| +| mcpp 发布程序 | `mcpp 2026.11.3.1` | 本次隔离构建使用的发布版 | +| mcpp 阅读基线 | `e0aee5cb7dfa5b13d1a5248d447947534d3cfa8b` | `origin/main`,含 11.3.1 bootstrap 更新 | +| mcpp-plugins 阅读基线 | `86c76ce7dcf148e8333a69fcecf7bd97f076f415` | `origin/main`;不使用本地未发布分支作为当前行为依据 | +| DPC++ 驱动 | 7.1.0,clang 22.1.0,Intel LLVM `880399f47228ac92f403e3b2b865cf1d6f0b1382` | 与 #887 报告版本对应的已安装驱动 | + +### 12.2 实际控制与限制 + +1. 从准确插件源码提取未修改的 SYCL 解析函数,独立编译执行。 + `sycl+{sm_89}` 得到零个 NVIDIA 目标;借用 CUDA 的单目标得到一个; + `sycl, cuda12.9+{sm_89,sm_90}` 得到零个。该控制确认解析行为,未执行 GPU 代码。 +2. 发布版 11.3.1 在同一源码、同一生成宏与 GCC 16.1.0 下实际构建: + + | 请求 | 指纹 | + |---|---| + | `sycl` | `ee212bbf74ad098d` | + | `sycl+{sm_89}` | `727064c6bcdf327d` | + | `sycl+{sm_89,sm_90}` | `afb8577403ee8e10` | + | 再次 `sycl` | `ee212bbf74ad098d` | + + 四次程序均成功执行。该控制没有 SYCL 编译器参与,不能排除报告者具体工具链、 + 命令入口或安装状态的另一条失效路径。旧版源码已有完整 accel 进入指纹的代码, + 不能把未复现简单解释成“新版本已经修复”。 +3. 私有 path host-module 的 `rules/` 下头文件从值一改为值二,11.3.1 自动拒绝 + 旧 fast path、重编构建程序并输出新值;未修改时复用。未修改任何已安装版本包。 +4. DPC++ 的实际 `-###` 展开分别规划单 NVIDIA 目标、两个 NVIDIA 目标及 + SPIR-V 与 NVIDIA 组合。两个 NVIDIA 目标均出现对应 ptxas 任务。 + 这些是驱动计划,未执行后续编译、产物检查或设备运行。 + +本地原始记录位于 `/tmp/mcpp-887-analysis/analysis-verified.json` 及其同目录日志。 +它们是本次分析证据,后续实现应将最小可复现控制转为仓库内测试;临时路径不作为 +未来发行版的保证。本文没有把 A1–A16 整体标为通过。 + +### 12.3 可定位的源码与规范 + +- [SYCL 目标读取](https://github.com/mcpp-community/mcpp-plugins/blob/86c76ce7dcf148e8333a69fcecf7bd97f076f415/rules/sycl.cppm):`parse_target`、目标参数与设备链接。 +- [插件工具声明](https://github.com/mcpp-community/mcpp-plugins/blob/86c76ce7dcf148e8333a69fcecf7bd97f076f415/mcpp.toml):SYCL 的条件 payload 与既有按需 provision。 +- [插件开发边界](https://github.com/mcpp-community/mcpp-plugins/blob/86c76ce7dcf148e8333a69fcecf7bd97f076f415/docs/plugin-development.md):核心、插件、包与工程的责任。 +- [核心指纹](https://github.com/mcpp-community/mcpp/blob/e0aee5cb7dfa5b13d1a5248d447947534d3cfa8b/src/build/prepare/scan.cpp):`step11_compute_fingerprint`。 +- [构建程序源码身份](https://github.com/mcpp-community/mcpp/blob/e0aee5cb7dfa5b13d1a5248d447947534d3cfa8b/src/build/prepare/host_graph.cpp):`program_identity`。 +- [多后端同产物示例](https://github.com/mcpp-community/mcpp/tree/e0aee5cb7dfa5b13d1a5248d447947534d3cfa8b/examples/09-heterogeneous/multi-backend):独立入口、条件源码与运行时分派。 +- [island 辅助工具](https://github.com/mcpp-community/mcpp-plugins/blob/86c76ce7dcf148e8333a69fcecf7bd97f076f415/docs/tools-island.md):替代实现与边界生成的当前约定。 +- [DPC++ 目标与设备链接](https://intel.github.io/llvm/UsersManual.html):工具接口背景,具体行为以锁定驱动与产物控制为准。 + +## 13. 首轮 Review 决策项 + +本节保留首轮取舍与依据,方向已获用户同意。第二轮实验和具体收敛见 §16; +尚待确认的细节由该节及研究记录更新,不将同意方向等同于实现完成。 + +### D1. 通用核心与 island 的施工范围 + +**推荐:** 保留 island 优先,以具有明确输入、产物和接口的组件作为可评估的最大 +施工范围。先验证组件内多 TU、设备链接及整组件 offload,再判断是否缺少核心契约。 +不为 CUDA、SYCL、OpenMP 分别引入核心构建模式。 + +**需要 review:** “可接入”是否还要求既有普通源码无需改写、C++ 模板或模块可直接 +跨边界使用,以及调试与增量成本的预算。这些目标不能由动作图可表达性代替。 +整组件方案若只能靠未声明源码接管或丢失使用需求完成,就仍有待解决的契约缺口。 +验收为 A11、A13、A16 与一个真实 offload 工程;尚不作全部 HPC 覆盖声明。 + +### D2. 实现选择、设备目标与工具供给 + +**推荐:** 一个请求保留“所属实现—设备目标集合”的关系。SYCL 面向 NVIDIA 只使 +SYCL 规则请求相应工具,不隐式选择 CUDA 源码实现。显式选择 CUDA、SYCL 两个成员 +仍表示选择两套实现;核心的源码覆盖与约束拒绝保持有效。 + +**需要 review:** 目标的缺省形式、混合可移植代码与 AOT、别名、重复及集合顺序, +以及现有版本修饰的精确含义。`sycl+{sm_89}` 只是待定候选,不能先补解析再宣布 +整套语义稳定。非法请求与不支持请求明确失败,不静默变为空集合。验收为 A1–A8。 + +### D3. 统一读取与按需工具请求 + +**推荐:** 核心定义已有通用线格式,L2 统一读取,L3 解释目标值与施工流程。 +优先复用声明内的 on-request provision;首次缺工具走 pending、安装、重跑。 + +**需要 review:** 先确认现有协议是否足够;只有确有跨规则的结构读取缺口时,才增加 +不含厂商枚举的核心接口。工具版本继续由依赖和工具链选定,不能在目标字符串中再建 +一套版本解析体系。SYCL 的某些目标确需 CUDA 工具,A1 限制的是误启用 CUDA 源码 +流程。验收同时覆盖首次安装、自备工具、无关工具不请求及离线诊断。 + +### D4. 替代、共存与运行时协作 + +**推荐:** M1 用选择关系,M2 用不同内部入口与外层分派,M3 用同一实现的设备目标 +集合。M4 的设备、内存和同步协议交给应用及运行时。辅助库可以生成重复的入口与 +注册代码,不能替应用决定分派优先级或回退策略。 + +**需要 review:** 共享逻辑接口的声明放在工程还是计算库,以及是否已经有足够重复 +代码值得抽公共辅助库。C 入口隔离符号并不隔离 C++ 运行库。现有显式 exclusive +capability 已实现;不能对所有后端施加全局互斥。若两个独立程序各需不同互斥 +provider,先复现当前选择范围是否过宽,再评估按链接闭包约束,不直接新增作用域。 + +### D5. 缓存与来源身份 + +**推荐:** 分别调查索引描述符及载荷更新、已安装源码污染、实际构建有效输入。 +三个问题分别核对来源修订、安装内容身份及动作失效链;清缓存不能作为修复验收。 + +**需要 review:** 继续保持发行包不可变、开发使用 path 的契约,还是正式支持修改 +安装目录。推荐前者,并另行设计有成本依据的污染检测与恢复诊断。安装时的一次摘要 +不能检测安装后的修改。本轮 11.3.1 通用目标切换与 path 头文件控制通过,#887 的 +具体缓存报告仍需独立复现;不得据此宣布不存在其他缺陷。验收为 A8–A10。 + +### D6. 当前交付与后续探索的边界 + +**推荐:** 已确认的解析 bug 先独立修复;目标接口定案后完成请求与工具闭环, +再验证共存及跨库场景。新的核心机制只由真实反例推动。#885 原有发布验收继续单独 +跟踪,设计草案不代表引擎、服务器或性能验收已通过。 + +**需要 review:** 默认平台升级是否另立 mcpp 补丁,以及进度可见性和兼容性审计 +是否进入后续任务。推荐它们与 #887 插件修复并行,避免形成无技术必要的串行依赖。 +APE 单文件、clang/lld 映像化继续属于显式后续项,见 §14.2。 + +### D7. openkal 默认平台版本与显式平台选择 + +**推荐:** 如需零配置项目继承 0.23.5 的更新,后续 mcpp 补丁升级内置平台 pin, +按完整平台组验收。已发布的 11.3.1 保持其实际 0.23.3 默认;显式选择与已有锁记录 +继续按既有规则约束,不通过索引更新静默替换已固定的平台。 + +**需要 review:** 本轮只要求消费者显式固定 0.23.5,还是同时要求 mcpp 的零配置 +默认跟进。前者已存在实际消费证据,后者仍需核心版本发布,二者不能互相替代。 +当前默认已含 Linux 0.24.1 的 Landlock 修复,不能把默认升级描述为该安全修复尚未 +交付。接受的代价是明确发布一次默认变化;收益是未显式配置者获得更新且可复现。 + +### D8. 平台声明与核心专属分支 + +**推荐:** 核心解释平台通用契约,平台包声明工具链、运行层、编译链接约定、seal、 +runner、carrier 和版本组,平台插件提供相应格式的施工。Linux/macOS/Windows 实现 +与 libc/runtime 的缺陷在相应包修复,核心不增加对应系统调用知识。 + +**需要 review:** 对载体安装宿主识别、后缀回退、launcher 自举逐项提供契约依据。 +优先使用已声明角色及运行宿主,不以搜索删除所有 `openkal` 字符串作为重构目标。 +launcher 自己的 os/platform 组合独立于应用平台;默认 pin 升级须同时复核该组合。 +非 openkal 平台的具体失败反例,才决定是否增加通用元数据,见 §14.4。 + +### D9. 设备兼容性比较的既有模型知识 + +**推荐:** 先承认并审计核心已有 `sm_`、`compute_`、`a/f`、PTX floor 和同主版本 +兼容假设。不将这些数字规则继续套给新后端,也不把通用读取与语义兼容比较混为一项。 + +**需要 review:** 最小可版本化的兼容关系是否可由声明描述,例如精确匹配和明确的 +覆盖关系;其数据来源、可信边界及旧描述符语义都需定案。未知模型宜保守匹配, +但改变现有行为需迁移控制。预构建包筛选不能为了比较而临时下载并执行构建插件, +也不为此引入任意表达式 DSL。尚未实测一个其他后端的错误接受或拒绝,故此项为 +已有语义耦合审计,不是已复现的通用兼容性 bug。 + +### D10. 嵌套构建的进度可见性 + +**推荐:** 保留外层 action 完成计数,另行呈现活动 action、阶段、耗时,以及子构建 +有依据的计数。由插件或执行适配报告子阶段,核心提供通用传递与呈现,不内置 LLVM +日志解析。普通日志仍可使用,但结构化进度需区分来源、顺序和完成状态。 + +**需要 review:** 先做独立的可见性优化还是制定结构化通道。后者还需覆盖 Ninja +缓冲、取消、并发输出、有界传输及现有机器输出契约。未知总量只显示阶段与时间; +心跳不表示编译仍有进展,子进度到 100% 也不替代 action 成功退出。AOT、打包、 +代码生成和外部 CMake 工程均可复用此机制。该项未实现,不是已证实的调度死锁。 + +review 后以追加具名修订或后续记录保存决策。已发布实现的使用说明与规范修订, +随实际落地范围分别进入 `docs/` 与 `docs/specs/`,不把本设计草案直接作为使用承诺。 + +## 14. 前案遗留与 openkal platform 复核(2026-11-05) + +### 14.1 旧清单与当前实现 + +复核来源为[平台设计](2026-10-28-platforms-design.md)的 L1–L10、 +[生态第 4 部分](2026-11-03-ecosystem-part4-beyond-native-design.md)及 #885。 +“源码和用例存在”与“本轮重新执行”分开记录,Issue 的开闭状态不独立决定实现状态。 + +| 项目 | 当前证据 | 本次结论 | +|---|---|---| +| L1 精确版本缺失的刷新;L2 compat 重试 | 前案记录 #847;当前 e2e 949 覆盖前者 | 已有交付记录,不重新列成待实现;本轮未重跑 | +| L3 带 C 边界的多版本改名 | `src/build/prepare/graph.cpp` 拒绝 header exports、target-side layers 等不安全组合 | 已有守卫;不宣称任意 ABI 都可重命名 | +| L4 索引修订与刷新原因诊断 | `tests/e2e/173_index_refresh_policy.sh` 覆盖修订、过期与策略 | 已有实现及覆盖;不是全部真实安装故障重新验收 | +| L5 测试先构建程序并提供路径 | `src/build/execute.cppm` 提供 `MCPP_BIN_` | 已实现,不保留为新功能 | +| L6 缓存报告重复行 | 前案实现记录及当前集中报告路径 | 历史已处理;本轮未重新构造重复报告场景 | +| L7 CI 自建 okrun | 已发布平台工具供给及消费者迁移记录 | 供给/消费问题,不是核心新增编译器逻辑 | +| L8 kernel-abi 进入共享映像 | e2e 950 | 已有核心实现与回归用例 | +| L9 os 的 hosted 头文件和路径 | 前案 os 0.2.1 修正,后续已发布 os 0.2.9 | 包侧修正,不把后续路径问题统归核心 | +| L10 载体错误取宿主 CRT | 平台载体路径与 e2e 995 等控制 | 已有实现;默认平台前移仍需回归自举组合 | +| 平台选择、覆盖、宿主图与通用性 | e2e 951、952、953;952 使用非 openkal 本地平台 | 已有通用机制;本轮不宣称全部重跑 | +| 外架构 runner、载体、打包与平台压缩 | e2e 960、961、988、994、995、1008 | 已有实现,变化时按实际影响复核 | +| std BMI 未继承平台编译参数 | 11.3.1 的实现及 e2e 1010 | 已修复,不再列待修核心项 | +| 显式 exclusive capability | `step6_capability_binding` 与 e2e 601 | 本轮发布版实跑:互斥双 provider 拒绝、普通双 provider 共存均通过 | +| #860 自包含包错误宣称所有系统架构 | `xpkg_descriptor` 读取 `runs_on`,单测 `AnEntryNamesWhereThePackageRuns` | 本轮发布版对公开 IDE 0.2.0 包实跑 emit xpkg:Linux 双架构、macOS ARM64、Windows x64;无额外架构 | + +### 14.2 剩余事项的责任与证据等级 + +| 项目 | 分类 | 责任及推进条件 | +|---|---|---| +| 核心默认平台仍为 0.23.3,消费者可选择 0.23.5 | 待 review 的默认升级 | mcpp 补丁发布;不是已确认安全回退,见 D7 | +| 长嵌套 action 缺少内部进度 | 已确认的可见性优化 | 核心执行/报告通道与插件报告合作;未修改实现,见 D10 | +| 核心的设备兼容性厂商假设 | 源码可确认的语义耦合 | mcpp 审计通用兼容契约;尚无其他后端错误复现,见 D9 | +| 通用请求不完整及同名成员顺序相关 | 第二轮已复现的核心问题 | 结构验证与重复成员政策,见 §16 | +| 独立程序的 capability 绑定与 exclusive 检查被全局合并 | 第二轮已复现的核心作用域问题 | requirement 与链接闭包分别检查,见 §16 | +| 包宿主判断中的 openkal 分支 | 源码可确认的抽象候选 | 有非 openkal carrier 场景后决定通用化,不能直接认定全部分支为 bug | +| #887 的缓存报告 | 发布版通用控制未复现 | 继续取得原始入口与有效输入;污染契约另行定案 | +| #524 同版本描述符/安装载荷更新报告 | 旧报告,本轮未复现 | 核对当前刷新、payload currency 与来源身份;不直接延用旧根因 | +| APE 跨系统、跨架构单文件(#849) | 前案明确后续 | openkal 后端、carrier 与工具链供给先行;当前 okb 不等于该目标已交付 | +| Windows `__FILE__` 分隔符影响字节复现(#850) | 编译器相关限制 | 工具链/上游处理;打包仍拒绝同名不同字节,不放宽完整性 | +| clang/lld 自身构建为 openkal 映像(#880) | 前案明确后续 | 工具链生态工作;部分前置已具备不等于整体完成 | +| musl 大目录缓存、runtime Windows 路径及 macOS profile 分类问题 | 包/后端专项 | 相应 libc/runtime/后端定位;升级平台不自动证明所有问题已修复 | +| #885 引擎、服务器与最终验收 | 原交付仍需独立闭环 | 不以平台已发布或本文设计通过代替实际产物及性能门槛 | + +长尾证据见 `/tmp/mcpp-885-build-tail-diagnosis.json`:`llvm:clangd-libraries` 是一个 +外层 action,内部包含 CMake/Ninja 的大量工作;三步各一秒的嵌套 Ninja 控制直到 +约 3.05 秒才输出。历史同类构建最终成功,不能由 `1630/1636` 推断死锁。 +恢复编译器缓存仍全 miss 的根因尚未确认,不能把“恢复成功”写成“编译已复用”。 + +### 14.3 默认平台与实际消费版本 + +平台阅读基线为 `mcpplibs/openkal-platform` 的 +`b9cc96d17a9595c341bd9e9b7029d86766f22456`(0.23.5),同时读取 0.23.3 tag。 +mcpp 的内置表仍是 §12.1 的发布基线,两条 openkal target 均固定 0.23.3。 + +| 组成 | 11.3.1 内置 0.23.3 | 已发布 0.23.5 | +|---|---|---| +| 最低 mcpp | 2026.11.3.1 | 2026.11.3.1 | +| 工具链 | llvm 23.1.3 | llvm 23.1.3 | +| openkal | 0.23.0 | 0.23.0 | +| Linux / macOS / Windows 后端 | 0.24.1 / 0.21.3 / 0.19.0 | 0.24.1 / 0.21.3 / 0.19.0 | +| musl | 0.27.3 | 0.27.5 | +| LLVM runtime | 0.18.4 | 0.18.6 | +| ld / run / tools | 0.4.3 | 0.4.4 | + +内置默认是发布数据,不是“运行时查询最新版本”的策略。平台包已经发布、某个 +消费者已经升级、mcpp 的默认已经前移,是三个不同结论。0.23.3 已使用 Linux +0.24.1,因此本轮前案 Landlock ABI 3 修复已经进入该默认组。 + +发布版 11.3.1 显式选择公开 0.23.5 的 cleanroom 控制已运行 x86_64-openkal 程序, +锁记录包含 platform 0.23.5、musl 0.27.5、runtime 0.18.6、tools 0.4.4。 +证据为 `/tmp/mcpp-885-platform0235-public-cleanroom-verified.json` 中的实际命令与 +锁条目;该记录末尾范围说明仍误写 0.23.4,本文不沿用该文字。 +这项控制不覆盖 macOS、Windows、完整 launcher 或零配置默认升级。 + +平台固定的是一起构建验证的依赖组。工程的覆盖若产生互相矛盾的精确版本,应展示 +平台、请求者与版本冲突,不把每个依赖无条件换成最新版本,也不通过改名绕过 ABI。 +目标图、宿主构建程序、工具与载体各自遵守现有图和使用角色,不能假定所有宿主工具 +都应链接目标图的 libc/runtime。平台造成的编译参数变化也应到达 std BMI 与其身份。 + +### 14.4 平台抽象的具体审计点 + +1. **核心默认表。** `modules/toolchain-model/src/triple.cppm::kKnownTargets` 的 + 两个 pin 是默认选择。存在具名内置目标不违反通用构建设计;审计对象是行为依赖 + 声明还是依赖目标名字。e2e 952 的非 openkal 平台是已有的通用性证据。 +2. **Windows launcher。** `src/pack/pack.cppm::launcher_platform` 使用核心默认 + 平台,`exe_launcher_manifest` 固定自己的 os 0.2.9。应用显式选择旧平台时, + launcher 仍应使用与自身源码匹配的自举组合。e2e 995 正是这一控制,不能简单 + 把 launcher 改为继承应用平台。后续默认升级应复核 os/platform/CRT/runner 组合。 +3. **载体的安装宿主。** `src/pack/distribution.cppm::program_host` 对 openkal + carrier 有专属判断。若其他非原生目标也使用 carrier,该关系可能应来自运行宿主 + 元数据。当前未构造失败反例;先核对已有 `runs_on` 能否覆盖,再决定最小变化。 +4. **文件角色。** `role_of` 保留 `.oke`、`.okso` 回退,但已声明角色优先,打包 + 流程也显式记录 entry、program 和 launcher。不能断言更换后缀必然打包失败。 + 新格式先声明角色;只有必要的旧包识别保留约定回退。 +5. **seal 与格式插件。** 工具校验失败由核心如实拒绝,格式/ABI/工具缺陷回到平台 + 包或实现包定位;若引擎错误传递参数、输入或阶段依赖,则由核心修正通用执行 + 契约。归因先核对实际命令与产物,不能只根据最终失败阶段决定。取消校验或放宽 + 发布门槛不能替代根因修复。 + +建议平台变更的验收分母如下;这是一份待执行矩阵,未整体标为通过。 + +| 编号 | 控制 | 断言 | +|---|---|---| +| PL1 | 不声明平台的新工程 | 使用新 mcpp 发布声明的默认组,来源可解释 | +| PL2 | 显式旧平台、显式新平台与既有锁 | 遵守选择和锁规则,不被默认或索引 latest 静默替换 | +| PL3 | 版本组冲突 | 指出请求者与矛盾版本;合法组合通过 | +| PL4 | 宿主图、目标图与跨架构 runner | 角色和工具链正确,不串用目标运行库或错误宿主程序 | +| PL5 | 平台编译参数及 std BMI | 同步生效,变化进入相应构建身份;无变化可复用 | +| PL6 | 旧应用平台与新版核心 launcher | launcher 自举组合正确,Windows 可执行及打包可用 | +| PL7 | 非 openkal 平台、显式角色与 carrier | 以声明驱动通用行为;无额外 carrier 时保留现有成功路径 | +| PL8 | self-contained/vendored 与 emit xpkg | 包装载体、所需 runner 和实际系统架构一致,不夸大覆盖 | +| PL9 | 索引普通安装及离线复用 | 完整发布组可获取;离线缺载荷明确诊断,不污染既有版本 | + +### 14.5 设备兼容性残留与核心改动准入 + +`src/pack/abi_tag.cppm::accel_arch_level` 识别 `sm_`、`compute_`; +`accel_arch_covers` 解释 `a/f` 后缀;`accel_accepts` 使用 PTX floor,并对 backend +名称应用同主版本兼容判断。这些是当前存在的语义知识,不是仅有括号切分规则。 +未知字符串中的前导数字也可能进入比较,不能据“核心未列举所有厂商名称”证明通用。 + +因此,“把解析收拢到插件 L2”不能解决全部核心边界问题。进一步设计应至少给出: +两个具有不同覆盖/版本规律的模型、旧元数据的兼容策略、未知关系的保守行为,以及 +不执行任意插件的预构建筛选方案。精确相等与显式覆盖数据是候选起点,不在本轮 +直接增加字段或改写既有接受集合。产物事实的真实性还需生产侧核验,不能由消费侧 +兼容比较独立保证。当前没有自动生成全部 `accel` 事实的交付证据。 + +### 14.6 补充源码与证据入口 + +- [平台声明及版本组](https://github.com/mcpplibs/openkal-platform/blob/b9cc96d17a9595c341bd9e9b7029d86766f22456/mcpp.toml)。 +- [核心默认平台表](https://github.com/mcpp-community/mcpp/blob/e0aee5cb7dfa5b13d1a5248d447947534d3cfa8b/modules/toolchain-model/src/triple.cppm)。 +- [打包宿主、角色、launcher 与描述符](https://github.com/mcpp-community/mcpp/blob/e0aee5cb7dfa5b13d1a5248d447947534d3cfa8b/src/pack/distribution.cppm)。 +- [核心设备兼容性比较](https://github.com/mcpp-community/mcpp/blob/e0aee5cb7dfa5b13d1a5248d447947534d3cfa8b/src/pack/abi_tag.cppm)。 +- [平台通用性控制](https://github.com/mcpp-community/mcpp/blob/e0aee5cb7dfa5b13d1a5248d447947534d3cfa8b/tests/e2e/952_a_platform_package_states_a_class_of_targets.sh)。 +- 本轮 emit xpkg 原始输出与包元数据:`/tmp/mcpp-887-analysis/xpkg860-public-probe.json`。 +- 两组版本的逐字段比对:`/tmp/mcpp-887-analysis/platform-version-groups.json`。 + +## 15. 多视角自审与修订结论 + +本节记录首轮对渲染成文的第二遍阅读;通过表示表述与当时证据相符,不表示提案已经 +实现。第二轮实验补充及结论见 §16 和调研记录。 + +| 视角 | 检出的歧义或风险 | 修订与结论 | +|---|---|---| +| 使用者语义 | NVIDIA 设备目标可能被误读成 CUDA 源码选择 | §3、D2 保留归属;A1 允许确有所需的 CUDA 工具,通过 | +| 核心架构 | 把理想分工写成当前核心没有厂商知识 | §2、§14.5 明确现状与审计准入,通过 | +| 插件实现 | 每条规则独立 split、重复解析导致计划不一致 | §7、D3 统一读取及同一目标集合驱动各阶段,通过 | +| island 覆盖 | 图可表达被扩大为所有旧源码与性能都能保持 | §4、D1 分开三层判据并保留真实组件验证,通过 | +| ABI 与同产物共存 | C 入口被当成完整运行库隔离 | §5.2、D4 保留运行库和链接闭包约束,通过 | +| 来源与增量 | 一次内容摘要被当作污染检测,旧报告被当作当前 bug | §8、D5、§14.2 明确检测缺口与未复现范围,通过 | +| 平台版本与安全 | 新平台已发布被等同默认已升级;误称旧默认未含 Landlock 修复 | D7、§14.3 分别记录默认、显式消费和准确组内容,通过 | +| 自举与交叉构建 | launcher 或宿主工具无条件继承应用目标平台 | D8、§14.4 保留独立角色与自举约束,通过 | +| 打包与部署 | 文件后缀回退被当作唯一机制,#860 开着被当作仍未实现 | 显式角色优先;公开包实跑校验系统架构,通过 | +| 可观测性 | 完成项比例当成耗时比例,心跳当成施工进展 | D10、§14.2 区分外层计数、子阶段与实际完成,通过 | +| 兼容与演进 | 将现有数字兼容规则直接移除或扩散到所有后端 | D9 保留迁移要求并禁止盲目泛化,通过 | +| HPC 运行验证 | 驱动展开、源码示例或单机探针被写成 GPU/集群已验证 | §9、§11、§12 明确验证层级与缺失环境,通过 | +| 发布与责任 | 重新打开已实现的 L 项,或新增设计挟持 #885 发布范围 | §14.1 分清旧项状态,D6 要求独立验收,通过 | +| 文档与 review | 决策表缺少具体取舍,读者无法决定接受的边界 | D1–D10 给出推荐、待定项、代价及验收,通过 | + +首轮 D1–D10 的方向现已同意;第二轮待 review 的细化见 §16。运行证据缺口按 +A1–A16、PL1–PL9 及调研记录的 V1–V9 跟踪,不由文字自审替代。 + +## 16. 第二轮调研后的收敛(2026-11-05) + +用户已同意首轮十项方向,并要求 D10 的进度能力可在 build.mcpp 中按需配置。 +本节更新仍未明确的接口、缺陷归属和实施顺序;原 §15 为首轮自审记录,第二轮 +自审和实验以[调研记录](2026-11-05-heterogeneous-build-research-and-experiments.md)为准。 + +### 16.1 可选子进度 + +推荐 `mcpp::action` 新增默认关闭的 `bool progress`,在 build.mcpp 中写 +`a.progress = true`。这是拟议接口,11.3.1 尚无该字段。配置只声明参与报告,实际 +计数由执行期工具或插件 adapter 提供,不在规划期运行子构建。 + +核心提供每运行、每 action 独立的快照通道;插件通用辅助负责可选写入,工具规则 +解释自己的阶段。采用最新状态而非无界日志;建议 8 KiB 上限、单写者原子替换、 +递增序号及每秒五次写入限频。普通 action 不受启用要求约束,计数和心跳不能声明 +构建成功。随机通道与实时内容不进入构建身份;修改构建程序启停开关仍可能重建。 + +Linux 原型通过普通、verbose、并行、关闭、失败及进程组取消控制。普通模式约 +0.469 s 读到子进度,外层 Ninja 约 1.347 s 才输出;子进度 3/3 后失败仍无 stamp。 +原型不是当前核心 UI;跨系统、NDJSON 兼容与实际调度集成仍需实现和验收。 + +### 16.2 新确认的核心正确性问题 + +1. **通用结构未完整验证。** 发布版实际接受 `sycl+{sm_89` 与 + `sycl+{sm_89}garbage`。核心负责完整消费结构和报告位置,插件负责目标值域。 +2. **重复成员顺序影响约束。** 源约束 sm_90 时,两个 sycl 成员先 89 后 90 被拒绝, + 先 90 后 89 则通过。推荐拒绝重复实现并提示合并目标集合,集合内去重及规范化 + 另行明确;不再无诊断地只比较第一项。 +3. **工作空间作用域过宽。** 两个独立程序各依赖一个 provider,单独构建运行通过, + 合并构建被 exclusive 检查拒绝。去掉 exclusive、增加各自的 requirement,仍被 + 全局多候选检查拒绝。推荐 requirement 在最终消费上下文内绑定,保留应用为 + 库声明的需求供给 provider 的能力,不能仅限于库自身的子依赖;exclusive + 在实际链接组合闭包检查,不新增用户可选的互斥模式。单程序双 provider 拒绝保留。 + +因此,“exclusive 已有实现”仍成立,但不能再据此把工作空间组合范围也标为正确。 +这些是通用核心缺陷,不要求插件增加 GPU 专属绕过。 + +### 16.3 请求与规则阶段 + +SYCL 推荐规范表达收敛为所属实现内的有域目标集合,例如 +`sycl+{spir64,nvptx64-sm_89,nvptx64-sm_90}`。既有核心形状可传递,现有发布插件 +尚未实现相应消费。裸 `sycl` 保留缺省 SPIR-V;显式集合只生成列出的形式。 +CUDA 工具由该 SYCL 请求按需取得,不以启用 CUDA 成员作为条件。 + +实际 DPC++ 控制完成了多 TU 的 sm_89/sm_90 与 SPIR-V/sm_89 两种组合,并从最终 +程序提取镜像;单目标跨静态库也通过。但多目标与静态库组合出现设备符号重复定义, +设备执行则被系统驱动 mismatch 阻止。后续规则验收保留这些交叉组合及失败诊断, +不把独立 SDK 编译通过写成插件、跨包或 GPU 执行通过。 + +驱动调用优先使用已验证的明确 triple 与后端参数。别名本身编译通过,失败限定为 +本版驱动的别名与 save-temps 组合,不能扩大为“别名不受支持”。 + +### 16.4 平台、兼容性和缓存的边界 + +默认平台升级按已同意的 D7 推进后续补丁设计,仍需 PL1–PL9。非 openkal 平台的 +e2e 952 本轮实际通过。合成 carrier 元数据实验确认专属分类存在,但没有真实其他 +carrier 的端到端交付证据;优先评估已有 `runs_on` 和角色,不新增重复宿主字段。 + +数字及同主版本兼容已存在公开契约。本轮优先修结构与作用域,新 SYCL 使用有域 +opaque 目标走精确路径;不为 #887 引入 `accel_match` 或覆盖 DSL。旧契约的迁移 +仍独立保留,不声称厂商语义已经全部移出核心。 + +集合换序实际造成不同指纹,属于冗余构建优化,不是错误命中证据。不可变发行包与 +可变 path 的来源边界保持;不以一次摘要声称能检测安装后污染,不承诺启用遥测时 +零重建,也不因本轮进度设计引入安装源码全量扫描。 + +### 16.5 本轮需要 review 的细化 + +| 编号 | 推荐定案 | 尚未完成的验证或实现 | +|---|---|---| +| R1 | `a.progress=true` 可选声明;执行期独立有界快照;成功仍由引擎判定 | 核心 UI/机器输出、跨系统替换及取消、协议兼容 | +| R2 | requirement 按最终消费上下文绑定并保留应用供给,exclusive 按实际链接组合闭包 | 通用作用域、绑定影响身份及共享库、测试、宿主工具控制 | +| R3 | 有域目标集合驱动同一组编译与设备链接阶段,工具按需请求 | 插件集成、多目标归档、跨包与 GPU 运行 | +| R4 | 平台默认单独发布;carrier 优先复用已有运行宿主和角色元数据 | 默认升级完整验收及真实非 openkal carrier | +| R5 | 先修结构验证和重复成员;保留旧匹配契约,暂不增加覆盖 DSL | 重复实现拒绝政策、集合规范化迁移及后续多模型匹配契约 | + +核心结构修复、插件读取修复、provider 作用域、可选进度及默认平台升级可以独立 +推进。多目标归档问题归入工具链/插件阶段调查,不预设新的核心构建模式。以上研究 +未修改生产代码、未发布版本、未向 Issue 提交内容;第二轮方案仍待用户 review。 From 64f5366e274e6cd9ab14c0d3e99c471c517f9074 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Thu, 5 Nov 2026 07:07:47 +0900 Subject: [PATCH 02/11] docs: register active design records in the generated index --- ...6-11-05-heterogeneous-build-implementation-plan.md | 2 +- ...05-heterogeneous-build-research-and-experiments.md | 2 +- ...uild-semantics-and-plugin-boundaries-supplement.md | 2 +- .agents/docs/README.md | 11 ++++++++++- 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/.agents/docs/2026-11-05-heterogeneous-build-implementation-plan.md b/.agents/docs/2026-11-05-heterogeneous-build-implementation-plan.md index 38be4d877..be7aecf7c 100644 --- a/.agents/docs/2026-11-05-heterogeneous-build-implementation-plan.md +++ b/.agents/docs/2026-11-05-heterogeneous-build-implementation-plan.md @@ -1,6 +1,6 @@ --- subject: heterogeneous-build -status: implementing +status: active --- # 生态 885 与异构构建补充方案的实现依赖 diff --git a/.agents/docs/2026-11-05-heterogeneous-build-research-and-experiments.md b/.agents/docs/2026-11-05-heterogeneous-build-research-and-experiments.md index a1f468c23..d02a4ea27 100644 --- a/.agents/docs/2026-11-05-heterogeneous-build-research-and-experiments.md +++ b/.agents/docs/2026-11-05-heterogeneous-build-research-and-experiments.md @@ -1,6 +1,6 @@ --- subject: heterogeneous-build -status: proposed +status: active --- # 异构构建补充方案的第二轮调研与实验 diff --git a/.agents/docs/2026-11-05-heterogeneous-build-semantics-and-plugin-boundaries-supplement.md b/.agents/docs/2026-11-05-heterogeneous-build-semantics-and-plugin-boundaries-supplement.md index 0c3f7b0da..8d21d009a 100644 --- a/.agents/docs/2026-11-05-heterogeneous-build-semantics-and-plugin-boundaries-supplement.md +++ b/.agents/docs/2026-11-05-heterogeneous-build-semantics-and-plugin-boundaries-supplement.md @@ -1,6 +1,6 @@ --- subject: heterogeneous-build -status: proposed +status: active --- # 异构构建语义与插件边界补充方案(mcpp#885、mcpp#887) diff --git a/.agents/docs/README.md b/.agents/docs/README.md index 14409ad17..6f97f7255 100644 --- a/.agents/docs/README.md +++ b/.agents/docs/README.md @@ -18,7 +18,7 @@ superseded_by: 2026-09-07-....md # when status is superseded --- ``` -346 records. +349 records. ## By subject @@ -80,6 +80,12 @@ Records that declare one. Everything else is listed by date below. - [The island boundary's names: one rule for both lanes, and the check that makes it true](2026-09-08-island-boundary-names.md) — active - [Implementation plan: the island boundary's names](2026-09-08-island-boundary-names-implementation-plan.md) — active +### heterogeneous-build + +- [异构构建语义与插件边界补充方案(mcpp#885、mcpp#887)](2026-11-05-heterogeneous-build-semantics-and-plugin-boundaries-supplement.md) — active +- [异构构建补充方案的第二轮调研与实验](2026-11-05-heterogeneous-build-research-and-experiments.md) — active +- [生态 885 与异构构建补充方案的实现依赖](2026-11-05-heterogeneous-build-implementation-plan.md) — active + ### home - [首次初始化、home 重定向与网络诊断优化设计](2026-10-19-first-run-home-and-network-design.md) — landed @@ -161,6 +167,9 @@ Records that declare one. Everything else is listed by date below. ### 2026-11 +- [异构构建语义与插件边界补充方案(mcpp#885、mcpp#887)](2026-11-05-heterogeneous-build-semantics-and-plugin-boundaries-supplement.md) — active +- [异构构建补充方案的第二轮调研与实验](2026-11-05-heterogeneous-build-research-and-experiments.md) — active +- [生态 885 与异构构建补充方案的实现依赖](2026-11-05-heterogeneous-build-implementation-plan.md) — active - [生态设计 part 4:超越原生——openkal 之上与之下的优化、可观测性与安全](2026-11-03-ecosystem-part4-beyond-native-design.md) — active - [生态设计 part 3:ok 映像的性能对等——引擎路径、openkal 运行时与观测](2026-11-02-ecosystem-part3-ok-image-performance-parity-design.md) — active ### 2026-10 From f3c1a5c9d25d7e29049693c61d8bc8e3b5885991 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Thu, 5 Nov 2026 07:20:15 +0900 Subject: [PATCH 03/11] fix: validate and canonicalize complete accelerator requests --- src/build/prepare/features.cpp | 8 +- src/build/prepare/toolchain.cpp | 18 ++- src/pack/abi_tag.cppm | 106 ++++++++++++++++++ ...tor_requests_are_complete_and_canonical.sh | 68 +++++++++++ tests/unit/test_abi_tag_accel.cpp | 46 ++++++++ 5 files changed, 241 insertions(+), 5 deletions(-) create mode 100755 tests/e2e/1011_accelerator_requests_are_complete_and_canonical.sh diff --git a/src/build/prepare/features.cpp b/src/build/prepare/features.cpp index 5cb51e0e6..93b46bf70 100644 --- a/src/build/prepare/features.cpp +++ b/src/build/prepare/features.cpp @@ -902,6 +902,10 @@ static std::expected step6_device_extensions_and_rules(Prepar auto& bc = pkg.manifest.buildConfig; std::set excludedGlobs; for (auto const& sc : bc.sourceConstraints) { + const auto checked = mcpp::pack::parse_accel_request(sc.accel); + if (!checked) return std::unexpected(std::format( + "`{}`: [build] sources entry '{}': {}", + pkg.manifest.package.name, sc.glob, checked.error())); const auto hits = mcpp::modgraph::expand_glob(pkg.root, sc.glob); if (hits.empty()) { return std::unexpected(std::format( @@ -920,7 +924,7 @@ static std::expected step6_device_extensions_and_rules(Prepar // optional, and a package that omits it has said nothing to // contradict. if (!pkg.manifest.package.accelerators.empty()) { - for (auto const& w : mcpp::pack::parse_accel(sc.accel)) { + for (auto const& w : *checked) { if (std::ranges::find(pkg.manifest.package.accelerators, w.backend) != pkg.manifest.package.accelerators.end()) continue; @@ -943,7 +947,7 @@ static std::expected step6_device_extensions_and_rules(Prepar } } if (buildAccel.empty()) { excludedGlobs.insert(sc.glob); continue; } - const auto want = mcpp::pack::parse_accel(sc.accel); + const auto& want = *checked; // A GLOB WHOSE BACKEND THIS BUILD NEVER NAMED IS NOT A // MISMATCH, IT IS ABSENT. diff --git a/src/build/prepare/toolchain.cpp b/src/build/prepare/toolchain.cpp index 4211a7eef..b14766acb 100644 --- a/src/build/prepare/toolchain.cpp +++ b/src/build/prepare/toolchain.cpp @@ -1211,10 +1211,21 @@ static std::expected step1_device_axis_and_layer_merge(Prepar // nothing. Measured 2026-09-05 with a build program that wrote the value to // a file, which is the only way to see it -- a program's stdout is shown // only when it fails. - state.resolvedAccel = [&]() -> std::string { - const auto sets = mcpp::pack::parse_accel( + const auto validateAccel = [&]() -> std::expected { + const bool overridden = !state.overrides.accel.empty(); + auto parsed = mcpp::pack::parse_accel_request( + overridden ? state.overrides.accel : state.m->buildConfig.accel); + if (!parsed) return std::unexpected(std::format("{}: {}", + overridden ? "--accel" : "[build] accel", parsed.error())); + return {}; + }; + if (auto r = validateAccel(); !r) return r; + state.resolvedAccel = [&state]() -> std::string { + const auto sets = mcpp::pack::parse_accel_request( state.overrides.accel.empty() ? state.m->buildConfig.accel : state.overrides.accel); - return sets.empty() ? std::string{} : mcpp::pack::accel_str(sets); + // Both the initial statement and the conditional merge below are + // checked before a consumer can use this closure. + return !sets || sets->empty() ? std::string{} : mcpp::pack::accel_str(*sets); }; // The cfg context, with the accelerator layer filled from the resolved // accel's backend names. `cfg(accelerator = "cuda")` is a membership test @@ -1251,6 +1262,7 @@ static std::expected step1_device_axis_and_layer_merge(Prepar if (!state.m->conditionalConfigs.empty()) { merge_conditional_config(*state.m, state.cfgCtx()); } + if (auto r = validateAccel(); !r) return r; // `[target..abi] threads` -- the ROOT's statement, rendered once, // into channels that already reach the whole artefact: the graph-global // dialect flag set (every C++ translation unit, the std module's own diff --git a/src/pack/abi_tag.cppm b/src/pack/abi_tag.cppm index 8fcd8da84..02873bb27 100644 --- a/src/pack/abi_tag.cppm +++ b/src/pack/abi_tag.cppm @@ -164,6 +164,12 @@ std::string accel_str(std::span sets); // never mentioned the dimension, and the safe one. std::vector parse_accel(std::string_view s); +// Build requests and source constraints must consume the complete input. +// Unlike the legacy descriptor reader above, this reports malformed structure +// and duplicate implementations. Members and architecture sets are canonical; +// architecture tokens remain opaque to this parser. +std::expected, std::string> parse_accel_request(std::string_view s); + // The `c++NN` segment as its numeric level, or 0 when unparseable. int standard_level(std::string_view standardSegment); @@ -308,6 +314,106 @@ std::vector parse_accel(std::string_view s) { return out; } +namespace { + +std::expected, std::string> +read_request_targets(std::string_view s, std::size_t& pos) { + std::vector targets; + // Empty is the existing unconstrained set, also emitted for a bare name. + if (pos < s.size() && s[pos] == '}') return targets; + while (true) { + const auto begin = pos; + while (pos < s.size() && s[pos] != ',' && s[pos] != '}') { + if (s[pos] == '{' || s[pos] == '+') return std::unexpected(std::format( + "invalid accelerator request at byte {}: invalid target-set delimiter", pos + 1)); + ++pos; + } + const auto value = trim_sv(s.substr(begin, pos - begin)); + if (value.empty() || std::ranges::any_of(value, [](unsigned char c) { return std::isspace(c); })) + return std::unexpected(std::format( + "invalid accelerator request at byte {}: expected one nonempty target token", begin + 1)); + targets.emplace_back(value); + if (pos == s.size() || s[pos] == '}') return targets; + ++pos; + } +} + +} // namespace + +std::expected, std::string> parse_accel_request(std::string_view s) { + std::size_t pos = 0; + auto spaces = [&] { + while (pos < s.size() && (s[pos] == ' ' || s[pos] == '\t')) ++pos; + }; + auto error = [&](std::string_view why) { + return std::unexpected(std::format("invalid accelerator request at byte {}: {}", + pos + 1, why)); + }; + spaces(); + if (pos == s.size() || trim_sv(s.substr(pos)) == "(none)") return std::vector{}; + + std::vector out; + std::set names; + while (pos < s.size()) { + spaces(); + const auto begin = pos; + while (pos < s.size() && std::isalpha(static_cast(s[pos]))) ++pos; + if (pos == begin) return error("expected an implementation name"); + AccelSet a; + a.backend = std::string(s.substr(begin, pos - begin)); + if (!names.insert(a.backend).second) + return error(std::format("implementation '{}' appears more than once; combine its targets in one set", + a.backend)); + const auto version = pos; + if (pos < s.size() && std::isdigit(static_cast(s[pos]))) { + while (pos < s.size()) { + const auto c = static_cast(s[pos]); + if (!std::isalnum(c) && c != '.' && c != '-' && c != '_') break; + ++pos; + } + } + a.version = std::string(s.substr(version, pos - version)); + spaces(); + if (pos < s.size() && s[pos] == '+') { + ++pos; + spaces(); + if (pos == s.size() || s[pos] != '{') return error("expected '{' after '+'"); + ++pos; + spaces(); + auto targets = read_request_targets(s, pos); + if (!targets) return std::unexpected(targets.error()); + a.archs = std::move(*targets); + if (pos == s.size() || s[pos] != '}') return error("expected '}' to close the target set"); + ++pos; + spaces(); + } + const auto tail = s.substr(pos); + const std::size_t floorPrefix = tail.starts_with("floor>=") ? 7 : tail.starts_with("ptx>=") ? 5 : 0; + if (floorPrefix) { + pos += floorPrefix; + spaces(); + const auto floor = pos; + while (pos < s.size() && s[pos] != ',' && s[pos] != ' ' && s[pos] != '\t') { + if (s[pos] == '{' || s[pos] == '}' || s[pos] == '+') return error("invalid portable-form floor"); + ++pos; + } + if (pos == floor) return error("expected a portable-form floor"); + a.ptxFloor = std::string(s.substr(floor, pos - floor)); + spaces(); + } + if (pos < s.size() && s[pos] != ',') return error("unexpected text after the implementation request"); + std::ranges::sort(a.archs); + a.archs.erase(std::unique(a.archs.begin(), a.archs.end()), a.archs.end()); + out.push_back(std::move(a)); + if (pos == s.size()) break; + ++pos; + spaces(); + if (pos == s.size()) return error("expected an implementation after ','"); + } + std::ranges::sort(out, {}, &AccelSet::backend); + return out; +} + int accel_arch_level(std::string_view arch) { auto digits = arch; if (digits.starts_with("sm_")) digits.remove_prefix(3); diff --git a/tests/e2e/1011_accelerator_requests_are_complete_and_canonical.sh b/tests/e2e/1011_accelerator_requests_are_complete_and_canonical.sh new file mode 100755 index 000000000..6fc9de4f3 --- /dev/null +++ b/tests/e2e/1011_accelerator_requests_are_complete_and_canonical.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash +# requires: gcc unix-shell python3 +# Invalid accelerator syntax must not become a successful CPU build, and an +# implementation cannot occur twice with order-dependent constraint coverage. +set -e +MCPP="${MCPP:-mcpp}" +work=$(mktemp -d) +trap 'rm -rf "$work"' EXIT +export MCPP_HOME="$work/home" +source "$(dirname "$0")/_inherit_toolchain.sh" +mkdir -p "$work/app/src" +cd "$work/app" +fail() { echo "FAIL: $*"; cat out.log 2>/dev/null; exit 1; } +manifest() { + cat > mcpp.toml <<'TOML' +[package] +name = "accel-request" +version = "0.1.0" +[targets.accel-request] +kind = "bin" +main = "src/main.cpp" +TOML +} +manifest +echo 'int main(){return 0;}' > src/main.cpp + +for request in 'sycl+{sm_89' 'sycl+{sm_89}garbage' 'sycl+{sm_89,,sm_90}' \ + 'sycl+{sm_89},sycl+{sm_90}' 'sycl+{sm_90},sycl+{sm_89}'; do + if "$MCPP" build --accel "$request" > out.log 2>&1; then + fail "accepted malformed or repeated request: $request" + fi + grep -q 'invalid accelerator request at byte' out.log || fail "missing syntax location" + grep -q -- '--accel' out.log || fail "missing request source" +done + +cat >> mcpp.toml <<'TOML' +[build] +accel = "sycl+{sm_89" +TOML +if "$MCPP" build > out.log 2>&1; then fail 'accepted malformed manifest request'; fi +grep -q '\[build\] accel' out.log || fail 'missing manifest field' + +manifest +cat >> mcpp.toml <<'TOML' +[build] +sources = [{ glob = "src/main.cpp", accel = "sycl+{sm_90" }] +TOML +if "$MCPP" build --no-accel > out.log 2>&1; then fail 'ignored malformed source constraint'; fi +grep -q "sources entry 'src/main.cpp'" out.log || fail 'missing source constraint' +grep -q 'invalid accelerator request' out.log || fail 'missing constraint syntax error' + +manifest +cat >> mcpp.toml <<'TOML' +[build] +sources = [{ glob = "src/main.cpp", accel = "sycl+{sm_90}" }] +TOML +"$MCPP" build --accel 'sycl+{sm_90,sm_89},newmodel2+{opaque}' > out.log 2>&1 \ + || fail 'valid open-vocabulary request did not build' +before=$(find target -maxdepth 3 -name build.ninja | sort) +[ -n "$before" ] || fail 'no build graph was generated' +"$MCPP" build --accel 'newmodel2+{opaque},sycl+{sm_89,sm_90,sm_89}' > out.log 2>&1 \ + || fail 'equivalent request did not build' +after=$(find target -maxdepth 3 -name build.ninja | sort) +[ "$before" = "$after" ] || fail 'equivalent sets allocated different variants' +grep -q 'fingerprint changed' out.log && fail 'equivalent sets changed the fingerprint' +"$MCPP" run --accel 'sycl+{sm_89,sm_90},newmodel2+{opaque}' > out.log 2>&1 \ + || fail 'the canonical variant did not run' +echo 'PASS: accelerator requests are complete, unique and canonical' diff --git a/tests/unit/test_abi_tag_accel.cpp b/tests/unit/test_abi_tag_accel.cpp index d82d25180..8cc35dcfd 100644 --- a/tests/unit/test_abi_tag_accel.cpp +++ b/tests/unit/test_abi_tag_accel.cpp @@ -219,3 +219,49 @@ TEST(AccelOpenGrammar, TheExistingPtxSpellingStillParses) { EXPECT_EQ(a[0].ptxFloor, "90"); EXPECT_EQ(mcpp::pack::accel_str(a), "cuda12.8+{sm_80,sm_90f} ptx>=90"); } + +TEST(AccelRequest, RejectsIncompleteSetsAndUnconsumedInput) { + for (auto input : {"sycl+{sm_89", "sycl+{sm_89}garbage", "sycl+sm_89", + "sycl+{sm_89,,sm_90}", "sycl+{,sm_89}", "sycl+{sm_89,}", + "sycl+{{sm_89}}", "sycl,", ",sycl", "sycl ptx>=", "12.8+{sm_89}"}) { + const auto parsed = mcpp::pack::parse_accel_request(input); + ASSERT_FALSE(parsed) << input; + EXPECT_NE(parsed.error().find("byte"), std::string::npos) << input; + } +} + +TEST(AccelRequest, DuplicateImplementationsCannotChangeCoverageByTheirOrder) { + for (auto input : {"sycl+{sm_89},sycl+{sm_90}", "sycl+{sm_90},sycl+{sm_89}"}) { + const auto parsed = mcpp::pack::parse_accel_request(input); + ASSERT_FALSE(parsed); + EXPECT_NE(parsed.error().find("appears more than once"), std::string::npos); + EXPECT_NE(parsed.error().find("sycl"), std::string::npos); + } +} + +TEST(AccelRequest, EquivalentSetsHaveOneCanonicalSpelling) { + const auto a = mcpp::pack::parse_accel_request( + "sycl+{spir64,nvptx64-sm_90,nvptx64-sm_89,spir64}, cuda12.9+{sm_90,sm_89} ptx>=90"); + const auto b = mcpp::pack::parse_accel_request( + "cuda12.9+{sm_89,sm_90} floor>=90, sycl+{nvptx64-sm_89,nvptx64-sm_90,spir64}"); + ASSERT_TRUE(a); + ASSERT_TRUE(b); + EXPECT_EQ(mcpp::pack::accel_str(*a), mcpp::pack::accel_str(*b)); + EXPECT_EQ(mcpp::pack::accel_str(*a), + "cuda12.9+{sm_89,sm_90} ptx>=90, sycl+{nvptx64-sm_89,nvptx64-sm_90,spir64}"); +} + +TEST(AccelRequest, RetainsEmptyAndUnconstrainedFormsAndOpenTargetVocabulary) { + EXPECT_TRUE(mcpp::pack::parse_accel_request("")->empty()); + EXPECT_TRUE(mcpp::pack::parse_accel_request(" (none) ")->empty()); + const auto bare = mcpp::pack::parse_accel_request("sycl"); + ASSERT_TRUE(bare); + ASSERT_EQ(bare->size(), 1u); + EXPECT_TRUE(bare->front().archs.empty()); + const auto explicitEmpty = mcpp::pack::parse_accel_request("sycl+{}"); + ASSERT_TRUE(explicitEmpty); + EXPECT_EQ(mcpp::pack::accel_str(*bare), mcpp::pack::accel_str(*explicitEmpty)); + const auto opaque = mcpp::pack::parse_accel_request("newmodel3.1+{future-device-v2,domain:opaque}"); + ASSERT_TRUE(opaque); + ASSERT_EQ(opaque->front().archs.size(), 2u); +} From 87d9c45fe6526e69b41efbde646ca3caabd6fb05 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Thu, 5 Nov 2026 07:37:38 +0900 Subject: [PATCH 04/11] fix: bind capabilities in each consumer dependency context --- ...heterogeneous-build-implementation-plan.md | 28 +++ src/build/prepare/features.cpp | 219 +++++++++++------- src/build/prepare/state.cppm | 6 +- ...abilities_bind_in_each_consumer_context.sh | 129 +++++++++++ 4 files changed, 291 insertions(+), 91 deletions(-) create mode 100755 tests/e2e/1012_capabilities_bind_in_each_consumer_context.sh diff --git a/.agents/docs/2026-11-05-heterogeneous-build-implementation-plan.md b/.agents/docs/2026-11-05-heterogeneous-build-implementation-plan.md index be7aecf7c..305e480ef 100644 --- a/.agents/docs/2026-11-05-heterogeneous-build-implementation-plan.md +++ b/.agents/docs/2026-11-05-heterogeneous-build-implementation-plan.md @@ -86,3 +86,31 @@ TLS 段,转为 `__emutls_get_address` 与 emulated storage。 `/tmp/mcpp-885-thinlto-tls-fix/results.json`。它不等于 clangd 完整资格通过。 该约定属于映像平台的链接/codegen 契约,优先在平台数据中补齐,使其他 LTO 消费者同样受益;不在通用 mcpp 核心增加 openkal TLS 分支。 + +## 5. 实现检查点(2026-11-05):请求、消费上下文及平台 + +I1 已在同一核心 PR #888 提交。新二进制通过 23 项 accelerator 单测和 E2E 1011。 +I3 第一批实现将 provider/requirement/exclusive 的身份改为包索引,按所选消费包的 +普通依赖闭包验证;宿主构建边不进入目标上下文。库 requirement 在最终消费者内 +绑定,应用可以通过兄弟依赖供给。工作空间根 pin、成员 pin、CLI pin 依次覆盖, +短名只有唯一匹配时可用,同短名跨 namespace 可以使用完整 `namespace:name`。 +绑定仍只验证,普通依赖继续决定编译和链接输入,没有借 pin 删除其他实现。 + +新二进制的 E2E 1012 通过独立 exclusive、应用供给、缺失供给、同程序互斥、成员 +与 CLI pin、跨 namespace 同短名和完整身份控制。原 E2E 81、601 同样通过。 +共享库、测试与宿主工具的进一步控制以及完整核心 CI 仍待完成,不据此宣布 I3 +全部验收。I4/I5 的可选进度尚未实现。 + +插件实际实现复用 #52(提交 `8d4b150`),保留原 CMake cross contract。46 项 +plugin-logic、真实 musl CMake cross build/run、真实 SYCL 旧写法、独占 NVIDIA、 +双 NVIDIA 与 SPIR-V/NVIDIA 混合构建通过。规则设备链接命令的中间产物已核验 +双 NVIDIA fatbin 与混合 SPIR-V/NVIDIA 镜像。当前驱动返回 CUDA 803,GPU 执行 +未获资格。多目标静态归档的 SDK 流程仍待定位。 + +平台修复 PR #19 的四种宿主完整 CI `41922140543` 全部通过,已发布 0.23.6。 +GitHub/GitCode 源码一致,SHA256 为 +`51903d50b436e81047b74b460ea1e7e23be25704ab601438b160b9370a7017c2`。 +索引 PR #709 的候选与消费者检查通过并合入,发布任务已触发。引擎 PR #16 的 +平台选择已更新到该版本,须待公开索引可安装后重跑完整资格。原性能、seal 和 +补丁台账门槛保留。服务器 PR #75 第三次尝试的独立 macOS job 已通过,目前失败 +只剩严格引擎候选门槛,服务器最终发布仍等待合格引擎。 diff --git a/src/build/prepare/features.cpp b/src/build/prepare/features.cpp index 93b46bf70..5232c7942 100644 --- a/src/build/prepare/features.cpp +++ b/src/build/prepare/features.cpp @@ -394,7 +394,7 @@ static std::expected step6_activate_features(PrepareState& st bool seedDefault = true) { return feature_closure(pm, requested, seedDefault); // single shared implementation }; - auto apply = [&](mcpp::modgraph::PackageRoot& pkg, + auto apply = [&](std::size_t packageIndex, mcpp::modgraph::PackageRoot& pkg, const std::vector& requested, bool seedDefault = true) { auto active = activate(pkg.manifest, requested, seedDefault); @@ -402,15 +402,15 @@ static std::expected step6_activate_features(PrepareState& st // feature-scoped provides/requires count only when the feature is // active. Requirements are bound after all packages are processed. const auto& pcap = pkg.manifest.package.name; - for (auto& cap : pkg.manifest.provides) state.capProviders[cap].push_back(pcap); - for (auto& cap : pkg.manifest.exclusive) state.capExclusive[cap].push_back(pcap); + for (auto& cap : pkg.manifest.provides) state.capProviders[cap].push_back(packageIndex); + for (auto& cap : pkg.manifest.exclusive) state.capExclusive[cap].push_back(packageIndex); for (auto& f : active) { if (auto it = pkg.manifest.featureProvides.find(f); it != pkg.manifest.featureProvides.end()) - for (auto& cap : it->second) state.capProviders[cap].push_back(pcap); + for (auto& cap : it->second) state.capProviders[cap].push_back(packageIndex); if (auto it = pkg.manifest.featureRequires.find(f); it != pkg.manifest.featureRequires.end()) - for (auto& cap : it->second) state.capRequires.emplace_back(cap, pcap); + for (auto& cap : it->second) state.capRequires.emplace_back(cap, packageIndex); if (auto it = pkg.manifest.featureRequiresAbiThreads.find(f); it != pkg.manifest.featureRequiresAbiThreads.end() && it->second) state.abiRequires.emplace_back(std::format("feature `{}`", f), pcap); @@ -715,7 +715,7 @@ static std::expected step6_activate_features(PrepareState& st if (state.overrides.strict) return std::unexpected(msg); mcpp::diag::warning("features/request", msg); } - apply(state.packages[0], rootReq); + apply(0, state.packages[0], rootReq); for (auto& f : activate(*state.m, rootReq)) state.activeRootFeatures.insert(f); } // #242/#243: the feature request for a dependency PACKAGE, aggregated @@ -752,7 +752,7 @@ static std::expected step6_activate_features(PrepareState& st // feature-gated sources must have those sources dropped by default. // depDefaultFeatures carries the consumer's `default-features = false` // (#242): when opted out, the dep's [features].default is not seeded. - apply(state.packages[i], req, depDefaultFeatures); + apply(i, state.packages[i], req, depDefaultFeatures); if (state.activeFeaturesByPackage.size() <= i) state.activeFeaturesByPackage.resize(i + 1); state.activeFeaturesByPackage[i] = @@ -2043,6 +2043,127 @@ static std::expected step6_dependency_build_programs(PrepareS return {}; } +// A capability belongs to a consumer's dependency closure, rather than the +// union of independent workspace products. Requirements of a library bind in +// that consumer's context too: an application may supply a sibling provider. +struct CapabilityContext { + std::size_t root; + std::set packages; +}; + +static std::string capability_package_name(const PrepareState& state, std::size_t i) { + const auto& p = state.packages[i].manifest.package; + return p.namespace_.empty() ? p.name : p.namespace_ + ":" + p.name; +} + +static std::vector capability_contexts(const PrepareState& state) { + std::vector> dependencies(state.packages.size()); + for (const auto& edge : state.dependencyEdges) { + if (edge.buildTable || state.packages[edge.dependencyPackageIndex].buildTimeOnly) + continue; + dependencies[edge.consumerPackageIndex].push_back(edge.dependencyPackageIndex); + } + std::vector roots; + for (std::size_t i = 0; i < state.packages.size(); ++i) { + if (!state.workspacePlan() ? i == 0 + : state.packages[i].selectedMember || state.selectedMemberAt(state.packages[i].root)) + roots.push_back(i); + } + std::vector contexts; + for (auto root : roots) { + CapabilityContext context{root, {root}}; + std::vector pending{root}; + // A worklist follows each edge once; cycles cannot grow it indefinitely. + for (std::size_t next = 0; next < pending.size(); ++next) { + const auto& children = dependencies[pending[next]]; + std::ranges::copy_if(children, std::back_inserter(pending), [&](auto child) { + return context.packages.insert(child).second; + }); + } + contexts.push_back(std::move(context)); + } + return contexts; +} + +static std::vector capability_candidates( + const std::vector& providers, const CapabilityContext& context) { + std::set unique; + for (auto p : providers) + if (context.packages.contains(p)) unique.insert(p); + return {unique.begin(), unique.end()}; +} + +static std::string capability_list(const PrepareState& state, + const std::vector& providers) { + std::string list; + for (auto p : providers) + list += (list.empty() ? "" : ", ") + capability_package_name(state, p); + return list; +} + +static std::expected bind_capability_context( + PrepareState& state, const CapabilityContext& context) { + const auto consumer = capability_package_name(state, context.root); + for (const auto& [cap, claimers] : state.capExclusive) { + auto claims = capability_candidates(claimers, context); + if (claims.empty()) continue; + auto it = state.capProviders.find(cap); + if (it == state.capProviders.end()) continue; + auto providers = capability_candidates(it->second, context); + if (providers.size() < 2) continue; + refusal::record(refusal::Code::ExclusiveCapability); + return std::unexpected(std::format( + "capability '{}' is EXCLUSIVE in consumer '{}'; providers: [{}]; " + "exclusive: [{}]. A [capabilities] pin selects a provider for a " + "requirement and cannot make two definitions of one symbol safe.", + cap, consumer, capability_list(state, providers), capability_list(state, claims))); + } + auto pins = state.m->capabilityPins; + for (const auto& [cap, provider] : state.packages[context.root].manifest.capabilityPins) + pins[cap] = provider; + // The command is the final override, including over a member's own pin. + for (std::size_t p = 0; p < state.overrides.capabilities.size();) { + auto c = state.overrides.capabilities.find_first_of(", ", p); + auto tok = state.overrides.capabilities.substr( + p, c == std::string::npos ? std::string::npos : c - p); + if (auto eq = tok.find('='); eq != std::string::npos) + pins[tok.substr(0, eq)] = tok.substr(eq + 1); + if (c == std::string::npos) break; + p = c + 1; + } + std::set> bound; + for (const auto& [cap, requirer] : state.capRequires) { + if (!context.packages.contains(requirer) || !bound.emplace(cap, requirer).second) + continue; + std::vector candidates; + if (auto it = state.capProviders.find(cap); it != state.capProviders.end()) + candidates = capability_candidates(it->second, context); + if (auto pin = pins.find(cap); pin != pins.end()) { + auto matches = std::ranges::count_if(candidates, [&](auto p) { + return pin->second == capability_package_name(state, p) + || pin->second == state.packages[p].manifest.package.name; + }); + if (matches == 1) continue; + return std::unexpected(std::format( + "capability '{}' pinned to provider '{}' (via [capabilities]) in " + "consumer '{}', but the pin must name exactly one provider; candidates: [{}]", + cap, pin->second, consumer, capability_list(state, candidates))); + } + if (candidates.empty()) + return std::unexpected(std::format( + "no package provides capability '{}' required by '{}' in consumer '{}'; " + "add a dependency that declares `provides = [\"{}\"]`", + cap, capability_package_name(state, requirer), consumer, cap)); + if (candidates.size() > 1) + return std::unexpected(std::format( + "capability '{}' required by '{}' has multiple providers in consumer '{}': [{}]; " + "select one with [capabilities] {} = \"\" or --cap {}=", + cap, capability_package_name(state, requirer), consumer, + capability_list(state, candidates), cap, cap)); + } + return {}; +} + static std::expected step6_capability_binding(PrepareState& state) { // ─── Capability binding (Stage 3) ────────────────────────────────── // For each required capability, bind exactly one provider from the @@ -2052,50 +2173,9 @@ static std::expected step6_capability_binding(PrepareState& s // requirements already flow through normal dependency mechanics — this // pass is the selection-and-validation layer. See the capability-model // design doc. - // --cap cap=provider[,cap=provider] overrides [capabilities] pins. - for (std::size_t p = 0; p < state.overrides.capabilities.size();) { - auto c = state.overrides.capabilities.find_first_of(", ", p); - auto tok = state.overrides.capabilities.substr( - p, c == std::string::npos ? std::string::npos : c - p); - if (auto eq = tok.find('='); eq != std::string::npos) - state.m->capabilityPins[tok.substr(0, eq)] = tok.substr(eq + 1); - if (c == std::string::npos) break; - p = c + 1; - } - - // EXCLUSIVE CAPABILITIES, CHECKED BEFORE REQUIREMENTS ARE BOUND. - // - // Ordering is deliberate. A requirement conflict is reported by naming - // the requirement; this one exists whether or not anything requires the - // capability, because the defect is that two implementations of one - // interface are in the same link. Reporting it first means the message - // names the real problem rather than a symptom of it. - for (auto const& [cap, claimers] : state.capExclusive) { - auto it = state.capProviders.find(cap); - if (it == state.capProviders.end()) continue; - std::vector providers; - for (auto const& p : it->second) - if (std::find(providers.begin(), providers.end(), p) == providers.end()) - providers.push_back(p); - if (providers.size() < 2) continue; - - std::string list, claimed; - for (auto const& p : providers) list += (list.empty() ? "" : ", ") + p; - for (auto const& c : claimers) claimed += (claimed.empty() ? "" : ", ") + c; - refusal::record(refusal::Code::ExclusiveCapability); - return std::unexpected(std::format( - "capability '{}' is provided by more than one package, and {} " - "declares it EXCLUSIVE.\n" - " providers: [{}]\n" - " exclusive: [{}]\n" - " Two implementations of one interface define the same " - "symbols, so the link would\n" - " resolve every call to whichever archive it reached " - "first. Keep one of them —\n" - " a `[capabilities]` pin selects a provider for a " - "REQUIREMENT and cannot make two\n" - " definitions of one symbol safe.", - cap, claimers.size() == 1 ? "it" : "they", list, claimed)); + for (const auto& context : capability_contexts(state)) { + if (auto result = bind_capability_context(state, context); !result) + return std::unexpected(result.error()); } // VERSION FLOORS. A package states what it needs of the machine; a @@ -2141,43 +2221,6 @@ static std::expected step6_capability_binding(PrepareState& s "exceptions", state.m->buildConfig.abiExceptions, state.abiRequiresExceptions)) return std::unexpected(*err); - std::set boundCaps; - for (auto& [cap, requirer] : state.capRequires) { - if (!boundCaps.insert(cap).second) continue; // one diagnosis per cap - auto& pins = state.m->capabilityPins; - // Dedup candidates, preserve first-seen order. - std::vector cands; - if (auto it = state.capProviders.find(cap); it != state.capProviders.end()) - for (auto& p : it->second) - if (std::find(cands.begin(), cands.end(), p) == cands.end()) - cands.push_back(p); - if (auto pit = pins.find(cap); pit != pins.end()) { - const auto& pin = pit->second; - if (std::find(cands.begin(), cands.end(), pin) == cands.end()) { - std::string list; - for (auto& c : cands) list += (list.empty() ? "" : ", ") + c; - return std::unexpected(std::format( - "capability '{}' pinned to provider '{}' (via [capabilities]), " - "but no such provider is in the graph; candidates: [{}]", - cap, pin, list)); - } - continue; // pin satisfied - } - if (cands.empty()) - return std::unexpected(std::format( - "no package provides capability '{}' required by '{}'; add a " - "dependency that declares `provides = [\"{}\"]`", cap, requirer, cap)); - if (cands.size() > 1) { - std::string list; - for (auto& c : cands) list += (list.empty() ? "" : ", ") + c; - return std::unexpected(std::format( - "capability '{}' has multiple providers in the graph: [{}]; select " - "one with [capabilities] {} = \"\" or --cap {}=", - cap, list, cap, cap)); - } - // exactly one → bound implicitly. - } - // The package that supplies the C++ layer when the graph does, as an index // into `packages`. Recorded where the provider is found so that the check // after planning (#641) reads the same package the resolution chose. diff --git a/src/build/prepare/state.cppm b/src/build/prepare/state.cppm index 1a22e8c9a..fda9c0b00 100644 --- a/src/build/prepare/state.cppm +++ b/src/build/prepare/state.cppm @@ -864,11 +864,11 @@ struct PrepareState { // ── P6-P8: feature activation, capability/ABI accumulation, target side, // host tool provisioning ───────────────────────────────────────────────── std::set activeRootFeatures; - std::map> capProviders; - std::vector> capRequires; + std::map> capProviders; + std::vector> capRequires; std::vector> abiRequires; std::vector> abiRequiresExceptions; - std::map> capExclusive; + std::map> capExclusive; std::map> deviceSourcesByPackage; // Keyed like `deviceSourcesByPackage`, by `pkg.root.string()` (root // package included: `packages[0].root == *root`). Holds a package whose diff --git a/tests/e2e/1012_capabilities_bind_in_each_consumer_context.sh b/tests/e2e/1012_capabilities_bind_in_each_consumer_context.sh new file mode 100755 index 000000000..d764a71ba --- /dev/null +++ b/tests/e2e/1012_capabilities_bind_in_each_consumer_context.sh @@ -0,0 +1,129 @@ +#!/usr/bin/env bash +# requires: gcc unix-shell python3 +# Independent products may select different providers. A library requirement +# still accepts a provider supplied by its final application's sibling edge. +set -e +MCPP="${MCPP:-mcpp}" +work=$(mktemp -d) +trap 'rm -rf "$work"' EXIT +export MCPP_HOME="$work/home" +source "$(dirname "$0")/_inherit_toolchain.sh" +cd "$work" +fail() { echo "FAIL: $*"; cat out.log; exit 1; } +cat > mcpp.toml <<'TOML' +[workspace] +members = ["app-a", "app-b"] +TOML +for suffix in a b; do + mkdir -p "provider-$suffix/src" "app-$suffix/src" + cat > "provider-$suffix/mcpp.toml" < "provider-$suffix/src/anchor.c" + cat > "app-$suffix/mcpp.toml" < "app-$suffix/src/main.cpp" +done +mkdir -p helper/src +cat > helper/mcpp.toml <<'TOML' +[package] +name = "helper" +version = "0.1.0" +[features] +default = ["need-provider"] +need-provider = { requires = ["probe-interface"] } +[targets.helper] +kind = "lib" +TOML +echo 'int helper_anchor(void){return 0;}' > helper/src/anchor.c +"$MCPP" build --workspace > out.log 2>&1 || fail 'independent exclusive providers collided' +"$MCPP" run -p app-a > out.log 2>&1 || fail 'app-a did not run' +"$MCPP" run -p app-b > out.log 2>&1 || fail 'app-b did not run' + +# A provider elsewhere in the workspace cannot satisfy a missing dependency. +cp app-a/mcpp.toml app-a/saved.toml +sed '/^provider-a =/d' app-a/saved.toml > app-a/mcpp.toml +if "$MCPP" build --workspace > out.log 2>&1; then fail 'another product supplied a missing provider'; fi +grep -q "no package provides capability 'probe-interface'" out.log || fail 'wrong missing-provider diagnosis' +grep -q "consumer 'app-a'" out.log || fail 'missing consumer context' +cp app-a/saved.toml app-a/mcpp.toml + +# A pin cannot remove the second implementation from one actual link closure. +cat >> app-a/mcpp.toml <<'TOML' +[capabilities] +probe-interface = "provider-a" +TOML +sed '/^helper =/a provider-b = { path = "../provider-b" }' app-a/mcpp.toml > changed.toml +mv changed.toml app-a/mcpp.toml +if "$MCPP" build --workspace > out.log 2>&1; then fail 'pin hid an exclusive collision'; fi +grep -qi exclusive out.log || fail 'wrong exclusive diagnosis' +grep -q "consumer 'app-a'" out.log || fail 'exclusive diagnosis omitted consumer' + +# Without exclusivity both providers may coexist; the member pin resolves its +# requirement while the other member binds implicitly to its own provider. +for suffix in a b; do + sed '/^exclusive =/d' "provider-$suffix/mcpp.toml" > changed.toml + mv changed.toml "provider-$suffix/mcpp.toml" +done +"$MCPP" build --workspace > out.log 2>&1 || fail 'member pin did not resolve local ambiguity' +"$MCPP" build --workspace --cap probe-interface=provider-b > out.log 2>&1 \ + || fail 'CLI pin did not override the member pin' +if "$MCPP" build --workspace --cap probe-interface=provider-a > out.log 2>&1; then + fail 'CLI pin escaped the second consumer closure' +fi +grep -q "consumer 'app-b'" out.log || fail 'invalid pin omitted its consumer' + +# Qualified provider identities distinguish packages sharing a short name. +mkdir -p namespaced/src +for ns in first second; do + mkdir -p "$ns/src" + cat > "$ns/mcpp.toml" < "$ns/src/anchor.c" +done +cat > namespaced/mcpp.toml <<'TOML' +[package] +name = "namespaced-consumer" +version = "0.1.0" +[features] +default = ["need"] +need = { requires = ["namespaced-interface"] } +[dependencies.first] +implementation = { path = "../first" } +[dependencies.second] +implementation = { path = "../second" } +[capabilities] +namespaced-interface = "implementation" +TOML +echo 'int main(){return 0;}' > namespaced/src/main.cpp +cd namespaced +if "$MCPP" build > out.log 2>&1; then fail 'ambiguous short provider identity was accepted'; fi +grep -q 'first:implementation' out.log || fail 'missing first qualified candidate' +grep -q 'second:implementation' out.log || fail 'missing second qualified candidate' +sed 's/= "implementation"$/= "first:implementation"/' mcpp.toml > changed.toml +mv changed.toml mcpp.toml +"$MCPP" build > out.log 2>&1 || fail 'qualified provider pin did not bind' + +echo 'PASS: capabilities bind in each consumer context' From df1d6307ad6f2ba5f9f6c915e848bb1aa58b5a30 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Thu, 5 Nov 2026 07:55:26 +0900 Subject: [PATCH 05/11] chore: bump version to 2026.11.5.1 --- mcpp.toml | 2 +- modules/versioning/src/version.cppm | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mcpp.toml b/mcpp.toml index 8c2f3a1f1..b2b34b1f0 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,6 +1,6 @@ [package] name = "mcpp" -version = "2026.11.3.1" +version = "2026.11.5.1" description = "Modern C++ build & package management tool" license = "Apache-2.0" authors = ["mcpp-community"] diff --git a/modules/versioning/src/version.cppm b/modules/versioning/src/version.cppm index 7c17f1ea8..104e219f6 100644 --- a/modules/versioning/src/version.cppm +++ b/modules/versioning/src/version.cppm @@ -31,6 +31,6 @@ import std; export namespace mcpp { -inline constexpr std::string_view MCPP_VERSION = "2026.11.3.1"; +inline constexpr std::string_view MCPP_VERSION = "2026.11.5.1"; } // namespace mcpp From 82fc9b3c73f901f825a6fd2a1d28df62d2150133 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Thu, 5 Nov 2026 08:05:52 +0900 Subject: [PATCH 06/11] feat: report optional action progress and update the default platform --- ...heterogeneous-build-implementation-plan.md | 45 +++++++ docs/06-features-and-capabilities.md | 15 +-- docs/24-openkal-cross.md | 4 +- docs/30-build-mcpp.md | 8 ++ docs/42-heterogeneous-builds.md | 5 +- docs/specs/README.md | 2 +- docs/specs/build-plugins.md | 20 +++- docs/zh/06-features-and-capabilities.md | 15 +-- docs/zh/24-openkal-cross.md | 4 +- docs/zh/30-build-mcpp.md | 8 ++ docs/zh/42-heterogeneous-builds.md | 4 + modules/buildmcpp/src/directives.cppm | 1 + modules/buildmcpp/src/program_protocol.cppm | 4 +- modules/manifest/src/types.cppm | 1 + modules/toolchain-model/src/triple.cppm | 4 +- src/build/action_progress.cppm | 96 +++++++++++++++ src/build/hostprogram.cppm | 2 + src/build/ninja_backend.cppm | 13 +- src/build/progress.cppm | 34 +++++- src/cli.cppm | 21 +++- ...abilities_bind_in_each_consumer_context.sh | 19 +++ ...ional_action_progress_preserves_outputs.sh | 113 ++++++++++++++++++ tests/unit/test_action_progress.cpp | 44 +++++++ tests/unit/test_build_directives.cpp | 17 ++- 24 files changed, 461 insertions(+), 38 deletions(-) create mode 100644 src/build/action_progress.cppm create mode 100755 tests/e2e/1013_optional_action_progress_preserves_outputs.sh create mode 100644 tests/unit/test_action_progress.cpp diff --git a/.agents/docs/2026-11-05-heterogeneous-build-implementation-plan.md b/.agents/docs/2026-11-05-heterogeneous-build-implementation-plan.md index 305e480ef..86afacae0 100644 --- a/.agents/docs/2026-11-05-heterogeneous-build-implementation-plan.md +++ b/.agents/docs/2026-11-05-heterogeneous-build-implementation-plan.md @@ -114,3 +114,48 @@ GitHub/GitCode 源码一致,SHA256 为 平台选择已更新到该版本,须待公开索引可安装后重跑完整资格。原性能、seal 和 补丁台账门槛保留。服务器 PR #75 第三次尝试的独立 macOS job 已通过,目前失败 只剩严格引擎候选门槛,服务器最终发布仍等待合格引擎。 + +## 6. 发布执行约束(2026-11-05,用户补充) + +核心 PR #888 同时携带构建版本 `2026.11.5.1`;bootstrap 保持公开的 +`2026.11.3.1`,不提前安装未发布版本。中英文使用文档、协议及插件文档与最终 +实现同轮更新。 + +最终提交的核心 CI 通过后,再独立完成一次生态视角 PR review。审查涵盖插件新旧 +接口、宿主/目标图、平台默认及显式覆盖、交叉构建、打包与发布资产、语言服务和 +增量行为;将发现、证据及限制记录在本方案中。未经该审查不得合入或创建发布 tag。 +如审查需要改代码或文档,则修正后以新提交重新完成适用检查。 + +确认无问题后合入 #888;先取消该合入触发的 main CI,再创建新版本 tag,优先 +执行 Release CI。Release 的完整平台资产、镜像和索引可安装性验收完成后,再从 +main 显式触发常规 CI。记录合入 SHA、被取消的 run、Release run、公开安装证据 +和重新触发的 main run,避免用旧提交或上一轮 CI 代替本轮验收。 + +I4 当前本地集成控制已通过五种 action 角色、真实对象链接运行、关闭开关、JSON +输出及内部计数完成后的失败退出。新增进程组取消控制检出中断退出未清理通道, +现已接入现有 `mcpp::run::add_cleanup`;仍待新版本二进制重建后复验,不将修改 +意图计为通过。 + +## 7. 新版本本地检查点(2026-11-05) + +候选二进制实际报告 `mcpp 2026.11.5.1`,SHA256 为 +`26bee63a21372057d01c95dc3a18e832b3ae67fc7b4251b2e2b90488464ddd50`。 +该二进制通过 E2E 01、1011、扩展 1012、1013。1012 追加共享 provider、测试链接 +单元与 host-only 不能供给 target requirement 控制;1013 的进程组取消清理复验 +通过。此前进度解析单测 3 项通过;新版本完整 directives 单测仍在执行。 + +默认平台已在现有隔离 GLOBAL 洁净室的新中性项目验证:没有 `platform` 覆盖, +实际从公开索引解析 0.23.6;ThinLTO 的 x86_64 映像封装并运行输出 TLS 值 7, +aarch64 映像交叉构建和封装通过。该项使用本地候选编译器和公开平台依赖,不能 +等同最终发布资产安装验收。证据为 +`/tmp/mcpp-885-core11351-default-platform-verified.json`。 + +插件进度辅助已追加至 #52(`d215e0d`),逻辑测试 49/49 通过。通用 enable/快照 +类型放在标准库辅助,原子文件 writer 为独立可选 feature,复用 os.fs;普通 CMake +消费方不因启用阶段报告引入该运行库。CMake adapter 在新核心下真实构建并运行, +独立实跑验证快照有效、正常阶段报告和不可写通道不影响构建。它只报告自己拥有的 +configure/build-install 阶段,不估计未掌握的编译任务总数。旧核心下退化保持可用。 + +中英文核心使用文档、SPEC-007 及其目录、平台默认示例已同步。结构与风格检查 +通过,修改的文档已用 pandoc 渲染并进行第二遍阅读。以上是 CI 前的本地检查; +用户要求的最终生态 review 仍待最终提交 CI 通过后执行。 diff --git a/docs/06-features-and-capabilities.md b/docs/06-features-and-capabilities.md index dcf94d840..66179c12b 100644 --- a/docs/06-features-and-capabilities.md +++ b/docs/06-features-and-capabilities.md @@ -190,6 +190,10 @@ resolver binds exactly one provider from the dependency graph. This is how a bui one of several interchangeable backends (OpenBLAS / MKL / …) without baking a choice into the library. + +From 2026.11.5.1, requirements bind within each selected consumer's ordinary dependency closure. A library can require a capability supplied by a sibling dependency of its application. Independent workspace products can use different providers; a provider belonging only to another product cannot satisfy a missing requirement. Build-time tool edges are excluded from the target context. Workspace root pins, member pins and `--cap` apply in that order. A short provider name is accepted only when it identifies one candidate; `namespace:name` distinguishes equal short names. Exclusive providers are checked within that same consumer closure, including linked shared libraries. A pin does not remove another provider from the link. + + ```toml # A provider package satisfies a capability for any dependent that requires it. [package] @@ -307,19 +311,12 @@ provides = ["gpu-blas"] exclusive = ["gpu-blas"] ``` -Two packages that both provide `gpu-blas`, where at least one declares it +Two packages in one consumer closure that both provide `gpu-blas`, where at least one declares it exclusive, are refused when capabilities are bound — before anything is compiled, naming the capability and both providers: ``` -error: capability 'gpu-blas' is provided by more than one package, and they - declare it EXCLUSIVE. - providers: [compat.cublas, compat.rocblas] - exclusive: [compat.cublas, compat.rocblas] - Two implementations of one interface define the same symbols, so the - link would resolve every call to whichever archive it reached first. - Keep one of them — a `[capabilities]` pin selects a provider for a - REQUIREMENT and cannot make two definitions of one symbol safe. +error: capability 'gpu-blas' is EXCLUSIVE in consumer 'app'; providers: [compat.cublas, compat.rocblas]; exclusive: [compat.cublas, compat.rocblas]. A [capabilities] pin selects a provider for a requirement and cannot make two definitions of one symbol safe. ``` The refusal reports `exclusive-capability` in `--format json` (chapter 11). diff --git a/docs/24-openkal-cross.md b/docs/24-openkal-cross.md index 793edab81..fd7df1b35 100644 --- a/docs/24-openkal-cross.md +++ b/docs/24-openkal-cross.md @@ -111,7 +111,7 @@ practice. ```toml [target.'cfg(any(linux, macos, windows))'] -platform = "openkal-platform@0.23.3" +platform = "openkal-platform@0.23.6" ``` One table (2026.10.27.1+). It selects the openkal platform for the host triples, @@ -411,6 +411,8 @@ Nothing else is declared (2026.10.27.1+): the target table selects `openkal-platform` for both rows, and the platform states the toolchain, the layers, the tools, the conventions below and the runner ([35](35-platforms.md)). +In mcpp 2026.11.5.1, both built-in openkal targets select platform 0.23.6 by default. An explicit `platform` configuration overrides that selection. This platform preserves the emulated TLS convention through the final ThinLTO link. + | | the platform's statement | the reason | |---|---|---| | compile | `--target=-unknown-none-elf -fPIC -femulated-tls -fvisibility=hidden -fasynchronous-unwind-tables -Xclang -fstack-clash-protection`; on AArch64 also `-ffixed-x18 -mno-outline-atomics`; `-ffreestanding` when the graph supplies no C library | an image is told no operating system; it does not address the thread pointer and does not use the platform register, which two systems reserve; it exports what its source marks `visibility("default")` | diff --git a/docs/30-build-mcpp.md b/docs/30-build-mcpp.md index a35a0bd2e..7ab9f58d1 100644 --- a/docs/30-build-mcpp.md +++ b/docs/30-build-mcpp.md @@ -820,6 +820,14 @@ a.arg("cp").arg("${mcpp.artifact:updater/updater}").arg(out.c_str()) A placeholder that names no `artifacts` entry fails planning and is named in the error. +#### Optional action progress (2026.11.5.1+, protocol 18) + +Set `a.progress = true` in build.mcpp to enable telemetry for that action. The default is `false`; all five roles support it. The command receives `MCPP_ACTION_PROGRESS_FILE` when reporting is available and treats an absent or empty value as disabled. + +The worker atomically replaces the file with its latest JSON snapshot, for example `{"v":1,"seq":2,"phase":"compile","done":3,"total":9}`. `phase` can be reported without a total. The build status displays the latest valid snapshot while the action runs. Counts describe work inside that action and do not contribute to the outer Ninja step count. The command's exit status and output checks determine success. + +The runtime channel is allocated for this execution and is not a declared input or output. Editing the build program to enable telemetry can replan the build. Existing NDJSON build events retain their meanings. Snapshot fields, limits and ordering are specified in [SPEC-007 §3.1](specs/build-plugins.md#31-可选-action-进度协议-18). + ### Deploying what the program generated: `deploy` (2026.9.12.3+, protocol 11) `[runtime] deploy` (docs/04 §2.11) places a file that already exists in the diff --git a/docs/42-heterogeneous-builds.md b/docs/42-heterogeneous-builds.md index 599129c2b..a168140fc 100644 --- a/docs/42-heterogeneous-builds.md +++ b/docs/42-heterogeneous-builds.md @@ -470,7 +470,10 @@ say and says nothing, and a probe that reaches no answer invents none. accel = "cuda12.8+{sm_80,sm_90f} ptx>=90" ``` -overridden for one build by `--accel`, which is the relationship `--target` +From 2026.11.5.1, a build request is read completely: unclosed sets, trailing text and empty target entries are errors. An implementation appears once per request; combine its targets as `sycl+{nvptx64-sm_89,nvptx64-sm_90}`. Implementation and target ordering, and repeated targets within a member, do not allocate different build variants. Target tokens belong to their implementation and are interpreted by its rule; selecting a SYCL NVIDIA target does not itself select CUDA source compilation. Source constraints use the same complete syntax check, including in a CPU-only build. + + +The manifest request is overridden for one build by `--accel`, which is the relationship `--target` has with `[toolchain]`. `--no-accel` is not the absence of `--accel`; it is an explicit request for no accelerator, which is what selects a CPU-only variant of a package that also publishes device builds. diff --git a/docs/specs/README.md b/docs/specs/README.md index 9427707a1..41469f509 100644 --- a/docs/specs/README.md +++ b/docs/specs/README.md @@ -36,7 +36,7 @@ | [SPEC-004](manifest-semantics.md) | `mcpp.toml` 的平面划分、条件化形状、解析轴与命名规约 | 草案 v1.17 | 2026-10-23 | 条件化形状 mcpp >= 2026.8.29.1;目标轴 mcpp >= 2026.9.6.4;`linkage` 默认值 mcpp >= 2026.9.15.2;链接 flag 的词读法 mcpp >= 2026.9.26.2;条件化的 `dialect_cxxflags` 与 `-p` 的包身份 mcpp >= 2026.9.28.1;条件表按具体程度生效 mcpp >= 2026.9.28.2;平台前缀规约 mcpp >= 2026.10.5.1;根包的继承、根位置的 profile、链接 flag 的作用域、链接期优化与导出发现 mcpp >= 2026.10.5.2;架构段的 `x86` 即 `i686` mcpp >= 2026.10.5.3;前缀即语义与工作空间层 mcpp >= 2026.10.10.1;依赖的库与配置(§12) mcpp >= 2026.10.11.1;值的四档层次与 `run_under` mcpp >= 2026.10.24.1 | | [SPEC-005](build-database.md) | 构建数据库:`mcpp emit build-database` 的内容、取值规则与不写工程目录的保证 | 评审中 v1.8 | 2026-10-11 | mcpp >= 2026.9.15.1;v1.3 条款 mcpp >= 2026.9.26.2;v1.4 条款 mcpp >= 2026.9.27.1;v1.5 条款 mcpp >= 2026.9.28.1;v1.6 条款 mcpp >= 2026.9.29.5;v1.7 条款 mcpp >= 2026.9.30.2;v1.8 条款 mcpp >= 2026.10.11.1 | | [SPEC-006](toolchain-management.md) | 工具链管理:身份、来源、选择与载荷契约 | 草案 v0.10 | 2026-10-23 | 逐条标注;已实现条款 mcpp >= 2026.9.24.1;§3.7 mcpp >= 2026.9.28.1;§3.7.1 mcpp >= 2026.9.28.2;§2.2.1 与 §3.3 的非缺省来源 mcpp >= 2026.10.1.3;§3.8 mcpp >= 2026.10.8.1;§3.9 至 §3.11 mcpp >= 2026.10.24.1 | -| [SPEC-007](build-plugins.md) | 构建插件:配置、施工与校验的分工,运行时与规划期的义务 | 草案 v0.9 | 2026-10-11 | 逐条标注;mcpp >= 2026.9.26.2;v0.3 条款 mcpp >= 2026.9.27.1;v0.4 条款 mcpp >= 2026.9.28.1;v0.5 条款 mcpp >= 2026.9.28.2;v0.6(§9)mcpp >= 2026.9.28.3;R3.9、R3.10 mcpp >= 2026.10.10.1;§10 mcpp >= 2026.10.11.1 | +| [SPEC-007](build-plugins.md) | 构建插件:配置、施工与校验的分工,运行时与规划期的义务 | 草案 v0.12 | 2026-11-05 | 逐条标注;mcpp >= 2026.9.26.2;v0.3 条款 mcpp >= 2026.9.27.1;v0.4 条款 mcpp >= 2026.9.28.1;v0.5 条款 mcpp >= 2026.9.28.2;v0.6(§9)mcpp >= 2026.9.28.3;R3.9、R3.10 mcpp >= 2026.10.10.1;§10 mcpp >= 2026.10.11.1;§3.1 协议 18 mcpp >= 2026.11.5.1 | | [SPEC-008](library-interface.md) | 库的接口:公开模块、发布闭包与两种形态的一致 | 草案 v0.2 | 2026-10-11 | 第一阶段(只警告)mcpp >= 2026.9.28.3;v0.2 mcpp >= 2026.10.11.1 | | [SPEC-009](toolchain-maintenance.md) | 工具链的支持与维护:版本线、默认值、来源、移动与退役 | 草案 v0.6 | 2026-10-09 | 逐条标注;本版只有规范,多数条款未实现;§6.4 mcpp >= 2026.10.8.1 | | [SPEC-010](home-initialization.md) | home 选择、单跳入口、初始化、网络边界与单次运行的 `--home` | 草案 v0.2 | 2026-10-26 | mcpp >= 2026.10.19.1;网络能力逐条标注;§6 mcpp >= 2026.10.26.1、xlings >= 2026.10.26.1 | diff --git a/docs/specs/build-plugins.md b/docs/specs/build-plugins.md index d71cb18ab..7eac05674 100644 --- a/docs/specs/build-plugins.md +++ b/docs/specs/build-plugins.md @@ -4,10 +4,10 @@ |---|---| | 规范编号 | SPEC-007 | | 标题 | 构建插件:配置、施工与校验的分工,运行时与规划期的义务 | -| 状态 | 草案 v0.9 | -| 版本 | 0.11 | -| 最后修改 | 2026-10-28 | -| 对应实现 | 逐条标注。未注明版本的「已实现」条款对应 mcpp >= 2026.9.26.1;注明 mcpp#702 的条款对应 mcpp >= 2026.9.26.2;注明 mcpp#707、#708、#709、#711 的条款对应 mcpp >= 2026.9.27.1;注明 mcpp#723 的条款对应 mcpp >= 2026.9.28.1;注明 mcpp 2026.9.28.2 的条款对应该版本;§9 与注明 mcpp#734 的条款对应 mcpp >= 2026.9.28.3;注明 mcpp#778 的条款对应 mcpp >= 2026.10.10.1;§10 对应 mcpp >= 2026.10.11.1;R9.10 与协议 17 对应 mcpp >= 2026.10.27.1 | +| 状态 | 草案 v0.12 | +| 版本 | 0.12 | +| 最后修改 | 2026-11-05 | +| 对应实现 | 逐条标注。未注明版本的「已实现」条款对应 mcpp >= 2026.9.26.1;注明 mcpp#702 的条款对应 mcpp >= 2026.9.26.2;注明 mcpp#707、#708、#709、#711 的条款对应 mcpp >= 2026.9.27.1;注明 mcpp#723 的条款对应 mcpp >= 2026.9.28.1;注明 mcpp 2026.9.28.2 的条款对应该版本;§9 与注明 mcpp#734 的条款对应 mcpp >= 2026.9.28.3;注明 mcpp#778 的条款对应 mcpp >= 2026.10.10.1;§10 对应 mcpp >= 2026.10.11.1;R9.10 与协议 17 对应 mcpp >= 2026.10.27.1;§3.1 与协议 18 对应 mcpp >= 2026.11.5.1 | | 相关设计文档 | `.agents/docs/2026-09-26-compile-database-and-issue-699-design.md`(§5)、`.agents/docs/2026-09-28-ecosystem-design-and-optimisation-plan.md`(WS1、WS3)、`.agents/docs/2026-09-28-build-cost-foreign-toolsets-and-library-surface-design.md`(§3、§4)、`.agents/docs/2026-10-10-one-source-one-meaning-design.md`(§4)、`.agents/docs/2026-10-11-build-dependencies-one-pipeline-design.md` | | 相关 issue | mcpp#699、mcpp#701、mcpp#702、mcpp#703、mcpp#707、mcpp#708、mcpp#709、mcpp#711、mcpp#734、mcpp#755、mcpp#778、mcpp#797 | | 使用文档 | [docs/30 - build.mcpp](../30-build-mcpp.md)、[docs/31 - 编写规则包](../31-authoring-a-rule-package.md) | @@ -145,6 +145,16 @@ `graph.packages[].outDir` 取得它,**禁止**拼写其路径。构建程序的二进制与缓存不按配置分开。 `${mcpp.out_dir}`(构建目录)不变。(**已实现**,mcpp#778) +### 3.1 可选 action 进度(协议 18) + +以下条款对应 mcpp 2026.11.5.1+。 + +- **R3.11** `action.progress` 是默认 `false` 的布尔字段,适用于五种角色。类型化 API 在 `false` 时省略该 JSON 字段;解码旧载荷按 `false` 处理。 +- **R3.12** 引擎按运行分配私有目录,按图中 action 分配快照文件,以 `MCPP_ACTION_PROGRESS_FILE` 交给命令。关闭或没有报告通道时该变量缺失或为空,工作程序须继续正常执行。文件名、随机运行目录和快照内容不得作为图输入或构建身份。嵌套构建不得继承外层写入通道。 +- **R3.13** 快照为一个 UTF-8 JSON 对象:`v` 为 `1`,`seq` 为非负递增整数,`phase` 为非空字符串;`done`、`total` 为可选非负整数,`detail` 为可选字符串。两种计数同时存在时 `done <= total`。`phase` 最多 128 字节,`detail` 最多 1024 字节;两者不得含 ASCII 控制字符。action 身份由引擎通道确定,不从快照接受。 +- **R3.14** 引擎在 JSON 解析前最多读取 8193 字节,超过 8192 字节即忽略快照;非法、缺失、重复或倒退序号均不改变最新有效状态,不使构建失败。写入方使用单写者原子替换,每秒最多五次,保留最新状态,不追加无界日志。 +- **R3.15** 快照和内部完成计数不能声明 action 成功,不改变外层完成项。退出码、输出和 `prepare` 后置条件仍是成功依据。`object`、`source`、`artifact` 的真实输出不得作为 stamp 写入。运行通道在执行结束后清理。 + ## 4. 运行时:程序依赖的共享库的查找 - **R4.1** 一个目录中的共享库由施工产生(`prepare`)或文件名不定时,插件**必须**用 @@ -259,6 +269,7 @@ | 15 | 2026.10.1.3 | 来源(R6.5、R6.6、R9.9):`xpkg_source`、`xpkg_program`、`xpkg_request`、`xpkg_pending`、`phase`、`decision`、`toolchain` | | 16 | 2026.10.26.2 | 镜像入口:`link_start`(`mcpp:link-start=`,docs/04 `start`) | | 17 | 2026.10.27.1 | 平台的链接与运行器字段(SPEC-013 §12):`link_driver`、`link_input`、`link_input_from`、`artifact_name`、`seal`、`run_host`;平台阶段(`mcpp::phase()` 为 `"platform"`) | + | 18 | 2026.11.5.1 | `action.progress` 与可选运行期快照通道(§3.1);旧载荷默认关闭 | - **R9.3** 构建信息以事实陈述解析出的工具链,不针对任何外部构建系统:`tool(role)`(这一行的 工具)、`abi_tool(role)`(目标 ABI 的原生工具,MSVC ABI 上为工具集的 `cl`、`link`、`lib`、 @@ -339,3 +350,4 @@ | 0.9 | 2026-10-11 | 随 mcpp 2026.10.11.1(mcpp#797):新增 §10 构建程序的库与构建插件(R10.1–R10.6,P1–P5);变更记录移为 §11。 | | 0.10 | 2026-10-27 | 随 mcpp 2026.10.26.2(mcpp#845):R9.2 的协议表增加协议 16(`link_start`,镜像入口)。 | | 0.11 | 2026-10-28 | 随 mcpp 2026.10.27.1(mcpp#847):R9.2 的协议表增加协议 17(平台的链接与运行器字段);新增 R9.10 平台阶段(SPEC-013),平台阶段可声明包格式与 artifact action(SPEC-014 §6)。 | +| 0.12 | 2026-11-05 | 随 mcpp 2026.11.5.1:新增 §3.1 可选 action 进度(协议 18),规定通道、限额、序号、关闭与失败/取消行为。 | diff --git a/docs/zh/06-features-and-capabilities.md b/docs/zh/06-features-and-capabilities.md index f1acb1d54..67809d19d 100644 --- a/docs/zh/06-features-and-capabilities.md +++ b/docs/zh/06-features-and-capabilities.md @@ -169,6 +169,10 @@ plugins = { version = "0.3.0", features = ["rules-spirv"] } 解析器会从依赖图中绑定**恰好一个** provider。这样就能在多个可互换的后端 (OpenBLAS / MKL / …)之间选出一个,而不必把选择写死进库里。 + +2026.11.5.1 起,requirement 在每个所选消费包的普通依赖闭包内绑定。库的 capability 需求可以由应用的兄弟依赖供给。工作空间内的独立产物可以选择不同 provider;仅属于其他产物的 provider 不能补齐本产物缺失的需求。宿主工具构建边不进入目标上下文。工作空间根 pin、成员 pin、`--cap` 依次覆盖。短 provider 名只有唯一匹配候选时可用;同短名用 `namespace:name` 区分。exclusive 在同一消费者闭包内检查,包括所链接的共享库。pin 不从链接中删除其他 provider。 + + ```toml # A provider package satisfies a capability for any dependent that requires it. [package] @@ -275,18 +279,11 @@ provides = ["gpu-blas"] exclusive = ["gpu-blas"] ``` -两个都提供 `gpu-blas` 的包,只要其中至少一个声明了独占,就在绑定 capability +同一消费者闭包内两个都提供 `gpu-blas` 的包,只要其中至少一个声明了独占,就在绑定 capability 时被拒绝 —— 在任何东西被编译之前,并点名该能力与双方 provider: ``` -error: capability 'gpu-blas' is provided by more than one package, and they - declare it EXCLUSIVE. - providers: [compat.cublas, compat.rocblas] - exclusive: [compat.cublas, compat.rocblas] - Two implementations of one interface define the same symbols, so the - link would resolve every call to whichever archive it reached first. - Keep one of them — a `[capabilities]` pin selects a provider for a - REQUIREMENT and cannot make two definitions of one symbol safe. +error: capability 'gpu-blas' is EXCLUSIVE in consumer 'app'; providers: [compat.cublas, compat.rocblas]; exclusive: [compat.cublas, compat.rocblas]. A [capabilities] pin selects a provider for a requirement and cannot make two definitions of one symbol safe. ``` 这个拒绝在 `--format json` 里报 `exclusive-capability`(见第 11 章)。 diff --git a/docs/zh/24-openkal-cross.md b/docs/zh/24-openkal-cross.md index 31e953584..353683d76 100644 --- a/docs/zh/24-openkal-cross.md +++ b/docs/zh/24-openkal-cross.md @@ -93,7 +93,7 @@ SDK,或者区分 `linux`/`windows`/`macos`。那些是 C 环境层或平台层 ```toml [target.'cfg(any(linux, macos, windows))'] -platform = "openkal-platform@0.23.3" +platform = "openkal-platform@0.23.6" ``` 一张表(2026.10.27.1+)。它为宿主三元组选择 openkal 平台,平台的宿主部分命名编译器与目标侧三个层的包; @@ -361,6 +361,8 @@ mcpp run --target x86_64-openkal # okrun bin/app.oke 无需其他声明(2026.10.27.1+):目标表为两行选择 `openkal-platform`,由平台陈述工具链、各层、工具、下表的约定 与 runner([35](35-platforms.md))。 +mcpp 2026.11.5.1 的两个内置 openkal 目标默认选择平台 0.23.6。显式 `platform` 配置可覆盖默认选择;该平台的最终 ThinLTO 链接保持 emulated TLS 约定。 + | | 平台的陈述 | 原因 | |---|---|---| | 编译 | `--target=-unknown-none-elf -fPIC -femulated-tls -fvisibility=hidden -fasynchronous-unwind-tables -Xclang -fstack-clash-protection`;AArch64 另加 `-ffixed-x18 -mno-outline-atomics`;依赖图不提供 C 库时加 `-ffreestanding` | 映像按“无操作系统”编译;不访问线程指针,不使用两个系统保留的平台寄存器;只导出源码标注 `visibility("default")` 的名字 | diff --git a/docs/zh/30-build-mcpp.md b/docs/zh/30-build-mcpp.md index f72d17ebf..30084caea 100644 --- a/docs/zh/30-build-mcpp.md +++ b/docs/zh/30-build-mcpp.md @@ -693,6 +693,14 @@ a.arg("cp").arg("${mcpp.artifact:updater/updater}").arg(out.c_str()) 占位符不对应任何 `artifacts` 条目时,规划失败,错误中点名该占位符。 +#### 可选 action 进度(2026.11.5.1+,协议 18) + +在 build.mcpp 中设置 `a.progress = true`,即可为该 action 启用进度报告。默认值为 `false`,五种角色均支持。可以报告时,命令收到 `MCPP_ACTION_PROGRESS_FILE`;变量缺失或为空时按关闭处理。 + +工作程序以原子替换方式写入最新 JSON 快照,例如 `{"v":1,"seq":2,"phase":"compile","done":3,"total":9}`。不知道总量时可以只报告 `phase`。构建状态在 action 执行期间显示最新有效快照。计数描述 action 内部的工作,不计入外层 Ninja 完成项;成功仍由命令退出状态与产物检查决定。 + +运行通道为本次执行分配,不是声明的输入或输出。修改构建程序启用报告仍可能重新规划构建。现有 NDJSON 构建事件的含义保持不变。快照字段、限额和次序见 [SPEC-007 §3.1](../specs/build-plugins.md#31-可选-action-进度协议-18)。 + ### 部署程序生成的东西:`deploy`(2026.9.12.3+,protocol 11) `[runtime] deploy`(docs/04 §2.11)把包里已经存在的一个文件,放到相对可执行 diff --git a/docs/zh/42-heterogeneous-builds.md b/docs/zh/42-heterogeneous-builds.md index d8168a6e1..0998ce63a 100644 --- a/docs/zh/42-heterogeneous-builds.md +++ b/docs/zh/42-heterogeneous-builds.md @@ -404,6 +404,10 @@ xlings 安装的工具包才是构建实际会用的那个,通常也是更新 accel = "cuda12.8+{sm_80,sm_90f} ptx>=90" ``` +2026.11.5.1 起,构建请求按完整输入读取:集合未闭合、尾随文本和空目标条目均报错。一个实现只出现一次;多个目标合写为 `sycl+{nvptx64-sm_89,nvptx64-sm_90}`。实现和目标的排列次序、单个成员内重复的目标,不会分配不同构建变体。目标 token 归所属实现,由它的规则解释;选择 SYCL 的 NVIDIA 目标本身不会选择 CUDA 源码编译。源码约束使用相同的完整语法校验,包括 CPU-only 构建。 + + + 可用 `--accel` 为单次构建覆盖——这与 `--target` 对 `[toolchain]` 的关系相同。 `--no-accel` 不是「没有写 `--accel`」,它是**显式请求不要加速器**,这正是在 一个同时发布了设备构建的包里选中 CPU-only 变体所需要的。 diff --git a/modules/buildmcpp/src/directives.cppm b/modules/buildmcpp/src/directives.cppm index ad4df83e7..f5b7d481d 100644 --- a/modules/buildmcpp/src/directives.cppm +++ b/modules/buildmcpp/src/directives.cppm @@ -1281,6 +1281,7 @@ std::optional decode_action(std::string_view payloa arr("imports", a.imports); arr("targets", a.targets); a.blocking = j.value("blocking", false); + a.progress = j.value("progress", false); a.depfile = j.value("depfile", std::string{}); a.description = j.value("description", std::string{}); // Omitted by every role but Prepare, exactly as `depfile` is omitted diff --git a/modules/buildmcpp/src/program_protocol.cppm b/modules/buildmcpp/src/program_protocol.cppm index bf92a4ce6..e270099f5 100644 --- a/modules/buildmcpp/src/program_protocol.cppm +++ b/modules/buildmcpp/src/program_protocol.cppm @@ -126,7 +126,9 @@ export namespace mcpp::build::program_protocol { // `link-input`, `artifact-name`, `seal`, `run-host` -- and `mcpp::phase()` // answering "platform" for a platform's plugin. No directive of v16 changes // spelling. -inline constexpr int kProtocolVersion = 17; +// v18: optional action.progress; runtime snapshots carry no graph inputs. +// Old action payloads default to progress=false. +inline constexpr int kProtocolVersion = 18; // ── Cache-format epoch ───────────────────────────────────────────────────── // diff --git a/modules/manifest/src/types.cppm b/modules/manifest/src/types.cppm index f68903c14..219dded23 100644 --- a/modules/manifest/src/types.cppm +++ b/modules/manifest/src/types.cppm @@ -709,6 +709,7 @@ struct BuildAction { std::vector imports; // Check only: make compilation wait for this to pass. Off by default. bool blocking = false; + bool progress = false; // optional runtime telemetry // Prepare only: the directory the command populates (mcpp#702). Absolute // once `prepare_actions` has run, exactly as `outputs`/`depfile` are. // Empty for every other role. The post-condition it carries -- the diff --git a/modules/toolchain-model/src/triple.cppm b/modules/toolchain-model/src/triple.cppm index 147af92fb..25f1af073 100644 --- a/modules/toolchain-model/src/triple.cppm +++ b/modules/toolchain-model/src/triple.cppm @@ -906,9 +906,9 @@ inline constexpr TargetInfo kKnownTargets[] = { // `openkal-platform` (SPEC-013), pinned here. The two architectures the // loader relocates. { "x86_64-openkal", "preview", "ok", "llvm@23.1.3","", true, - "mcpplibs.openkal-platform@0.23.3" }, + "mcpplibs.openkal-platform@0.23.6" }, { "aarch64-openkal", "preview", "ok", "llvm@23.1.3","", true, - "mcpplibs.openkal-platform@0.23.3" }, + "mcpplibs.openkal-platform@0.23.6" }, }; inline std::span known_targets() { return kKnownTargets; } diff --git a/src/build/action_progress.cppm b/src/build/action_progress.cppm new file mode 100644 index 000000000..1f7bf9c64 --- /dev/null +++ b/src/build/action_progress.cppm @@ -0,0 +1,96 @@ +// Optional action telemetry. Snapshots describe work inside an action; the +// command's exit status and output postconditions remain authoritative. +export module mcpp.build.action_progress; +import std; +import mcpp.libs.json; + +export namespace mcpp::build::action_progress { +inline constexpr std::string_view kRootEnv = "MCPP_ACTION_PROGRESS_ROOT"; +inline constexpr std::string_view kFileEnv = "MCPP_ACTION_PROGRESS_FILE"; +inline constexpr std::size_t kMaxBytes = 8192; +struct Snapshot { + std::uint64_t seq = 0; + std::string phase; + std::optional done, total; + std::string detail; +}; +std::optional parse(std::string_view text); +std::optional read(const std::filesystem::path& file); +std::filesystem::path channel(const std::filesystem::path& root, std::string_view output); +std::filesystem::path make_directory(const std::filesystem::path& buildDir); +std::string describe(const Snapshot& snapshot); +} + +namespace mcpp::build::action_progress { +namespace { +bool safe_text(const std::string& value, std::size_t limit) { + return value.size() <= limit && std::ranges::none_of(value, [](unsigned char c) { + return c < 32 || c == 127; + }); +} +std::optional count(const nlohmann::json& object, const char* key) { + const auto it = object.find(key); + if (it == object.end() || !it->is_number_unsigned()) return std::nullopt; + return it->get(); +} +} +std::optional parse(std::string_view text) { + if (text.size() > kMaxBytes) return std::nullopt; + try { + const auto j = nlohmann::json::parse(text); + if (!j.is_object() || !j.contains("v") || j["v"] != 1) return std::nullopt; + auto seq = count(j, "seq"); + if (!seq || !j.contains("phase") || !j["phase"].is_string()) return std::nullopt; + Snapshot out{.seq = *seq, .phase = j["phase"].get()}; + if (out.phase.empty() || !safe_text(out.phase, 128)) return std::nullopt; + out.done = count(j, "done"); out.total = count(j, "total"); + if ((j.contains("done") && !out.done) || (j.contains("total") && !out.total)) + return std::nullopt; + if (out.done && out.total && *out.done > *out.total) return std::nullopt; + if (auto detail = j.find("detail"); detail != j.end()) { + if (!detail->is_string()) return std::nullopt; + out.detail = detail->get(); + if (!safe_text(out.detail, 1024)) return std::nullopt; + } + return out; + } catch (...) { return std::nullopt; } +} +std::optional read(const std::filesystem::path& file) { + // Bound the read before JSON parsing, including files which grow while + // opened. An invalid or transient snapshot cannot fail a build. + std::ifstream input(file, std::ios::binary); + std::array bytes{}; + input.read(bytes.data(), static_cast(bytes.size())); + const auto size = static_cast(input.gcount()); + if (size > kMaxBytes) return std::nullopt; + return parse(std::string_view(bytes.data(), size)); +} +std::filesystem::path channel(const std::filesystem::path& root, std::string_view output) { + // The identity comes from the graph, never from the telemetry document. + std::uint64_t hash = 14695981039346656037ULL; + for (unsigned char c : output) { hash ^= c; hash *= 1099511628211ULL; } + return root / std::format("{:016x}.json", hash); +} +std::filesystem::path make_directory(const std::filesystem::path& buildDir) { + // Allocated at execution time; neither this nonce nor snapshots are graph + // inputs. create_directory is the collision check, even across processes. + std::error_code ec; + for (unsigned attempt = 0; attempt < 16; ++attempt) { + auto candidate = buildDir / std::format(".mcpp-action-progress-{:x}-{:x}", + std::chrono::steady_clock::now().time_since_epoch().count(), attempt); + if (std::filesystem::create_directory(candidate, ec)) + return std::filesystem::absolute(candidate, ec); + ec.clear(); + } + return {}; +} +std::string describe(const Snapshot& snapshot) { + std::string text = snapshot.phase; + if (snapshot.done) { + text += std::format(" {}", *snapshot.done); + if (snapshot.total) text += std::format("/{}", *snapshot.total); + } + if (!snapshot.detail.empty()) text += " · " + snapshot.detail; + return text; +} +} diff --git a/src/build/hostprogram.cppm b/src/build/hostprogram.cppm index 392c29796..4fef5e6dc 100644 --- a/src/build/hostprogram.cppm +++ b/src/build/hostprogram.cppm @@ -289,6 +289,7 @@ struct action { const char* id = ""; const char* role = roles::source; // one of `roles::{source, check, object, artifact, prepare}` const char* description = ""; + bool progress = false; // optional runtime telemetry, all roles bool blocking = false; // check only: gate compilation on it // A Make-style dependency file the COMMAND writes as a side effect (gcc/ // clang `-MD -MF`, glslangValidator `--depfile`, glslc `-MD -MF`, slangc @@ -346,6 +347,7 @@ struct action { // on every action would perturb the cache for every build.mcpp that // has nothing to do with depfiles. The decoder's default (empty // string) is identical either way, so omission costs nothing on read. + if (progress) std::printf(",\"progress\":true"); if (depfile[0]) { std::printf(",\"depfile\":"); esc(depfile); } // Same omission rule as `depfile`, for the same reason: an action // that never calls `output_dir()` serialises to the same bytes it did diff --git a/src/build/ninja_backend.cppm b/src/build/ninja_backend.cppm index b763f9924..5b975d105 100644 --- a/src/build/ninja_backend.cppm +++ b/src/build/ninja_backend.cppm @@ -56,6 +56,7 @@ import mcpp.build.command_line; import mcpp.run; import mcpp.ui; import mcpp.log; +import mcpp.build.action_progress; import mcpp.build.progress; export namespace mcpp::build { @@ -3918,7 +3919,7 @@ std::string emit_ninja_string(const BuildPlan& plan, std::string* placements, // same graph is then assertable on every host. const bool crtOnPath = mcpp::toolchain::is_msvc_target(plan.toolchain) && !plan.toolchain.base.msvc.redistDir.empty(); - const bool named = !a.env.empty() || !a.cwd.empty() || crtOnPath; + const bool named = !a.env.empty() || !a.cwd.empty() || crtOnPath || a.progress; if (stamped || named) { // `mcpp_exe_path()`, not `self_exe_path()` directly: this file // already has one spelling of "where am I" and a second would be @@ -3936,6 +3937,8 @@ std::string emit_ninja_string(const BuildPlan& plan, std::string* placements, }; std::string wrapped = q(mcpp_exe_path().string()) + (named ? " __action" : " __action-stamp"); + if (a.progress) wrapped += " --progress " + q( + mcpp::build::progress::normalise(a.outputs.front())); for (auto const& e : a.env) wrapped += " --env " + q(e); if (!a.cwd.empty()) wrapped += " --cwd " + q(a.cwd); if (crtOnPath) @@ -3997,7 +4000,7 @@ std::string emit_ninja_string(const BuildPlan& plan, std::string* placements, append(std::format("build{} : mcpp_action_{}{}\n", outs, i, ins)); // A check or prepare action reports its start through the wrapper, // by its first output (design §6.4); the record gives it its label. - if (attribution && stamped) + if (attribution && (stamped || a.progress)) attribution->action(std::filesystem::path(a.outputs.front()).generic_string(), a.description.empty() ? a.id : a.description); append("\n"); @@ -4907,6 +4910,8 @@ std::expected NinjaBackend::build(const BuildPlan& plan // even into the file of a build that runs this one (design §6.4). auto preEnv = nenv; preEnv.emplace_back(std::string(mcpp::build::progress::kStartsEnv), ""); + preEnv.emplace_back(std::string(mcpp::build::action_progress::kRootEnv), ""); + preEnv.emplace_back(std::string(mcpp::build::action_progress::kFileEnv), ""); (void)mcpp::run::capture_exec_deadline(pre, preEnv, preDeadline, nullptr); } stage("ninja-staged-cache"); @@ -4960,6 +4965,8 @@ std::expected NinjaBackend::build(const BuildPlan& plan } else { auto scanEnv = nenv; scanEnv.emplace_back(std::string(mcpp::build::progress::kStartsEnv), ""); + scanEnv.emplace_back(std::string(mcpp::build::action_progress::kRootEnv), ""); + scanEnv.emplace_back(std::string(mcpp::build::action_progress::kFileEnv), ""); auto cap = mcpp::run::capture_exec_deadline( scan, scanEnv, scanDeadline, &buildTimedOut); if (cap.exit_code != 0 || buildTimedOut) { @@ -4987,6 +4994,8 @@ std::expected NinjaBackend::build(const BuildPlan& plan } else { auto quietEnv = nenv; quietEnv.emplace_back(std::string(mcpp::build::progress::kStartsEnv), ""); + quietEnv.emplace_back(std::string(mcpp::build::action_progress::kRootEnv), ""); + quietEnv.emplace_back(std::string(mcpp::build::action_progress::kFileEnv), ""); auto cap = mcpp::run::capture_exec_deadline( nargv, quietEnv, deadline, &buildTimedOut); out = std::move(cap.output); diff --git a/src/build/progress.cppm b/src/build/progress.cppm index 03e429f98..7d4658e5d 100644 --- a/src/build/progress.cppm +++ b/src/build/progress.cppm @@ -33,6 +33,8 @@ module; export module mcpp.build.progress; import std; +import mcpp.run; +import mcpp.build.action_progress; import mcpp.ui; import mcpp.ui.dots_screen; import mcpp.log; @@ -693,10 +695,13 @@ struct Running { std::string label; std::string stamp; long long since = 0; // ms since command start + std::optional snapshot; }; struct Build::Impl { std::filesystem::path dir; + std::filesystem::path progressDir; + int progressCleanup = -1; std::vector declared; std::optional record; bool recordTried = false; @@ -1025,6 +1030,18 @@ void read_starts(Report& r, Build::Impl& b, std::vector& out) { } } +// Read only active graph actions. Snapshot counts never complete an action; +// the ninja log removes it from running only after the command has finished. +void read_action_progress(Build::Impl& build) { + if (build.progressDir.empty()) return; + for (auto& action : build.running) { + auto snapshot = action_progress::read( + action_progress::channel(build.progressDir, action.stamp)); + if (snapshot && (!action.snapshot || snapshot->seq > action.snapshot->seq)) + action.snapshot = std::move(snapshot); + } +} + // The builds whose Build object is alive: the frame, the poll and the status // line read these. `Finished` reads every build of the command. std::vector> live_builds(Report& r) { @@ -1062,6 +1079,7 @@ std::string phase_status(Report& r, std::string_view cells) { oldest = a.since; current = std::format("{}: {} {}", b->record->packages[a.package].subject, a.label, mcpp::ui::format_clock(ms(now_ms() - a.since))); + if (a.snapshot) current += " · " + action_progress::describe(*a.snapshot); } } std::string counts; @@ -1170,6 +1188,7 @@ void poll() { for (auto const& b : live_builds(r)) { read_starts(r, *b, out); read_log(r, *b, out); + read_action_progress(*b); } } write_lines(out); @@ -1480,6 +1499,14 @@ void finished(std::string_view profile, std::string_view descriptor) { Build::Build(const std::filesystem::path& dir) : impl_(std::make_shared()) { impl_->dir = dir; + impl_->progressDir = action_progress::make_directory(dir); + if (!impl_->progressDir.empty()) { + const auto path = impl_->progressDir; + impl_->progressCleanup = mcpp::run::add_cleanup([path] { + std::error_code ec; + std::filesystem::remove_all(path, ec); + }); + } auto& r = report(); std::lock_guard lock(r.m); r.builds.push_back(impl_); @@ -1491,6 +1518,9 @@ Build::~Build() { auto& r = report(); std::lock_guard lock(r.m); impl_->closed = true; + if (impl_->progressCleanup >= 0) mcpp::run::remove_cleanup(impl_->progressCleanup); + std::error_code ec; + if (!impl_->progressDir.empty()) std::filesystem::remove_all(impl_->progressDir, ec); } void Build::declare(std::vector packages) { @@ -1521,7 +1551,9 @@ std::vector> Build::environment() const { // nested ninja's (see kStatusFormat). return {{"NINJA_STATUS", std::string(kStatusFormat)}, {"CLICOLOR_FORCE", "0"}, - {std::string(kStartsEnv), (impl_->dir / kStartsFile).string()}}; + {std::string(kStartsEnv), (impl_->dir / kStartsFile).string()}, + {std::string(action_progress::kRootEnv), impl_->progressDir.string()}, + {std::string(action_progress::kFileEnv), ""}}; } void Build::pass_begin(PassKind kind) { diff --git a/src/cli.cppm b/src/cli.cppm index 8e30f664e..63ace8541 100644 --- a/src/cli.cppm +++ b/src/cli.cppm @@ -39,6 +39,7 @@ import mcpp.runtime.search; import mcpp.run; import mcpplibs.os.posix; import mcpp.home.files; +import mcpp.build.action_progress; import mcpp.build.progress; // __action reports its start import mcpp.build.dirlock; // the build directory lock names this command import mcpp.build.execute; // the sweep after a build (design 2026-10-31 §8.3) @@ -1552,6 +1553,7 @@ int run(int argc, char** argv) { // (`mcpp::action::output_dir`). A flag in both spellings, so it cannot // be mistaken for a positional stamp path. std::string requireDir; + std::string progressOutput; std::string cwd; std::vector> env; std::vector stamps; @@ -1560,7 +1562,7 @@ int run(int argc, char** argv) { const std::string_view a = argv[i]; const bool takesValue = a == "--require-dir" || (named && (a == "--env" || a == "--cwd" || a == "--stamp" - || a == "--path-prepend")); + || a == "--path-prepend" || a == "--progress")); if (!takesValue) { if (named) { std::println(stderr, "error: __action: unknown option '{}'", a); @@ -1575,6 +1577,7 @@ int run(int argc, char** argv) { } const std::string v = argv[++i]; if (a == "--require-dir") requireDir = v; + else if (a == "--progress") progressOutput = v; else if (a == "--cwd") cwd = v; else if (a == "--stamp") stamps.push_back(v); else if (a == "--path-prepend") pathPrepend.push_back(v); @@ -1624,7 +1627,21 @@ int run(int argc, char** argv) { // design 2026-09-29, §6.4), by its first stamp as build.ninja spells // it: the step's first output. Nothing is written when the build that // runs it reads no progress. - if (!stamps.empty()) mcpp::build::progress::record_action_start(stamps.front()); + if (!progressOutput.empty()) + mcpp::build::progress::record_action_start(progressOutput); + else if (!stamps.empty()) mcpp::build::progress::record_action_start(stamps.front()); + namespace telemetry = mcpp::build::action_progress; + std::string progressFile; + if (!progressOutput.empty()) { + if (auto root = mcpp::invocation::get(telemetry::kRootEnv); root && !root->empty()) + progressFile = telemetry::channel(*root, progressOutput).string(); + } + // A command cannot inherit a parent's channel or allocate siblings. + std::erase_if(env, [](const auto& entry) { + return entry.first == telemetry::kFileEnv || entry.first == telemetry::kRootEnv; + }); + env.emplace_back(std::string(telemetry::kFileEnv), std::move(progressFile)); + env.emplace_back(std::string(telemetry::kRootEnv), ""); // The command's own ninja, if it runs one (a CMake or vcpkg build), // prints ninja's usual status and reports into no file of this // build. An action that declares either variable keeps its value. diff --git a/tests/e2e/1012_capabilities_bind_in_each_consumer_context.sh b/tests/e2e/1012_capabilities_bind_in_each_consumer_context.sh index d764a71ba..55f7525b7 100755 --- a/tests/e2e/1012_capabilities_bind_in_each_consumer_context.sh +++ b/tests/e2e/1012_capabilities_bind_in_each_consumer_context.sh @@ -55,12 +55,31 @@ echo 'int helper_anchor(void){return 0;}' > helper/src/anchor.c "$MCPP" run -p app-a > out.log 2>&1 || fail 'app-a did not run' "$MCPP" run -p app-b > out.log 2>&1 || fail 'app-b did not run' +# Test link units and a shared provider keep the same per-consumer boundary. +for suffix in a b; do + mkdir -p "app-$suffix/tests" + echo 'int main(){return 0;}' > "app-$suffix/tests/context.cpp" +done +sed 's/kind = "lib"/kind = "shared"/' provider-a/mcpp.toml > changed.toml +mv changed.toml provider-a/mcpp.toml +"$MCPP" test --workspace > out.log 2>&1 || fail 'test/shared contexts crossed independent products' +"$MCPP" run -p app-a > out.log 2>&1 || fail 'shared provider consumer did not run' + # A provider elsewhere in the workspace cannot satisfy a missing dependency. cp app-a/mcpp.toml app-a/saved.toml sed '/^provider-a =/d' app-a/saved.toml > app-a/mcpp.toml if "$MCPP" build --workspace > out.log 2>&1; then fail 'another product supplied a missing provider'; fi grep -q "no package provides capability 'probe-interface'" out.log || fail 'wrong missing-provider diagnosis' grep -q "consumer 'app-a'" out.log || fail 'missing consumer context' +# Host-only providers cannot satisfy the missing target-side requirement. +cat >> app-a/mcpp.toml <<'TOML' +[build-dependencies] +provider-a = { path = "../provider-a" } +TOML +echo 'int main(){return 0;}' > app-a/build.mcpp +if "$MCPP" build --workspace > out.log 2>&1; then fail 'a host-only provider satisfied a target requirement'; fi +grep -q "no package provides capability 'probe-interface'" out.log || fail 'wrong host/target scope diagnosis' +rm app-a/build.mcpp cp app-a/saved.toml app-a/mcpp.toml # A pin cannot remove the second implementation from one actual link closure. diff --git a/tests/e2e/1013_optional_action_progress_preserves_outputs.sh b/tests/e2e/1013_optional_action_progress_preserves_outputs.sh new file mode 100755 index 000000000..785d1ba7e --- /dev/null +++ b/tests/e2e/1013_optional_action_progress_preserves_outputs.sh @@ -0,0 +1,113 @@ +#!/usr/bin/env bash +# requires: gcc unix-shell python3 +set -e +export MCPP="${MCPP:-mcpp}" +work=$(mktemp -d) +trap 'rm -rf "$work"' EXIT +export MCPP_HOME="$work/home" +source "$(dirname "$0")/_inherit_toolchain.sh" +export PROGRESS_TEST_WORK="$work" +python3 - <<'PY' +from pathlib import Path +import json,os,pty,subprocess,sys,time,select,fcntl,termios,struct,signal +work=Path(os.environ['PROGRESS_TEST_WORK']);app=work/'app';(app/'src').mkdir(parents=True) +(app/'mcpp.toml').write_text('[package]\nname="action-progress"\nversion="0.1.0"\n') +(app/'src/main.cpp').write_text('extern "C" int answer();\nint main(){return answer()==42?0:1;}\n') +(app/'answer.c').write_text('int answer(void){return 42;}\n') +(app/'worker.py').write_text('''from pathlib import Path +import json,os,subprocess,sys,time +mode,output,compiler,source,proof=sys.argv[1:] +channel=os.environ.get('MCPP_ACTION_PROGRESS_FILE','') +Path(proof).write_text(channel) +Path(proof+".output").write_text(output) +if channel: + p=Path(channel) + for seq in range(3): + temporary=p.with_suffix('.tmp') + temporary.write_text(json.dumps(dict(v=1,seq=seq,phase='internal-'+mode,done=seq,total=2))) + os.replace(temporary,p) + time.sleep(.25) +if mode=='slow': time.sleep(30) +p=Path(output);p.parent.mkdir(parents=True,exist_ok=True) +if mode=='object': + subprocess.run([compiler,'-x','c','-c',source,'-o',output],check=True) +elif mode=='prepare': + p.mkdir(exist_ok=True);(p/'installed.txt').write_text('installed') +elif mode in ('source','artifact'): + p.write_text('generated') +elif mode=='failure': + sys.exit(7) +''') +def build_program(enabled=True,failure=False,slow=False): + return '''import std; +import mcpp; +int main(){ + for(const auto* role : {mcpp::roles::check,mcpp::roles::prepare,mcpp::roles::source,mcpp::roles::object,mcpp::roles::artifact}) { + const std::string name=role; + const auto output=std::string(mcpp::out_dir())+"/"+name+(name=="object"?".o":".out"); + const auto proof=std::string(mcpp::manifest_dir())+"/proof-"+name; + const auto source=std::string(mcpp::manifest_dir())+"/answer.c"; + const auto worker=std::string(mcpp::manifest_dir())+"/worker.py"; + const auto mode=SLOW && name=="check"?"slow":FAILURE && name=="check"?"failure":role; + mcpp::action a;a.id=role;a.role=role;a.description=role;a.progress=ENABLED; + a.arg("python3").arg(worker.c_str()).arg(mode); + const auto directory=std::string(mcpp::out_dir())+"/installed"; + a.arg(name=="prepare"?directory.c_str():output.c_str()).arg(mcpp::tool("cc")).arg(source.c_str()).arg(proof.c_str()); + a.input(worker.c_str()).input(source.c_str()).output(output.c_str()); + if(name=="prepare")a.output_dir(directory.c_str()); + a.submit(); + } +} +'''.replace('ENABLED','true' if enabled else 'false').replace('FAILURE','true' if failure else 'false').replace('SLOW','true' if slow else 'false') +# Human output through a real terminal exercises the UI's asynchronous poll, +# while ninja is still buffering the command's stdout. +(app/'build.mcpp').write_text(build_program()) +master,slave=pty.openpty();fcntl.ioctl(slave,termios.TIOCSWINSZ,struct.pack('HHHH',30,160,0,0)) +env=dict(os.environ,TERM='xterm',MCPP_PROGRESS='plain') +process=subprocess.Popen([env['MCPP'],'build'],cwd=app,stdout=slave,stderr=slave,env=env);os.close(slave) +data=bytearray();deadline=time.monotonic()+180 +while process.poll() is None or select.select([master],[],[],0)[0]: + if time.monotonic()>deadline:process.kill();raise AssertionError('progress build timed out') + if select.select([master],[],[],.1)[0]: + try:chunk=os.read(master,65536) + except OSError:break + if not chunk:break + data.extend(chunk) +os.close(master);assert process.wait()==0,data.decode(errors='replace') +text=data.decode(errors='replace');assert 'internal-' in text,text +channels=[(app/('proof-'+role)).read_text() for role in ('check','prepare','source','object','artifact')] +assert all(channels) and len(set(channels))==5,channels +assert all(not Path(channel).exists() for channel in channels),'execution channels survived the build' +subprocess.run([env['MCPP'],'run'],cwd=app,env=env,check=True,stdout=subprocess.PIPE,stderr=subprocess.STDOUT) +# Turning telemetry off leaves ordinary source/object outputs usable. +(app/'build.mcpp').write_text(build_program(enabled=False)) +r=subprocess.run([env['MCPP'],'build','--message-format','json'],cwd=app,env=env,capture_output=True,text=True) +assert r.returncode==0,r.stdout+r.stderr +rows=[json.loads(line) for line in r.stdout.splitlines() if line.strip()] +assert rows and rows[-1]['kind']=='mcpp.build',rows +assert all((app/('proof-'+role)).read_text()=='' for role in ('check','prepare','source','object','artifact')) +subprocess.run([env['MCPP'],'run'],cwd=app,env=env,check=True,stdout=subprocess.PIPE,stderr=subprocess.STDOUT) +# Internal 2/2 followed by failure cannot create a successful check stamp. +previous_check=Path((app/'proof-check.output').read_text()) +previous_stamp_time=previous_check.stat().st_mtime_ns +(app/'build.mcpp').write_text(build_program(failure=True)) +r=subprocess.run([env['MCPP'],'build'],cwd=app,env=env,capture_output=True,text=True) +assert r.returncode!=0,'the internal completion count hid exit 7' +failed_check=Path((app/'proof-check.output').read_text()) +assert not failed_check.exists() or (failed_check==previous_check and failed_check.stat().st_mtime_ns==previous_stamp_time),'failed check refreshed its success stamp' +# Cancellation ends the execution channel without refreshing successful work. +(app/'proof-check').unlink() +(app/'build.mcpp').write_text(build_program(slow=True)) +with (work/'cancel.log').open('w') as log: + process=subprocess.Popen([env['MCPP'],'build'],cwd=app,env=env,stdout=log,stderr=log,start_new_session=True) + deadline=time.monotonic()+40 + while not (app/'proof-check').exists(): + assert process.poll() is None,(work/'cancel.log').read_text() + assert time.monotonic() +import std; +import mcpp.build.action_progress; +namespace telemetry = mcpp::build::action_progress; + +TEST(ActionProgress, AcceptsUnknownTotalsAndExplicitCounts) { + const auto unknown = telemetry::parse(R"({"v":1,"seq":0,"phase":"configure","detail":"compiler checks"})"); + ASSERT_TRUE(unknown); + EXPECT_FALSE(unknown->total); + EXPECT_EQ(telemetry::describe(*unknown), "configure · compiler checks"); + const auto known = telemetry::parse(R"({"v":1,"seq":7,"phase":"compile","done":3,"total":9})"); + ASSERT_TRUE(known); + EXPECT_EQ(telemetry::describe(*known), "compile 3/9"); +} +TEST(ActionProgress, RejectsInvalidSnapshotsWithoutInferringSuccess) { + for (const auto text : { + R"({"v":2,"seq":1,"phase":"compile"})", + R"({"v":1,"seq":-1,"phase":"compile"})", + R"({"v":1,"seq":1,"phase":""})", + R"({"v":1,"seq":1,"phase":"compile","done":4,"total":3})", + R"({"v":1,"seq":1,"phase":"compile","done":1.5})", + R"({"v":1,"seq":1,"phase":"compile\nforged"})", + R"({"v":1,"seq":1,"phase":"compile","detail":"\u001b[31m"})", + R"({"v":1,"seq":1,"phase":"compile")"}) + EXPECT_FALSE(telemetry::parse(text)) << text; + EXPECT_FALSE(telemetry::parse(std::string(telemetry::kMaxBytes + 1, ' '))); + // Finishing internal work is data, not a successful command or a stamp. + EXPECT_TRUE(telemetry::parse(R"({"v":1,"seq":2,"phase":"link","done":1,"total":1})")); +} +TEST(ActionProgress, AllocatesIndependentRunDirectoriesAndBoundsReads) { + const auto base = std::filesystem::temp_directory_path(); + const auto first = telemetry::make_directory(base); + const auto second = telemetry::make_directory(base); + ASSERT_FALSE(first.empty()); ASSERT_FALSE(second.empty()); + EXPECT_NE(first, second); + const auto a = telemetry::channel(first, "obj/a.o"); + EXPECT_NE(a, telemetry::channel(first, "obj/b.o")); + EXPECT_NE(a, telemetry::channel(second, "obj/a.o")); + std::ofstream(a) << std::string(telemetry::kMaxBytes + 1, ' '); + EXPECT_FALSE(telemetry::read(a)); + std::ofstream(a) << R"({"v":1,"seq":0,"phase":"work"})"; + EXPECT_TRUE(telemetry::read(a)); + std::filesystem::remove_all(first); std::filesystem::remove_all(second); +} diff --git a/tests/unit/test_build_directives.cpp b/tests/unit/test_build_directives.cpp index df38f3fb2..2d3d72578 100644 --- a/tests/unit/test_build_directives.cpp +++ b/tests/unit/test_build_directives.cpp @@ -1062,13 +1062,13 @@ TEST(BuildDirectives, DeployRowIsProtocolElevenWithLinkGlobalScopeAndATag) { EXPECT_EQ(def->scope, dirs::Scope::LinkGlobal); EXPECT_EQ(def->sinceProtocol, 11); EXPECT_FALSE(def->tag.empty()); - EXPECT_EQ(dirs::kProtocolVersion, 17); + EXPECT_EQ(dirs::kProtocolVersion, 18); } -TEST(BuildDirectives, ProtocolSeventeenIsAcceptedAndEighteenIsNot) { - auto ok = parse("mcpp:protocol=17\n"); +TEST(BuildDirectives, ProtocolEighteenIsAcceptedAndNineteenIsNot) { + auto ok = parse("mcpp:protocol=18\n"); EXPECT_FALSE(dirs::protocol_error(ok).has_value()); - auto no = parse("mcpp:protocol=18\n"); + auto no = parse("mcpp:protocol=19\n"); EXPECT_TRUE(dirs::protocol_error(no).has_value()); } @@ -1297,3 +1297,12 @@ TEST(BuildDirectives, RuntimeSearchDirIsPersistedAndRoundTrips) { ASSERT_EQ(m.runtimeConfig.linkIntent.runtimeSearchDirs.size(), 1u); EXPECT_EQ(m.runtimeConfig.linkIntent.runtimeSearchDirs[0].string(), under_root("rtlib")); } + +TEST(BuildDirectives, OptionalActionProgressDefaultsOffAndRejectsWrongType) { + const std::string prefix=R"({"id":"work","role":"object","command":["worker"],"outputs":["result.o"])"; + const auto old=dirs::decode_action(prefix+"}"); + ASSERT_TRUE(old); EXPECT_FALSE(old->progress); + const auto opted=dirs::decode_action(prefix+R"(,"progress":true})"); + ASSERT_TRUE(opted); EXPECT_TRUE(opted->progress); + EXPECT_FALSE(dirs::decode_action(prefix+R"(,"progress":"true"})")); +} From 70a75fbe43fd3acb4132b2e52a89da95f07c2ff4 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Thu, 5 Nov 2026 08:21:53 +0900 Subject: [PATCH 07/11] fix: keep build-only layers out of the target ABI --- ...heterogeneous-build-implementation-plan.md | 36 +++++++++ docs/30-build-mcpp.md | 2 + docs/zh/30-build-mcpp.md | 2 + src/build/prepare/scan.cpp | 7 +- src/build/prepare/target_side.cpp | 8 +- ...ional_action_progress_preserves_outputs.sh | 33 +++++--- ...nly_layers_do_not_select_the_target_abi.sh | 78 +++++++++++++++++++ 7 files changed, 151 insertions(+), 15 deletions(-) create mode 100755 tests/e2e/1014_host_only_layers_do_not_select_the_target_abi.sh diff --git a/.agents/docs/2026-11-05-heterogeneous-build-implementation-plan.md b/.agents/docs/2026-11-05-heterogeneous-build-implementation-plan.md index 86afacae0..fc840406f 100644 --- a/.agents/docs/2026-11-05-heterogeneous-build-implementation-plan.md +++ b/.agents/docs/2026-11-05-heterogeneous-build-implementation-plan.md @@ -159,3 +159,39 @@ configure/build-install 阶段,不估计未掌握的编译任务总数。旧 中英文核心使用文档、SPEC-007 及其目录、平台默认示例已同步。结构与风格检查 通过,修改的文档已用 pandoc 渲染并进行第二遍阅读。以上是 CI 前的本地检查; 用户要求的最终生态 review 仍待最终提交 CI 通过后执行。 + +## 8. 集成边界检查点(2026-11-05) + +新版本 directives 单测 74/74 通过。进度 E2E 另行通过 verbose 与未修改 action +不重新执行的控制。 + +插件 writer 的集成定位到一个既有核心缺陷:build-only 库声明的 target-side +provider 仍参与最终产物的层选择。最小包只在 build.mcpp 中使用 +`provides=["mcpp:kernel-abi=openkal"]` 的库,目标源码却收到 `__OPENKAL__`, +复现见 `/tmp/mcpp-885-host-layer-probe/before.json`。核心现按 `buildTimeOnly` +过滤 target-side provider、requirement、条件 requirement、kernel interface +需求及 hosted 标准库资格;宿主子构建中的实际 target instance 仍照常选择层。 + +E2E 1014 的直接、传递与同包双角色控制通过。修正后二进制 SHA256 为 +`b440228697715797c80f9e297070ef4ea33f625a5e50f29be827861d298b54cd`。 +其进度正常/verbose/关闭/JSON/失败/取消/增量控制通过;默认公开平台的中性 +ThinLTO native run 和 aarch64 seal 复验通过,证据为 +`/tmp/mcpp-885-core11351-hostfix-default-platform-verified.json`。 + +插件 #52 的 CI 又检出 object storage 的既有增量缺陷:shader 更新后 `.S` 的 +`.incbin` 路径没有变化,`restat` 剪枝阻断实际 payload 到汇编对象的更新;header +与 sidecar 控制通过。修正责任在插件 surface body 生成器:在执行期为 assembly +记录内容校验值,保持接口半部稳定。该修正及等长内容与未变化时间戳控制仍在验证, +不靠撤销核心 `restat` 掩盖缺失的数据依赖。 + +SYCL SDK 归档控制确认:多架构直接对象链接通过,同对象组成的多架构 archive +仍报 `add_seven` 重定义;关闭自动 SPIR-V 或 code split 不改变结果。单架构 +archive 供给多架构 consumer 在样例中通过,但两种架构的 device IR 字节和 +`target-cpu` / `target-features` 实际不同,不能将单份 IR 复用提升为一般等价。 +`__CUDA_ARCH__` 控制在该 SYCL 前端未定义,所得 -1 不能作为架构专化的证据。 +因此保留插件/工具链阶段划分问题,不增加核心后端枚举或岛屿模式;尚无通用 +多架构静态库交付验收。原始记录位于 `/tmp/mcpp-885-sycl-archive-controls/`。 + +核心 CI 的权威入口是名为 `ci` 的工作流;同提交的 Windows CRT measurement +是另一工作流。`82fc9b3c` 对应 `ci` run `41930942590`,不是已通过的 +`41930942462` measurement。新增层隔离修正需要以更新后的提交重新完成 CI。 diff --git a/docs/30-build-mcpp.md b/docs/30-build-mcpp.md index 7ab9f58d1..f6768931a 100644 --- a/docs/30-build-mcpp.md +++ b/docs/30-build-mcpp.md @@ -1731,6 +1731,8 @@ of the cache key — an override is an escape hatch, not a reproducible input. ### Libraries of a build program: `[build-dependencies]` (mcpp 2026.10.11.1+) +Build-only libraries supply layers and requirements to the build-program sub-build. They do not select the final artifact’s ABI, including when reached transitively (2026.11.5.1+). A package also reached through an ordinary dependency retains its target instance. + A rule (say "run protoc over these `.proto` files") is written once, as an ordinary mcpp library package, and so is any library a build program needs — a TOML parser, a formatter. A build program uses a library its package names diff --git a/docs/zh/30-build-mcpp.md b/docs/zh/30-build-mcpp.md index 30084caea..e8dab3359 100644 --- a/docs/zh/30-build-mcpp.md +++ b/docs/zh/30-build-mcpp.md @@ -1453,6 +1453,8 @@ MCPP_TOOL_PROTOBUF_PROTOC=/usr/bin/protoc mcpp build ### 构建程序的库:`[build-dependencies]`(mcpp 2026.10.11.1+) +仅供构建使用的库向构建程序的子构建供给层和需求,不选择最终产物的 ABI;经传递依赖到达时同样如此(2026.11.5.1+)。同一包也被普通依赖引用时,仍保留其目标实例。 + 一条规则(例如"对这些 `.proto` 文件运行 protoc")只写一次,作为普通的 mcpp 库包发布; 构建程序需要的任何库(TOML 解析器、格式化库)也是如此。构建程序使用其包在 `[build-dependencies]` 中写出的库: diff --git a/src/build/prepare/scan.cpp b/src/build/prepare/scan.cpp index 6796cf397..28f04b5ea 100644 --- a/src/build/prepare/scan.cpp +++ b/src/build/prepare/scan.cpp @@ -829,8 +829,11 @@ step11_std_module_availability_gate(PrepareState& state, bool needsStdModule) { // A capability rather than a triple, because the fact is a property of // the graph and not of the target, and because dependency resolution is // the earliest time at which it is known. - const bool hostedStdProvided = - state.capProviders.find("hosted-standard-library") != state.capProviders.end(); + const auto hostedProviders = state.capProviders.find("hosted-standard-library"); + const bool hostedStdProvided = hostedProviders != state.capProviders.end() + && std::ranges::any_of(hostedProviders->second, [&](auto i) { + return !state.packages[i].buildTimeOnly; + }); if (auto ft = mcpp::toolchain::triple::parse(state.tc->targetTriple); ft && ft->is_freestanding() && !hostedStdProvided) { diff --git a/src/build/prepare/target_side.cpp b/src/build/prepare/target_side.cpp index 1863885ed..9373c6670 100644 --- a/src/build/prepare/target_side.cpp +++ b/src/build/prepare/target_side.cpp @@ -163,6 +163,10 @@ step9_gather_target_side_candidates(PrepareState& state) { : why.error())); } + // Build-program-only packages supply the host sub-build's layers. + // Their declarations must not select or constrain this artifact. + if (pkg.buildTimeOnly) continue; + for (auto const& entry : pkg.manifest.provides) { std::optional decl; if (auto parsed = tsd::parse_capability(entry); parsed && *parsed) @@ -925,6 +929,7 @@ step9_kernel_abi_interface_enumeration(PrepareState& state) { // costs nothing to a graph where the provider does declare. std::size_t uncheckedRequirements = 0; for (auto& pkg : state.packages) { + if (pkg.buildTimeOnly) continue; const auto& need = pkg.manifest.kernelAbiRequiresInterfaces; if (need.empty()) continue; if (providerId.empty()) { @@ -1402,7 +1407,8 @@ static std::expected step9_layer_conditional_config(PrepareSt } }; gather_from(*state.m); - for (auto const& pkg : state.packages) gather_from(pkg.manifest); + for (auto const& pkg : state.packages) + if (!pkg.buildTimeOnly) gather_from(pkg.manifest); if (auto why = tsd::check_requirements( state.resolvedTargetSide, layerRequirements, tc_origin_is_user_explicit(state.tcOrigin) ? tc_origin_name(state.tcOrigin) diff --git a/tests/e2e/1013_optional_action_progress_preserves_outputs.sh b/tests/e2e/1013_optional_action_progress_preserves_outputs.sh index 785d1ba7e..61de545fb 100755 --- a/tests/e2e/1013_optional_action_progress_preserves_outputs.sh +++ b/tests/e2e/1013_optional_action_progress_preserves_outputs.sh @@ -62,23 +62,32 @@ int main(){ # Human output through a real terminal exercises the UI's asynchronous poll, # while ninja is still buffering the command's stdout. (app/'build.mcpp').write_text(build_program()) -master,slave=pty.openpty();fcntl.ioctl(slave,termios.TIOCSWINSZ,struct.pack('HHHH',30,160,0,0)) env=dict(os.environ,TERM='xterm',MCPP_PROGRESS='plain') -process=subprocess.Popen([env['MCPP'],'build'],cwd=app,stdout=slave,stderr=slave,env=env);os.close(slave) -data=bytearray();deadline=time.monotonic()+180 -while process.poll() is None or select.select([master],[],[],0)[0]: - if time.monotonic()>deadline:process.kill();raise AssertionError('progress build timed out') - if select.select([master],[],[],.1)[0]: - try:chunk=os.read(master,65536) - except OSError:break - if not chunk:break - data.extend(chunk) -os.close(master);assert process.wait()==0,data.decode(errors='replace') -text=data.decode(errors='replace');assert 'internal-' in text,text +def terminal_build(extra=()): + master,slave=pty.openpty();fcntl.ioctl(slave,termios.TIOCSWINSZ,struct.pack('HHHH',30,160,0,0)) + process=subprocess.Popen([env['MCPP'],'build',*extra],cwd=app,stdout=slave,stderr=slave,env=env);os.close(slave) + data=bytearray();deadline=time.monotonic()+180 + while process.poll() is None or select.select([master],[],[],0)[0]: + if time.monotonic()>deadline:process.kill();raise AssertionError('progress build timed out') + if select.select([master],[],[],.1)[0]: + try:chunk=os.read(master,65536) + except OSError:break + if not chunk:break + data.extend(chunk) + os.close(master);assert process.wait()==0,data.decode(errors='replace') + return data.decode(errors='replace') +text=terminal_build() +assert 'internal-' in text,text channels=[(app/('proof-'+role)).read_text() for role in ('check','prepare','source','object','artifact')] assert all(channels) and len(set(channels))==5,channels assert all(not Path(channel).exists() for channel in channels),'execution channels survived the build' +proof_times={role:(app/('proof-'+role)).stat().st_mtime_ns for role in ('check','prepare','source','object','artifact')} subprocess.run([env['MCPP'],'run'],cwd=app,env=env,check=True,stdout=subprocess.PIPE,stderr=subprocess.STDOUT) +assert all((app/('proof-'+role)).stat().st_mtime_ns==stamp for role,stamp in proof_times.items()),'runtime channels dirtied unchanged actions' +# Verbose mode also polls the channel while the nested command is running. +(app/'worker.py').touch() +verbose_text=terminal_build(('--verbose',)) +assert 'internal-' in verbose_text,verbose_text # Turning telemetry off leaves ordinary source/object outputs usable. (app/'build.mcpp').write_text(build_program(enabled=False)) r=subprocess.run([env['MCPP'],'build','--message-format','json'],cwd=app,env=env,capture_output=True,text=True) diff --git a/tests/e2e/1014_host_only_layers_do_not_select_the_target_abi.sh b/tests/e2e/1014_host_only_layers_do_not_select_the_target_abi.sh new file mode 100755 index 000000000..c9488d0db --- /dev/null +++ b/tests/e2e/1014_host_only_layers_do_not_select_the_target_abi.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash +# requires: gcc unix-shell +# A library used by build.mcpp may supply a host layer. Its direct or +# transitive host-only instance cannot select the final artifact's ABI. +set -e +MCPP="${MCPP:-mcpp}" +work=$(mktemp -d) +trap 'rm -rf "$work"' EXIT +export MCPP_HOME="$work/home" +source "$(dirname "$0")/_inherit_toolchain.sh" +mkdir -p "$work/app/src" "$work/provider/src" "$work/adapter/src" +cd "$work" +cat > provider/mcpp.toml <<'TOML' +[package] +name = "host-provider" +version = "0.1.0" +provides = ["mcpp:kernel-abi=openkal"] +[targets.host-provider] +kind = "lib" +TOML +cat > provider/src/provider.cppm <<'CPP' +export module host.provider; +export int host_probe(){return 7;} +CPP +cat > adapter/mcpp.toml <<'TOML' +[package] +name = "host-adapter" +version = "0.1.0" +[dependencies] +host-provider = { path = "../provider" } +[targets.host-adapter] +kind = "lib" +TOML +cat > adapter/src/adapter.cppm <<'CPP' +export module host.adapter; +import host.provider; +export int adapter_probe(){return host_probe();} +CPP +cat > app/mcpp.toml <<'TOML' +[package] +name = "target-consumer" +version = "0.1.0" +[build-dependencies] +host-provider = { path = "../provider", host-module = true } +TOML +cat > app/build.mcpp <<'CPP' +import host.provider; +int main(){return host_probe()==7?0:1;} +CPP +cat > app/src/main.cpp <<'CPP' +#ifdef __OPENKAL__ +#error host_kernel_layer_leaked_to_target +#endif +int main(){return 0;} +CPP +cd app +"$MCPP" run > out.log 2>&1 || { cat out.log; echo 'FAIL: direct host layer leaked'; exit 1; } +sed 's/host-provider = { path = "..\/provider"/host-adapter = { path = "..\/adapter"/' mcpp.toml > changed.toml +mv changed.toml mcpp.toml +cat > build.mcpp <<'CPP' +import host.adapter; +int main(){return adapter_probe()==7?0:1;} +CPP +"$MCPP" run > out.log 2>&1 || { cat out.log; echo 'FAIL: transitive host layer leaked'; exit 1; } + +# The same package reached on both axes retains a genuine target instance. +cat >> mcpp.toml <<'TOML' +[dependencies] +host-provider = { path = "../provider" } +TOML +cat > src/main.cpp <<'CPP' +#ifndef __OPENKAL__ +#error target_kernel_layer_was_withdrawn +#endif +int main(){return 0;} +CPP +"$MCPP" run > out.log 2>&1 || { cat out.log; echo 'FAIL: target instance lost its layer'; exit 1; } +echo 'PASS: host-only layers do not select the target ABI' From 4e5fbfdc9c3b5863f9163dfb2598b39a2b31b761 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Thu, 5 Nov 2026 09:04:05 +0900 Subject: [PATCH 08/11] fix: preserve portable action progress paths and file sharing --- ...heterogeneous-build-implementation-plan.md | 38 ++++++++++++++ docs/30-build-mcpp.md | 2 +- docs/specs/build-plugins.md | 2 +- docs/zh/30-build-mcpp.md | 2 +- src/build/action_progress.cppm | 10 ++-- src/build/progress.cppm | 2 +- src/cli.cppm | 3 +- ...n_progress_paths_are_utf8_on_every_host.sh | 50 +++++++++++++++++++ 8 files changed, 98 insertions(+), 11 deletions(-) create mode 100755 tests/e2e/1015_action_progress_paths_are_utf8_on_every_host.sh diff --git a/.agents/docs/2026-11-05-heterogeneous-build-implementation-plan.md b/.agents/docs/2026-11-05-heterogeneous-build-implementation-plan.md index fc840406f..8373c7578 100644 --- a/.agents/docs/2026-11-05-heterogeneous-build-implementation-plan.md +++ b/.agents/docs/2026-11-05-heterogeneous-build-implementation-plan.md @@ -195,3 +195,41 @@ archive 供给多架构 consumer 在样例中通过,但两种架构的 device 核心 CI 的权威入口是名为 `ci` 的工作流;同提交的 Windows CRT measurement 是另一工作流。`82fc9b3c` 对应 `ci` run `41930942590`,不是已通过的 `41930942462` measurement。新增层隔离修正需要以更新后的提交重新完成 CI。 + +## 9. 可移植通道与插件集成检查点(2026-11-05) + +进度通道现在复用 os.fs 的有界共享读取,并在环境变量边界显式进行 UTF-8 路径 +转换。E2E 1015 在非 ASCII 工程路径中启动原生 Python worker,验证启用通道、 +有效快照和结束清理;不依赖 PTY 或 Unix 信号,可进入 Windows 与 macOS 的 +现有 E2E 矩阵。本地 Linux 实跑通过,不能替代尚待执行的其他宿主验收。 +新二进制 SHA256 为 +`ee2d7fc270d3cec32d699396c10fde19de507b439f6d13bb337d139a168e87f1`; +3 项快照单测和 E2E 1013 的正常、verbose、关闭、NDJSON、失败、取消与增量 +控制复验通过。中英文手册及 SPEC-007 同步明确路径编码,内部快照仍只更新 +状态显示,不新增 NDJSON 子任务事件。 + +插件 object storage 修正已完成验证:旧版 2026.11.3.1 与新核心均通过 50 项 +逻辑测试;真实 shader 的变长及等长内容修改均到达对象和最终二进制;未修改 +内容保留生成 assembly 的时间戳。证据为 +`/tmp/mcpp-885-object-incremental-verified.json` 与 +`/tmp/mcpp-885-object-equal-size/verified.json`。包含该修正的 #52 提交 +`3e9fc36` 的 CI `41934375976` 已通过。 + +独立 worker 的 `plugins-progress` feature 只包含标准库快照类型和可选 os.fs +writer,不再隐式引入只能由构建程序导入的 plugins-core。普通可执行 worker +在已发布 2026.11.3.1 下构建并运行,关闭通道和显式通道两种控制通过;新核心 +通过真实 action 启动 C++ worker、写出快照并运行最终应用,同时确认 host-only +OS 库未泄露目标 ABI。该修正和 CI fixture 已追加至同一 #52 的 `aa2c16e`, +其 CI `41937665962` 正在执行。 + +原始引擎工作已独立推进:platform 0.23.6 修正后的 engine `6129288` 在 +`41927099461` 的两种 Linux 映像、原生 Linux/Windows、Ubuntu 20.04 下限及 +四平台映像运行验收均通过,原生 macOS 仍在编译。server #75 的候选引用已在 +`4ab6263` 切换至该精确生产提交与 run,CI `41937027539` 正在重新接纳;旧的 +macOS 无 runner 取消记录不能作为新候选失败的证据。 + +Linux 原生与 bundle 产物已齐备,故先以不可变 artifact ID、SHA256、生产提交 +和 run 冻结输入,启动独立对照 `41938352961`,不等待原生 macOS。对照仍使用 +原来的工作量、计量方式和阈值,且不代替完整发布接纳或 ledger promotion。 +最终核心提交仍须完整 CI 后再执行用户要求的独立生态 review;核心版本保持 +2026.11.5.1,发布后再推进插件新版集成与原始 engine/server 发布链。 diff --git a/docs/30-build-mcpp.md b/docs/30-build-mcpp.md index f6768931a..99f2cfbf5 100644 --- a/docs/30-build-mcpp.md +++ b/docs/30-build-mcpp.md @@ -822,7 +822,7 @@ error. #### Optional action progress (2026.11.5.1+, protocol 18) -Set `a.progress = true` in build.mcpp to enable telemetry for that action. The default is `false`; all five roles support it. The command receives `MCPP_ACTION_PROGRESS_FILE` when reporting is available and treats an absent or empty value as disabled. +Set `a.progress = true` in build.mcpp to enable telemetry for that action. The default is `false`; all five roles support it. The command receives `MCPP_ACTION_PROGRESS_FILE` when reporting is available and treats an absent or empty value as disabled. The path is encoded as UTF-8 on every host, including Windows. The worker atomically replaces the file with its latest JSON snapshot, for example `{"v":1,"seq":2,"phase":"compile","done":3,"total":9}`. `phase` can be reported without a total. The build status displays the latest valid snapshot while the action runs. Counts describe work inside that action and do not contribute to the outer Ninja step count. The command's exit status and output checks determine success. diff --git a/docs/specs/build-plugins.md b/docs/specs/build-plugins.md index 7eac05674..8dde4ffd4 100644 --- a/docs/specs/build-plugins.md +++ b/docs/specs/build-plugins.md @@ -150,7 +150,7 @@ 以下条款对应 mcpp 2026.11.5.1+。 - **R3.11** `action.progress` 是默认 `false` 的布尔字段,适用于五种角色。类型化 API 在 `false` 时省略该 JSON 字段;解码旧载荷按 `false` 处理。 -- **R3.12** 引擎按运行分配私有目录,按图中 action 分配快照文件,以 `MCPP_ACTION_PROGRESS_FILE` 交给命令。关闭或没有报告通道时该变量缺失或为空,工作程序须继续正常执行。文件名、随机运行目录和快照内容不得作为图输入或构建身份。嵌套构建不得继承外层写入通道。 +- **R3.12** 引擎按运行分配私有目录,按图中 action 分配快照文件,以 `MCPP_ACTION_PROGRESS_FILE` 交给命令;路径在所有宿主上均以 UTF-8 编码。关闭或没有报告通道时该变量缺失或为空,工作程序须继续正常执行。文件名、随机运行目录和快照内容不得作为图输入或构建身份。嵌套构建不得继承外层写入通道。 - **R3.13** 快照为一个 UTF-8 JSON 对象:`v` 为 `1`,`seq` 为非负递增整数,`phase` 为非空字符串;`done`、`total` 为可选非负整数,`detail` 为可选字符串。两种计数同时存在时 `done <= total`。`phase` 最多 128 字节,`detail` 最多 1024 字节;两者不得含 ASCII 控制字符。action 身份由引擎通道确定,不从快照接受。 - **R3.14** 引擎在 JSON 解析前最多读取 8193 字节,超过 8192 字节即忽略快照;非法、缺失、重复或倒退序号均不改变最新有效状态,不使构建失败。写入方使用单写者原子替换,每秒最多五次,保留最新状态,不追加无界日志。 - **R3.15** 快照和内部完成计数不能声明 action 成功,不改变外层完成项。退出码、输出和 `prepare` 后置条件仍是成功依据。`object`、`source`、`artifact` 的真实输出不得作为 stamp 写入。运行通道在执行结束后清理。 diff --git a/docs/zh/30-build-mcpp.md b/docs/zh/30-build-mcpp.md index e8dab3359..52707fa39 100644 --- a/docs/zh/30-build-mcpp.md +++ b/docs/zh/30-build-mcpp.md @@ -695,7 +695,7 @@ a.arg("cp").arg("${mcpp.artifact:updater/updater}").arg(out.c_str()) #### 可选 action 进度(2026.11.5.1+,协议 18) -在 build.mcpp 中设置 `a.progress = true`,即可为该 action 启用进度报告。默认值为 `false`,五种角色均支持。可以报告时,命令收到 `MCPP_ACTION_PROGRESS_FILE`;变量缺失或为空时按关闭处理。 +在 build.mcpp 中设置 `a.progress = true`,即可为该 action 启用进度报告。默认值为 `false`,五种角色均支持。可以报告时,命令收到 `MCPP_ACTION_PROGRESS_FILE`;变量缺失或为空时按关闭处理。该路径在包括 Windows 在内的所有宿主上均以 UTF-8 编码。 工作程序以原子替换方式写入最新 JSON 快照,例如 `{"v":1,"seq":2,"phase":"compile","done":3,"total":9}`。不知道总量时可以只报告 `phase`。构建状态在 action 执行期间显示最新有效快照。计数描述 action 内部的工作,不计入外层 Ninja 完成项;成功仍由命令退出状态与产物检查决定。 diff --git a/src/build/action_progress.cppm b/src/build/action_progress.cppm index 1f7bf9c64..30a64d7e0 100644 --- a/src/build/action_progress.cppm +++ b/src/build/action_progress.cppm @@ -3,6 +3,7 @@ export module mcpp.build.action_progress; import std; import mcpp.libs.json; +import mcpplibs.os.fs; export namespace mcpp::build::action_progress { inline constexpr std::string_view kRootEnv = "MCPP_ACTION_PROGRESS_ROOT"; @@ -58,12 +59,9 @@ std::optional parse(std::string_view text) { std::optional read(const std::filesystem::path& file) { // Bound the read before JSON parsing, including files which grow while // opened. An invalid or transient snapshot cannot fail a build. - std::ifstream input(file, std::ios::binary); - std::array bytes{}; - input.read(bytes.data(), static_cast(bytes.size())); - const auto size = static_cast(input.gcount()); - if (size > kMaxBytes) return std::nullopt; - return parse(std::string_view(bytes.data(), size)); + const auto bytes = mcpplibs::os::fs::read(file, kMaxBytes); + if (!bytes) return std::nullopt; + return parse(*bytes); } std::filesystem::path channel(const std::filesystem::path& root, std::string_view output) { // The identity comes from the graph, never from the telemetry document. diff --git a/src/build/progress.cppm b/src/build/progress.cppm index 7d4658e5d..9e18040fa 100644 --- a/src/build/progress.cppm +++ b/src/build/progress.cppm @@ -1552,7 +1552,7 @@ std::vector> Build::environment() const { return {{"NINJA_STATUS", std::string(kStatusFormat)}, {"CLICOLOR_FORCE", "0"}, {std::string(kStartsEnv), (impl_->dir / kStartsFile).string()}, - {std::string(action_progress::kRootEnv), impl_->progressDir.string()}, + {std::string(action_progress::kRootEnv), mcpplibs::os::fs::utf8(impl_->progressDir)}, {std::string(action_progress::kFileEnv), ""}}; } diff --git a/src/cli.cppm b/src/cli.cppm index 63ace8541..157a50c59 100644 --- a/src/cli.cppm +++ b/src/cli.cppm @@ -1634,7 +1634,8 @@ int run(int argc, char** argv) { std::string progressFile; if (!progressOutput.empty()) { if (auto root = mcpp::invocation::get(telemetry::kRootEnv); root && !root->empty()) - progressFile = telemetry::channel(*root, progressOutput).string(); + progressFile = mcpplibs::os::fs::utf8(telemetry::channel( + mcpplibs::os::fs::from_utf8(*root), progressOutput)); } // A command cannot inherit a parent's channel or allocate siblings. std::erase_if(env, [](const auto& entry) { diff --git a/tests/e2e/1015_action_progress_paths_are_utf8_on_every_host.sh b/tests/e2e/1015_action_progress_paths_are_utf8_on_every_host.sh new file mode 100755 index 000000000..00814e6eb --- /dev/null +++ b/tests/e2e/1015_action_progress_paths_are_utf8_on_every_host.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +# requires: python3 +# Native worker/environment/file transport, including a non-ASCII project +# directory. This avoids pty and Unix signals so Windows runs it too. +set -e +MCPP="${MCPP:-mcpp}" +if command -v cygpath >/dev/null 2>&1; then + export MCPP_PROGRESS_TEST_EXE="$(cygpath -w "$(command -v "$MCPP")")" +else + export MCPP_PROGRESS_TEST_EXE="$(command -v "$MCPP")" +fi +python3 - <<'PY' +from pathlib import Path +import json,os,subprocess,sys,tempfile +with tempfile.TemporaryDirectory(prefix='mcpp-progress-') as temporary: + root=Path(temporary)/'项目-进度';(root/'src').mkdir(parents=True) + (root/'mcpp.toml').write_text('[package]\nname="portable-progress"\nversion="0.1.0"\n',encoding='utf-8') + (root/'src/main.cpp').write_text('int main(){return 0;}\n',encoding='utf-8') + worker=root/'worker.py';proof=root/'target/proof.json' + worker.write_text('''from pathlib import Path +import json,os,sys,time +file=os.environ.get('MCPP_ACTION_PROGRESS_FILE','') +assert file, 'the enabled action has no execution channel' +p=Path(file) +for seq in range(2): + state=dict(v=1,seq=seq,phase='compile',done=seq,total=1) + staged=p.with_suffix('.tmp');staged.write_text(json.dumps(state),encoding='utf-8') + os.replace(staged,p);time.sleep(.25) +Path(sys.argv[1]).write_text(json.dumps(dict(channel=file,snapshot=state)),encoding='utf-8') +''',encoding='utf-8') + quote=lambda value:json.dumps(str(value),ensure_ascii=False) + (root/'build.mcpp').write_text('''import std; +import mcpp; +int main(){ + mcpp::action action; + action.id="portable-worker";action.role=mcpp::roles::check;action.progress=true; + const auto stamp=std::string(mcpp::out_dir())+"/worker.stamp"; + action.arg(PYTHON).arg(WORKER).arg(PROOF).input(WORKER).output(stamp.c_str()); + action.submit(); +} +'''.replace('PYTHON',quote(sys.executable)).replace('WORKER',quote(worker)).replace('PROOF',quote(proof)),encoding='utf-8') + exe=os.environ['MCPP_PROGRESS_TEST_EXE'] + result=subprocess.run([exe,'build'],cwd=root,env=dict(os.environ,MCPP_PROGRESS='plain'),capture_output=True,text=True,encoding='utf-8',errors='replace',timeout=180) + assert result.returncode==0,result.stdout+result.stderr + result=json.loads(proof.read_text(encoding='utf-8')) + assert '项目-进度' in result['channel'],result + assert result['snapshot']==dict(v=1,seq=1,phase='compile',done=1,total=1),result + assert not Path(result['channel']).exists(),'execution channel was not cleaned' +print('PASS: native action progress paths are UTF-8 and cleaned') +PY From 2c0983ddbfc1bcc3e95cdae3ba47b0cf053f8342 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Thu, 5 Nov 2026 09:11:58 +0900 Subject: [PATCH 09/11] docs: locate SYCL archive target identity mixing in the SDK --- ...heterogeneous-build-implementation-plan.md | 13 +++++++++ ...ogeneous-build-research-and-experiments.md | 27 +++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/.agents/docs/2026-11-05-heterogeneous-build-implementation-plan.md b/.agents/docs/2026-11-05-heterogeneous-build-implementation-plan.md index 8373c7578..8c826e238 100644 --- a/.agents/docs/2026-11-05-heterogeneous-build-implementation-plan.md +++ b/.agents/docs/2026-11-05-heterogeneous-build-implementation-plan.md @@ -233,3 +233,16 @@ Linux 原生与 bundle 产物已齐备,故先以不可变 artifact ID、SHA256 原来的工作量、计量方式和阈值,且不代替完整发布接纳或 ledger promotion。 最终核心提交仍须完整 CI 后再执行用户要求的独立生态 review;核心版本保持 2026.11.5.1,发布后再推进插件新版集成与原始 engine/server 发布链。 + +## 10. 多目标 archive 的责任定位(2026-11-05) + +已进一步定位 §8 留下的 SYCL archive 失败。锁定 SDK 的独立 bundler 为 sm_89 +和 sm_90 分区解包时,各自都输出两种架构成员;与普通对象精确解出的设备 +bitcode 字节一致。驱动生成的五段 bundle ID 与 archive 匹配器的环境/target ID +划分不一致,结合相应提交源码可解释该混合。只补全 bundle ID 的环境位置、 +保留原有 payload 字节的合成控制恢复了每个分区一个正确成员。 + +完整命令、版本、输出成员和 SHA256 已保存,并追加至 +[实验 §5.3](2026-11-05-heterogeneous-build-research-and-experiments.md#53-实现期追加sdk-archive-的目标身份混合2026-11-05)。 +这确定了调查应落在 SDK 身份匹配与插件阶段,不构成生产 SDK 已修复或多目标 +静态库已交付的声明。当前 #52 不隐式删掉架构,也不向核心增加专属链接模式。 diff --git a/.agents/docs/2026-11-05-heterogeneous-build-research-and-experiments.md b/.agents/docs/2026-11-05-heterogeneous-build-research-and-experiments.md index d02a4ea27..e54575c70 100644 --- a/.agents/docs/2026-11-05-heterogeneous-build-research-and-experiments.md +++ b/.agents/docs/2026-11-05-heterogeneous-build-research-and-experiments.md @@ -260,6 +260,33 @@ SPIR64 镜像具有 SPIR-V 魔数。不能用四份镜像推断四种硬件目 规则不能只让单目标 fixture 通过。验收增加目标集合、TU 数量、归档边界的交叉控制; 可用组合实际检查镜像,不可用组合应给出明确工具链和阶段诊断,不能静默少生成目标。 +### 5.3 实现期追加:SDK archive 的目标身份混合(2026-11-05) + +锁定 DPC++ 7.1.0,编译器报告 intel/llvm 提交 +`880399f47228ac92f403e3b2b865cf1d6f0b1382`。直接调用其 bundler 的 archive +解包路径,分别请求 sm_89、sm_90;两个输出 archive 都包含两种架构的设备成员。 +每个成员的 SHA256 与先前从普通对象精确解出的对应 bitcode 一致,因此重复定义 +并非 mcpp 重复提交链接输入。`excluded-targets` 对照同样未隔离这两个成员。 + +对应版本的 [OffloadBundler.cpp](https://github.com/intel/llvm/blob/880399f47228ac92f403e3b2b865cf1d6f0b1382/clang/lib/Driver/OffloadBundler.cpp) +把五段 bundle ID 中的末段作为 triple 的环境,而仅从第六段取得独立 target ID。 +驱动实际产生 `sycl-nvptx64-nvidia-cuda-sm_89` 等五段名字。由代码与实测可推断: +此处丢失了架构的独立身份,archive 的兼容性匹配把两个设备变体混入每个分区; +普通对象精确解包不走同一匹配路径。 + +正向控制只为原有 host/device 字节重新封装名字,在环境位置显式加入 `unknown`, +形成 `sycl-nvptx64-nvidia-cuda-unknown-sm_89` 和对应 sm_90。相同 SDK 此时将 +archive 分成各含一个成员的两个输出,且设备字节与原始对应架构完全一致。 +这是合成身份控制,不是生产驱动修复、最终设备链接或 GPU 执行验收。 + +责任收敛为锁定 SDK 的 bundle 身份与 archive 匹配契约;插件仍负责选择受支持的 +阶段和准确诊断。保留两个真实架构,不以删除其中一个变体或复用单份 IR 规避。 +该组合的正式支持需要修正工具链并重跑最终设备链接及跨包控制,或验证显式的 +逐目标 archive 流程;通用核心无需新增厂商目标解析或专属链接模式。 +证据在 `/tmp/mcpp-885-sycl-archive-controls/` 的 `driver-plans/`、 +`archive-unbundle/verified.json`、`archive-unbundle/excluded-controls.json` +与 `normalized-id-control/verified.json`。 + ## 6. E5:组件边界与平台通用性 普通 C++ 组件放在 `component/`,主图源码列表只含 `src/main.cpp`。build.mcpp From 290a5591b238851ee835d710fd5e6ecb81c50854 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Thu, 5 Nov 2026 09:42:49 +0900 Subject: [PATCH 10/11] fix: bind selected static library contracts in their consumers --- ...heterogeneous-build-implementation-plan.md | 30 +++++++++++++++++++ docs/06-features-and-capabilities.md | 2 ++ docs/zh/06-features-and-capabilities.md | 2 ++ src/build/prepare/features.cpp | 22 +++++++++++++- ...abilities_bind_in_each_consumer_context.sh | 12 +++++++- 5 files changed, 66 insertions(+), 2 deletions(-) diff --git a/.agents/docs/2026-11-05-heterogeneous-build-implementation-plan.md b/.agents/docs/2026-11-05-heterogeneous-build-implementation-plan.md index 8c826e238..93b4ec61b 100644 --- a/.agents/docs/2026-11-05-heterogeneous-build-implementation-plan.md +++ b/.agents/docs/2026-11-05-heterogeneous-build-implementation-plan.md @@ -246,3 +246,33 @@ bitcode 字节一致。驱动生成的五段 bundle ID 与 archive 匹配器的 [实验 §5.3](2026-11-05-heterogeneous-build-research-and-experiments.md#53-实现期追加sdk-archive-的目标身份混合2026-11-05)。 这确定了调查应落在 SDK 身份匹配与插件阶段,不构成生产 SDK 已修复或多目标 静态库已交付的声明。当前 #52 不隐式删掉架构,也不向核心增加专属链接模式。 + +## 11. 所选静态库成员的上下文修正(2026-11-05) + +集成预检追加了常见布局:helper、provider 与 app 都列入 workspace members, +helper 声明 requirement,app 同时依赖两库。旧候选把已由 app 覆盖的 helper +又作为独立消费者检查,实际拒绝构建,见 +`/tmp/mcpp-885-cap-member-library-probe/before.log`。这与应用可供给库需求的 +契约冲突,前一用例只把应用列为成员,没有覆盖这一布局。 + +修正按现有目标类型和可达关系消除纯静态库/entry 成员的重复上下文;成员自身 +的可执行、共享库或测试目标仍独立检查。库被单独选择时仍检查其自己的闭包。 +不从其他应用借 provider,也不放宽同一产物的 exclusive 冲突。 + +新二进制 SHA256 为 +`c7717799809941e8f72613a7cc290fc89a342bb1b5e03ccc15f03733e858dc53`。 +最小布局实际构建通过;扩展 E2E 1012 将两应用、helper 与两 provider 全部列为 +成员,构建和运行通过,并验证 helper 单独选择和自有测试不能借应用的 provider。 +原共享库、测试、pin、独立产物、宿主边及限定身份控制均通过;既有 E2E 81、601 +复验通过。中英文手册同步说明该边界。这是 CI 前的集成修正,最终提交仍须完整 +CI 后再进行独立生态 review。 + +原引擎的精确 Linux 对照 `41938352961` 已收集并按未修改的脚本重算:两宿主 +共 16 个语义会话及就绪、热 CPU、缺页、路径对等控制通过,冷 CPU 分别为 +1.195(x64)和 1.153(ARM64),均高于 1.15。原始 ZIP、输入身份和失败保留; +不据此发布引擎或提升台账。精确当前产物的冷采样与 syscall 计数已完成,仍是 +定位证据,不是资格替代。采样中的独立 snapshot 后台进程另行标记,不能把其 +样本全部归入原有 readiness CPU。证据目录为 +`/tmp/mcpp-885-engine612-early-parity/`、 +`/tmp/mcpp-885-engine612-cold-perf/` 和 +`/tmp/mcpp-885-engine612-cold-syscalls/`。 diff --git a/docs/06-features-and-capabilities.md b/docs/06-features-and-capabilities.md index 66179c12b..51488a3c4 100644 --- a/docs/06-features-and-capabilities.md +++ b/docs/06-features-and-capabilities.md @@ -193,6 +193,8 @@ into the library. From 2026.11.5.1, requirements bind within each selected consumer's ordinary dependency closure. A library can require a capability supplied by a sibling dependency of its application. Independent workspace products can use different providers; a provider belonging only to another product cannot satisfy a missing requirement. Build-time tool edges are excluded from the target context. Workspace root pins, member pins and `--cap` apply in that order. A short provider name is accepted only when it identifies one candidate; `namespace:name` distinguishes equal short names. Exclusive providers are checked within that same consumer closure, including linked shared libraries. A pin does not remove another provider from the link. +A selected member containing only static-library or entry targets does not add a duplicate binding context when another selected consumer reaches it. Its own executable, shared-library or test targets retain an independent context. Selecting the library alone validates its own dependency closure. + ```toml # A provider package satisfies a capability for any dependent that requires it. diff --git a/docs/zh/06-features-and-capabilities.md b/docs/zh/06-features-and-capabilities.md index 67809d19d..21f384d23 100644 --- a/docs/zh/06-features-and-capabilities.md +++ b/docs/zh/06-features-and-capabilities.md @@ -172,6 +172,8 @@ plugins = { version = "0.3.0", features = ["rules-spirv"] } 2026.11.5.1 起,requirement 在每个所选消费包的普通依赖闭包内绑定。库的 capability 需求可以由应用的兄弟依赖供给。工作空间内的独立产物可以选择不同 provider;仅属于其他产物的 provider 不能补齐本产物缺失的需求。宿主工具构建边不进入目标上下文。工作空间根 pin、成员 pin、`--cap` 依次覆盖。短 provider 名只有唯一匹配候选时可用;同短名用 `namespace:name` 区分。exclusive 在同一消费者闭包内检查,包括所链接的共享库。pin 不从链接中删除其他 provider。 +所选成员只含静态库或 entry target,且已有另一个所选消费者到达它时,不额外产生重复的绑定上下文。库自身的可执行程序、共享库或测试 target 仍保留独立上下文。单独选择该库时,校验它自己的依赖闭包。 + ```toml # A provider package satisfies a capability for any dependent that requires it. diff --git a/src/build/prepare/features.cpp b/src/build/prepare/features.cpp index 5232c7942..3dc95c397 100644 --- a/src/build/prepare/features.cpp +++ b/src/build/prepare/features.cpp @@ -2082,7 +2082,27 @@ static std::vector capability_contexts(const PrepareState& st } contexts.push_back(std::move(context)); } - return contexts; + // A selected static library is also built when it is a workspace member, + // but archiving it does not create another final consumer. Its contract + // binds in the selected products that reach it. Keep a separate context + // for its own programs/shared libraries/tests, and for a standalone root. + std::vector independent; + for (std::size_t i = 0; i < contexts.size(); ++i) { + const auto& context = contexts[i]; + const bool links = std::ranges::any_of( + state.packages[context.root].manifest.targets, [](const auto& target) { + return target.kind != mcpp::manifest::Target::Library + && target.kind != mcpp::manifest::Target::Entry; + }); + const bool consumed = std::ranges::any_of(contexts, [&](const auto& other) { + return other.packages.contains(context.root) + && !context.packages.contains(other.root); + }); + if (links || !consumed) independent.push_back(i); + } + std::vector consumers; + for (auto i : independent) consumers.push_back(std::move(contexts[i])); + return consumers; } static std::vector capability_candidates( diff --git a/tests/e2e/1012_capabilities_bind_in_each_consumer_context.sh b/tests/e2e/1012_capabilities_bind_in_each_consumer_context.sh index 55f7525b7..c0fbdaebb 100755 --- a/tests/e2e/1012_capabilities_bind_in_each_consumer_context.sh +++ b/tests/e2e/1012_capabilities_bind_in_each_consumer_context.sh @@ -12,7 +12,7 @@ cd "$work" fail() { echo "FAIL: $*"; cat out.log; exit 1; } cat > mcpp.toml <<'TOML' [workspace] -members = ["app-a", "app-b"] +members = ["app-a", "app-b", "helper", "provider-a", "provider-b"] TOML for suffix in a b; do mkdir -p "provider-$suffix/src" "app-$suffix/src" @@ -55,6 +55,16 @@ echo 'int helper_anchor(void){return 0;}' > helper/src/anchor.c "$MCPP" run -p app-a > out.log 2>&1 || fail 'app-a did not run' "$MCPP" run -p app-b > out.log 2>&1 || fail 'app-b did not run' +# A standalone selection still needs its own binding. The enclosing +# applications do not supply an executable or test owned by the library. +if "$MCPP" build -p helper > out.log 2>&1; then fail 'standalone library borrowed an unselected consumer'; fi +grep -q "consumer 'helper'" out.log || fail 'standalone library lost its context' +mkdir -p helper/tests +echo 'int main(){return 0;}' > helper/tests/context.cpp +if "$MCPP" test --workspace > out.log 2>&1; then fail 'library test borrowed an application provider'; fi +grep -q "consumer 'helper'" out.log || fail 'library test lost its own context' +rm -rf helper/tests + # Test link units and a shared provider keep the same per-consumer boundary. for suffix in a b; do mkdir -p "app-$suffix/tests" From 850bbf53b5cf8a98dc8b6e3da894da77059cfbc4 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Thu, 5 Nov 2026 10:20:25 +0900 Subject: [PATCH 11/11] docs: prepare release notes and record cold-path qualification --- ...heterogeneous-build-implementation-plan.md | 34 +++++++++++++++++++ CHANGELOG.md | 33 ++++++++++++++++++ 2 files changed, 67 insertions(+) diff --git a/.agents/docs/2026-11-05-heterogeneous-build-implementation-plan.md b/.agents/docs/2026-11-05-heterogeneous-build-implementation-plan.md index 93b4ec61b..5373a95e7 100644 --- a/.agents/docs/2026-11-05-heterogeneous-build-implementation-plan.md +++ b/.agents/docs/2026-11-05-heterogeneous-build-implementation-plan.md @@ -276,3 +276,37 @@ CI 后再进行独立生态 review。 `/tmp/mcpp-885-engine612-early-parity/`、 `/tmp/mcpp-885-engine612-cold-perf/` 和 `/tmp/mcpp-885-engine612-cold-syscalls/`。 + +## 12. 冷阶段后续定位与发布准备(2026-11-05) + +引擎 `6129288` 的完整 CI `41927099461` 已结束:四平台原生构建、两个 Ubuntu +20.04 下限、两架构映像、bundle、四平台映像运行与另外三个 warm-prime 宿主通过。 +Linux x64 冷 CPU 为 14.66 / 12.35 = 1.187,高于 1.15;该项是唯一失败。 +此前冻结对照的负向结果继续保留,不以其他宿主通过替代它。 + +实际 syscall 计数中的约 68,000 次 `fstatfs` 定位到 musl realpath 的目录属性预查。 +规范已规定可选 resolved-name 操作在不支持时返回 `kal_err_not_supported`;保留弱 +入口、词法后备、错误与边界检查,直接调用操作即可。musl #84 准备 0.27.6,原生 +GCC/LLVM 的实际 helper 控制与候选 POSIX 工程通过。密封组件在同一输入上交替 +运行 14 次,CPU 中位比为 0.859,syscall 控制的 `fstatfs` 从 222 降到 0;符号链接 +重定向、词法后备与缺失路径语义保持。该数字只属于组件,不是 clangd 达标声明。 + +新测试遇到的 macOS 宿主头文件类型冲突已限定在测试中处理;Mach-O 上刻意缺失 +弱符号的控制明确允许该单一未绑定名字,并实际运行检查空入口。生产 ELF 映像 +的弱入口规则不变。该控制现已在 macOS CI 通过,完整包矩阵仍待结束。 + +runtime #65 准备 0.18.7,通过已有同名分支图检验两种分配器都选择 musl 0.27.6; +platform 0.23.7 的一致版本组已在本地准备,待两个前置公开后执行完整平台验收。 +这些工作不要求核心 #888 的默认平台提前依赖未发布版本;核心仍选公开 0.23.6。 + +服务器 #75 的 Windows dev 归档已实际通过 43 项服务器与 10 项 devtools 测试, +随后清理退出程序时遇到 sharing violation 32。现只对共享/锁冲突作最长 10 秒的 +重试,其余错误立即失败;本地瞬时冲突、非共享错误与超时控制通过。同一 PR 的 +最终 CI 已重新启动,候选接纳与性能门槛未放宽。 + +发布准备补齐 CHANGELOG 的 2026.11.5.1 条目;Release 按该版本标题提取说明, +缺失条目会拒绝发布。包含本次说明的最终提交仍须完成核心 CI,随后再独立进行 +生态 review。新增证据已逐文件校验并验证归档往返: +`/home/speak/workspace/mcpp-885-realpath-and-final-core-diagnostic-evidence.tar.gz`, +152 个文件,SHA256 为 +`4d7b783c27bb25dc75c354af72e98dc4b47037ab75043f792a28dceeeb10083a`。 diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c62819b4..29813fe1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,39 @@ > Each `## []` section is that release's notes. Entries are written in English > from 2026.9.28.3 on; earlier entries remain as written. +## [2026.11.5.1] - 2026-11-05 + +### Added + +- Build actions can opt into internal stage reporting with `action.progress` + (protocol 18). Each execution supplies a separate UTF-8 snapshot path; + bounded, ordered snapshots update the build status. Exit codes and output + checks still determine success, and completion or cancellation cleans the + channel. Existing actions leave reporting disabled. + +### Changed + +- Both built-in openkal targets select platform 0.23.6. Its final ThinLTO link + retains emulated TLS; the group includes musl 0.27.5, runtime 0.18.6 and + loader/tools 0.4.4. Explicit platform selections continue to override the + built-in default. + +### Fixed + +- Accelerator requests and constrained source declarations are validated in + full. Unclosed sets, trailing text and repeated implementation members are + refused; target and implementation ordering share one canonical identity. + Published legacy artifact matching remains compatible. +- Capability requirements and exclusive providers are checked within each + consumer's dependency closure. Independent workspace products can use + different providers, and applications can supply library requirements. + Selected static libraries do not add duplicate consumer contexts; their own + programs, shared libraries, tests and standalone selections retain checks. + Provider pins accept qualified identities and preserve all link inputs. +- Build-only providers and requirements no longer select the final artifact's + target ABI. Transitive build dependencies remain in the host graph, while + packages also used as ordinary dependencies retain their target instances. + ## [2026.11.3.1] - 2026-11-03 The built-in openkal platform is 0.23.3 (openkal 0.23: an image at a base of