Skip to content

Latest commit

 

History

History
555 lines (384 loc) · 27.9 KB

File metadata and controls

555 lines (384 loc) · 27.9 KB

裸机方向的优化方案

全部实施完毕(2026-08-21)。 每一节末尾新增「已实施」小节,记录实测结果 与本文写错的地方。⚠️ 其中 §4 的判据 1 被机制本身否掉,而否掉它的正是尝试 实现它的过程。

2026-08-21。本文为 2026-08-21-baremetal-ecosystem-assessment.md 列出的四项优先事项给出 可执行方案。每项包含:成因(读码得出)、设计、实施步骤、判据、依赖与刻意不做的事。

0. ⚠️ 一处凭印象会写错、已被读码推翻的记录

先前记录中有一条:「build.mcpp 新增一条指令要改 9 处」。这条已经过时。

src/build/directives.cppm 的当前形态:

  • 指令词表是 inline constexpr std::array<Def, 15> kTable,唯一真源;
  • 引用 Slot:: 的只有两个文件(directives.cppm 26 处、build_program.cppm 4 处);
  • 缓存记录的 d <tag> 词表在注释里明确写着「由 kTable 拥有,不在此处拼写 —— 这份 列表曾被复制到四个地方并且漂移过」;
  • serialize 按表序遍历,accept_cache_record 对未知 tag 返回 false(视为陈旧条目)。

新增一条指令的成本比记录里低得多,而且缓存的写入与重放是表驱动的、免费的。 本文 §1 的方案依赖这个事实;若不先读码,会按九处改动去规划,得出错误的代价评估。

⚠️ 这与本次会话的其它教训同形:结论会被复查,理由不会。 上面这条是理由,它错了 三个月无人发现。


1. 【高】给构建程序一条「成功但有话说」的通道

1.1 成因(读码 + 实测)

src/build/build_program.cppm 的运行段落:

auto rres = capture_exec_deadline({bin.string()}, childEnv, deadline, &timedOut, root.string());
if (timedOut)            { return std::unexpected(... "Output so far:\n{}" ... rres.output); }
if (rres.exit_code != 0) { return std::unexpected(... "build.mcpp exited with {}:\n{}" ...); }
Directives d;
dirs::accept_output(d, dial, root, rres.output);      // 只挑出 `mcpp:` 行

rres.output 只在超时或非零退出时被打印。 构建程序成功时,它写的任何非指令输出被 静默丢弃。而 kTable 十五行里没有一行承担「向用户说一句话」。

实测后果:清单的 [xlings] deps 是声明而非安装触发器,干净机器上 mcpp::xpkg_dir 返回 空,构建程序静默地不配 runner,mcpp run 给出一条一般情况下正确、在此处不正确的 建议。为此写过一条 std::cerr 提示,实测后删除:它在最需要它的那些构建上一个字都不 打印,留着比不留更糟,因为它看起来像个修复。

1.2 两个候选,以及为什么选前者

候选 A:新增 mcpp:warning= 指令 + 类型化 API mcpp::warning(...)

候选 B:构建程序成功时,把非指令输出打印出来。

B 的诱人之处是零兼容性成本:不需要新指令、不需要协议号、不需要新 API,老引擎只是 不打印而已。

⚠️ B 否掉,理由是可测量的而不是审美的: 它会改变索引里每一个既有包的行为。构建 程序合法地会打印进度(一个调用 protoc 的生成器就会),而这些输出此前从不显示。开启它 等于把 120 个包的输出一次性放出来,而没有任何办法先验证哪些是噪声。一个改变所有既有 包行为的开关,不能用来修一条诊断缺口。

采纳 A。

1.3 ⚠️ A 的兼容性成本,以及它为什么可以接受

协议门的注释给出了决定性事实:

一个针对新 mcpp 写的包到达老 mcpp 时,身上盖的是老引擎的协议号 —— 那个声明是在 build.mcpp 编译期由正在运行的引擎替换进去的,不是由包携带的。

⇒ 所以用了 mcpp::warning(...) 的构建程序在老引擎上根本编译不过(类型化 API 不 存在),失败形态是 clang 的「no member named 'warning'」而不是 mcpp 的诊断。

