Skip to content

feat: Formula writes rook-keg-marker.json, the keg's own identity record - #16

Merged
samyakLambda merged 2 commits into
mainfrom
feat/keg-marker
Aug 20, 2026
Merged

feat: Formula writes rook-keg-marker.json, the keg's own identity record#16
samyakLambda merged 2 commits into
mainfrom
feat/keg-marker

Conversation

@samyakLambda

Copy link
Copy Markdown
Contributor

What

def install writes the keg's own identity record — rook-keg-marker.json, {v: 1, formula: "rook", version} — at the keg root beside Homebrew's INSTALL_RECEIPT.json. test do asserts all three fields, and brew-smoke inherits the assertion through the brew test call it already makes.

One file, no absolute paths inside the record, written unconditionally (the opoo fallback still installs rook, and the keg is rook's either way).

Why

rook update (LambdatestIncPrivate/rook#576) must establish that the keg its running copy sits in is rook's own before it prints brew upgrade <tap>/rook — the tap is read from the receipt, and a receipt that isn't rook's must never supply it.

The receipt cannot carry that fact. Read against a real one: its keys are aliases, arch, built_as_bottle, …, source, and source holds path, spec, tap, tap_git_head, versionsno field names the formula. For an API-loaded formula, source.path is the shared formula.jws.json cache, not a per-formula path. Without a record of our own, "is this keg rook's?" is only answerable by inferring from the Cellar/rook/<v> path shape, which the reader's design forbids — and a receipt alone, found above a package root, could be planted by an untrusted repository to get its source.tap printed into a pasteable command (#576, review round 6, #35).

No absolute paths inside the record: bottles are poured into whatever prefix the host uses, and cellar: :any_skip_relocation means nothing would rewrite one.

The shipped 0.1.0 bottle predates the marker. Those installs degrade on the private side to printing candidates rather than the fully-qualified upgrade line, and heal on the next brew upgrade — the same migration-by-omission as pre-manifest curl installs (#15).

Evidence

Verified by a real from-source install, staged through a throwaway local tap (brew tap-new + brew install --build-from-source):

  • the marker lands at the keg root beside INSTALL_RECEIPT.json, byte-exact: {"v":1,"formula":"rook","version":"0.1.0"}
  • all three new test do assertions pass, executed against the installed keg
  • the keg's own launcher runs: bin/rook --version0.1.0

Without the change the file does not exist — the shipped 0.1.0 bottle demonstrates the absence — so the test's JSON.parse(read) raises rather than passing vacuously.

Two honest caveats: brew test itself could not run on the dev machine (the keg would not link past a developer symlink already occupying HOMEBREW_PREFIX/bin/rook; the assertions were executed manually against the keg instead — CI runners link cleanly), and the bottled path is verified by the next release's brew-smoke run, not by this PR, since bottles are cut by build-bottles.yml after merge.

ruby -c clean. brew style reports only the pre-existing, deliberate FormulaAudit/ComponentsOrder offense documented in the formula. The update-formula.yml sed anchors (url/sha256/version) are untouched — the write sits inside def install, far from the patched lines.

Not in this PR

🤖 Generated with Claude Code

`rook update` (LambdatestIncPrivate/rook#576) must establish that the keg
its running copy sits in is rook's own before it prints `brew upgrade` for
it. Homebrew's INSTALL_RECEIPT.json cannot carry that fact: it has no
formula-name field, and for an API-loaded formula source.path is the shared
formula.jws.json cache — so without a record of our own, the only
implementation is inferring from the Cellar path, which the reader refuses
to do.

def install now writes {v: 1, formula: "rook", version} to
<keg>/rook-keg-marker.json, unconditionally (the opoo fallback still
installs rook), with no absolute paths inside so bottles pour and relocate
cleanly under :any_skip_relocation. test do asserts all three fields, and
brew-smoke inherits the assertion through the brew test call it already
makes.

Verified by a real from-source install via a throwaway local tap: the
marker lands at the keg root beside INSTALL_RECEIPT.json with exactly
{"v":1,"formula":"rook","version":"0.1.0"}, and all three test-do
assertions pass executed against the installed keg. Without this change
the file does not exist (the shipped 0.1.0 bottle demonstrates it) and
the test's read raises ENOENT. brew style reports only the pre-existing,
deliberate ComponentsOrder offense; the update-formula.yml sed anchors
(url/sha256/version) are untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- guard the test do marker assertions on existence: bottle do's sha256
  and version are unchanged by this PR, so brew install keeps pouring
  the pre-marker 0.1.0 bottle until a rebuild lands one that has it —
  unguarded, any brew-smoke run dispatched against 0.1.0 in that
  window hits an uncaught Errno::ENOENT instead of a clean pass
- write the marker via temp-file-then-rename so a crash mid-write
  can't leave a truncated file on disk (same reasoning as install.sh's
  manifest write, #15)
- derive formula: name instead of hardcoding "rook" at both the write
  and test sites
- test do independently re-derives the "marker sits five levels above
  pkg_dir" layout claim instead of only reading back through prefix,
  which def install also wrote through

Two findings from the same review are out of scope for this PR and
tracked separately, since the fix lives in the private CLI repo:
readKegRecord() never validates the marker's version field
(LambdatestIncPrivate/rook#635), and there's no contract test between
this Formula's output and that reader (LambdatestIncPrivate/rook#636).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@samyakLambda

Copy link
Copy Markdown
Contributor Author

Ran a fresh code-review pass (find → adversarial verify) and pushed 7a91587 fixing the real findings.

Finding Fix
test do's marker read has no existence guard — and bottle do's sha256/version are unchanged by this PR, so brew install keeps pouring the pre-marker 0.1.0 bottle until a rebuild. Confirmed via build-bottles.yml (manual-dispatch only, not triggered by this merge) and brew-smoke.yml (brew install + brew test, no --build-from-source): any brew-smoke run dispatched against 0.1.0 before the next rebuild hits an uncaught Errno::ENOENT instead of a clean pass. guarded on existence — asserts normally when present, opoos and skips when the keg predates the marker
marker write (Pathname#write) isn't atomic temp-file-then-mv in the same directory, same reasoning as install.sh's manifest write (#15)
formula: "rook" hardcoded at both the write and test sites derived from name instead
test do's marker check only reads back through prefix, the same variable def install wrote through — positionally circular w.r.t. the write-side comment's "five levels above pkg_dir" claim test do now independently re-derives that path arithmetic and asserts it

Two findings from the same pass are real but out of scope here (the fix lives in the private CLI repo, not this Formula) — filed and linked rather than touched:

  • readKegRecord() never validates the marker's version field — LambdatestIncPrivate/rook#635
  • no contract test between this Formula's marker output and that reader — LambdatestIncPrivate/rook#636

Verified: ruby -c clean, brew style reports only the pre-existing documented ComponentsOrder offense (no new ones). The guard and atomic-write logic were also verified in isolation with a standalone Ruby reproduction — including confirming the old code really does raise Errno::ENOENT in the missing-marker case the guard now handles — since a full brew install --build-from-source needs a real npm download I didn't want to gate this on.

@samyakLambda
samyakLambda merged commit 99723b2 into main Aug 20, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant