install: skip .bin link recreate when the existing symlink already matches
Fixes #14736.
Repro
With a project whose node_modules (including node_modules/.bin/tsc) is already fully installed and then mounted read-only (the docker-compose ./:/home/bun/app:ro pattern from the issue, or chmod -R a-w as an unprivileged user):
$ bun install && bun index.ts
error: Failed to link typescript: EEXIST
Cause
On every install, including a no-op one, the hoisted/isolated bin linker re-links each package bin. create_symlink in src/install/bin.rs handles the common case where node_modules/.bin/<name> already exists by unconditionally unlink + re-symlink. On a read-only tree the unlink fails (error discarded) and the retry symlink returns the original EEXIST, which surfaces as Failed to link <pkg>: EEXIST and fails the install even though nothing needed to change.
Fix
When the first symlink returns EEXIST, readlink the existing destination and compare against the relative target we were about to write. If they already match, return success and skip the delete+recreate entirely. A stale link (or a non-symlink, where readlink fails) still falls through to the existing delete+retry path, so version upgrades and the "existing non-symlink" case keep replacing the destination.
Verification
New test binaries > reinstall keeps an already-correct .bin link untouched on a read-only tree in test/cli/install/bun-install-registry.test.ts:
- installs
what-bin@1.0.0, makesnode_modules/.binmode0555, reinstalls - asserts no
Failed to link/error:in stderr and exit 0 - asserts the symlink's
ino/ctimeNsare unchanged, so the skip is observable even on hosts where root bypasses the chmod
Before: ctime/inode change on every reinstall; on unprivileged hosts the reinstall errors with Failed to link what-bin: EEXIST.
After: second install is a true no-op on the .bin entry.
no test proof · iteration 1 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/cli/install/bun-install-registry.test.ts