fix(hardlink): resolve an ambiguous out/ toward unsharing, and pin the linked-output scenario on a shape that exhibits it
CI / shellcheck + selftests (pull_request) Skipped
CI / shellcheck + selftests (pull_request) Skipped
Two review findings on #16. The `out/` discriminator keyed on Cargo's record of a build-script execution, which Cargo writes only AFTER the script exits successfully. A build script that populates OUT_DIR and then fails leaves a unit with no record at all, so its OUT_DIR read as a compile unit's artifact directory and stayed shared — a regression against the old `-name build` selection, which real-copied that state by construction. Reproduced on cargo 1.93.1 stable. An `out` directory now stays shared only when two independent signals agree: it holds an `.rlib`/`.rmeta` of its own, and its unit carries no execution record. Either one missing real-copies it. The cost is unchanged to the byte — the newly-unshared directories hold only executables and `*.d`, both already privately owned by the file rules. The live linked-test-binary scenario was built on the lib+bin probe crate, whose test binaries relink to a fresh inode — a shape gitdan-actions#17 records as measured safe. Both halves passed green against the unfixed selection on dep-info mutations the previous scenario already covers. It now builds a bin-only crate with a unit test, reads only executables, and skips loudly with a warning rather than passing quietly if the toolchain does not exhibit the rewrite at all. Also: drop a clause asserting the linker writes in place "whenever the path has no other hard link", which this change's own evidence denies; move the load-bearing comment block back above `unshare_mutable_paths`; correct a superseded 99.998% figure; and record both cost rows in the README rather than only the flattering whole-tree one.
This commit is contained in:
@@ -57,14 +57,35 @@ Layout v2 regroups everything per build unit under
|
||||
`.fingerprint` and no `deps` to key off and `build/` is no longer a proxy for
|
||||
"metadata" — it is the whole tree. The one place the two layouts genuinely
|
||||
differ is that under v2 a build script's `OUT_DIR` and a compile unit's rlib
|
||||
are both a directory called `out`; the run unit is told apart by Cargo's record
|
||||
of the execution beside it (`run/root-output` under v2, a loose `root-output`
|
||||
under v1). v2 is the nightly default and stabilises in cargo 1.100.0 on
|
||||
2026-11-12.
|
||||
are both a directory called `out`.
|
||||
|
||||
Measured real-copied share of a 5.5 GB Bevy target directory: **9.0% before,
|
||||
14.1% after** — the increase is the linker-output rule, not the layout work.
|
||||
On a scratch crate the layout fix alone takes v2 from 99.996% to 0.2%.
|
||||
**Ambiguity there resolves toward unsharing**, because over-unsharing costs
|
||||
bytes and under-unsharing costs corruption. An `out` directory stays shared
|
||||
only when two independent signals agree it is a compile unit's: it holds an
|
||||
`.rlib`/`.rmeta` of its own, *and* its unit carries no record of a build-script
|
||||
execution beside it (`run/` under v2, a loose `root-output` under v1). The
|
||||
execution record alone is not enough — Cargo writes it only after the script
|
||||
succeeds, so a build script that populates `OUT_DIR` and then fails leaves a
|
||||
unit that reads as a compile unit. v2 is the nightly default and stabilises in
|
||||
cargo 1.100.0 on 2026-11-12.
|
||||
|
||||
### What it costs
|
||||
|
||||
Real-copied share of a 5.5 GB Bevy target directory, before and after the
|
||||
linker-output rule landed:
|
||||
|
||||
| tree | before | after |
|
||||
|---|---|---|
|
||||
| **excluding `incremental/`** — the figure to plan against, since the quick-start below sets `CARGO_INCREMENTAL: 0` | **36.4%** | **57.0%** |
|
||||
| whole tree, `incremental/` included (a local dev checkout, not CI) | 9.0% | 14.1% |
|
||||
|
||||
The first row is the one a CI consumer gets. The increase is the linker-output
|
||||
rule, not the layout work: on a scratch crate the layout fix alone takes v2
|
||||
from 99.996% to 0.2%.
|
||||
|
||||
The copy is paid per clone and does not amortise — a fresh `cp -al` leaves
|
||||
every file with `nlink >= 2`, so the `-links +1` filter cannot skip anything —
|
||||
and a clone happens twice per job, once seeding and once publishing.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user