这不是新问题,而是既有的代价模型。 link-script 在协议 3 加入时具有完全相同的 性质,注释里也记着那次「adding link-script in protocol 3 proved it wrong」。

⇒ 结论:使用警告通道会抬高该包的最低 mcpp 版本,这与使用任何新指令一样。对 motivating case(openarch 模板)不构成问题,它本来就 pin 了近期版本。

1.4 设计

取值 理由
wire warning mcpp: 前缀连读为 mcpp:warning=
tag warning(非空) 见 §1.5,这是整个设计里最容易漏的一条
slot Slot::Warnings(新增) 不能塞进任何既有槽:它既不是编译输入也不是链接输入
scope Scope::PackagePrivate 一个包的警告不传播给消费者。传播会让一个深层依赖的提示出现在与它无关的构建上
transform Verbatim 值是给人读的句子,不是路径也不是 flag
mustExistAfterRun false 值不是路径
sinceProtocol 当前 kProtocolVersion + 1 link-script 同法

打印形态:经 mcpp::ui::warning,前缀为包名。引擎知道是哪个包在说话,而构建程序 自己拼包名会在 workspace 里拼错。

1.5 ⭐ 最容易漏的一条:警告必须在缓存命中时重放

构建程序的结果是被缓存的(write_cache / d <tag> <value> 行),命中时程序不再运行

⚠️ 若警告只在运行路径上打印,它会出现一次然后再也不出现。 那正是被删掉的 std::cerr 提示的失败形态的镜像:一条时有时无的提示看起来像「问题已解决」。

好消息是重放是免费的:serialize 按表序遍历、accept_cache_record 按 tag 派发, 所以只要 tag 非空,警告就随缓存一起写入并读回。

⚠️ 要做的是把打印点放在两条路径的汇合处之后,而不是放在「刚跑完程序」那一段里。 这是本项唯一的真实实现风险,也是判据 3 存在的理由。

⚠️ 另一条:kCacheEpoch 必须递增。旧缓存条目没有 d warning 行,而语义变了。

1.6 实施步骤

  1. Slot::Warnings 加入枚举(在 Count 之前);
  2. kTable 增加一行,数组长度 15 → 16;
  3. kProtocolVersion +1,kCacheEpoch +1;
  4. 打印点:build_program.cppmdirs::apply(m, d) 之前,且在缓存命中的早退路径上 同样经过;
  5. 类型化 API mcpp::warning(std::string_view) 加入随附 mcpp 模块;
  6. 文档:docs/07-build-mcpp.md 的指令表 + docs/13-baremetal.md 板级包一节;
  7. openarch / riscv-virt-rt 的构建程序改用它,把「一次性 xlings install」这句话从 README 移回它该在的地方。

1.7 判据

必须同时满足,任缺其一视为未完成:

  1. 一个发 mcpp:warning= 的构建程序,其文字出现在 mcpp build 的输出里,带包名前缀;
  2. 同一个工程连续构建两次,第二次(缓存命中)仍然打印同一条警告 —— 这条是 §1.5 的 直接判据,且必须先看到它失败再看到它通过,否则无法区分「重放正确」与「缓存根本 没命中」;
  3. 一个 workspace 里两个成员各发一条警告,两条都出现且包名不同;
  4. 警告不使构建失败,mcpp build 退出码仍为 0;
  5. openarch 模板在干净机器上 mcpp build 后,用户看到的是「模拟器未安装,运行 xlings install qemu-riscv qemu-arm qemu-x86 -y」,而不是 mcpp run 之后一条泛泛 建议。

⚠️ 判据 2 的「先看到它失败」不是形式主义。本谱系里 build.ninja 图混叠的回归测试 先删产物或 touch 源码都会把缺陷盖住;一个从未被观察到失败过的测试,不能证明它在测 它声称的东西。

1.8 刻意不做

  • 不做 mcpp:error= 构建程序要报错,非零退出即可,而那条路径已经打印输出。
  • 不做严重级别分层(note/warn)。 一个级别不够用之前不加第二个。
  • 不做「警告即失败」开关。 那是消费者的策略,不是引擎的。

1.9 已实施(mcpp 2026.8.21.2)

