Skip to content

install: remove dangling links from the global node_modules/.bin on bun remove -g - #43403

Open
robobun wants to merge 3 commits into
mainfrom
robobun/f1d60175/remove-g-sweep-global-node-modules-bin
Open

robobun wants to merge 3 commits into
mainfrom
robobun/f1d60175/remove-g-sweep-global-node-modules-bin

Conversation

@robobun

@robobun robobun commented Sep 19, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • bun remove -g <pkg> leaves a dangling bin link in the node_modules/.bin of the global dir. Repro: bun add -g a, bun add -g b, bun remove -g a. node_modules/.bin/a then points at the deleted ../a/cli.js (on Windows, the a.bunx/a.exe shims stay).
  • The cause is remove_leftover_node_modules (src/install/PackageManager/updatePackageJSONAndInstall.rs:741). It sweeps dangling links only in options.bin_path. In a global install that is the global bin dir.
  • No issue tracks this. Severity is low: the directory is internal.

Fix

Background

  • The global dir ($BUN_INSTALL/install/global) is a normal project with a package.json and a node_modules. It is also the bun link registry. The global bin dir ($BUN_INSTALL/bin) is on $PATH.
  • A global command links only the packages it names into the global bin dir (link_tree_bins, src/install/PackageInstaller.rs:646). Every other top-level package links into the global node_modules/.bin. So the second bun add -g links the first package there.
  • bun remove deletes the folder of the removed package. Then it unlinks each dangling symlink in options.bin_path.
Notes

no test proof · iteration 0 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/cli/install/bun-remove.test.ts

