mirror of
https://github.com/pnpm/pnpm.git
synced 2026-04-27 10:30:58 -04:00
fix: handle dangling symlink when linking node binary (#10972)
existsSync() follows symlinks and returns false for broken symlinks, so a dangling symlink from a previous node install would not be removed before creating the new symlink, causing an EEXIST error. Use rimraf unconditionally instead, which handles both existing files and non-existent paths gracefully.
This commit is contained in:
6
.changeset/fix-link-bin-dangling-symlink.md
Normal file
6
.changeset/fix-link-bin-dangling-symlink.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@pnpm/link-bins": patch
|
||||
"pnpm": patch
|
||||
---
|
||||
|
||||
Fixed `EEXIST` error when globally installing `node` while a dangling symlink exists from a previous installation.
|
||||
@@ -310,13 +310,10 @@ async function linkBin (cmd: CommandInfo, binsDir: string, opts?: LinkBinOptions
|
||||
} else if (cmd.name === 'node') {
|
||||
// On non-Windows, node should be symlinked directly to the binary
|
||||
// instead of wrapped in a shell shim.
|
||||
try {
|
||||
if (existsSync(externalBinPath)) {
|
||||
await rimraf(externalBinPath)
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
if ((err as NodeJS.ErrnoException).code !== 'ENOENT') throw err
|
||||
}
|
||||
// Use rimraf unconditionally instead of existsSync check, because
|
||||
// existsSync follows symlinks and returns false for broken symlinks,
|
||||
// causing EEXIST when the dangling symlink still exists on disk.
|
||||
await rimraf(externalBinPath)
|
||||
await fs.symlink(cmd.path, externalBinPath, 'file')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -630,6 +630,43 @@ describe('node binary linking', () => {
|
||||
expect(stat.isSymbolicLink()).toBe(true)
|
||||
expect(fs.realpathSync(binLocation)).toBe(path.join(nodeBinDir, 'node'))
|
||||
})
|
||||
|
||||
test('linkBinsOfPackages() replaces a dangling symlink when linking node binary', async () => {
|
||||
const binTarget = temporaryDirectory()
|
||||
const nodeDir = temporaryDirectory()
|
||||
|
||||
const nodeBinDir = path.join(nodeDir, 'bin')
|
||||
fs.mkdirSync(nodeBinDir, { recursive: true })
|
||||
fs.writeFileSync(path.join(nodeBinDir, 'node'), 'fake-node-binary', 'utf8')
|
||||
|
||||
// Create a dangling symlink at the target path (simulates a previous
|
||||
// node install whose store entry was removed).
|
||||
const binLocation = path.join(binTarget, 'node')
|
||||
fs.mkdirSync(binTarget, { recursive: true })
|
||||
const danglingTarget = path.join(temporaryDirectory(), 'non-existent-target')
|
||||
fs.symlinkSync(danglingTarget, binLocation)
|
||||
// Verify it's dangling: lstat succeeds but existsSync returns false
|
||||
expect(fs.lstatSync(binLocation).isSymbolicLink()).toBe(true)
|
||||
expect(fs.existsSync(binLocation)).toBe(false)
|
||||
|
||||
await linkBinsOfPackages(
|
||||
[
|
||||
{
|
||||
location: nodeDir,
|
||||
manifest: {
|
||||
name: 'node',
|
||||
version: '20.0.0',
|
||||
bin: { node: 'bin/node' },
|
||||
},
|
||||
},
|
||||
],
|
||||
binTarget
|
||||
)
|
||||
|
||||
const stat = fs.lstatSync(binLocation)
|
||||
expect(stat.isSymbolicLink()).toBe(true)
|
||||
expect(fs.realpathSync(binLocation)).toBe(path.join(nodeBinDir, 'node'))
|
||||
})
|
||||
}
|
||||
|
||||
testOnWindows('linkBinsOfPackages() hardlinks node.exe instead of creating a cmd-shim', async () => {
|
||||
|
||||
Reference in New Issue
Block a user