diff --git a/.agents/docs/2026-09-04-commercial-grade-baremetal-embedded-plan.md b/.agents/docs/2026-09-04-commercial-grade-baremetal-embedded-plan.md index 95cf9a0d..b79cafe7 100644 --- a/.agents/docs/2026-09-04-commercial-grade-baremetal-embedded-plan.md +++ b/.agents/docs/2026-09-04-commercial-grade-baremetal-embedded-plan.md @@ -1,6 +1,6 @@ # 商业级可用:mcpp × xlings 的裸机与嵌入式总体方案 -2026-09-04 · 多仓库总体方案 · **v4:P0 引擎切片已实施**(PR #550);其余批次待实施 +2026-09-04 · 多仓库总体方案 · **v5:P0 引擎切片 + B/C/D/E 四轴已实施**(PR #550、#551) 前置讨论: [`2026-08-21-baremetal-ecosystem-assessment.md`](2026-08-21-baremetal-ecosystem-assessment.md)(七角度评估) · @@ -471,3 +471,42 @@ clang 对一次 float 乘法仍发出 `vmul.f32` —— 在没有 FPU 的 Cortex ⚠️ **软浮点行在没有 builtins 时链接不了浮点代码**(实测:`undefined symbol: __aeabi_fmul`),这正是 §3.1.1 把 `compiler-rt-builtins` 与 C 库并列为 P0 的理由。 整数程序不受影响 —— e2e 332 的四行启动用例即为整数程序。 + +--- + +## 11. 第二轮实施(2026-09-04,PR #551) + +### 11.1 六轴读数的变化 + +| 轴 | v4 | 现在 | +|---|---|---| +| **A 覆盖** | 🟡 引擎能编七行 | 🟡 不变(C 库源码包与板级包仍未做) | +| **B 可信** | ❌ 零真机 | 🟢 **模拟器与真机成为同一个包的两个 feature**;真机路径可声明、可解析、判据齐备,尚无实机运行记录 | +| **C 闭环** | ❌ 三个槽都没有 | ✅ `flash`/`monitor`/`debug` + `runner-exclusive`,四槽一读点 | +| **D 可复现** | ❌ `--locked` 不存在 | ✅ `--locked`/`--frozen` 断言并点名漂移;关掉快路径以免空转 | +| **E 可交付** | ❌ 全空白 | 🟢 `mcpp sbom`(CycloneDX 1.5)+ `docs/19` 支持窗口;许可闭包门与离线快照仍未做 | +| **F 可扩展** | ✅ | ✅ 未受损:新板 = 新包,引擎 diff 为零 | + +### 11.2 ⭐⭐ 方案 §2.2 的判断被实施证实,§2.3 的被加强 + +* **两值语义是对的。** `debug` 起服务端、客户端归 IDE 这条边界成立,`debug` 与 + `monitor` 在实现里逐字段同形,没有出现方案担心的「会话协议」。 +* **`runner-exclusive` 比方案写的更必要。** 方案说它是「第一块真板挖出的一列」; + 实施时发现它还必须**只紧不松** —— 图里任何一个包知道设备是互斥的,它就是互斥 + 的,后来的包保持沉默不得放松它。 + +### 11.3 ⚠️ 实施挖出的、方案没有的两条 + +1. **一条规则的第二份拷贝。** 依赖提供的 RunGlobal 条目抵达根工程走的是与 + `apply()` **不同**的路径(`prepare.cppm` 的 BFS 之后)。只接了前者时, + `mcpp flash` 报「没有配置」而 `mcpp run` 找得到同一个构建程序发出的 runner。 + 两处现在都遍历槽表。 +2. **快路径会让新槽与 `--locked` 双双空转。** `try_fast_run` 直接 exec 缓存产物, + 于是 `mcpp flash` 打印 `Running target/…/bin/p`;`try_fast_build` 跳过解析, + 于是被改坏的锁通过了 `--locked`。两处都按**性质**设闸(槽是不是 run、是不是 + 要求断言),不是按旗标。 + +### 11.4 仍未做 + +`mcpplibs/picolibc` + `compiler-rt-builtins` 源码包 · 三个板级包 · `xim:probe-rs` · +真机 CI · 许可闭包门(`--deny-license`)· 离线整仓快照 · openarch 第四后端(P3)。 diff --git a/.agents/docs/2026-09-04-named-runners-and-the-universal-command-surface.md b/.agents/docs/2026-09-04-named-runners-and-the-universal-command-surface.md new file mode 100644 index 00000000..3bc82b43 --- /dev/null +++ b/.agents/docs/2026-09-04-named-runners-and-the-universal-command-surface.md @@ -0,0 +1,650 @@ +# 具名 runner、通用命令面、部分后端,与生态闭环 + +2026-09-04 · 生态级方案 **v8:四条已定,批次已排**(引擎与 openarch 已实施;§11 是实施回填)· 取代 +`2026-09-04-commercial-grade-…-plan.md` §2 的槽表设计 + +前置:[`2026-09-04-commercial-grade-baremetal-embedded-plan.md`](2026-09-04-commercial-grade-baremetal-embedded-plan.md) +· PR #550(Cortex-M 七行,已合)· PR #551(旧设计,**按本文重做**) + +--- + +## 0. 三条约束,和它们推翻的四版设计 + +> **① 一级命令必须所有场景都用得到。** 不通用的用 `--xxx`。 +> **② 默认行为覆盖 80%,同时提供可复杂自定义的选项。** +> **③ 核心只放通用框架;其余走配置驱动与插件化。** + +| 版本 | 形态 | 被哪条推翻 | +|---|---|---| +| v1 | `mcpp flash`/`monitor`/`debug` 三个一级命令 | ① 非固件工程里是死命令;嵌入式词汇写进引擎 | +| v2 | `mcpp ` 动态分派 | ① 更糟:一级命令面**因工程而异** | +| v3 | `mcpp run --runner flash` | ② **把 80% 的场景做成了 20% 的写法** | +| v4 | + `hardware` feature 重定义**默认** runner | (方向对,但生态零件不全) | +| **v5** | + 生态五零件 + openarch 能力拆分 + **板级包不再拼路径** | —— | + +--- + +## 1. 全局 review:五条发现 + +本节是本轮最有价值的部分 —— 前四条都是**已有的东西没被用上**,而不是缺东西。 + +### 1.1 ⭐⭐⭐ 板级包在做引擎已经替它们做了的事 + +`mcpp.build.runner_lookup`(#544,2026-09-02)的模块注释原文: + +> *"That is what lets a runner name a program the project declared under +> `[xlings] deps` **without writing the payload's home-and-version path into the +> manifest**"* + +它按顺序搜索:**已声明 `[xlings] deps` 的载荷 `bin/` → PATH**,并且在哪儿都找不到 +时给出点名搜索过哪些路径的错误。 + +而两个现有板级包**都还在用 #544 之前的写法**: + +```cpp +// riscv-virt-rt / aarch64-virt-rt 今天 +if (const char* qemu = mcpp::xpkg_dir("xim", "qemu-arm"); qemu && *qemu) { + mcpp::runner(std::format("{}/bin/qemu-system-aarch64", qemu).c_str()); + … +} else { + mcpp::warning("qemu-arm is not installed, so `mcpp run` has no runner…"); +} +``` + +⇒ **正确写法是一行,并且更健壮:** + +```cpp +mcpp::runner("qemu-system-aarch64"); // 引擎去找;找不到时它自己会说清楚 +``` + +⚠️⚠️ **连带结论:`mcpp::warning()` 兜底在这个用例上不再需要。** 那条 advisory +通道是我在 2026-08-21 的评估里列为「最高优先级引擎缺口」并在 2026.8.21.2 落地的, +用来补救「`xpkg_dir` 返回空 ⇒ 静默不配 runner ⇒ `mcpp run` 报一句在此处不对的 +建议」。**#544 用另一条路解决了同一个问题,而没有人把两者连起来。** +(`warning` 本身仍有别的正当用途,不撤。) + +⭐ 这条对本轮的直接影响:`cortex-m-rt` 的 `build.mcpp` 会明显更短,且**没有那个 +「声明≠安装」的失败模式**。两个既有板级包应当同步简化。 + +### 1.2 ⭐⭐ 烧录就是运行(80/20) + +真板上「跑起来」= 烧进去 + 复位 + 接输出 + 取回退出码,`probe-rs run` 一条命令 +就是这四件事。⇒ **`hardware` feature 重定义的是默认 runner**,不是新增具名的。 + +```bash +mcpp run # 模拟器上跑 ← 默认 feature +mcpp run # 真板上跑 ← hardware feature,命令一个字不改 +``` + +具名 runner 只服务 20% 的例外:只烧不跑、看串口、调试服务端、擦片。 + +### 1.3 ⭐⭐ openarch 的「规范决定」不需要,机制已在 + +Cortex-M 没有 MMU、没有页表项,只满足可行性闸(上下文切换 + 页表项)的一半。 +我原以为要先做一个规范决定。**实测:openarch 早就用能力绑定后端** +(`provides = ["openarch-backend"]` / `requires = […]`)⇒ **把能力拆细即可**: + +```toml +有 MMU 的后端: provides = ["openarch-backend", "openarch:address-space", "openarch:percpu-register"] +Cortex-M: provides = ["openarch-backend"] +``` + +需要地址空间的内核在**解析期**得到点名的话,而不是链接期一堆 +`undefined reference to arch_pte_*`。**加法,不破坏既有消费者。** + +### 1.4 ⭐ 同一个机制,现在用在三处 + +| 用处 | 声明者 | 引擎知道 | +|---|---|---| +| 目标侧五层(`docs/14`) | `provides = ["mcpp:c-abi=musl"]` | 层名,不知实现 | +| openarch 后端 | `provides = ["openarch-backend", …]` | 有后端这件事 | +| **具名 runner** | `mcpp:runner-named=flash:…` | 有具名 runner 这件事,**不知名字** | + +**三处共用一个机制,不是三个机制。** 这正是「核心只放通用框架」。 + +⚠️ **而 §12 的工具分档不是第四处** —— 它对齐的是 mcpp 已有的**依赖种类**词汇 +(`dependencies` / `build-dependencies` / `dev-dependencies`),不是能力机制。复用既有 +词汇同样是好事,但把四条并列成「同一条纪律」是修辞上的合并,不准确。**两个既有机制, +各用其所。** + +### 1.5 ⚠️ 唯一的新风险:首次构建的墙钟 + +C 库改为**源码包**后,干净机器上第一次 `mcpp run` 要编一遍 picolibc。全局依赖缓存 +(`docs/05 §2.10`,跨工程)使它是**每台机器每个目标档一次**,但第一次仍是第一次。 + +⚠️ **这与「默认覆盖 80%」直接冲突,必须实测。** 判据写在 §9;若 >60s,就要给 +`mcpp new` 的模板加一句「首次构建会编译 C 库,约 N 秒」的状态行,而不是让人干等。 + +--- + +## 2. 唯一的概念:runner 有名字,默认的那个没有 + +``` +包(build.mcpp) mcpp::runner("qemu-system-arm") 默认 —— 覆盖 80%,写裸名 + mcpp::runner("flash", tok) 例外 —— 覆盖 20% + mcpp::runner_longlived("monitor") + mcpp::run_exclusive() ← 由 runner_exclusive 改名 + +线协议 mcpp:runner= + mcpp:runner-named=: + mcpp:runner-longlived= + mcpp:run-exclusive=1 + +工程 [target.X] runner = [...] + [target.X.runners] flash = [...] + +用户 mcpp run / mcpp run --runner flash / mcpp run --list-runners +``` + +⚠️ **`longLived` 是声明的,不是从名字推的**:`openocd -c "program … exit"` 会终止、 +`openocd -c "init"` 不会,拼写到最后一个参数为止都一样。从名字推只对引擎认识的名字 +有效,而引擎不认识任何名字。 + +⭐ **`run-exclusive` 是改过的名字**(原 `runner-exclusive`)。它说的是「这个目标的 +运行不能重叠」—— 对一块板、一个探针、一张 GPU、一个 license 受限的工具同样成立, +而 "device" 把它读窄了。 + +--- + +## 3. 一级命令面:全部通用,本轮零新增 + +``` +new build run test clean add remove update search +publish pack emit toolchain cache index self +``` + +| 能力 | 归属 | +|---|---| +| 抵达产物的例外方式 | `mcpp run --runner ` | +| 物料清单 | `mcpp emit sbom`(`emit` 已是「生成描述本工程的文档」) | +| 可复现断言 | `--locked` / `--frozen`(与 `--offline` 同一条侧信道) | + +--- + +## 4. 生态闭环:五个零件 + +判据是一句话:**干净机器上,`mcpp new blinky --template cortex-m-rt && mcpp run` +打印出东西。** 今天缺三件。 + +| # | 零件 | 仓 | 状态 | 缺了会怎样 | +|---|---|---|---|---| +| 1 | 编译器(llvm 载荷) | 目标表 | ✅ | —— | +| 2 | 模拟器 `xim:qemu-arm` | xim-pkgindex | ✅ 已发布**零消费者** | —— | +| 3 | **C 库 `mcpplibs/picolibc`(源码包)** | mcpp-index | ❌ | `'stdio.h' not found` | +| 4 | **`mcpplibs/compiler-rt-builtins`(源码包)** | mcpp-index | ❌ | 软浮点行 `undefined __aeabi_fmul` | +| 5 | **板级 `mcpplibs/cortex-m-rt` + 模板** | 新建仓 | ❌ | 用户自己写链接脚本与向量表 | +| 6 | 真机工具 `xim:probe-rs` | xim-pkgindex | ❌ | `hardware` feature 无从落地 | + +### 4.1 ⭐ `[xlings.workspace]` 是闭环的接线点 + +```toml +# cortex-m-rt/mcpp.toml +[xlings.workspace] +"xim:qemu-arm" = "9.2.4-1" +"xim:probe-rs" = "0.24.0" +``` + +⚠️ **本文 v5 在这里写错了两处,已更正:** + +| v5 写的 | 实际(2026.9.3 起) | +|---|---| +| `[xlings] deps = [...]` | **`deps` 已从清单里退休**,`[xlings.workspace]` 是作者写的那一张表。`deps` 仍被接受(根清单里报错、依赖清单里只提示),但不是该写的拼法 | +| 「声明 ≠ 安装,真正触发安装的是 `xpm.<平台>.deps`」 | **对根工程已经不对了。** `prepare.cppm:3246` 的原话是 *"the contract being added is 'what you declared gets installed'"* —— 根/workspace 清单声明的东西由 mcpp 自动装 | + +### 4.1.1 ⚠️⚠️ 但由此浮出一个真正的设计问题:自动安装只覆盖根 + +`prepare.cppm:3150` — `runtimeOwnerManifest = wsManifest ? *wsManifest : *m`, +即 **workspace 或根工程的清单,永远不是依赖的**。于是: + +| | 查找(`xlingsDepBinDirs`) | 安装(provisioning) | +|---|---|---| +| 根工程声明 | ✅ | ✅ 自动装 | +| **依赖(板级包)声明** | ✅ **本轮刚修** | ❌ **不装** | + +⇒ 「板级包知道环境、消费者什么都不声明」这个故事,**查找那一半通了,安装那一半没通**。 +干净机器上仍要靠**索引描述符的 `xpm.<平台>.deps`** 把工具随包装上。 + +⭐ **而这个不对称可能是对的,不是缺陷。** 两者的性质不同: + +* **查找只读机器**。让它跨图是安全的 —— 依赖说「我要 qemu」,mcpp 去看看装没装。 +* **安装写机器**。让它跨图是一次**权限升级**:一个传递依赖可以让 mcpp 往你机器上 + 装任意包。 + +⇒ 于是分工可以是:**清单里的声明是未经审查的通道,只用于查找;索引描述符的 +`xpm.<平台>.deps` 是发布时被审查过的通道,才有资格触发安装。** 板级包两处都写, +而那不是重复 —— 它们回答的是两个不同的问题(「去哪找」与「谁有权装」)。 + +⚠️ **这一条我没有把握,列为待定** —— 见 §12。 + +### 4.2 为什么 C 库是源码包而不是 xim 预编译 + +| prebuilt 要做的 | 源码包 | +|---|---| +| 7 个多库各建一次 | **没有多库** —— 用与程序完全相同的 `compile_flags` 编,ABI 一致按构造成立 | +| `libdir` 列与包目录逐字节对上(#481 的形状) | 列为空 | +| builtins 必须同包否则第一次 printf 挂 | 两个包,由依赖边表达 | +| 五宿主镜像、`.sha256`、CDN 等待 | 一个源码 tarball,宿主无关 | +| 版本钉在目标表里,**在 lock 之外** | 进 `mcpp.lock` | +| 每架构一个包(现已三个) | **一个包服务全部 11 行裸机目标** | + +⚠️ 代价见 §1.5(首次墙钟),前提是 `--gc-sections`(已随 #550 落地)。 + +--- + +## 5. openarch:部分后端 + 真实应用 + +| 顺序 | 后端 | 为什么 | 真实应用 | +|---|---|---|---| +| 1 | 能力拆分 | 见 §1.3;加法,不破坏 | 现有三后端补声明 | +| 2 | **aarch32**(Cortex-A/R 32 位) | 14 个函数**一个不缺**(CP15 的 `TPIDRPRW`/`TPIDRURW` 恰是两个指针槽、真 MMU、`VBAR`、DMB/DSB/ISB);第一台 **32 位**机器,挖出 `arch_pte_make_leaf` 返回 `arch_u64` 的宽度假设 | `examples/switch` 扩到四机同源 | +| 3 | **Cortex-M**(部分后端) | 挖出「每台机器都有地址空间」这条从没被问过的基数假设 | ⭐ **抢占式任务切换器**,`examples/preempt` | + +⚠️ **Cortex-M 的真实应用不是跑通探针,是一个能抢占的调度器。** 只做 +`arch_context_switch` 往返的探针,与一个被 SysTick 打断、在 PendSV 里换栈再恢复的 +调度器,考的不是同一件事 —— 后者才会暴露 `arch_trap_*` 在 M-profile 上「向量表是 +按异常号索引的数组,不是单一入口」的语义变化。 + +--- + +## 6. 与主流对比 + +| | 抵达产物的模型 | 弱点 | +|---|---|---| +| **Cargo** | `cargo run` + `.cargo/config.toml` 的 `runner` | 每目标**只有一个**;由**用户**配置而非包提供 | +| **PlatformIO** | `pio run -t upload/monitor` | 动作词汇由平台固定 | +| **west (Zephyr)** | `west flash` + `runners.yaml` | **两套 CLI**;runner 是 Zephyr 专用 Python | +| **CMake** | 自定义 target | 无「抵达产物」概念、无发现机制,每工程重造 | +| **npm** | `npm run