三个设计判断都成立,而兼容性一节写得比实际更悲观

§1.3 说用了 mcpp::warning 的包在老引擎上「失败形态是 clang 的 『no member named』而不是 mcpp 的诊断」。错了 —— 引擎早有那个机制。 mentions_missing_mcpp_api命名空间 'mcpp' 匹配,所以新 API 自动被它 覆盖。实测(旧引擎 2026.8.20.3):

error: 'warning' is not a member of 'mcpp'
       The `mcpp` build module this engine bundles does not have that name.
       Either the package was written for a newer mcpp (try `mcpp self update`;
       this is mcpp 2026.8.20.3), or the name is misspelled …

§0 的教训在本节内部又应验一次:凭读码不足以下结论,要去跑。

⚠️ §1.5 预判的「唯一真实实现风险」是对的,而测试写法上有一个未预料到的坑: 不修改任何东西的第二次构建走全工程 fast path(Finished dev in 0.00s), 根本到不了 build.mcpp 阶段 —— 断言它等于测了 fast path。回归测试必须先 touch 一个源文件。由此定下一条边界并写进 docs/07:全工程 no-op 构建什么都不打印, 包括这一条。

判据 2 的「先看到它失败」真做了:临时移除缓存命中处的发射,第二条断言变 红而第一条仍绿。

采用侧:openarch 0.6.0 与 riscv-virt-rt 0.5.2。干净机器实测,而 riscv-virt-rt 那条归属到依赖而不是根 —— 包名由引擎传入,不由构建程序自己拼。


2. 【高】补 aarch64 / x86_64 的目标 C 库

2.1 成因(读码)

目标表四行裸机里,后两行的 C 库列为空。这一半是设计(零 libc 层),一半是索引里没有 这两个架构的 picolibc 构建

2.2 ⭐ 好消息:构建器已经是 profile 驱动的

xim-pkgindex/.agents/tools/build-baremetal-sysroot.sh(216 行)的形状:

PROFILES=( "rv64gc lp64d" "rv32imac ilp32" )   # march/mabi 对
… meson cross file: -target $triple -march=$march -mabi=$mabi -mcmodel=medany
布局:include/<march>/<mabi>/                  # picolibc 自己的 multilib 约定

而且脚本注释里写明了两个 profile 的用意:「都需要,以证明布局可以推广 —— 单 profile 的树会把 profile 意外地烤进路径里」

⇒ 这条路已经为「加第三、第四个 profile」铺好了。riscv 专属的只有两处:-mcmodel=medany 与 triple。

2.3 ⚠️ 必须先测量、不得假设的一条

picolibc 对 aarch64-none-elfx86_64-none-elf 的支持程度未经本项目测量。 aarch64 有上游端口;x86_64 的端口存在但受关注远少于 arm/riscv。

⇒ 第一步是一次最小测量而不是直接建包:用现有脚本的 cross file 形状,对两个 triple 各跑一次 meson setup + ninja,记录第一条错误(若有)。在这次测量之前,本节余下的 步骤都是条件性的。

⚠️ 若 x86_64 不成立,那不是本方案的失败,而是一条应当被记录的事实 —— 与 std-freestanding 的 libstdc++ 一栏同法:判据是那份实现有没有为这个目标 configure 过。

2.4 ⚠️ 需要 review 的一处决策:目标表的行填不填

这是本方案里唯一一个两种答案都站得住、且后果不同的地方。

填(与 riscv 一致) 不填(保持零 libc)
默认行为 针对这两个 triple 的工程默认拿到 C 库 默认落在零 libc 层,要 C 库须声明
与 riscv 的一致性 一致 不一致,而不一致需要被解释
对 openarch 的影响 它引用不到任何 C 库符号,静态链接下未用对象不贡献任何东西 ⇒ 无功能影响,但它所处的层变了 无影响
「零 libc 层」的存在证据 四行里零行默认在该层 ⇒ 该层变成一个只在文档里存在的概念 两行默认在该层

我的建议:不填。 理由:

⭐ 零 libc 不是一个过渡状态,而是这一层的性质本身 —— openarch 引用不到任何 C 库符号 这件事,是它能同时服务三台机器的原因之一。如果四行里没有一行默认在该层,那么「零 libc 层」就没有任何东西在证明它可用。

