From a8797c4e59a9746c1209947b37d15e751f2196ea Mon Sep 17 00:00:00 2001 From: Ryo Matsukawa <76232929+ryo-manba@users.noreply.github.com> Date: Mon, 13 Oct 2025 17:00:06 +0900 Subject: [PATCH] fix: handle EISDIR error when bin field points to directory (#10080) close #9441 --- .changeset/gold-colts-brush.md | 6 ++++++ cspell.json | 1 + pkg-manager/link-bins/src/index.ts | 4 ++-- .../test/fixtures/bin-is-directory/.gitignore | 2 ++ .../node_modules/invalid-bin/dist/readme.txt | 1 + .../node_modules/invalid-bin/package.json | 5 +++++ pkg-manager/link-bins/test/index.ts | 12 ++++++++++++ 7 files changed, 29 insertions(+), 2 deletions(-) create mode 100644 .changeset/gold-colts-brush.md create mode 100644 pkg-manager/link-bins/test/fixtures/bin-is-directory/.gitignore create mode 100644 pkg-manager/link-bins/test/fixtures/bin-is-directory/node_modules/invalid-bin/dist/readme.txt create mode 100644 pkg-manager/link-bins/test/fixtures/bin-is-directory/node_modules/invalid-bin/package.json diff --git a/.changeset/gold-colts-brush.md b/.changeset/gold-colts-brush.md new file mode 100644 index 0000000000..089a4545ce --- /dev/null +++ b/.changeset/gold-colts-brush.md @@ -0,0 +1,6 @@ +--- +"@pnpm/link-bins": patch +"pnpm": patch +--- + +Fixed EISDIR error when bin field points to a directory [#9441](https://github.com/pnpm/pnpm/issues/9441). diff --git a/cspell.json b/cspell.json index 6e9cee2729..4d37bfb416 100644 --- a/cspell.json +++ b/cspell.json @@ -57,6 +57,7 @@ "ebusy", "ehrkoext", "eintegrity", + "eisdir", "elifecycle", "elit", "emfile", diff --git a/pkg-manager/link-bins/src/index.ts b/pkg-manager/link-bins/src/index.ts index e4da2e902b..36114f5e86 100644 --- a/pkg-manager/link-bins/src/index.ts +++ b/pkg-manager/link-bins/src/index.ts @@ -284,7 +284,7 @@ async function linkBin (cmd: CommandInfo, binsDir: string, opts?: LinkBinOptions await symlinkDir(cmd.path, externalBinPath) await fixBin(cmd.path, 0o755) } catch (err: any) { // eslint-disable-line - if (err.code !== 'ENOENT') { + if (err.code !== 'ENOENT' && err.code !== 'EISDIR') { throw err } globalWarn(`Failed to create bin at ${externalBinPath}. ${err.message as string}`) @@ -308,7 +308,7 @@ async function linkBin (cmd: CommandInfo, binsDir: string, opts?: LinkBinOptions nodeExecPath: cmd.nodeExecPath, }) } catch (err: any) { // eslint-disable-line - if (err.code !== 'ENOENT') { + if (err.code !== 'ENOENT' && err.code !== 'EISDIR') { throw err } globalWarn(`Failed to create bin at ${externalBinPath}. ${err.message as string}`) diff --git a/pkg-manager/link-bins/test/fixtures/bin-is-directory/.gitignore b/pkg-manager/link-bins/test/fixtures/bin-is-directory/.gitignore new file mode 100644 index 0000000000..5867a0493e --- /dev/null +++ b/pkg-manager/link-bins/test/fixtures/bin-is-directory/.gitignore @@ -0,0 +1,2 @@ +!**/node_modules/**/* +!/node_modules/ diff --git a/pkg-manager/link-bins/test/fixtures/bin-is-directory/node_modules/invalid-bin/dist/readme.txt b/pkg-manager/link-bins/test/fixtures/bin-is-directory/node_modules/invalid-bin/dist/readme.txt new file mode 100644 index 0000000000..d2de8bf8d9 --- /dev/null +++ b/pkg-manager/link-bins/test/fixtures/bin-is-directory/node_modules/invalid-bin/dist/readme.txt @@ -0,0 +1 @@ +This is a directory, not a file. diff --git a/pkg-manager/link-bins/test/fixtures/bin-is-directory/node_modules/invalid-bin/package.json b/pkg-manager/link-bins/test/fixtures/bin-is-directory/node_modules/invalid-bin/package.json new file mode 100644 index 0000000000..174040dbfa --- /dev/null +++ b/pkg-manager/link-bins/test/fixtures/bin-is-directory/node_modules/invalid-bin/package.json @@ -0,0 +1,5 @@ +{ + "name": "invalid-bin", + "version": "1.0.0", + "bin": "./dist" +} diff --git a/pkg-manager/link-bins/test/index.ts b/pkg-manager/link-bins/test/index.ts index 9192808ee9..0449f3de99 100644 --- a/pkg-manager/link-bins/test/index.ts +++ b/pkg-manager/link-bins/test/index.ts @@ -32,6 +32,7 @@ const f = fixtures(__dirname) beforeEach(() => { jest.mocked(binsConflictLogger.debug).mockClear() + jest.mocked(globalWarn).mockClear() }) const POWER_SHELL_IS_SUPPORTED = isWindows() @@ -535,6 +536,17 @@ testOnWindows('linkBins() should remove an existing .exe file from the target di expect(fs.readdirSync(binTarget)).toEqual(getExpectedBins(['simple'])) }) +test('linkBins() should handle bin field pointing to a directory gracefully', async () => { + const binTarget = tempy.directory() + const binIsDirFixture = f.prepare('bin-is-directory') + const warn = jest.fn() + + await linkBins(path.join(binIsDirFixture, 'node_modules'), binTarget, { warn }) + + expect(fs.readdirSync(binTarget)).toEqual([]) + expect(globalWarn).toHaveBeenCalled() +}) + describe('enable prefer-symlinked-executables', () => { test('linkBins()', async () => { const binTarget = tempy.directory()