@@ -229,6 +229,35 @@ publish pack emit toolchain cache index self
229229
230230⚠️ 代价见 §1.5(首次墙钟),前提是 ` --gc-sections ` (已随 #550 落地)。
231231
232+ #### 4.2.1 回填(2026-09-04):我先走了被否掉的那条,而测量证实了这张表
233+
234+ ⚠️⚠️ ** 我先做了 prebuilt(` xim:picolibc-arm ` ),把上表右列的每一项代价都付了一遍。**
235+ 七个多库分别构建、` libdir ` 列填满(本表说它应为空)、9 MB 载荷两端镜像。走完之后
236+ 才回到源码包。记在这里,是因为** 结论会被复查而理由不会** ,而这次的理由拿到了证据:
237+
238+ | 测到的 | 意味着 |
239+ | ---| ---|
240+ | 七份 ` include/ ` 树** 逐字节相同** (含 meson ** 生成** 的 ` picolibc.h ` /` newlib.h ` ) | prebuilt 发了同一个目录的七份拷贝;源码包** 根本没有多库** |
241+ | 七个档位编译的源文件列表** 完全相同** (1130 个,sha ` b109bf04 ` ) | 档位之间只差** 编译标志** —— 正是「用消费者自己的 ` compile_flags ` 编」 |
242+ | 按 ` <march>/<mabi> ` 索引,七档** 塌成五个目录** ,软浮点行拿到带 ` Tag_ABI_HardFP_use ` 的库 | ARM 的 ` mabi ` 是过程调用标准不是浮点 ABI ⇒ ** prebuilt 的键在 ARM 上根本不成立** ,而构建期毫无迹象 |
243+
244+ ⇒ ** §4.2 的判断是对的,而且比它自己写下的更强** :不只是「代价更小」,是 prebuilt 的
245+ 多库键在这个架构上** 不可能正确** 。
246+
247+ ⭐ 源码包这条路自己的代价也测出来了,四条都是「上游按** 归档** 分层,而一个扁平库不
248+ 能」:` semihost ` /` dummyhost ` 同名符号靠两个 ` .a ` 分开;` semihost/fake ` 是
249+ ` common ` 的** 补集** (26 选 13),而补集** 无法从文件名算出** —— ` fake_io.c ` 与
250+ ` common/iob.c ` 撞在 ` stdin ` /` stdout ` /` stderr ` 上;machine 目录含每个 ARM 档位的变体;
251+ 以及** 基础集合里的负 glob 作用于合并后的集合** ,会把按目标追加回来的那一份取消掉。
252+
253+ ⚠️ 还有一条只有** 跑** 才能发现:板级启动必须设线程指针。picolibc 经 TLS 取
254+ ` stdout ` ,没有它则程序** 链接干净、运行、什么都不打印、然后挂住** —— 没有任何诊断,
255+ 唯一的证据是沉默。` cortex-m-rt ` 的 ` Reset_Handler ` 因此调 ` _init_tls ` /` _set_tls ` ,
256+ 链接脚本给出它们读的** 五个** 符号(只给 ` __tls_base ` 会链接通过再在另外四个上失败)。
257+
258+ ` xim:picolibc-arm ` 保持已发布状态(发布过就是发布过),但** 不是** 推荐路径,其描述符
259+ 已写明。
260+
232261---
233262
234263## 5. openarch:部分后端 + 真实应用
@@ -648,3 +677,131 @@ MCPP_NO_AUTO_INSTALL=1 mcpp run 的输出里 必须 出现 run 档的工具
648677* 批 2、批 5 与批 1'/1" ** 互不依赖** ,可同时进行。
649678* 批 4 依赖批 1'(要有 Cortex-M 后端才能在上面实现 ` arch_trap_switch ` )。
650679* 批 6 独立,任何时候都能做;它只在批 3 的 ` hardware ` feature 要真跑时才成为阻塞。
680+
681+ ## 17. Measured outcomes of the ecosystem batch (2026-09-04)
682+
683+ This section records what the implementation measured, including the two places
684+ where a decision written earlier in this document was wrong.
685+
686+ ### 17.1 A package that vendors upstream sources takes upstream's identity
687+
688+ The rule the batch now follows is that both halves of a package's identity come
689+ from upstream when the content does. ` picolibc.picolibc ` and
690+ ` llvm.compiler-rt-builtins ` are named for the projects whose code they carry,
691+ and are versioned ` 1.8.12 ` and ` 22.1.8 ` for the releases they vendor. Only a
692+ package whose content this organisation wrote takes ` mcpplibs ` — ` cortex-m-rt ` ,
693+ ` openarch ` , and the two board packages do.
694+
695+ The claim is verified rather than asserted. Every one of picolibc's 2109
696+ vendored files is byte-identical to the upstream 1.8.12 release, whose
697+ ` meson.build ` declares that version; every one of the 347 vendored builtins is
698+ byte-identical to ` compiler-rt/lib/builtins ` at the tag ` llvmorg-22.1.8 ` . Both
699+ comparisons were made file by file against upstream, not inferred from a
700+ directory name.
701+
702+ A packaging change moves a fourth segment, which is what allows the first three
703+ to stay upstream's. ` 1.8.12.3 ` is upstream 1.8.12 with this organisation's third
704+ packaging of it.
705+
706+ ### 17.2 A BARE REQUIREMENT IN mcpp IS AN EXACT PIN, NOT A CARET
707+
708+ Two releases shipped broken because of a belief this document did not check.
709+ ` mcpp.version_req ` documents a caret default — ` "1.2.3" ` means ` >=1.2.3, <2.0.0 `
710+ — and that is true of the MATCHING grammar and false of the RESOLUTION path.
711+ ` mcpp::pm::is_version_constraint ` returns false for a bare number, so
712+ ` resolveSemver ` never runs and the literal string becomes the wire address.
713+
714+ Measured, with only ` 22.1.8.2 ` in the index and a dependency written ` "22.1.8" ` :
715+
716+ Downloading llvm.compiler-rt-builtins v22.1.8
717+ error: package 'llvm.compiler-rt-builtins@22.1.8' install path missing after fetch
718+
719+ xlings had resolved the requirement to the real version and installed it; mcpp
720+ then looked for a directory named after the requirement. THE DIAGNOSTIC NAMES
721+ THE SYMPTOM AND NOT THE CAUSE, which is why the belief survived being tested —
722+ "install path missing after fetch" reads like a corrupt download.
723+
724+ The consequence for the scheme in 17.1 is that a consumer writes the packaging
725+ revision in full, and a packaging release costs a re-pin in every consumer. That
726+ is the same cost every other version bump has here, and it is stated in both
727+ manifests so the next reader does not rediscover it.
728+
729+ ### 17.3 A dependency path that escapes the package cannot be released
730+
731+ Both broken releases carried a ` path = "../sibling" ` override, which is how each
732+ package is developed against an unreleased neighbour. IT IS INVISIBLE TO THE
733+ PUBLISHING REPOSITORY'S OWN CI, because the sibling is there: the build succeeds
734+ and the job is green. It fails only for a consumer, after the version has been
735+ published and mirrored.
736+
737+ Both repositories now refuse it in CI, and the index audit checks published
738+ manifests for it as well — the audit that passed the broken release checked
739+ identity, version and checksum, and none of those three were wrong.
740+
741+ ### 17.4 A ` main ` that returns hung for ever
742+
743+ ` cortex-m-rt ` called ` board_main ` for effect and then spun, on the reasoning
744+ that a freestanding entry has no caller to report to. C defines returning from
745+ ` main ` as ` exit(status) ` , and both tiers have somewhere to report: with a C
746+ library, ` exit ` runs the ` atexit ` handlers and flushes the streams; with none,
747+ semihosting is how the package's own console already reached the host.
748+
749+ EVERY TEST OF THE PACKAGE TOOK THE ONE PATH THAT WORKED, because the template
750+ writes an explicit ` board::exit(0) ` . A program ending in ` return 0 ` printed its
751+ output and then ran until something killed it. The CI step added for it imposes
752+ its own ` timeout ` , since a regression does not fail but hangs.
753+
754+ ### 17.5 Open: ` mcpp run ` folds every non-zero exit status to 1
755+
756+ Measured on a hosted target: a program whose ` main ` returns 3 causes ` mcpp run `
757+ to exit 1. Both spawn sites end in ` return rc == 0 ? 0 : 1 ` , deliberately, so
758+ that 2 can mean "could not start" as distinct from 1 meaning "ran and failed".
759+ The freestanding path behaves the same way: qemu returns 3 and ` mcpp run `
760+ reports 1.
761+
762+ This is a documented-by-code contract rather than an accident, so it is recorded
763+ here rather than changed. It is worth deciding, because the universal command
764+ surface is the claim that running on a device is like running hosted, and a
765+ hosted ` run ` that cannot report a program's status is not that. Changing it
766+ means choosing what mcpp's own failures return once the child's status is
767+ passed through.
768+
769+ ### 17.6 Open: ` mcpp run ` 's ninja spins at 100% of one core with nothing to do
770+
771+ Observed three times during the sandbox verification — twice on
772+ ` riscv64-none-elf ` , once on ` aarch64-none-elf ` — always on the ` mcpp run ` that
773+ follows a ` mcpp build ` in the same project.
774+
775+ What was measured while it was happening:
776+
777+ | observation | reading |
778+ | ---| ---|
779+ | ninja processes | 3, each at 99.9% CPU |
780+ | child processes of each | ** zero** — no compiler was running |
781+ | system time (` stime ` ) | ** 0** — a pure user-space loop, not an I/O wait |
782+ | the graph | 8 edges, every output present, no missing input, no future mtime |
783+ | host load | 3.65 on 32 cores, i.e. one core each and nothing else |
784+
785+ So ninja had no work and burned 27 minutes of CPU deciding that.
786+
787+ IT IS NOT THE GRAPH. The same project built and run on the host completes in
788+ about two seconds, and its ` build.ninja ` carries the same two ` stage_file ` edges
789+ — the build-cache staging that first looked like the correlation. Running the
790+ same ninja binary over the same directory by hand exits 0.
791+
792+ IT DOES NOT REPRODUCE ON DEMAND. Re-running both sequences afterwards inside the
793+ same sandbox — ` mcpp run ` alone, and ` mcpp build ` followed by ` mcpp run ` —
794+ succeeded, printing the program's output and exiting 0. Whatever the trigger is,
795+ it is not the command sequence by itself.
796+
797+ One difference is recorded without a claim attached: the sandbox's ninja and the
798+ host's are DIFFERENT BINARIES AT THE SAME VERSION — 273768 bytes dynamically
799+ linked against 2202320 bytes static, different SHA-256, both answering ` 1.12.1 ` .
800+ That is worth resolving on its own terms, since it means "ninja 1.12.1" does not
801+ name one artefact, but it has not been shown to cause this.
802+
803+ A DETERMINISTIC COROLLARY, WORTH FIXING WHATEVER THE CAUSE IS: killing `mcpp
804+ run` does not kill the ninja it spawned. Each timed-out step left an orphan
805+ spinning at 100% of a core with its working directory already deleted, and one
806+ survived the removal of the whole sandbox it belonged to. Any CI that wraps
807+ ` mcpp ` in ` timeout ` leaks a busy core per timeout.
0 commit comments