与 riscv 的不一致有一个可陈述的理由:riscv 两行是 verified,且有一个写在 picolibc 上 的板级包;后两行是 preview,其第一个消费者在零 libc 层。⇒ 不一致反映的是成熟度 差异,不是随意。

⚠️ 但这条决策会被用户的实际使用推翻,所以列在这里请 review。

2.5 实施步骤(条件于 §2.3 的测量)

  1. 测量 —— 两个 triple 各一次最小构建,记录结果;
  2. -mcmodel 与 triple 从脚本里的字面量提为 profile 的字段(riscv 用 medany, aarch64/x86_64 用各自的默认或 small);
  3. 构建产物,按 picolibc-riscv 的形状发布:xlings-res/<pkg> 双端 + 每资产 .sha256 侧文件;⚠️ 判定上传成功只能靠回探下载并逐字节核验;
  4. xim:picolibc-aarch64 / xim:picolibc-x86 描述符,形状照 picolibc-riscv (载荷是目标侧的,archs宿主架构,一份归档服务所有宿主);
  5. 文档:docs/13-baremetal.md 的四行表更新 C 库列的说明。

2.6 判据

  1. 一个声明了该 C 库的工程,在对应 triple 上编出并运行一个用到 printf 的镜像;
  2. 不声明该 C 库的工程,其编译行与本方案落地前逐字节相同 —— 这是「不填行」这个 决策的判据,与 libcxx-headers 的判据 4 同法(包已安装而载荷仍应赢);
  3. 描述符的资产两端逐字节一致。

2.7 刻意不做

  • 不追 newlib / musl 的裸机变体。 一个实现先跑通,第二个由需求驱动。
  • 不为 arm32(arm-none-eabi)开新行。 那是新目标而非本项范围。

2.8 已实施 —— ⭐ 覆盖两个目标而不是一个,且三条缺陷全是第二个族才暴露的

§2.3 要求先测量。测量结果推翻了「可能只有 aarch64 成立」的预期:两个都成立 (libc.a 7,070,160 / 6,458,288)。

构建器泛化为一个脚本三个族(而不是加第二份拷贝 —— 那正是 §4/R2 要防的)。 加两个族只暴露了三条 riscv 两年没暴露的缺陷:

缺陷 症状 riscv 为什么没暴露
稀疏检出少 third-party 'siphash/SipHash.h' file not found emupac.cpp 是 aarch64 专属
没设 CMAKE_CXX_COMPILER_TARGET unknown register name 'x30' in asm riscv 的 builtins 全是 C 与汇编,没有 C++ 源
链接脚本默认加载地址 链接成功,一跑就挂 0x80000000 是 riscv virt 的 RAM 基址

⭐ 第三条最值得记:它链接成功,只表现为机器不动。给 qemu 4GB 内存就跑起来 —— 假设由此被证实。一个只在配置慷慨的机器上才工作的加载地址,是最坏的一种错: 它通过。

⚠️⚠️ x86_64 的链接问题上有一次假绿差点定案。 -fuse-ld=lld 看起来解决了; 把 PATH 收到只剩 coreutils 后裸名与 -B<llvm>/bin 两种都倒在 collect2 ... [cannot find ld] —— 之前的通过是环境里恰好有 lld。真解与 mcpp 引擎对这个 triple 的做法逐字相同:把链接从驱动手里拿走。

判据:aarch64 的 printf 镜像在 qemu virt 默认 128MB 下链接并运行; x86_64 在 0x100000 链接成功(⚠️ 它没有 semihosting —— 不给控制台时报 undefined symbol: stdout正确答案)。

⚠️ 发布时又踩了一次「照抄上一版实测数字」:描述符里的 sha256 是加载地址修复 之前那一版的。发布出去的产物是对的,写错的是描述符。修完做了四方核验: 描述符 = 本地归档 = GLOBAL = CN。另一条:CN 端漏传 .sha256 侧文件,表现为 HTTP 404

§2.4 的决策按建议执行:目标表的行没有填。 这两个包是可声明的,不是默认的。

2.9 ⚠️⚠️ 发布并核验了资产 ≠ 使用者能用上它(mcpp 2026.8.21.3)

