Commit b794856
* fix(build): stage BMIs through mcpp instead of an in-place shell copy (#311)
Windows-only symptom, three overlapping defects.
1. `rule cp_bmi` overwrote the destination IN PLACE
(`powershell Copy-Item -Force` / `cp -f`). mcpp writes the staged std BMI
path into compile_commands.json so clangd can resolve `import std;`, clangd
memory-maps that very file, and Windows refuses to replace a file with an
open user-mapped section — error 1224, reported as a bare "build failed".
Staging now runs through `mcpp stage` (new internal subcommand, same shape
as `mcpp dyndep`): skip when the destination is already equivalent, else
temp-file + rename, else in-place, with retries, and a failure message that
names the file and the likely holder. Never downgraded to a warning — a
stale BMI turns into a confusing "module 'std' not found" or a silently
mismatched link.
The rule is shared with Windows runtime-DLL deployment, which had the same
hazard against a program still running from a previous `mcpp run`.
2. `default_cache_root()` was a private copy of the home resolution, unchanged
since v0.0.1: no %USERPROFILE% branch, no self-contained detection. On
Windows PowerShell (no $HOME) the std BMI cache landed in the *current
working directory* as `.mcpp-bmi/` while dep BMIs went to
%USERPROFILE%\.mcpp\bmi — two roots for one cache, the second cwd-dependent,
which is what made a re-stage a routine event. New leaf module `mcpp.home`
is now the single resolver (config, stdmod, prepare's git cache and two more
copies in doctor all route through it).
3. The staging rule carried no `restat`, so any re-stage recompiled everything
that imports std even when the bytes were identical. Adding `restat = 1` on
top of the no-write path fixes that — measured: touching the cache-side BMI
now runs the stage edge alone, and the next build is "no work to do".
Note for the record: aligning the destination's mtime with the source
defeats restat and re-triggers the cascade, so a skipped stage touches no
timestamps at all.
Also: `FAILED: <target>` survives the ninja output filter (normalized to
`failed: <target>`) — dropping it is why the report couldn't tell a staging
failure from a compile error; `mcpp new` ignores `.mcpp/`; `mcpp doctor` points
at a leftover `.mcpp-bmi/`.
Design + plan: .agents/docs/2026-07-30-issue311-*.md
Tests: unit test_home (4), test_build_stage (10), test_ninja_backend (+4),
e2e 170 (no-cascade + cache root), e2e 171 (held destination, PowerShell
MemoryMappedFile on Windows — reproduces #311 without needing clangd).
* chore: 2026.7.30.1 — BMI staging + cache-root convergence (#311)
Bootstrap pin stays at 2026.7.29.1: it names an already-published mcpp and is
bumped separately, after the release exists in xim-pkgindex.
* fix(build): report the staged file's absolute path and drop ninja's trailing pad
Two cosmetics found by driving a real staging failure end-to-end through ninja:
the `FAILED: <target>` line ninja emits carries a trailing space, and `$out` is
relative because ninja runs staging with cwd = the build directory — the reader
of that diagnostic is the person who has to go unlock the file.
* test(e2e): make the locked-destination case unable to pass for the wrong reason
Two holes in 171 as first written: a PowerShell holder that failed to start left
the build trivially succeeding (silent pass), and `chmod 444` is toothless under
root — which the container e2e job runs as. The holder now signals readiness
through a sentinel and the test fails if it never appears, and the load-bearing
assertion is inode+mtime+size invariance of the destination: 'nothing was
written', not 'the write happened to succeed'.
* fix(build): verify staging by content, and route the last in-place copy through it
Windows CI caught two things the Linux-only self-checks structurally could not.
1. `rule runtime_alias` was a SECOND `Copy-Item -Force`. PE has no soname
symlink, so an alias is a copy of a freshly built DLL — the same hazard as
BMI staging (a program still running from a previous `mcpp run` holds the old
one). Its Windows branch now goes through `$mcpp stage` too; POSIX keeps
`ln -s`, where the symlink is semantics and not merely how the file is
written. The 'no Copy-Item anywhere' assertion is a no-op on Linux, which is
why only the Windows job could find this.
2. Size-only equivalence was unsound for exactly those DLL payloads: PE section
padding makes 'genuinely rebuilt, identical size' ordinary, so a stale DLL
could survive in the build dir. Content comparison is unconditionally
correct — a destination equal to the source needs no write — and it only runs
when ninja has already decided the edge is dirty. It is now the default;
`--verify size` / MCPP_STAGE_VERIFY=size stays for callers that know the
source is fingerprint-scoped, and an unrecognized value falls back to the
SAFE mode rather than the fast one.
Also from CI: a read-only destination is replaceable on POSIX (rename rewrites
the directory entry) but not on Windows, so that test now asserts both outcomes
per platform instead of one; the e2e scripts unescape ninja node names (a
Windows drive letter arrives as `C$:/Users/...`) and use BSD `stat -f` when GNU
`stat -c` is absent (macOS).
* test(e2e): don't let `wait` on the killed holder abort 171 (exit 143)
`wait` on a job we just killed returns 143, and it sat as the last command of an
`&&` list under `set -e` — so on Windows the script died the moment the mapping
was released, before asserting anything about the build it had just run. Probed
the shell semantics directly rather than guessing: the old form exits 143 on
Linux too, it only never ran there because HOLDER is empty on POSIX.
* fix(env): private-glibc strip was defeated by the composed override
Found while attributing an e2e 156 failure on this branch (NOT caused by the
staging change — proven by diffing the generated build.ninja for that exact
project: it differs only in an unused rule's text).
`process.cppm::merged_environ` strips mcpp's private-glibc payload entries from
an INHERITED LD_LIBRARY_PATH, and plan.cppm's comment states that guarantee. But
merged_environ takes explicit `extra` overrides verbatim, and
`env::prepend_path_list` — which composes exactly such an override for ninja and
for run targets — appended the inherited value RAW. So the strip was bypassed
precisely in the case it exists for: a nested `mcpp run` → `mcpp test` chain,
where a payload tool patched against a different glibc then segfaults inside the
dynamic linker before main (bare `__vdso_time`, then SIGSEGV).
The predicate moves to mcpp.platform.env, next to path-list composition, since
both halves of the guarantee need it; prepend_path_list now sanitizes only the
inherited TAIL, so a payload dir the caller passed explicitly — the entry the
sandbox binary actually needs — always survives. PATH is untouched.
Why it looked like a regression: the two CI runs restored DIFFERENT sandbox
caches (…-01baa227… vs …-0e74cc64…), so the payload version set differed and the
latent bug only showed on one side. Before this fix, 156 was green only when the
poisoned payload version happened to match what the tools expected.
* fix(build): verify fingerprint-scoped staging by size, so a read-denying holder can't fail the build
The Windows job's own repro exposed the gap: my holder maps the destination with
FileShare.None, so `same_content` cannot even OPEN it → equivalence is
undecidable → mcpp attempts the write → ERROR_SHARING_VIOLATION(32) → build
failed. Which is precisely the #311 outcome, arrived at from the other side.
Reading needs an open; SIZE comes from directory metadata and does not. So the
verify mode is now per-edge rather than one global setting:
std BMI / std.o / std.compat.* --verify size fingerprint-scoped: the cache
dir and the build dir share
the fp covering compiler
identity, triple, stdlib, std
source hash and dialect flags
⇒ equal size IS equivalence
Windows DLL deploy / runtime_alias content not fp-scoped, and a rebuilt
DLL keeps its size (PE
sections are page-padded)
So #311's actual path — clangd mapping the std BMI — now survives even an
EXCLUSIVE lock, while a possibly-stale DLL is still gated byte-for-byte.
Carried on a per-edge `$verify` ninja variable; the separating space lives in the
rule's command string because ninja trims trailing whitespace in variable values.
Verified on Linux with chmod 000 (the closest analogue of a read-denying holder):
the fp-scoped edge skips and the build succeeds.
Also: bootstrap pin 2026.7.29.1 → 2026.7.29.2. Unrelated to this work and
pre-existing on main — the index no longer serves .1 (`available: 2026.7.29.2`),
so the aarch64 fresh-install job fails on main too. .2 is released and indexed,
and the guard's rule (both pins equal, never newer than the building version)
still holds.
---------
Co-authored-by: sunrisepeak <speakshen@163.com>
1 parent 3b1cb6b commit b794856
24 files changed
Lines changed: 1942 additions & 131 deletions
File tree
- .agents/docs
- src
- build
- cli
- platform
- scaffold
- toolchain
- tests
- e2e
- unit
Lines changed: 386 additions & 0 deletions
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
3 | 3 | | |
4 | 4 | | |
5 | 5 | | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
6 | 47 | | |
7 | 48 | | |
8 | 49 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | | - | |
| 3 | + | |
4 | 4 | | |
5 | 5 | | |
6 | 6 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
190 | 190 | | |
191 | 191 | | |
192 | 192 | | |
193 | | - | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
194 | 198 | | |
195 | 199 | | |
196 | 200 | | |
| |||
0 commit comments