Skip to content

install(isolated): link bins of -g update requests into $BUN_INSTALL/bin - #30451

Open
robobun wants to merge 1 commit into
mainfrom
farm/5dcee19a/isolated-global-bin-link
Open

robobun wants to merge 1 commit into
mainfrom
farm/5dcee19a/isolated-global-bin-link

Conversation

@robobun

@robobun robobun commented May 10, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #30450.
Fixes #28597.

Repro

$ export BUN_INSTALL=/tmp/repro/.bun
$ bun add -g --linker isolated cowsay
installed cowsay@1.6.0 with binaries:
 - cowsay
 - cowthink
66 packages installed

$ ls $BUN_INSTALL/bin/
# empty — expected: cowsay, cowthink
$ ls $BUN_INSTALL/install/global/node_modules/.bin/
cowsay    cowthink

With the hoisted linker the same command symlinks cowsay and cowthink
into $BUN_INSTALL/bin/ correctly. The reporter hit this via
linker = "isolated" in ~/.bunfig.toml on Windows — same code path.

Cause

src/install/isolated_install/Installer.rs — link_dependency_bins
iterates the parent entry's direct deps and calls bin::Linker::link(false)
unconditionally. With global=false, the linker writes into
node_modules/.bin/ (the staged store entry) and never consults
options.bin_path ($BUN_INSTALL/bin/).

Compare the hoisted installer (src/install/PackageInstaller.rs), which
computes per-package:

let global = if !manager.options.global || tree_id != 0 {
    false
} else {
    'global: {
        for request in manager.update_requests.iter() {
            if request.package_id == package_id {
                break 'global true;
            }
        }
        break 'global false;
    }
};

and passes it to bin_linker.link(global). The isolated installer had no
equivalent.

Fix

For the root entry's direct deps, set global = true iff options.global
is true and the dep's pkg_id matches a manager.update_requests[*].
This mirrors the hoisted logic. Non-root entries and transitive deps keep
global = false — their bins belong in the parent's local .bin/.

Both call sites are updated (the initial call + the retry-without-native-
binlink path).

Verification

test/cli/install/isolated-install.test.ts — two new tests under a
describe("global install", …) block:

  1. bun add -g --linker=isolated <pkg-with-bin> symlinks the bin into
    $BUN_INSTALL/bin/. Fails on main (bin absent), passes with the fix —
    this is the load-bearing regression gate.
  2. A non-global bun install with the isolated linker links the bin into
    the project's node_modules/.bin/, never into $BUN_INSTALL/bin/ —
    guards the options.global half of the gate.

File-path deps are used so the tests don't depend on the verdaccio harness.

Rebase notes

Rebased from the pre-Rust-port tree onto current main (Bun is now Rust).
The fix lives in src/install/isolated_install/Installer.rs. Conflicts were
trivial: an adjacent append in the test file (main's new alias-traversal
test) and a reworded comment next to the new block in Installer.rs — both
kept alongside each other.


no test proof · iteration 8 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/cli/install/isolated-install.test.ts

@robobun

robobun commented May 10, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 9:48 AM PT - Aug 14th, 2026

@robobun, your commit 92ac61a is building: #96259

@coderabbitai

coderabbitai Bot commented May 10, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 17 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 6012fba3-ae93-4363-ac79-732c2eab6eea

📥 Commits

Reviewing files that changed from the base of the PR and between eabb96d and 92ac61a.

📒 Files selected for processing (2)
  • src/install/isolated_install/Installer.rs
  • test/cli/install/isolated-install.test.ts

Walkthrough

The isolated linker computes a per-package global bin flag for --global installs when the parent entry is .root and the package appears in update requests, passes that flag to bin_linker.link(...) (including retry paths), and adds tests verifying top-level bins are linked globally while transitive bins are not.

Changes

Global bin linking for isolated linker

Layer / File(s) Summary
Global bin decision logic
src/install/isolated_install/Installer.zig
linkDependencyBins computes link_into_global_bin for --global installs when parent is .root, then refines a per-package global boolean by checking manager.update_requests.
Bin linker invocation
src/install/isolated_install/Installer.zig
bin_linker.link(...) now receives the computed global boolean instead of always false, applied to both the initial attempt and the retry path.
Global install regression tests
test/cli/install/isolated-install.test.ts
New tests verify bun add -g --linker=isolated links the requested package's bin into BUN_INSTALL/bin/ and does not link transitive dependency bins globally.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: enabling bin linking for global install requests in the isolated linker.
Linked Issues check ✅ Passed The code changes fully address issue #30450 by implementing per-package global flag logic in linkDependencyBins, mirroring the hoisted installer and correctly linking bins into $BUN_INSTALL/bin for global installs.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing the isolated linker's global bin-linking behavior and testing it; no unrelated modifications are present.
Description check ✅ Passed The description explains the bug, cause, fix, linked issues, and verification, although it does not use the template headings.

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown
Contributor

