Skip to content

Commit 1c2603c

Browse files
docs(plan): §4.2 回填 —— 测量证实了源码包这一列 (#554)
* docs(plan): §4.2 回填 —— 我先走了被否掉的那条,而测量证实了这张表 ⚠️⚠️ 我先做了 prebuilt(`xim:picolibc-arm`),把 §4.2 右列的每一项代价都付了一遍, 走完之后才回到源码包。记下来,是因为结论会被复查而理由不会 —— 而这次理由拿到了证据: * 七份 include 树逐字节相同(含 meson 生成的 picolibc.h/newlib.h)⇒ prebuilt 发了 同一个目录的七份拷贝; * 七个档位的源文件列表完全相同 ⇒ 档位之间只差编译标志; * 按 <march>/<mabi> 索引时七档塌成五个目录,软浮点行拿到带 Tag_ABI_HardFP_use 的 库 ⇒ prebuilt 的多库键在 ARM 上不可能正确,而构建期毫无迹象。 源码包自己的代价也测出来了(四条「上游按归档分层而扁平库不能」),外加一条只有跑 才能发现的:板级启动必须设线程指针,否则程序链接干净、运行、什么都不打印、挂住。 * docs(plan): §17 —— the batch's measured outcomes, including two wrong decisions Records what the ecosystem batch measured: upstream-derived packages take upstream's namespace and version (verified byte for byte against both upstream releases); a bare requirement in mcpp is an exact pin and not a caret, which shipped two broken releases before the diagnostic was understood; an escaping path dependency is invisible to the publishing repository's own CI; a `main` that returns hung for ever because every test took the one path that worked; and one open question — `mcpp run` folds every non-zero exit status to 1. * docs(plan): §17.6 —— mcpp run 的 ninja 空转满一个核 Observed three times in the sandbox verification, always on the `mcpp run` after a `mcpp build`: ninja at 99.9% CPU with zero children and zero system time, on a complete eight-edge graph with nothing to do. Not the graph — the host runs the same project, with the same two `stage_file` edges, in about two seconds. Not reproducible on demand afterwards in the same sandbox. One difference recorded without a claim: the sandbox's ninja and the host's are different binaries at the same version. The deterministic corollary is worth fixing regardless: killing `mcpp run` does not kill its ninja, so every timed-out step leaks a spinning core — one orphan outlived the removal of its entire sandbox. --------- Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com>
1 parent feeb611 commit 1c2603c

1 file changed

Lines changed: 157 additions & 0 deletions

File tree

.agents/docs/2026-09-04-named-runners-and-the-universal-command-surface.md

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)