上面那句「可声明」在写下时还不是真的,而我报告 §2 完成时没有验证它。

验证用的是手工 -isystem / -L,它跳过了整条解析链。补验受支持的路径:

[target.aarch64-none-elf]
sysroot = "xim:picolibc-aarch64@1.8.12"
'stdio.h' file not found

成因:picolibc 是 multilib 布局(include/<march>/<mabi>/),而引擎从 src/freestanding/target.cppmlibdir 列推那个子目录 —— 那两行的 libdir 是空的。 而在索引里没有对应 picolibc 之前,空是对的:它命名的是一个当时 不存在的东西。

A/B:

引擎 结果
2026.8.21.3 -isystem …/include/armv8-a/aapcs,头解析成功
2026.8.21.2 'stdio.h' file not found

(链接仍缺 printf,因为裸工程没有 crt0 —— riscv 在同样条件下同样如此,那是板级 包的活。)

⚠️ 填这一列不改变默认。 libdir 只在 sysroot 已被解析后才被读,而目标表对这 两行仍不绑定任何 C 库 ⇒ §2.4 的决策不受影响,零 libc 仍是默认,包仍是可选加入。 单元测试把这一点也断言了。

⚠️ 一条单元测试此前断言 libdir 为空,而它当时是对的。事实变了,测试跟着改 并在注释里写明它为什么曾经正确 —— 结论会被复查,理由不会。

⚠️ 追查途中一次假绿:xlings install picolibc-aarch64 报「✓ 1 package(s) installed」而安装目录为空。成因是我自己先前用 local-index 测过同名包, xim: 安装撞上了(它的提示写着 still resolves to local:1.8.12)。清掉重装即 正确 —— 是测试顺序的产物,不是包的缺陷。

判据教训写在这里,因为它不属于任何一节而属于全部四节:「资产已发布并双端 核验」与「使用者能通过受支持的路径用上它」是两件事,而只验证前者会把整条解析链 跳过去。


3. 【中】第二块板

3.1 成因

板级包模型目前只被 一块板验证过(riscv-virt-rt,qemu virt)。

这与 openarch 在两台 RISC 机器上的处境完全同形:一个只服务过一块板的抽象,分不清 它是对的还是它只适配了那块板。

3.2 ⭐ 第二块板应当变什么

riscv-virt-rt 同时固定了三件事:ISA(riscv64)有 C 库(picolibc)模拟器板(qemu virt)

按 openarch 的方法论,第二块板应当变的是前两件:

建议:aarch64-virt-rt —— 不同 ISA,且写在零 libc 层上。

理由:

  • 不同 ISA 才能让「板级包」与「机器机制」的边界被检验。同 ISA 的第二块板(如 sifive_u)只变第三件,得到的证据最弱;
  • 零 libc 检验的是一个此前从未被问过的问题:「板级包」与「C 库提供者」是不是两个 可分离的角色? riscv-virt-rt 的注释里写着它「曾经负责定位 picolibc 并交出它的 include 与 library」,后来不再 —— 这说明这两个角色已经在被分开的路上,而一块零 libc 的板会给出它是否真的分开了的答案;
  • 不依赖 §2。若依赖,两项就串行了。

3.3 板级包要提供什么(读码)

riscv-virt-rt 的构建程序发出的指令:runner(绝对路径的模拟器 + argv)、 link_script(virt.ld)、以及 crt0 的选择。⚠️ 注释里明确:「crt0 是一个板级选择」 —— crt0-semihost 经调试通道到达宿主,没有该通道的板选 UART crt0。

⇒ 零 libc 的板需要自带最小 crt0(设栈、清 bss、调 main),而这正好是它要证明的东西。

3.4 判据

  1. mcpp new x --template aarch64-virt-rt 生成的工程 mcpp run 打印预期行;
  2. 两块板的构建程序发出的指令集合相同,只有值不同 —— 若第二块板需要一条第一块板 没有的指令,那说明板级包的接口不完整,而那正是这一项要发现的东西;
  3. 该板不声明任何 C 库,且工程仍能构建并运行。

⚠️ 判据 2 是本项的真正产出。它可能失败,而失败比通过更有价值。

