Skip to content

fix: self-contained Homebrew launcher, node back to :build - #11

Merged
samyakLambda merged 1 commit into
mainfrom
fix/homebrew-self-contained-launcher
Aug 17, 2026
Merged

fix: self-contained Homebrew launcher, node back to :build#11
samyakLambda merged 1 commit into
mainfrom
fix/homebrew-self-contained-launcher

Conversation

@samyakLambda

Copy link
Copy Markdown
Contributor

Real fix for #497 (the extra-Node-install complaint) — not the caveats-text
option, the shell-wrapper option. Both were on the table after the revert in
#10; this is the one I picked, with reasoning below.

Why this, not documenting the waste as intentional

rook's published npm package ships bin/rook.cjs: a small trampoline with
a #!/usr/bin/env node shebang, designed to bootstrap under whatever Node
launched it, resolve the bundled runtime via require.resolve, and re-exec
into it. Under the old, :required depends_on "node", Homebrew's own
Cleaner#rewrite_shebangs (language/node.rb) silently rewrote that
shebang to the absolute path of Homebrew's own node at build time — which
is what made the bottle work, and also exactly the extra install #497
complains about.

The trampoline's whole reason to exist is not knowing at publish time
whether a bundled runtime will be present. Homebrew's def install does
know — it just finished downloading and chmod'ing that exact binary. So
instead of relying on npm's shim plus Homebrew's shebang-rewrite trick, this
PR has def install write its own launcher directly:

#!/bin/sh
exec "<absolute path to the bundled node binary>" "<absolute path to dist/cli.js>" "$@"

Only in the success path (bundled runtime installed and verified) — the
existing opoo-and-fall-back-to-system-Node path is untouched, since npm's
original trampoline is exactly the right behavior there.

This is strictly better than either alternative on the table:

  • Actually fixes #497: nothing in the installed tree has an env node
    shebang left for Homebrew to rewrite (or fail to) — depends_on "node"
    goes back to => :build, this time correctly, since nothing depends on
    the rewrite happening.
  • No changes needed to the private monorepobin/rook.cjs is
    untouched; it's still exactly right for npm-direct/internal installs.
  • Simpler signal handling than the trampoline it replaces, for this
    path
    : exec replaces the shell's own process image, so a signal sent
    to the launcher's PID lands on the bundled node process natively, same
    PID, no relay. bin/rook.cjs's ~150 lines of SIGINT/SIGTERM/SIGHUP
    forwarding exist specifically because spawn() creates a child
    process that needs that relay — exec in a shell script doesn't have
    that problem to begin with.

Absolute paths baked into an installed script this way are ordinary,
relocatable Homebrew practice — any formula that writes a wrapper
referencing #{libexec} does the same; nothing new here.

Verification

Local brew install --build-bottle turned out to be broken in my
environment for reasons unrelated to this change — I confirmed this by
installing the already CI-verified, currently-live formula (pre-this-PR)
and getting the identical FormulaUnavailableError. Rather than fight that,
I pulled the real published @testmuai/rook and bundled-runtime npm
packages, reconstructed def install's exact pkg_dir layout by hand, and
ran the actual generated wrapper end to end with PATH=/usr/bin:/bin (no
node, no Homebrew, nothing):

$ env -i PATH="/usr/bin:/bin" ./rook --version
0.1.0
$ env -i PATH="/usr/bin:/bin" ./rook --help
Usage: rook [options] [command]
...

All 6 scripts/test-*.sh harnesses green, ruby -c, actionlint clean.

What it takes to actually ship

A bottle pours a pre-built keg — it never runs def install. Merging
this alone changes nothing for real users; def install's new code only
takes effect once build-bottles.yml rebuilds and republishes the 0.1.0
bottle. I'll dispatch that after merge and live-verify brew-smoke green
on both platforms before considering this done — the same live-verification
discipline that caught the #9 regression, this time before declaring
victory instead of after.

New test do assertion added as a structural guard on the actual
mechanism (checks the launcher's shebang, not just that rook --version
happens to work) — brew-smoke.yml's existing "node stays required" guard
is flipped back too, this time for the right reason.


Agent-authored: Claude Sonnet 5, Claude Code CLI.

@samyakLambda
samyakLambda merged commit b32f06a into main Aug 17, 2026
2 checks passed
samyakLambda added a commit that referenced this pull request Aug 17, 2026
Flip brew-smoke's node dependency-scope guard to match #11 (was asserting the pre-#11 invariant).
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