Fix bun install through subst drives, cross-drive junctions, and symlinked package.json

RO
2mo ago
+186-18

Fixes #39357

Problem

  • On Windows, bun install run through a path alias whose drive letter differs from the real path (subst P: D:\Projects, or a cross-drive junction) either hangs forever (isolated linker, ~100% CPU, alternating STATUS_OBJECT_NAME_COLLISION / STATUS_OBJECT_NAME_INVALID on a bogus <root>\D: path) or silently rewrites every workspace key in bun.lock from "packages/a" to a machine-absolute "D:/Projects/ws/packages/a" (hoisted linker).
  • Cause: PackageManager::init resolves ROOT_PACKAGE_JSON_PATH with get_fd_path (GetFinalPathNameByHandle) while top_level_dir stays the cwd (src/install/PackageManager.rs:1852 and :1695). When the two sit on different drive letters, no relative path exists between them, so resolve_path::relative returns the absolute to path (matching path.win32.relative), which Package::parse_dependency stores as a workspace key (src/install/lockfile/Package.rs:1965) and the isolated linker then appends onto top_level_dir, producing P:\ws\C:\....
  • The hang itself: make_path_with (src/paths/component_iterator.rs) steps back on ENOENT and forward on EEXIST with no progress guard. The bogus mid-path C: component fails NtCreateFile with STATUS_OBJECT_NAME_INVALID, which translates to ENOENT (libuv's ERROR_INVALID_NAME mapping), so the walk ping-pongs between the existing parent and the invalid child forever.
  • Same root cause on POSIX in a milder form: a symlinked root package.json made workspace discovery run in the symlink target's directory, so bun install reported "No packages!" and deleted the lockfile.

Fix

  • Derive ROOT_PACKAGE_JSON_PATH (and the package.json path used during the workspace-root walk) from the cwd-space path the file was opened at, instead of realpath'ing the fd. The project root, top_level_dir, and every workspace path now stay on the same alias, so relative-path computations never cross drives. This matches how bun add already builds the path (updatePackageJSONAndInstall.rs).
  • Make make_path_with return the carried error when the same component reports NotFound twice with no successful create in between, so recursive mkdir can never loop forever regardless of how an errno maps.
  • Verified:
    • test/cli/install/bun-workspaces.test.ts: new "aliased project root" tests. POSIX: symlinked root package.json installs workspaces with relative lockfile keys (fails on current main). Windows: installs via a subst drive with both linkers, asserting relative keys and no machine-absolute paths (fails on current main: both linkers write absolute keys).
    • Rust unit test for the make_path_with termination guard.
    • Windows end-to-end, exact issue repro (subst + real deps): unfixed canary 8326d1bd3 hangs >45s; fixed build completes in 263ms with "packages/a" keys.
    • Full bun-workspaces.test.ts (74 tests) green on Linux and Windows; bun-add, bun-lock, bun-pm, isolated-install, isolated-relink, bad-workspace, frozen-lockfile-missing-workspace green on Linux.

Background

  • top_level_dir is the project root bun chdirs into; it is seeded from getcwd and is the base every install path (node_modules, store, lockfile keys) is joined onto or made relative to. getcwd preserves path aliases (a subst drive, a junction), while GetFinalPathNameByHandle resolves them to the physical path.
  • Windows has no relative path between two drive letters, so path.relative across drives returns the absolute destination; any code that treats that result as relative builds paths like P:\ws\C:\repro\... with a drive designator as a middle component, which NT rejects as STATUS_OBJECT_NAME_INVALID.
  • make_path_with is bun's shared recursive-mkdir walk: try the full path, step back one component on "parent missing", step forward on "exists/created". ERROR_INVALID_NAME maps to ENOENT (libuv/Node convention), which the walk read as "parent missing", hence the infinite back-and-forth.
Unfixed vs fixed behavior through a subst drive
# unfixed (1.4.0-canary.1+8326d1bd3), cwd P:\ws -> real C:\repro3\ws
bun install                 # hangs forever, killed after 45s
bun install --linker=hoisted  # exit 0, bun.lock:
    "C:/repro3/ws/packages/a": {
    "pkg-a": ["pkg-a@workspace:C:/repro3/ws/packages/a"],

# fixed, same setup
bun install                 # exit 0, 263ms, bun.lock:
    "packages/a": {
    "pkg-a": ["pkg-a@workspace:packages/a"],

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-workspaces.test.ts


Rebase note: main's bun pm diff (#39229) added a no_project mode that special-cased ROOT_PACKAGE_JSON_PATH because get_fd_path cannot run on that mode's invalid fd. The top_level_dir-based construction here does not touch the fd and produces the same "where the file would be" path in that mode, so the special case is subsumed (verified: bun-pm-diff.test.ts 46 pass, bun-workspaces.test.ts 76 pass after rebase).