…bun remove -g`

A global command links only the packages it names into the global bin
dir. Every other top-level package links into the node_modules/.bin of
the global dir. `bun remove` swept dangling links only in
`options.bin_path`, which is the global bin dir in a global install, so
the link in node_modules/.bin stayed behind.

In a global install, also run `prune::prune_bins` on the node_modules of
the global dir. It removes only the dangling entries of .bin, symlinks
on POSIX and .bunx/.exe shim pairs on Windows.
@robobun

robobun commented Sep 19, 2026

Copy link
Copy Markdown
Collaborator Author

Status: the fix and two tests are pushed. Waiting for CI.

Reproduction (1.4.3-canary 26e7a4b, Linux x64 and Windows x64, with a private BUN_INSTALL):

export BUN_INSTALL=$(mktemp -d)
bun add -g /path/to/what-bin     # a folder package with the bin `what-bin`
bun add -g /path/to/other-bin    # a second folder package with the bin `other-bin`
bun remove -g what-bin
ls -la $BUN_INSTALL/install/global/node_modules/.bin
# what-bin -> ../what-bin/cli.js   (dangling, the folder is gone)
# other-bin -> ../other-bin/cli.js

With this PR the listing holds only other-bin. On Windows the entries are the what-bin.bunx and what-bin.exe shims, and they are removed too.

Test: bun bd test test/cli/install/bun-remove.test.ts -t "global node_modules". It fails without the change in src/ (.bin still holds what-bin) and passes with it.

@coderabbitai

coderabbitai Bot commented Sep 19, 2026 •

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository: oven-sh/bun/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Essentials

Run ID: 05b64363-f9ac-4b38-a414-a5be56df9407

📥 Commits

Reviewing files that changed from the base of the PR and between 26e7a4b and 91dd1ed.

📒 Files selected for processing (3)
  • src/install/PackageManager/updatePackageJSONAndInstall.rs
  • src/install/prune.rs
  • test/cli/install/bun-remove.test.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 5 remain after this review.


Walkthrough

Global package removal now prunes dangling bins in the global node_modules directory while preserving the linked package registry. Tests cover hoisted and isolated linkers and Windows shim filenames.

Changes

Global removal cleanup

Layer / File(s) Summary
Global bin pruning
src/install/PackageManager/updatePackageJSONAndInstall.rs, src/install/prune.rs
Global cleanup calls crate-visible prune_bins on the global node_modules directory. The global registry remains intact.
Global removal validation
test/cli/install/bun-remove.test.ts
Tests add environment overrides, Windows-specific shim expectations, and global removal coverage for package, registry, and .bin cleanup.

Priority: ⬇️ Low

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly and concisely describes the main change: removing dangling links from the global node_modules/.bin during bun remove -g.
Description check ✅ Passed The description explains the problem, fix, scope, verification steps, test coverage, and platform behavior. It provides the information required by the repository template, although it uses Problem an…

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

@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.

Additional findings (outside the current diff — GitHub can't attach inline comments there):

  • 🟣 src/install/PackageManager/updatePackageJSONAndInstall.rs — Windows users who run bun remove -g <pkg> still get a stale command on PATH after this merges; the PR title promises the dangling links are gone. The global bin dir sweep at updatePackageJSONAndInstall.rs:747 matches only EntryKind::SymLink, so the removed package's <name>.bunx and <name>.exe pair stays in bin_path. The new block at :781 sweeps only node_modules/.bin, the internal dir. Fix: sweep bin_path with the same shim-aware logic as the Windows prune_bins arm (read the .bunx target, resolve it relative to the parent of the bin dir, remove the pair when dangling) so both dirs are covered on every platform.

    Extended reasoning...

    On Windows, bun add -g what-bin writes what-bin.bunx and what-bin.exe into $BUN_INSTALL/bin (bin.rs:1133-1190), the directory on PATH. bun remove -g what-bin deletes node_modules/what-bin at updatePackageJSONAndInstall.rs:737. The bin_path sweep at :741-767 iterates entries and only acts on EntryKind::SymLink (:747); .bunx/.exe are regular files and hit the _ => {} arm at :764. The new global block at :781-788 calls prune_bins on node_modules, whose .bin is not on PATH. So the user types what-bin and the shim fails on a missing ..\install\global\node_modules\what-bin\cli.js. Population: every Windows bun remove -g user, once per removal. The PR ships a Windows shim sweep (prune.rs:2018-2051) that reads each .bunx target with is_dangling(dir, target); applying it with dir = parent of bin_path closes the same class in the same PR. The dismissal relied on the PR description pointing to #35585, which is a claim, not a merged fix.

    Verification: pre-existing. Trigger: bun remove -g <pkg> on Windows, for a package whose shims were written into the global bin dir ($BUN_INSTALL/bin). Mechanism verified: the bin_path sweep at /home/claude/bun/src/install/PackageManager/updatePackageJSONAndInstall.rs:741-779 only acts on bun_sys::EntryKind::SymLink (line 747) and falls into _ => {} (line 764) for everything else; on Windows the…

Comment thread test/cli/install/bun-remove.test.ts Outdated
@robobun

robobun commented Sep 19, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 1:03 AM PT - Sep 19th, 2026

✅ @robobun, your commit efaa1e90f1f7b24226e0b6dea62fb29c10c030b8 passed in Build #118203! 🎉


🧪   To try this PR locally:

bunx bun-pr 43403

That installs a local version of the PR into your bun-43403 executable, so you can run:

bun-43403 --bun

An inherited BUN_INSTALL_GLOBAL_DIR or BUN_INSTALL_BIN, or a globalDir in
the home bunfig, takes precedence over BUN_INSTALL. Set all three so the
test cannot reach the real global folder of the machine that runs it.
Comment thread src/install/PackageManager/updatePackageJSONAndInstall.rs Outdated
The test already asserts that a `bun link` registration survives
`bun remove -g`, so the comment does not need to carry that rule.
@robobun

robobun commented Sep 19, 2026

Copy link
Copy Markdown
Collaborator Author

On the additional finding about the Windows shims in the global bin dir: that is #11970, and #35585 is the open fix for it. It extends the bin_path sweep with the .bunx/.exe case and adds tests for the global bin dir. This PR does not duplicate it.

The title and the body limit this PR to the node_modules/.bin of the global dir, and the Notes list #11970 as not fixed here. When both PRs are merged, bun remove -g cleans both directories on every platform.

@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 env-pinning commit resolves the earlier concern, and the remaining change is a small, best-effort reuse of the existing prune_bins sweep.

What was reviewed:

  • remove_leftover_node_modules tail block: cwd is the global dir (global mode fchdirs into it in PackageManager.rs:1502), and prune_bins only unlinks .bin entries whose target fails fstatat with ENOENT/ENOTDIR (prune.rs:1953), so bun link registrations and live bins are untouched.
  • Test isolation: BUN_INSTALL_GLOBAL_DIR and BUN_INSTALL_BIN are read before the bunfig install.globalDir and BUN_INSTALL (PackageManagerOptions.rs:327-341), so pinning them keeps the test off a developer's real global dir.
  • Swallowed open_at error: only skips an auxiliary cleanup when node_modules is absent/unopenable; no new exit path, matching the "no ENOTDIR crash" property the PR describes.
Extended reasoning...

Overview

The PR adds a 7-line block to remove_leftover_node_modules in /home/claude/bun/src/install/PackageManager/updatePackageJSONAndInstall.rs that, for global installs, opens node_modules under the current directory and calls crate::prune::prune_bins on it. prune_bins in /home/claude/bun/src/install/prune.rs changes visibility from private to pub(crate) on both the POSIX and Windows variants with no body edits. The test file /home/claude/bun/test/cli/install/bun-remove.test.ts gains a runWithEnv helper, a binFiles helper for Windows shim pairs, and one new concurrent test per linker that exercises add/add/link/remove against a temp global dir.

Security risks

None material. The new code deletes only entries in node_modules/.bin that are already dangling (POSIX: symlinks whose fstatat fails with ENOENT/ENOTDIR; Windows: .bunx/.exe pairs whose embedded relative target no longer resolves). open_real_subdir uses O_NOFOLLOW so a .bin symlink is not traversed. No user-controlled string becomes a path beyond the directory-entry names already present on disk, which is the same surface bun pm prune has had since #38333.

Level of scrutiny

Moderate. The behavioral change is small and reuses an existing, already-tested helper rather than adding new sweep logic. I confirmed the two facts the fix rests on: global mode fchdirs into the global dir before this runs (PackageManager.rs:1502), so Dir::cwd() is the global project; and prune_bins only removes dangling entries, so bun link registrations and the other package's live bin survive. The prior-run finding (test could touch a developer's real global dir when BUN_INSTALL_GLOBAL_DIR/BUN_INSTALL_BIN were inherited) was addressed in commit 9b02db6: open_global_dir checks BUN_INSTALL_GLOBAL_DIR first and open_global_bin_dir checks BUN_INSTALL_BIN first, both ahead of the bunfig and BUN_INSTALL fallbacks, so pinning them is sufficient.

Other factors

The test follows the file's existing conventions (it.concurrent, tempDir, expect(stderr).not.toContain("error:"), exit code asserted last) and branches on isWindows for the shim layout rather than skipping. A debug build was not available in this environment, so I did not execute the test locally; the PR states it fails on the base commit and passes with the fix on Linux and Windows, and CI covers both. The silent if let Ok around open_at is acceptable for a best-effort auxiliary cleanup and avoids the ENOTDIR exit-1 path the PR notes was present in an earlier shape. No CODEOWNERS entry covers the changed files. The bug-hunt exit reason was dry_streak with no findings.

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

Development

Successfully merging this pull request may close these issues.

1 participant