fix(exe): hardlink binary as extensionless file on Windows#11090
Conversation
There was a problem hiding this comment.
Pull request overview
Fixes a Windows-specific issue in the @pnpm/exe distribution where npm-generated .cmd/.ps1 shims could point at an extensionless pnpm placeholder file, causing commands to silently no-op. The PR ensures the extensionless path is the actual binary and refreshes workflow usage of pnpm/action-setup.
Changes:
- Hardlink the real Windows platform binary as both
pnpm.exe/pn.exeand extensionlesspnpm/pnduring@pnpm/exeinstall (setup.js). - Update pnpm’s internal
linkExePlatformBinary()to also create an extensionlesspnpmhardlink on Windows. - Bump the pinned
pnpm/action-setupcommit SHA across multiple GitHub workflows.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| pnpm/artifacts/exe/setup.js | On Windows, creates extensionless hardlinks (pnpm, pn) to the real binary so npm shims execute correctly. |
| engine/pm/commands/src/self-updater/installPnpm.ts | Updates linkExePlatformBinary() to create an extensionless pnpm hardlink on Windows for scriptless installs/version switching. |
| .github/workflows/update-lockfile.yml | Updates pinned pnpm/action-setup SHA. |
| .github/workflows/test.yml | Updates pinned pnpm/action-setup SHA. |
| .github/workflows/release.yml | Updates pinned pnpm/action-setup SHA. |
| .github/workflows/ci.yml | Updates pinned pnpm/action-setup SHA. |
| .github/workflows/benchmark.yml | Updates pinned pnpm/action-setup SHA. |
| .github/workflows/audit.yml | Updates pinned pnpm/action-setup SHA. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Also hardlink the binary as 'pnpm' (no extension). | ||
| // npm's bin shims and pnpm's linkBins may reference the extensionless | ||
| // name from the published package.json bin entry. The extensionless | ||
| // file must be the real binary so it can be executed. | ||
| const extlessLink = path.join(exePkgDir, 'pnpm') | ||
| try { | ||
| fs.unlinkSync(extlessLink) | ||
| } catch (err: unknown) { | ||
| if (!util.types.isNativeError(err) || !('code' in err) || err.code !== 'ENOENT') { | ||
| throw err | ||
| } | ||
| } | ||
| fs.linkSync(src, extlessLink) |
There was a problem hiding this comment.
On Windows this links only the extensionless pnpm file, but @pnpm/exe also publishes a pn bin (and setup.js hardlinks both pnpm and pn). In scriptless installs/version switching, the pn shim may still resolve to the placeholder pn file and fail. Consider also hardlinking src to pn.exe and/or an extensionless pn alongside pnpm here, and updating exePkg.bin.pn accordingly (mirroring setup.js).
| // Also hardlink the binary as 'pnpm' (no extension). | ||
| // npm's bin shims and pnpm's linkBins may reference the extensionless | ||
| // name from the published package.json bin entry. The extensionless | ||
| // file must be the real binary so it can be executed. | ||
| const extlessLink = path.join(exePkgDir, 'pnpm') | ||
| try { | ||
| fs.unlinkSync(extlessLink) | ||
| } catch (err: unknown) { | ||
| if (!util.types.isNativeError(err) || !('code' in err) || err.code !== 'ENOENT') { | ||
| throw err | ||
| } | ||
| } | ||
| fs.linkSync(src, extlessLink) |
There was a problem hiding this comment.
This adds new Windows-only behavior (creating an extensionless hardlink) but the existing linkExePlatformBinary tests only assert that pnpm.exe is replaced. Please extend the test suite to assert that, on Windows, node_modules/@pnpm/exe/pnpm is also created/replaced with the platform binary content (and similarly for pn if you add that link).
fbba6c7 to
a87ce5d
Compare
On Windows, npm's .cmd/.ps1 bin shims reference the extensionless
`pnpm` file from the published package.json bin entry. Previously,
setup.js and linkExePlatformBinary wrote a dummy text file ("This file
intentionally left blank") at that path, causing the shim to silently
fail — PowerShell's $LASTEXITCODE stays $null, so `exit $LASTEXITCODE`
exits with code 0, making all pnpm commands appear to succeed while
doing nothing.
Fix by hardlinking the real platform binary as both `pnpm.exe` and
`pnpm` (no extension), so the shim executes the actual binary.
Summary
.cmd/.ps1bin shims reference the extensionlesspnpmfile from the publishedpackage.jsonbin entry. Previously,setup.jsandlinkExePlatformBinarywrote a dummy text file ("This file intentionally left blank") at that path, causing the shim to silently fail — PowerShell's$LASTEXITCODEstays$null, soexit $LASTEXITCODEexits with code 0, making all pnpm commands appear to succeed while doing nothing.pnpm.exeandpnpm(no extension), so the shim executes the actual binary.@pnpm/exeinstall script (setup.js) and pnpm's internallinkExePlatformBinary(used formanage-package-manager-versionsversion switching).Test plan