3.5 刻意不做

  • 不上真实硬件。 CI 里没有硬件,而一个只能在作者机器上验证的板级包,其判据无法被 他人复核。真实硬件是第三块板的事。

3.6 已实施(mcpplibs/aarch64-virt-rt 0.1.0)

判据 2 达成,而且比预测的更强。 §3.4 预期「两块板的指令集合相同」,并预留 了它可能失败。由两个构建程序自己的 build.mcpp.cached <tag> 行实测 —— 那是引擎记下的「发出过什么」,不是读源码:

riscv-virt-rt      ldflag runner
aarch64-virt-rt    ldflag runner

同样两条。 C 库体现为 ldflag不同:

riscv-virt-rt      -lcrt0-semihost  -lc  -lsemihost
                   -lclang_rt.builtins-riscv64  -T …/picolibcpp.ld
aarch64-virt-rt    -T …/virt.ld

「板级包」与「C 库提供者」可分离,板级包接口完整。 CI 把它写成硬断言对着 字面量比,所以任何一侧变了都必须来说明。

§3.3 预判「零 libc 的板需要自带最小 crt0」是对的,而它的性质比预判更准确: 那是内容的差别,不是接口的差别 —— 包多了一个 boot.S,指令一条没多。

⚠️ .bss 不被任何别的东西清零(-kernel 只加载 PT_LOAD 段),而那种失败 数据相关、不会在写代码的机器上复现 ⇒ 例子断言 bss zeroed 而不是假设它。


4. 【中】把「判据放在能观察的环境里」变成可检查的

4.1 成因:三次同源事故(实测)

事故 为什么看不见
0.4.0 模板断言放在不装模拟器的作业里 物理上只可能断言 mcpp build;一个「三台都编得过、一台都跑不起来」的工程通过了
0.5.0 修好后放在 gate一行 每行只装自己那台机器的模拟器 ⇒ 能支持的主张只有「模板在 riscv64 上能跑」
qemu-x86 闭包检查 ldd … 2>/dev/null | grep -v <载荷> ldd 崩在 stderr空输出与「没有越界」不可区分

共同形状:断言必须放在能观察到所要主张之现象的环境里,而矩阵的每一行是一个独立的 观察环境。 前两次把这条规则执行到了作业一级,没执行到行一级。

4.2 三条可机器检查的规则

R1 —— 矩阵作业里,步骤名含字面架构/目标名的,是嫌疑。

name: The probe runs on x86_64          # 嫌疑:矩阵里为什么只提一个?
name: The probe runs on ${{ matrix.arch }}   # 由构造保证逐行

可 grep:在含 strategy.matrix 的 job 内,步骤 namerun 里出现矩阵取值集合中的 字面量而未使用 ${{ matrix.* }}⚠️ 会有误报(如对照组步骤),所以是警告加豁免注释 而不是硬失败。

R2 —— 一处逻辑的两份拷贝必须逐字节相同,并由 diff 断言。

0.5.1 已对 examples/switch/build.mcpptemplates/three-machines/build.mcpp.in 落地此法。规则可推广:仓内声明一张「必须一致的文件对」清单,CI 逐对 diff

⚠️ 修法不是「更仔细」。 更仔细已经失败过一次。

R3 —— 空输出不得被当作通过。

任何形如「列出违规项,期望为空」的检查,必须同时断言被检查的集合非空:

n=$(LD_TRACE_LOADED_OBJECTS=1 "$loader" "$bin" | wc -l)
[ "$n" -ge 5 ] || { echo "闭包检查只看到 $n 个对象,工具没有工作"; exit 1; }

这条是三条里最一般的:「没有发现问题」与「没有去看」在输出上完全相同。

4.3 实施

一个 tools/lint-ci-assertions.sh,在 mcpp 仓内实现并作为可复用工作流导出,生态各仓 引用。⚠️ 跨仓强制是做不到的,也不该做 —— 规则的价值在于被读到,而不在于被强制。

4.4 判据

  1. 把 0.4.0、0.5.0 的两份历史工作流喂给 linter,两份都必须被标出;
  2. 把修好后的当前工作流喂给它,不得报警;
  3. R3 的检查对一个故意坏掉的工具(把 ldd 换成一个只写 stderr 的脚本)必须失败

