mirror of
https://github.com/pnpm/pnpm.git
synced 2026-07-22 21:53:02 -04:00
fix: run @pnpm/exe setup logic in-process to fix version switching without Node.js (#10696)
When pnpm is installed as a standalone executable in environments without a system Node.js (e.g. Docker containers), the `@pnpm/exe` preinstall script (`node setup.js`) fails because `node` is not on PATH. This broke version switching via the `packageManager` field in package.json since v10.30.2, which changed `getCurrentPackageName()` to return `@pnpm/exe` instead of platform-specific package names like `@pnpm/linux-x64`. Install with `--ignore-scripts` and link the platform-specific binary in-process instead. The setup logic is inlined because setup.js can't be loaded at runtime: `require()` fails on ESM (pnpm v11+) and `import()` is intercepted by pkg's virtual filesystem in standalone executables. Closes #10687
This commit is contained in:
6
.changeset/fix-exe-version-switch-no-nodejs.md
Normal file
6
.changeset/fix-exe-version-switch-no-nodejs.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@pnpm/tools.plugin-commands-self-updater": patch
|
||||
"pnpm": patch
|
||||
---
|
||||
|
||||
Fixed version switching via `packageManager` field failing when pnpm is installed as a standalone executable in environments without a system Node.js [#10687](https://github.com/pnpm/pnpm/issues/10687).
|
||||
@@ -39,17 +39,23 @@ export async function installPnpmToTools (pnpmVersion: string, opts: SelfUpdateC
|
||||
try {
|
||||
// The reason we don't just run add.handler is that at this point we might have settings from local config files
|
||||
// that we don't want to use while installing the pnpm CLI.
|
||||
// We use --ignore-scripts because `@pnpm/exe` has a `preinstall` script that runs `node setup.js`,
|
||||
// which fails in environments without a system Node.js (e.g. when pnpm is installed as a standalone executable).
|
||||
// Instead, we link the platform-specific binary in-process after install.
|
||||
runPnpmCli([
|
||||
'add',
|
||||
`${currentPkgName}@${pnpmVersion}`,
|
||||
'--loglevel=error',
|
||||
'--allow-build=@pnpm/exe',
|
||||
'--no-dangerously-allow-all-builds',
|
||||
'--ignore-scripts',
|
||||
'--config.strict-dep-builds=false',
|
||||
// We want to avoid symlinks because of the rename step,
|
||||
// which breaks the junctions on Windows.
|
||||
'--config.node-linker=hoisted',
|
||||
'--config.bin=bin',
|
||||
], { cwd: stage })
|
||||
if (currentPkgName === '@pnpm/exe') {
|
||||
linkExePlatformBinary(stage)
|
||||
}
|
||||
// We need the operation of installing pnpm to be atomic.
|
||||
// However, we cannot use a rename as that breaks the command shim created for pnpm.
|
||||
// Hence, we use a symlink.
|
||||
@@ -67,3 +73,40 @@ export async function installPnpmToTools (pnpmVersion: string, opts: SelfUpdateC
|
||||
binDir,
|
||||
}
|
||||
}
|
||||
|
||||
// This replicates the logic from @pnpm/exe's setup.js (pnpm/artifacts/exe/setup.js).
|
||||
// We can't run setup.js via require() or import() because:
|
||||
// - require() fails when setup.js is ESM (pnpm v11+)
|
||||
// - import() is intercepted by pkg's virtual filesystem in standalone executables
|
||||
// So we inline the logic: find the platform-specific binary and hard-link it
|
||||
// into the @pnpm/exe package directory.
|
||||
function linkExePlatformBinary (stageDir: string): void {
|
||||
const platform = process.platform === 'win32'
|
||||
? 'win'
|
||||
: process.platform === 'darwin'
|
||||
? 'macos'
|
||||
: process.platform
|
||||
const arch = platform === 'win' && process.arch === 'ia32' ? 'x86' : process.arch
|
||||
const executable = platform === 'win' ? 'pnpm.exe' : 'pnpm'
|
||||
const platformPkgDir = path.join(stageDir, 'node_modules', '@pnpm', `${platform}-${arch}`)
|
||||
const src = path.join(platformPkgDir, executable)
|
||||
if (!fs.existsSync(src)) return
|
||||
const exePkgDir = path.join(stageDir, 'node_modules', '@pnpm', 'exe')
|
||||
const dest = path.join(exePkgDir, executable)
|
||||
try {
|
||||
fs.unlinkSync(dest)
|
||||
} catch (err: unknown) {
|
||||
if ((err as NodeJS.ErrnoException).code !== 'ENOENT') {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
fs.linkSync(src, dest)
|
||||
fs.chmodSync(dest, 0o755)
|
||||
if (platform === 'win') {
|
||||
const exePkgJsonPath = path.join(exePkgDir, 'package.json')
|
||||
const exePkg = JSON.parse(fs.readFileSync(exePkgJsonPath, 'utf8'))
|
||||
fs.writeFileSync(path.join(exePkgDir, 'pnpm'), 'This file intentionally left blank')
|
||||
exePkg.bin.pnpm = 'pnpm.exe'
|
||||
fs.writeFileSync(exePkgJsonPath, JSON.stringify(exePkg, null, 2))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user