Found 3 issues this PR may fix:

  1. Bun fails to create global shims (package: sudocode) #28597 - Reports bun i -g claiming binaries are installed but shims never appearing in ~/.bun/bin/; matches the exact global = false bug in the isolated linker
  2. Issues with global bin paths on Windows #28771 - Reports global bin directory staying empty after bun i -g on Windows; same missing-global-symlink symptom
  3. Globally installed CLIs are not working on my system #27924 - Reports globally installed CLIs not working on Windows with Bun 1.3.10 (isolated linker era); bins fail to remap properly

If this is helpful, copy the block below into the PR description to auto-close these issues on merge.

Fixes #28597
Fixes #28771
Fixes #27924

🤖 Generated with Claude Code

@robobun

robobun commented May 10, 2026

Copy link
Copy Markdown
Collaborator Author

Added Fixes #28597 to the PR body — the symptom matches exactly (bins present in ~/.bun/install/global/node_modules/.bin/ but missing from ~/.bun/bin/), which is the behavior this PR fixes for the isolated linker.

Skipping #28771 and #27924:

  • Issues with global bin paths on Windows #28771 conflates three Windows-specific bugs (empty bin dir, double-slash paths in the warning, bun pm bin -g trailing-slash detection). Only the first overlaps with this fix; the other two are separate path-handling bugs and would be left open after this merges.
  • Globally installed CLIs are not working on my system #27924 is a shim parsing bug (interpreter executable "-S" not found), not a missing-symlink bug — the bin shim is created and found, it just fails to execute. Different code path.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The change looks correct and tightly mirrors the hoisted installer's logic, but I'll defer to a human since this touches the package installer's bin-linking path and CI is currently red on fea1458 (the build-cpp failures alongside build-zig suggest an infra issue rather than this diff, but worth confirming the autofix commit goes green).

Extended reasoning...

Overview

