Fix bun install through subst drives, cross-drive junctions, and symlinked package.json
RO
2mo ago+186+186+012345678901234567890123456789-18-18-01234567890123456789
Fixes #39357
Problem
- On Windows,
bun installrun 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, alternatingSTATUS_OBJECT_NAME_COLLISION/STATUS_OBJECT_NAME_INVALIDon a bogus<root>\D:path) or silently rewrites every workspace key inbun.lockfrom"packages/a"to a machine-absolute"D:/Projects/ws/packages/a"(hoisted linker). - Cause:
PackageManager::initresolvesROOT_PACKAGE_JSON_PATHwithget_fd_path(GetFinalPathNameByHandle) whiletop_level_dirstays the cwd (src/install/PackageManager.rs:1852and:1695). When the two sit on different drive letters, no relative path exists between them, soresolve_path::relativereturns the absolutetopath (matchingpath.win32.relative), whichPackage::parse_dependencystores as a workspace key (src/install/lockfile/Package.rs:1965) and the isolated linker then appends ontotop_level_dir, producingP:\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-pathC:component failsNtCreateFilewithSTATUS_OBJECT_NAME_INVALID, which translates to ENOENT (libuv'sERROR_INVALID_NAMEmapping), 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.jsonmade workspace discovery run in the symlink target's directory, sobun installreported "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 howbun addalready builds the path (updatePackageJSONAndInstall.rs). - Make
make_path_withreturn 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 asubstdrive 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_withtermination 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-workspacegreen on Linux.
Background
top_level_diris the project root bun chdirs into; it is seeded fromgetcwdand is the base every install path (node_modules, store, lockfile keys) is joined onto or made relative to.getcwdpreserves path aliases (a subst drive, a junction), whileGetFinalPathNameByHandleresolves them to the physical path.- Windows has no relative path between two drive letters, so
path.relativeacross drives returns the absolute destination; any code that treats that result as relative builds paths likeP:\ws\C:\repro\...with a drive designator as a middle component, which NT rejects asSTATUS_OBJECT_NAME_INVALID. make_path_withis bun's shared recursive-mkdir walk: try the full path, step back one component on "parent missing", step forward on "exists/created".ERROR_INVALID_NAMEmaps 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).