范围:mcpp-community/mcpp #455 · #456 · #457 · #458 · #459,以及同期的
openxlings/xim-pkgindex #651/#652/#653(+ 三条版本 bump)、mcpplibs/mcpp-index
#219/#220、两个新建仓库 mcpplibs/riscv-virt-rt 与 mcpplibs/std-freestanding。
时间:2026-08-19 一日之内(#455 合入 → #459 合入),发布 2026.8.19.1 … 2026.8.19.4。
配套:用户面场景与伪代码见 裸机使用场景。
引擎侧的形状是对的,而且收敛得比计划更小;真正的成本几乎全在 「我把属于 target 的东西写成了包的依赖」 这一类边界判断上 —— 五个已修缺陷里有三个是同一个根因的不同表现。
判据(用已发布二进制、从零验证):
mcpp new blinky --template riscv-virt-rt
cd blinky && mcpp run # qemu 里打印;manifest 里没有 [target.*] 段[package] name = "blinky" / version = "0.1.0"
[build] target = "riscv64-none-elf"
[dependencies] riscv-virt-rt = "0.3.0"| PR | 规模 | 内容 | 版本 |
|---|---|---|---|
| #455 | +6355/−18,34 文件 | 裸机 target 成立:riscv64/32-none-elf 进目标表、新建 src/freestanding/(target/linkline/runner)、mcpp:link-script= 指令(protocol 3)、mcpp::xpkg_dir |
2026.8.19.1 |
| #456 | +2/−2 | 发布 | — |
| #457 | +1388/−49,24 文件 | runner 归 BSP(mcpp:runner=,新 Slot::Runner + Scope::RunGlobal,protocol 4)、裸机 mcpp test、产物集(.bin/.map/size 摘要) |
2026.8.19.2 |
| #458 | +109/−3,5 文件 | 修 mcpp build 之后 mcpp run 在宿主上直接 exec 裸机 ELF |
2026.8.19.3 |
| #459 | +753/−45,21 文件 | 异常/RTTI 整图关闭、目标自带 sysroot 轴、toolchain_dir/sysroot_dir、依赖缓存键补 target flags、std 子集打通 |
2026.8.19.4 |
累计(从 #455 之前的 9c5cc8d 算起,含同期其它工作):87 文件 / +10330 / −161。
其中 src/freestanding/ 三个模块共 432 行,裸机 e2e 四个脚本共 983 行,
test_freestanding.cpp 25 个单测。
指令表 13 → 15 行,protocol 2 → 4。 两条新指令(link-script、runner)各自
只占表里一行 —— 这是 #455/#457 最值得记的一点,见 §2.1。
| 仓库 | 内容 |
|---|---|
openxlings/xim-pkgindex |
#651 qemu-riscv 9.2.4-1(五个宿主目标)· #653 picolibc-riscv 1.8.12(自建,rv64gc/lp64d + rv32imac/ilp32)· #652 修镜像工具两个缺陷 · #654/#655/#657 版本 bump |
mcpplibs/mcpp-index |
#219 收录 riscv-virt-rt(已合)· #220 std-freestanding 0.2.0 + riscv-virt-rt 0.3.0(待 2026.8.19.4 生效) |
mcpplibs/riscv-virt-rt |
板级支持包,0.1.0 → 0.3.0;含 templates/blinky(mcpp new --template)、examples、仓库 CI(rv64+rv32 真跑 qemu) |
mcpplibs/std-freestanding |
freestanding std 子集,0.1.0 → 0.2.0;tools/regenerate.sh 实测生成 |
两个包 GitHub + GitCode 双镜像,tarball 逐字节校验一致。
裸机需要三样东西是引擎原本表达不了的:链接脚本、执行方式、目标 C 库位置。 三样落地为:
link-script—— 指令表一行(Scope::LinkGlobal,Transform::LinkerScript);runner—— 指令表一行 + 一个新Slot+ 一个新Scope;- 目标 sysroot ——
TargetInfo一个字段,与pin并列。
| 计划写的 | 实测 | 结果 |
|---|---|---|
| W8「带序 startup-objects 具名槽」 | -lcrt0-semihost 从归档里拉得进来,顺序由链接脚本 section 序决定 |
⛔ 整个槽位机制不需要,真正缺的只有 -T |
§4「裸机 mcpp test 要 batch 模式 + 结构化 stdout 协议」 |
qemu 冷启动 12ms(不是计划写的 0.4s);semihosting 把固件 main 返回值原样传给 qemu 退出码 |
⛔ 六个设计决定里四个不必做,R3 缩成「走 mcpp run 用的同一个 runner」 |
W12 requires-hard 能力硬门 |
同一个 token 要同时表达「平台本来没有」与「runner 配错了」 | ⛔ 实现后用了一次,macOS e2e 一小时内证伪,整个机制撤销 |
⇒ 这三条是本轮质量的主要来源:计划里凡是带具体数字或「A 不成立所以要 B」的句子, 数字与断言本身就是必须先测的探针。
directives.cppm 里 link-search/link-lib/link-script 是 LinkGlobal(到达消费者),
include-dir/cflag/cfg 是 PackagePrivate(不到达)。这条供应链纪律让
引擎完全不需要 sysroot 概念就能让 BSP 工作:目标头由包私有 include、对外只 export 一个模块。
e2e/131 从两侧钉它:目标 C 头必须到达消费者,板级包自己的头必须不到达。
riscv-virt-rt 与 std-freestanding 都曾声明
[xlings] deps = ["xim:picolibc-riscv@1.8.12"],后者还声明了 xim:llvm。
这是用户 review 时指出的,三条都对:不该绑 libc、不该绑架构、不该绑编译器实现。
真因是引擎的结构性缺口,不是包写得随意:
| 编译器 | 目标 libc | |
|---|---|---|
hosted(x86_64-linux-musl) |
目标表 pin 自动 |
musl 在 gcc 载荷里 / glibc 走 PayloadPaths —— 自动,从没人写过 xim:glibc |
| freestanding | 目标表 pin = llvm@22.1.8 自动 |
一条轴都没有 ⇒ 外溢到每个包 |
⇒ #459 给 TargetInfo 加 sysroot 轴,复用现有 [xlings] deps 物化通道安装,
引擎发 -isystem <sysroot>/include/<档位> 与 -L <sysroot>/lib/<档位>。
再补两个「问引擎」的接口 mcpp::toolchain_dir() / mcpp::sysroot_dir()。
结果:std-freestanding 变成零依赖,riscv-virt-rt 只剩模拟器(那确实是板级事实)。
分界线也随之清楚了:位置是目标的事实,选择是板级的事实。
picolibc-riscv / qemu-riscv 把 target 写进包名,对吗? —— 对,而且是 xim 的既有规则:
aarch64-linux-musl-gcc riscv64-linux-musl-gcc x86_64-linux-musl-gcc
mingw-cross-gcc mingw-w64 musl-gcc
picolibc-riscv qemu-riscv
⭐ 名字承载 TARGET,archs 轴承载 HOST(llvm 的 archs = {x86_64, arm64} 是宿主)。
llvm 名字里没有 target,因为它一份载荷服务所有 target —— 规则是一致的,不是巧合。
粒度落在 ISA 家族而不是 triple:riscv64-none-elf 与 riscv32-none-elf 共用一个
picolibc-riscv(内含两个 multilib 档位),这正是 picolibc 上游一次构建的产物单位。
picolibc 该在 mcpp-index 侧还是 xim 侧? —— xim 侧,构建期由目标行解析,也就是 #459 的做法。
| 判据 | picolibc |
|---|---|
可 import 吗 |
❌ 没有 C++ 模块,是头 + libc.a + crt0 + 链接脚本 |
| 进依赖图吗 | ❌ 由 target 选定,不由用户的依赖图选定 |
| 生态里同类的东西在哪 | glibc musl musl-cross-make 全在 xim;mcpp-index 里 libc 包 0 个 |
⇒ 放进 mcpp-index 会把 libc 重新塞回包依赖图,正好撤销 #459。
TargetEntry 今天有 toolchain/linkage/runner/cxxRuntime,
没有 sysroot ⇒ 工程无法把 picolibc 换成 newlib。
自然形态是 [target.X] sysroot = "xim:newlib-riscv@…",与 [toolchain] 覆盖编译器同构。
未实现,列入 §8。
choose_runner(ctx) 是「产物怎么执行」的唯一读取点,mcpp run 与 mcpp test 共用。
这是本仓库反复付过学费的形状(#233/#240/#242/#344:同一决策两处推导)。
覆盖语义也留了:[target.X].runner 覆盖 BSP 供的,调试时换 -bios 是正当需求。
索引 min_mcpp 没有抬(memory:index-floor-must-degrade)。
新包进索引不会让老客户端拿不到任何包。riscv-virt-rt 0.1.0/0.2.0 留在索引里,
因为「已经发布过的版本就是可能有人 pin 过的版本」。
if constexpr (requires { mcpp::runner("x"); }) // 名字不存在 ⇒ 硬错误,不是 falserequires 表达式作用在不存在的限定名上是 ill-formed。⇒ 包无法优雅降级;
而 wire 协议那条点名 mcpp self update 的诊断也够不着(它要求程序先编译得过)。
⇒ 补在引擎侧:build.mcpp 编译失败且错误含「不是 mcpp 的成员」时追加升级提示,
三个前端三种拼写都认。对以后每一次类型化 API 新增都生效,不只是 runner。
依赖缓存键有 targetTriple,没有 triple 隐含的那组 flag。而哪些 flag 由 triple 隐含
是 mcpp 的决定,会随版本变、triple 字符串不变。
⇒ 加 -fno-exceptions 那一刻,所有已建过的裸机工程复用升级前的 BMI,硬失败,
而错误只点名一个 .pcm 文件:
error: exception handling was enabled in precompiled file
'mcpplibs.riscv_virt_rt.pcm' but is currently disabled
已加 targetImpliedFlags 轴(hosted 为空,不动任何现有键),单测钉「加一个 flag 键必须变」。
第二阶段的 import std 诊断末尾给了可直接粘贴的
mcpplibs.std.freestanding = "0.1" —— 这个包当时没发布。用户粘完下一条命令报
package not found。⇒ 诊断里的每条建议都是承诺;现在包发布了,那一行才放回去。
| 变更 | 影响面 | 处置 |
|---|---|---|
mcpp run 位置参数改名 target → bin |
零(只出现在 --help,cmd_run 按下标读) |
— |
mcpp run --target-triple |
保留为别名(2026.8.19.1 已发布的拼写),e2e/130 两个拼写都钉 | — |
freestanding 加 -fno-exceptions -fno-rtti |
裸机工程全体 | 由 §3.3 的缓存键保证重编;异常在裸机上本来就不可用(无 unwinder) |
| 目标自带 sysroot | 老包的 [xlings] deps 仍可留(去重),不破坏 |
新包不必写 |
好的:
src/freestanding/三个模块 432 行承载全部裸机知识,且linkline是纯字符串构造 (不依赖CompileFlags,避免模块环)。- ISA 表是数据:
riscv32支持靠表里加一行,引擎与 BSP 零改动 —— 这条被 N3 判据实测过。 - 模板随包走:
cmd_new的内建注册表刻意冻结在bin,其余是包模板;mcpp new把自依赖按解析到的版本写进 manifest ⇒ 模板不可能与库脱节。⚠️ 计划里原本要给 mcpp 加内建--template baremetal-riscv,是错的形状。 - std 子集不手写导出表:libc++ 自带 110 个
std/<header>.inc,子集是机械挑选, 生成物 217 行全是#include;tools/regenerate.sh每次运行都打印宿主对照组。
不够好的:
⚠️ src/build/prepare.cppm与execute.cppm各 +136/+137 行。这两个文件已经很大, 本轮又往里加了 sysroot 解析、runner 选择、size 报告、快路径前置条件。 它们正在变成「什么都知道的地方」,下一轮应当先拆再加。build.mcpp里合成__config_site是 40 行字符串拼接。可用但不优雅; 若第二个包也要合成配置,应当抽成mcpp::的一个接口。
| # | 缺陷 | 发现方式 | |
|---|---|---|---|
| 1 | mcpp run <binary> 被 --target 吃掉 |
CI(e2e/73) | 位置参数上原本就有注释写着这个碰撞,我为了「一致性」覆盖了它 |
| 2 | mcpp build 后 mcpp run 在宿主 exec 裸机 ELF |
发布后用发布二进制走新用户流程 | 130/131/132 三个 e2e 都恰好先 run 后 build,顺序本身是被测对象 |
| 3 | 诊断指向不存在的包 | 自查 | 写诊断时没验证那一行今天能跑通 |
| 4 | 包声明了 target 的 libc/编译器 | 用户 review | 在已装好的机器上,这条边在与不在完全一样 |
| 5 | 缓存键缺 target 隐含 flags | 实施 #459 时的升级路径自测 | 只有跨版本才暴露 |
这是本轮最值得记的一类问题 —— 测试是绿的,但它测的不是那件事。
- 第一版 build-then-run 回归测试是假绿:加进 e2e/131 的既有工程后,
关掉修复它照样过。给
try_fast_run的 16 个return nullopt各打编号探针后定位到:mcpp.toml比build.ninja新,快路径本来就没被走到。 真因:重建不会重写内容未变的build.ninja⇒ 在原地编辑过 manifest 的目录里, manifest 永远最新,快路径永久关闭。⇒ 这类测试必须新建干净工程,并先确认它变红。 - e2e/131 的私有性断言判据失效:它用「
#include <stdio.h>编不过」证明依赖的include-dir不到达消费者。libc 归目标之后<stdio.h>本来就该编得过(和宿主一样)。 原来那条测的其实是「libc 从哪来」,不是「作用域对不对」。 - 「拿走再装回来」:xim 安装期依赖边在已装好的机器上不可验证。
判据必须是把 store 里的包改名藏起来 →
mcpp add+mcpp build把它装回来。
-Wl,-Map= 是链接命令上的 flag,不是独立边,所以很容易只写不声明 ——
删掉 firmware.map,ninja 认为 ELF 是新的、什么都不做,map 就永远不回来。
已声明为链接边的 implicit output,e2e/132 用「删掉必须回来」钉住(这是唯一能在修复前失败的形式)。
- 单测:
test_freestanding.cpp25 个 +test_build_directives33 个 + cache_key 19 个。 - e2e:130(引擎链)/131(生态链)/132(test + 产物)/133(std 子集),共 983 行。
- CI:新增
baremetaljob,两个 home 都装 qemu(shim 按拥有它的 home 派发), 并且断言每条 PASS 行真的出现 ——run_all.sh跳过时退出码是 0。 - 包侧 CI:
riscv-virt-rt与std-freestanding各自仓库 CI 真跑 qemu,rv64+rv32 双档。
2026.8.19.1 → .2 → .3 → .4,其中 .3 与 .4 都是在发布之后发现问题才发的。
不是坏事的部分:.3 的缺陷是靠「用发布二进制从零走一遍新用户流程」发现的 ——
这一步如果省掉,它会留在生态里直到有用户撞上。这一步的价值被证明了。
是问题的部分:
.2发布时,mcpp build && mcpp run这条新用户最可能敲的两条命令没有被任何测试覆盖。 四个裸机 e2e 都先run,是巧合而非设计。.4的架构问题(§2.3)是用户 review 发现的,不是我自查发现的。我在.3的收尾报告里 甚至把 std 子集写成「要从头实现、不该在收尾时半做」——而调研文档 X1–X10 早就把这条路测到了底。 ⇒ 写「这条做不了」之前必须回查自己的调研文档。
改进项(下一轮的硬约束):
- 发布前必须跑一遍命令序列矩阵(
build→run、run→build、build→test),而不是单条命令。 - 任何「某某做不了 / 代价很大」的结论,若同一课题有调研文档,必须引用文档中的具体测量才能下。
- 回归测试必须做 revert-A 探针(关掉修复看它是否变红),否则不算写完。
⭐ 裸机 target 反而是最省跨平台成本的一类:clang/lld 是 cross-compiler by construction,
任何能装 llvm 载荷的宿主都能产出 RISC-V 镜像,不需要 per-host 交叉载荷。
host_can_serve 对 freestanding 直接返回 true,这条在目标表注释里写明了。
| 轴 | 状态 |
|---|---|
| 宿主 | 引擎侧 21 个 CI job 全绿(linux/macOS/Windows/aarch64/hermetic/mingw-cross) |
| 模拟器 | xim:qemu-riscv 覆盖 五个宿主目标(linux x64/arm64 · darwin x64/arm64 · win32 x64) |
| 目标 sysroot | picolibc-riscv 与宿主无关(目标侧产物),三平台同一份 |
qemu-riscv win32-arm64 上游无资产 ⇒ 该宿主装不上,行为正确但会失败 |
|
| 两个包的仓库 CI 只有 ubuntu-24.04;macOS/Windows 上的裸机链没有持续验证 |
tests/examples/ workspace 成员在三个平台上无条件跑,没有能力门
⇒ 需要模拟器/目标 sysroot 的包不能加成员,验证只能放包自己的仓库 CI。
这是一个已知的覆盖缺口,不是遗漏。
| 项 | 性质 | 备注 |
|---|---|---|
T3 档(std::format、标量 std::sort、完整 std::string) |
真实边界 | libc++ 把这些实体放在编译版库里(标量 __sort 是 extern template,无宏可关)⇒ 需为目标编 libc++.a,是新载荷 |
mcpp-index #220 |
待发布生效 | 依赖 2026.8.19.4 |
[target.X] sysroot 覆盖 |
缺口 | 见 §2.4:今天换不了 libc 实现 |
| 目标表索引化 | 已定为阶段二 | 把 pin + sysroot 搬到索引,含老客户端降级路径;阶段一不会让它更难做 |
| 第二块板 / ARM Cortex-M | 未开始 | rv32 已作为「ISA 表是数据」的证据 |
prepare.cppm / execute.cppm 体量 |
技术债 | 见 §4 |
| 包侧 CI 单平台 | 覆盖缺口 | 见 §7 |
| 维度 | 评价 | 依据 |
|---|---|---|
| 架构 | 好 | 能力增长未换来概念增长(2 条指令 + 1 个字段);三个计划中的大机制被实测砍掉;单一读取点 |
| 兼容性 | 好,但有一次真实风险 | 索引底线未抬、别名保留、破坏面为零;-fno-exceptions 差点造成升级即坏,靠缓存键轴补上 |
| 简洁优雅 | 好 | 432 行承载全部裸机知识;ISA 表是数据;模板随包走;子集不手写导出表 |
| 稳定性 | 五个自造缺陷,两个是发布后才发现;三条判据本身是错的 | |
| 跨平台 | 好,有已知缺口 | 引擎侧 21 job 全绿、模拟器五宿主; |
| 一致性 | 好 | --target 在所有子命令上拼写一致(靠位置参数改名换来,而非放弃一致性) |
| 无感升级 | 缓存键轴与诊断升级提示都是补出来的,不是设计时就有的 |
- ⭐ 计划里的数字就是探针。 本轮被实测推翻的计划主张:qemu 冷启动(0.4s→12ms)、 「裸机没有退出码」(semihosting 有)、builtins 缺口触发者(不是 64 位除法,是 128 位移位)、 「libc++ 头一个都用不了」(103/110)。四条全是我没测就写下的。
- ⭐ 在「已经配置好的机器」上,很多边验不出来。 目标 libc 的依赖边、xim 安装期依赖边、 快路径前置条件 —— 三者的共同点是存在与不存在长得一模一样。 判据必须是拿走(stash 后重装)或新建(干净工程),不能是「在我这儿是好的」。
⚠️ 顺序本身是被测对象。mcpp run对、mcpp build && mcpp run错, 而四个裸机 e2e 恰好都先run。命令序列矩阵要进发布前检查表。