⚠️ 判据 1 是这项唯一有意义的判据。一个对已知的三次事故都报不出来的 linter,没有理由 相信它能报出第四次。

4.5 已实施 —— ⚠️ 判据 1 不可达,而这是本文最重要的一处更正

tools/lint-ci-assertions.sh 已落地并接进 mcpp 的 CI(打印,不失败)。

⚠️⚠️ 判据 1「把 0.4.0、0.5.0 两份历史工作流喂给它,两份都必须被标出」在这个 机制下不可达,而发现这一点花了一次实现。

0.5.0 的缺陷是可 lint 的:if: matrix.arch == 'riscv64' 把一个步骤门到矩阵的 一行上,一行文本就说明白了。0.4.0 的缺陷不是:那一步只断言了 mcpp build, 也只声称了这么多 —— 文件里没有任何东西是错的,它只是比仓库在别处做出的主张更 弱。⭐ linter 检查写下来的东西,不检查没写的东西。

新增 R4 抓的是同一个形状的未来版本(把执行断言放在跑不了东西的作业里), 这是这个机制能给的全部。该限制写在工具头部第一段,以免下一个读者去找一条从不 运行的路径。

两条规则在跑过之后被收窄,两次都是误报率决定的:

  • R3 原本对任何 2>/dev/null | grep 报警。而 cmd 2>/dev/null | grep -q . 在机器看来一模一样,做的却是相反的事 —— 它断言输出非空,正是本规则要求的。 收窄到只对 -v / -c / 取反管道(按缺席判定)报警。
  • R4 原本对任何 mcpp run 报警,在六个完全正确的作业上误报:宿主目标的 mcpp run 不需要模拟器。收窄到只对 freestanding 目标。

⚠️ 一条误报率那么高的规则不会被读,而这条工具的全部价值就是被读到。

对 mcpp 自身跑,剩一条真的:nm "$BIN" 2>/dev/null | grep -cnm 失败 时打印「0 (good)」。已修成可区分两种情况。

R2 已在 openarch 上落地并被反向验证过:.ci-identical-files 声明「示例与 模板的构建程序自 // The emulator. 起逐字节相同」,故意弄坏一侧,它报了。

⚠️ 写那条检查时,它先在自己身上犯了它要防的错。 第一版用 sed 地址,而标记 是一行 C++、含 / —— sed 把它当模式结尾,报 unknown command: '/',两边都空, diff 通过。改用 grep -F + tail,并断言两侧非空。


5. 【低】单 ISA 的第二个后端(riscv S 态)

结构已支持:backend-riscv64 命名的是后端而不是架构。需求尚未出现 —— 现有后端陷入 M 态,而 SBI 之下的内核陷入 S 态。

在有一个真实的 S 态消费者之前不做。 没有消费者的第二个后端,与没有第二块板的板级 包模型同病:分不清它是对的还是它凑合了。


6. 顺序与依赖

§1 warning 通道 ──────────────────┐  (独立,改 mcpp)
                                  │
§2 目标 C 库 ── §2.3 先测量 ──────┤  (独立,改 xim-pkgindex)
                                  ├──> §3 第二块板(不依赖 §2,因为它在零 libc 层)
§4 CI linter ─────────────────────┘  (独立,改 mcpp + 各仓引用)

四项互不阻塞,可并行。唯一的内部串行是 §2.3 的测量必须先于 §2.5。

⚠️ §1 需要改 mcpp 侧。 这与上一轮「能不动 mcpp 侧就不要动」的约束相反 —— 那条约束 是针对上一轮任务的,而本项的成因就在引擎里,在生态侧无法修复。这一点请在 review 时 确认。


7. 本方案刻意不做的事

  • 不重构指令表。 §0 的读码显示它已经是唯一真源,加一行就够;
  • 不改协议门的语义。 「未知指令是硬错误」是对的:warn-and-ignore 会把缺失的指令变成 一个静默不同的构建;
  • 不为兼容性发明新机制。 §1.3 的代价模型由 link-script 在协议 3 建立,沿用即可;
  • 不把 CI 规则做成硬门。 见 §4.3。