Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,35 @@ publish pack emit toolchain cache index self

⚠️ 代价见 §1.5(首次墙钟),前提是 `--gc-sections`(已随 #550 落地)。

#### 4.2.1 回填(2026-09-04):我先走了被否掉的那条,而测量证实了这张表

⚠️⚠️ **我先做了 prebuilt(`xim:picolibc-arm`),把上表右列的每一项代价都付了一遍。**
七个多库分别构建、`libdir` 列填满(本表说它应为空)、9 MB 载荷两端镜像。走完之后
才回到源码包。记在这里,是因为**结论会被复查而理由不会**,而这次的理由拿到了证据:

| 测到的 | 意味着 |
|---|---|
| 七份 `include/` 树**逐字节相同**(含 meson **生成**的 `picolibc.h`/`newlib.h`) | prebuilt 发了同一个目录的七份拷贝;源码包**根本没有多库** |
| 七个档位编译的源文件列表**完全相同**(1130 个,sha `b109bf04`) | 档位之间只差**编译标志** —— 正是「用消费者自己的 `compile_flags` 编」 |
| 按 `<march>/<mabi>` 索引,七档**塌成五个目录**,软浮点行拿到带 `Tag_ABI_HardFP_use` 的库 | ARM 的 `mabi` 是过程调用标准不是浮点 ABI ⇒ **prebuilt 的键在 ARM 上根本不成立**,而构建期毫无迹象 |

⇒ **§4.2 的判断是对的,而且比它自己写下的更强**:不只是「代价更小」,是 prebuilt 的
多库键在这个架构上**不可能正确**。

⭐ 源码包这条路自己的代价也测出来了,四条都是「上游按**归档**分层,而一个扁平库不
能」:`semihost`/`dummyhost` 同名符号靠两个 `.a` 分开;`semihost/fake` 是
`common` 的**补集**(26 选 13),而补集**无法从文件名算出** —— `fake_io.c` 与
`common/iob.c` 撞在 `stdin`/`stdout`/`stderr` 上;machine 目录含每个 ARM 档位的变体;
以及**基础集合里的负 glob 作用于合并后的集合**,会把按目标追加回来的那一份取消掉。

⚠️ 还有一条只有**跑**才能发现:板级启动必须设线程指针。picolibc 经 TLS 取
`stdout`,没有它则程序**链接干净、运行、什么都不打印、然后挂住** —— 没有任何诊断,
唯一的证据是沉默。`cortex-m-rt` 的 `Reset_Handler` 因此调 `_init_tls`/`_set_tls`,
链接脚本给出它们读的**五个**符号(只给 `__tls_base` 会链接通过再在另外四个上失败)。

`xim:picolibc-arm` 保持已发布状态(发布过就是发布过),但**不是**推荐路径,其描述符
已写明。

---

## 5. openarch:部分后端 + 真实应用
Expand Down Expand Up @@ -648,3 +677,131 @@ MCPP_NO_AUTO_INSTALL=1 mcpp run 的输出里 必须 出现 run 档的工具
* 批 2、批 5 与批 1'/1" **互不依赖**,可同时进行。
* 批 4 依赖批 1'(要有 Cortex-M 后端才能在上面实现 `arch_trap_switch`)。
* 批 6 独立,任何时候都能做;它只在批 3 的 `hardware` feature 要真跑时才成为阻塞。

## 17. Measured outcomes of the ecosystem batch (2026-09-04)

This section records what the implementation measured, including the two places
where a decision written earlier in this document was wrong.

### 17.1 A package that vendors upstream sources takes upstream's identity

The rule the batch now follows is that both halves of a package's identity come
from upstream when the content does. `picolibc.picolibc` and
`llvm.compiler-rt-builtins` are named for the projects whose code they carry,
and are versioned `1.8.12` and `22.1.8` for the releases they vendor. Only a
package whose content this organisation wrote takes `mcpplibs` — `cortex-m-rt`,
`openarch`, and the two board packages do.

The claim is verified rather than asserted. Every one of picolibc's 2109
vendored files is byte-identical to the upstream 1.8.12 release, whose
`meson.build` declares that version; every one of the 347 vendored builtins is
byte-identical to `compiler-rt/lib/builtins` at the tag `llvmorg-22.1.8`. Both
comparisons were made file by file against upstream, not inferred from a
directory name.

A packaging change moves a fourth segment, which is what allows the first three
to stay upstream's. `1.8.12.3` is upstream 1.8.12 with this organisation's third
packaging of it.

### 17.2 A BARE REQUIREMENT IN mcpp IS AN EXACT PIN, NOT A CARET

Two releases shipped broken because of a belief this document did not check.
`mcpp.version_req` documents a caret default — `"1.2.3"` means `>=1.2.3, <2.0.0`
— and that is true of the MATCHING grammar and false of the RESOLUTION path.
`mcpp::pm::is_version_constraint` returns false for a bare number, so
`resolveSemver` never runs and the literal string becomes the wire address.

Measured, with only `22.1.8.2` in the index and a dependency written `"22.1.8"`:

Downloading llvm.compiler-rt-builtins v22.1.8
error: package 'llvm.compiler-rt-builtins@22.1.8' install path missing after fetch

xlings had resolved the requirement to the real version and installed it; mcpp
then looked for a directory named after the requirement. THE DIAGNOSTIC NAMES
THE SYMPTOM AND NOT THE CAUSE, which is why the belief survived being tested —
"install path missing after fetch" reads like a corrupt download.

The consequence for the scheme in 17.1 is that a consumer writes the packaging
revision in full, and a packaging release costs a re-pin in every consumer. That
is the same cost every other version bump has here, and it is stated in both
manifests so the next reader does not rediscover it.

### 17.3 A dependency path that escapes the package cannot be released

Both broken releases carried a `path = "../sibling"` override, which is how each
package is developed against an unreleased neighbour. IT IS INVISIBLE TO THE
PUBLISHING REPOSITORY'S OWN CI, because the sibling is there: the build succeeds
and the job is green. It fails only for a consumer, after the version has been
published and mirrored.

Both repositories now refuse it in CI, and the index audit checks published
manifests for it as well — the audit that passed the broken release checked
identity, version and checksum, and none of those three were wrong.

### 17.4 A `main` that returns hung for ever

`cortex-m-rt` called `board_main` for effect and then spun, on the reasoning
that a freestanding entry has no caller to report to. C defines returning from
`main` as `exit(status)`, and both tiers have somewhere to report: with a C
library, `exit` runs the `atexit` handlers and flushes the streams; with none,
semihosting is how the package's own console already reached the host.

EVERY TEST OF THE PACKAGE TOOK THE ONE PATH THAT WORKED, because the template
writes an explicit `board::exit(0)`. A program ending in `return 0` printed its
output and then ran until something killed it. The CI step added for it imposes
its own `timeout`, since a regression does not fail but hangs.

### 17.5 Open: `mcpp run` folds every non-zero exit status to 1

Measured on a hosted target: a program whose `main` returns 3 causes `mcpp run`
to exit 1. Both spawn sites end in `return rc == 0 ? 0 : 1`, deliberately, so
that 2 can mean "could not start" as distinct from 1 meaning "ran and failed".
The freestanding path behaves the same way: qemu returns 3 and `mcpp run`
reports 1.

This is a documented-by-code contract rather than an accident, so it is recorded
here rather than changed. It is worth deciding, because the universal command
surface is the claim that running on a device is like running hosted, and a
hosted `run` that cannot report a program's status is not that. Changing it
means choosing what mcpp's own failures return once the child's status is
passed through.

### 17.6 Open: `mcpp run`'s ninja spins at 100% of one core with nothing to do

Observed three times during the sandbox verification — twice on
`riscv64-none-elf`, once on `aarch64-none-elf` — always on the `mcpp run` that
follows a `mcpp build` in the same project.

What was measured while it was happening:

| observation | reading |
|---|---|
| ninja processes | 3, each at 99.9% CPU |
| child processes of each | **zero** — no compiler was running |
| system time (`stime`) | **0** — a pure user-space loop, not an I/O wait |
| the graph | 8 edges, every output present, no missing input, no future mtime |
| host load | 3.65 on 32 cores, i.e. one core each and nothing else |

So ninja had no work and burned 27 minutes of CPU deciding that.

IT IS NOT THE GRAPH. The same project built and run on the host completes in
about two seconds, and its `build.ninja` carries the same two `stage_file` edges
— the build-cache staging that first looked like the correlation. Running the
same ninja binary over the same directory by hand exits 0.

IT DOES NOT REPRODUCE ON DEMAND. Re-running both sequences afterwards inside the
same sandbox — `mcpp run` alone, and `mcpp build` followed by `mcpp run` —
succeeded, printing the program's output and exiting 0. Whatever the trigger is,
it is not the command sequence by itself.

One difference is recorded without a claim attached: the sandbox's ninja and the
host's are DIFFERENT BINARIES AT THE SAME VERSION — 273768 bytes dynamically
linked against 2202320 bytes static, different SHA-256, both answering `1.12.1`.
That is worth resolving on its own terms, since it means "ninja 1.12.1" does not
name one artefact, but it has not been shown to cause this.

A DETERMINISTIC COROLLARY, WORTH FIXING WHATEVER THE CAUSE IS: killing `mcpp
run` does not kill the ninja it spawned. Each timed-out step left an orphan
spinning at 100% of a core with its working directory already deleted, and one
survived the removal of the whole sandbox it belonged to. Any CI that wraps
`mcpp` in `timeout` leaks a busy core per timeout.
Loading