Skip to content

Commit 9edd107

Browse files
committed
docs: root-cause remediation design for the 0.0.85 rollout incidents (A1-A4 + toolchain contract)
1 parent c74ea4a commit 9edd107

1 file changed

Lines changed: 170 additions & 0 deletions

File tree

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
# Root-cause remediation for the 0.0.85 rollout incidents (Design)
2+
3+
The 0.0.85 train shipped green, but four incident classes were handled with
4+
workarounds. This doc specifies the ROOT fixes — each with the exact code
5+
anchor of the defect, the design, and the exit criterion that retires the
6+
corresponding workaround. Companion to the roadmap's ops-incident log
7+
(`2026-07-08-descriptor-index-evolution-roadmap.md` §0).
8+
9+
Shared diagnosis, worth stating once: every one of these is the same abstract
10+
defect — **an implicit cross-boundary assumption about who provides something**
11+
(a symbol, a directory, an asset, a version). The fixes all take the same
12+
shape: make the provider explicit, verify at the boundary, fail loudly at the
13+
moment of divergence.
14+
15+
---
16+
17+
## A1 — macOS: test binaries must link the toolchain's own libc++ (repo `mcpp`)
18+
19+
**Defect anchor**: `src/build/flags.cppm:36-42` — distributable targets already
20+
link the static LLVM libc++ (correct, hermetic), but TestBinary targets
21+
deliberately take the SYSTEM `-lc++`. That exception is itself a workaround:
22+
static libc++ SIGABRTs during static destruction unless the entry point guards
23+
with `_Exit` (mcpp/xlings do; gtest's main does not). So the real chain is:
24+
25+
static-libc++ teardown crash → workaround: tests use system -lc++
26+
→ latent header/dylib split (toolchain headers vs Apple dylib)
27+
→ libc++ 22 moves string hashing out of line (__hash_memory)
28+
→ undefined symbol in every gtest link (incident A1)
29+
30+
**Root design**: tests are host-only by definition, so an rpath into the
31+
toolchain registry is acceptable FOR TESTS in a way it isn't for
32+
distributables. TestBinary on macOS (clang-with-cfg toolchains) links the
33+
toolchain's own libc++ **dynamically**:
34+
35+
ldStdlibTest = "-L<llvmRoot>/lib -lc++ -Wl,-rpath,<llvmRoot>/lib"
36+
37+
- Same-version headers and dylib → no symbol-surface gambling, ever. Future
38+
libc++ out-of-lining is self-consistent by construction.
39+
- Dynamic teardown avoids the static-destruction SIGABRT that motivated the
40+
system-lib exception — the exception's own root cause dissolves.
41+
- Distributables keep the static LLVM libc++ (unchanged, already hermetic).
42+
- `llvmRootForStdlib` is already threaded through `compute_flags`
43+
(`flags.cppm:173`); the change is confined to how `ldStdlibTest` is formed.
44+
45+
**Verification**: e2e on macOS+llvm — build a gtest test using
46+
`std::unordered_map<std::string,...>` (forces `__hash_memory`-class symbols);
47+
assert `otool -L` on the test binary references `<llvmRoot>/lib/libc++.1.dylib`
48+
and NOT `/usr/lib/libc++`. Add the inverse for a distributable (no libc++
49+
dylib reference at all).
50+
51+
**Exit criteria**: remove the `llvm@20.1.7` pin in `ci-macos.yml` (restore
52+
`xlings install llvm -y` first); delete the "TestBinary → system -lc++"
53+
comment block. LLVM 22.1.8 becomes usable on macOS.
54+
55+
## A2 — GCC 15 module instantiation: raise the cross-toolchain floor (repos `xlings-res` packaging + `mcpp` CI)
56+
57+
**Defect anchor**: `src/manifest/types.cppm` `force_template_instantiations()`
58+
— an anchor that recreates, deliberately, what the old single-file module
59+
provided by accident. It works but taxes every FUTURE module: any
60+
module-attached struct whose member instantiations aren't odr-used inside its
61+
own TU re-triggers the GCC 15 bug with an error that points nowhere.
62+
63+
**Root design**: the bug is fixed in GCC 16 (proven: native x86 gcc 16.1.0
64+
links the same code). The root fix is a toolchain floor, not code:
65+
66+
1. Package `xim-x-aarch64-linux-musl-gcc@16.1.0` (same GCC that the native
67+
builds already use) into xlings-res + xim-pkgindex — mirroring how the
68+
15.1.0 cross package was produced.
69+
2. Bump the cross toolchain in `cross-build-test.yml` and the release cross
70+
job to 16.1.0.
71+
3. Delete `force_template_instantiations()` and its declaration; the deletion
72+
commit is the regression test (cross CI goes red if the floor didn't take).
73+
74+
**Interim guard** (until the package exists): keep the anchor, but move the
75+
knowledge out of oral tradition — a paragraph in `docs/04-build-from-source.md`
76+
naming the constraint and its exit criterion, so the next person who hits
77+
"undefined reference to std::map<...>::map()" on cross finds it by grep.
78+
79+
## A3 — xlings: staged + atomic index acquisition (repo `xlings`)
80+
81+
**Defect anchor**: `xlings src/core/xim/downloader.cppm:53-73` — "already
82+
cloned → pull; pull failed → **remove and re-clone**". Two windows where the
83+
index dir is absent or partial (between remove and clone; after a failed
84+
clone), and the failure surfaces much later as
85+
`failed to build catalog: pkgs/ directory not found` — observed twice on
86+
2026-07-08 (macOS CI, xim-pkgindex linux-install-test). This is the concrete
87+
evidence for the staged-refresh half of the D3 design that mcpp couldn't
88+
implement from its side.
89+
90+
**Root design** (in xlings' downloader):
91+
92+
1. **Acquire into staging, never in place**: clone/pull into
93+
`<data>/.staging/<index>-<pid>/`; the live dir is not touched during
94+
acquisition.
95+
2. **Validate before swap**: `pkgs/` exists and is non-empty; if the tree
96+
carries `index.toml`, it parses. (This is also where xlings can honor
97+
`min_mcpp`-style contracts for its own consumers later.)
98+
3. **Atomic swap**: `rename(live, live.old); rename(staging, live);
99+
remove_all(live.old)`. Any failure → discard staging, keep the previous
100+
live tree, and error AT FETCH TIME with one retry round — not at catalog
101+
time with a hint telling the user to run `xlings update` themselves.
102+
4. **Catalog self-heal**: if catalog build still finds a broken index dir
103+
(legacy states), trigger one refetch instead of erroring into the void.
104+
105+
**Verification**: unit-style test in xlings — kill the clone mid-flight
106+
(cancellable path already exists, `downloader.cppm:101`), assert the live dir
107+
still serves the previous catalog; a full fetch then swaps cleanly.
108+
109+
**Exit criterion**: the `pkgs/ directory not found` class disappears from CI;
110+
the "try running: xlings update" hint becomes dead code and is removed.
111+
112+
## A4 — release pipeline: verified, gated mirroring (repo `mcpp`, `.github/`)
113+
114+
**Defect anchors**: `release.yml` publish-ecosystem job has no
115+
`timeout-minutes` (hung >1h; 6h ceiling); `mirror_res.sh` GH section trusts
116+
`gh release upload` ("gh --clobber, reliable") with no per-file verification —
117+
exactly the protection its own GitCode section already has. One stuck asset →
118+
silent incomplete mirror → downstream 404s (incidents 3 and 4 shared this
119+
root).
120+
121+
**Root design** — the mirror is done only when PROVEN done, on both hosts:
122+
123+
1. `publish-ecosystem`: `timeout-minutes: 20`.
124+
2. `mirror_res.sh`: wrap every upload (`gh` AND `gtc`) in `timeout 300`; after
125+
each GH upload, verify the asset's download URL answers 200/302 (same
126+
curl-check the GitCode section does), retry ≤5 with delete-and-reupload
127+
(the observed bad state cleared on re-upload).
128+
3. **Completeness gate**: after both hosts, diff actual asset lists against
129+
the expected list; any gap → exit 1. Drop "best-effort/non-blocking" for
130+
the mirror: both hosts are user-serving (GLOBAL and CN install paths), so
131+
an incomplete mirror must fail the job visibly instead of leaving a 404
132+
for the first user (or CI) to find.
133+
4. **Downstream race**: `ci-fresh-install` (workflow_run after release) races
134+
the index bump merge by design. Give it a bounded wait: poll
135+
`xlings search mcpp` / the index artifact until the released version
136+
appears (≤15 min) before installing, converting the race into a wait.
137+
138+
**Exit criterion**: a re-run of the 0.0.85-style failure (kill an upload
139+
mid-flight) fails the job with the missing asset named, instead of
140+
succeeding silently.
141+
142+
## C — toolchain channel version contract (repos `xim-pkgindex` + `mcpp`; the meta-fix)
143+
144+
The llvm 20.1.7→22.1.8 default bump propagated ungated and broke three things
145+
in one day. This is the same disease the index floor just cured for
146+
descriptors: **a channel whose evolution has no compatibility contract with
147+
its consumers**. Two complementary gates:
148+
149+
1. **Publisher-side (xim-pkgindex CI)**: toolchain-package bump PRs gain a
150+
consumer smoke — install the bumped toolchain, then `mcpp new + build + test`
151+
a program exercising std (string hashing included) on each platform. The
152+
llvm 22.1.8 bump would have failed this gate in the PR, not in production.
153+
2. **Consumer-side (mcpp)**: per-platform known-good ranges for toolchains it
154+
auto-selects (`toolchain/detect.cppm` defaults) — e.g. macOS llvm
155+
`>=20,<22` until A1 lands, then relaxed. Explicit user pins always win;
156+
the range only governs what mcpp picks silently.
157+
158+
A1's root fix removes the specific macOS ceiling; the contract remains as
159+
defense-in-depth for the next regression of any kind.
160+
161+
## Order of execution
162+
163+
| # | Fix | Cost | Unblocks |
164+
|---|---|---|---|
165+
| 1 | A4 (script + timeouts + gate) | ~1h, shell only | stops silent-404 class immediately |
166+
| 2 | A1 (ldStdlibTest → toolchain libc++) | ~0.5 day + macOS CI cycle | unpins llvm on macOS; users on 22.1.8 |
167+
| 3 | C.1 (xim-pkgindex consumer smoke) | ~0.5 day CI work | gates all future toolchain bumps |
168+
| 4 | A3 (xlings staged fetch) | ~1 day in xlings | kills the fetch-flake class |
169+
| 5 | A2 (gcc 16 cross package + floor) | packaging effort | deletes the instantiation anchor |
170+
| 6 | C.2 (consumer ranges) | small | optional once 1–5 land |

0 commit comments

Comments
 (0)