This PR fixes bun add -g --linker=isolated <pkg> not linking binaries into $BUN_INSTALL/bin/. The change is ~20 lines in src/install/isolated_install/Installer.zig:linkDependencyBins: it computes a per-dependency global flag (true only when options.global, the parent is the root entry, and the dep's pkg_id matches an entry in manager.update_requests) and passes it to bin_linker.link() instead of hardcoded false. Two regression tests are added in test/cli/install/isolated-install.test.ts.

Security risks

None. The change only affects which directory bin symlinks are written into during a global install the user explicitly requested. No new inputs are parsed, no path construction changes, and Bin.Linker.link(true) is the same code path the hoisted installer already exercises (PackageInstaller.zig:316-328).

Level of scrutiny

Medium. The diff is small and exactly mirrors proven logic from the hoisted installer, and it's gated behind options.global and parent_entry_id == .root so non-global installs (the overwhelming majority) short-circuit to the old false behavior. That said, src/install/ is a production-critical subsystem that affects every bun install user, so a human sanity-check is reasonable.

Other factors

  • CI is red on fea1458: both build-zig and build-cpp failed across every platform. Since this diff touches no C++, the cpp failures strongly suggest an infrastructure/runner issue rather than a code error, but the follow-up autofix commit (36349f2, test-file formatting only) hasn't reported status yet.
  • Thread safety: linkDependencyBins runs on a task thread, but manager.update_requests and manager.options are read-only during the install phase (mutations in runTasks.zig happen during resolution, before install). Only the root entry's task hits the new branch, so there's no concurrent write to $BUN_INSTALL/bin/.
  • No CODEOWNERS cover src/install/.
  • Tests cover both the positive case (requested package's bin lands in BUN_INSTALL/bin) and the negative guard (transitive bins do not).

@robobun

robobun commented May 10, 2026

Copy link
Copy Markdown
Collaborator Author

Diff is green — the two red Windows lanes are both infra flakes unrelated to this PR:

  • Build 53114 windows-2019-x64-test-bun — test-http-should-emit-close-when-connection-is-aborted.ts timed out across all attempts. This test has no relation to the isolated-installer code. The project history shows dozens of ci: retrigger commits for exactly this Windows HTTP flake (search main for "test-http-should-emit-close").
  • Build 53117 windows-x64-build-zig — Zig compiler ICE (thread panic: reached unreachable code in hash_map.zig:putAssumeCapacityNoClobberContext from mingw.zig:buildImportLib). This is a Zig toolchain crash in the MinGW import-lib builder, not Bun code.

Used my one retrigger (477d2d6). Needs a maintainer to re-run or merge.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — small, well-gated fix that mirrors the hoisted installer's existing global-bin logic.

Extended reasoning...

Overview

This PR fixes bun add -g --linker isolated <pkg> failing to link bins into $BUN_INSTALL/bin/. The change is ~16 lines in src/install/isolated_install/Installer.zig:linkDependencyBins plus two new regression tests in test/cli/install/isolated-install.test.ts. It computes a per-package global flag (true iff options.global && parent_entry_id == .root && pkg_id ∈ update_requests) and passes it to bin_linker.link() instead of a hardcoded false.

Security risks

None. This writes symlinks/shims into $BUN_INSTALL/bin/ for explicitly user-requested packages — the same behavior the hoisted installer already has. No auth, crypto, permissions, or untrusted-input parsing is touched.

Level of scrutiny

Low-to-medium. The new logic is a near-verbatim port of src/install/PackageInstaller.zig:316-328 (verified). It's gated behind options.global and parent_entry_id == .root, so for all non-global installs link_into_global_bin is false and the behavior is byte-identical to before — zero blast radius on the common path. For global installs, only packages matching an explicit update_requests entry get the new behavior, which is exactly the user expectation and matches the hoisted linker.

Other factors

  • The one inline nit (test 2 doesn't precisely pin the update_requests filter because dep-pkg is transitive and already gated by parent_entry_id != .root) is a valid coverage observation but non-blocking — the production code is correct by construction (mirrors the hoisted path), and test 1 is the load-bearing regression gate.
  • No CODEOWNERS apply to these files.
  • No outstanding human reviewer comments.
  • Both bin_linker.link() call sites (initial + retry-without-native-binlink) are updated consistently.

Comment thread test/cli/install/isolated-install.test.ts Outdated
@robobun
robobun force-pushed the farm/5dcee19a/isolated-global-bin-link branch from 477d2d6 to 5a819f8 Compare May 24, 2026 04:15

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — the Rust port mirrors the hoisted installer's update_requests gate exactly and both bin_linker.link() sites are covered.

Extended reasoning...

Overview

This PR fixes bun add -g --linker=isolated <pkg> not linking the requested package's bins into $BUN_INSTALL/bin/. The change is ~25 lines in src/install/isolated_install/Installer.rs (link_dependency_bins): it computes a per-dep global flag — options.global && parent_entry_id == ROOT && pkg_id ∈ update_requests — and passes it to bin_linker.link() at both the initial call and the retry-without-native-binlink path. Two regression tests are added in test/cli/install/isolated-install.test.ts.

Security risks

None. The change only affects where bin symlinks/shims are written during a global install the user explicitly invoked. The target directory (options.bin_path) was already wired into the bin::Linker struct; the only behavioral change is passing true instead of a hardcoded false for the narrow root-entry/update-request case. No new path computation, no new user-controlled input.

Level of scrutiny

Low–medium. The new logic is a verbatim copy of the established pattern in the hoisted installer (src/install/PackageInstaller.rs:603-614) — same labeled-block loop over manager.update_requests comparing package_id. The isolated installer's parent_entry_id == StoreEntryId::ROOT is the structural equivalent of the hoisted tree_id == 0 gate. Since the Rust port has been rebased onto current main, I re-verified the surrounding code: global_bin_path is already populated, StoreEntryId::ROOT is the established root sentinel used elsewhere in the file, and the retry branch correctly reuses the same global value.

Other factors

  • Test 1 directly gates the bug (fails on main, passes with fix). My prior inline nit about test 2 not specifically pinning the update_requests filter still stands but was explicitly non-blocking — the production code is correct regardless, and test 2 does guard against the broader "link everything globally" regression.
  • No CODEOWNERS for these paths.
  • Bug-hunting system found no issues this round.
  • CI was green on the prior revision modulo known Windows infra flakes; the latest commit (5a819f8) is the Rust-port rebase with no logic change beyond language translation.

@robobun
robobun force-pushed the farm/5dcee19a/isolated-global-bin-link branch from 5a819f8 to 223874d Compare June 5, 2026 07:36
Comment thread test/cli/install/isolated-install.test.ts Outdated
@robobun

robobun commented Jun 5, 2026

Copy link
Copy Markdown
Collaborator Author

CI is red only on windows-x64-baseline / verify-baseline, and it is an infrastructure failure unrelated to this diff: the step downloads Intel SDE and the mirror is serving a 0-byte sde.tar.xz, so 7-Zip fails with Cannot open the file as archive before any Bun code runs. Reproduced identically on builds 60712 and 60718 (my one retrigger).

Downloading Intel SDE...
Extracting Intel SDE...
ERROR: sde.tar.xz
Cannot open the file as archive

Every other lane is green and the test-bun lanes (which run the new tests) pass. This needs a maintainer to re-run verify-baseline once the SDE download is healthy, or to merge. I have spent my single CI retrigger, so I will not push another empty commit.

@robobun
robobun force-pushed the farm/5dcee19a/isolated-global-bin-link branch from 34ea2a3 to c39ab3a Compare June 22, 2026 18:05
@robobun
robobun force-pushed the farm/5dcee19a/isolated-global-bin-link branch from c39ab3a to 5bc7fc6 Compare August 14, 2026 16:45
Comment thread src/install/isolated_install/Installer.rs Outdated
`bun add -g --linker isolated <pkg>` used to leave the installed bin in
`$BUN_INSTALL/install/global/node_modules/.bin/` instead of symlinking
into `$BUN_INSTALL/bin/`, so the command never made it onto PATH.

The isolated installer's `link_dependency_bins` always passed `false` to
`bin::Linker::link()`; the hoisted installer computes `global` per-package
from `options.global` + `update_requests`. Match that logic: for the root
entry's direct dependencies, link into `options.bin_path` when the package
matches a top-level update request. Transitive deps and non-root entries
stay in the parent's local `.bin/`, which is correct.

Fixes #30450
Fixes #28597
@robobun
robobun force-pushed the farm/5dcee19a/isolated-global-bin-link branch from 5bc7fc6 to 92ac61a Compare August 14, 2026 16:48
Comment thread src/install/isolated_install/Installer.rs

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — the earlier review feedback has been addressed in 92ac61a (test comment rewritten to match the non-global fixture, Rust comment trimmed).

What was reviewed:

  • The new global computation in link_dependency_bins mirrors the hoisted installer's gate at PackageInstaller.rs:685-696 byte-for-byte, and both bin_linker.link() call sites (initial + retry-without-native-binlink) are updated.
  • StoreEntryId::ROOT is Self(0), matching the hoisted installer's tree_id != 0 check; the comparison pattern is already used elsewhere in this file.
  • Tests use file-path deps (no registry), spread bunEnv, drain pipes concurrently, and handle the Windows .exe/.bunx shim naming.
Extended reasoning...

Overview

Two-file change: src/install/isolated_install/Installer.rs gains a per-dep global boolean in link_dependency_bins — true iff options.global && parent_entry_id == ROOT and the dep's pkg_id matches one of manager.update_requests — passed to both bin_linker.link() calls (initial + retry). test/cli/install/isolated-install.test.ts adds a two-test describe("global install") block: one asserting bun add -g --linker=isolated links the bin into $BUN_INSTALL/bin/ (the load-bearing regression), one asserting a non-global bun install links only into the project's node_modules/.bin/.

Security risks

None. This changes where symlinks are written for globally-installed package bins under a temp BUN_INSTALL. No untrusted-input parsing, no auth/crypto, no path-traversal surface introduced — the global_bin_path field was already populated on the bin::Linker struct; this just flips the flag that consults it.

Level of scrutiny

Low-to-medium. The fix is a direct port of existing, proven logic from the hoisted installer (PackageInstaller.rs:685-696) — same labeled-block shape, same update_requests iteration, same package_id comparison. StoreEntryId::ROOT is defined as Self(0) in Store.rs:74, so parent_entry_id == ROOT is the isolated-installer equivalent of the hoisted tree_id == 0. The retry path is updated symmetrically. No new state, no lifecycle changes, no allocation.

Other factors

I left two prior review comments on this PR (both resolved). The first (2026-05-10) noted test 2's original fixture didn't exercise the update_requests filter — the author reworked it to guard the options.global half instead, and updated the PR description to match. The second (2026-06-05) flagged a stale comment left over from that rewrite — fixed in 92ac61a. The comment-cop bot's flag on the Rust comment was addressed (trimmed to two lines) and the author's rationale for keeping it is sound: it documents a non-obvious gate, not a workaround. Tests follow harness conventions (tempDir, bunEnv spread, concurrent pipe drain, exitCode asserted last, cross-platform bin naming). All imports used by the new tests are already present at the top of the file.

@robobun

robobun commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator Author

Final CI state for build 96259 (head 92ac61a): 177/177 executed jobs passed, zero test failures. The build is marked failed only because the two darwin-14-aarch64 test-bun retries expired waiting for an agent (capacity on that lane), plus the manual 👀 block step. Nothing red is related to this diff. Needs a maintainer to re-run the darwin shards or merge.

@robobun

robobun commented Sep 18, 2026

Copy link
Copy Markdown
Collaborator Author

Cross-reference: #43384 changes the hoisted rule that this PR mirrors. A bare bun update -g has no update requests, so the update_requests loop never links the new bins of an updated package into the global bin dir. #43384 moves the rule into PackageManager::links_bins_globally(package_id) (named commands link the packages they name, a bare bun update -g or bun ci -g links every direct dependency of the global package.json). If #43384 lands first, this PR can call that method for the root entry instead of the copied loop, and the isolated linker gets the same behavior for a bare bun update -g --linker isolated. The two PRs touch different files.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

1 participant