Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit ec7cf70

Browse files
authored
fix(dlx): shorten dlx cache path to avoid Windows MAX_PATH failures (#12605)
The dlx cache path is <cacheDir>/dlx/<key>/<prepare>/node_modules/.pnpm/ <pkgId>/node_modules/<pkg>. The <key> (64-char sha256 hex) and <prepare> (<time>-<pid> in hex) segments are dlx overhead on top of pnpm's already- deep virtual-store layout. For a transitive dep with a long name this tips the package directory over Windows' MAX_PATH (260) — measured at 259 chars for @pnpm.e2e/pre-and-postinstall-scripts-example in the dlx e2e test. A lifecycle script then runs with that directory as its cwd, and CreateProcess fails to resolve an over-length cwd, which Node surfaces as the confusing "spawn C:\Windows\system32\cmd.exe ENOENT". Flaky rather than constant because the <prepare> and temp-dir segments vary in length, straddling 260. Shorten both dlx-specific segments: - cache key: createShortHash (32 hex, 128 bits) instead of createHexHash (64). pacquet already truncated to 32, so this also restores parity. - prepare dir: encode time and pid in base36 instead of hex.
1 parent 2b4952e commit ec7cf70

6 files changed

Lines changed: 49 additions & 12 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@pnpm/exec.commands": patch
3+
"pnpm": patch
4+
---
5+
6+
Shortened the `pnpm dlx` cache path so deep dependency trees no longer overflow Windows' `MAX_PATH`, which could make a dependency's lifecycle script fail with `spawn cmd.exe ENOENT`.

pacquet/crates/cli/src/cli_args/dlx.rs

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -451,7 +451,28 @@ fn get_valid_cache_dir(
451451
/// (<https://github.com/pnpm/pnpm/blob/d4a2b0364c/exec/commands/src/dlx.ts#L433-L436>).
452452
fn get_prepare_dir(cache_path: &Path, now: SystemTime, pid: u32) -> PathBuf {
453453
let millis = now.duration_since(UNIX_EPOCH).map_or(0, |elapsed| elapsed.as_millis());
454-
cache_path.join(format!("{millis:x}-{pid:x}"))
454+
// base36 (vs hex) keeps this segment short: it sits between the cache key
455+
// and pnpm's deep virtual-store layout, and long dlx paths overflow
456+
// Windows' MAX_PATH (260), which makes lifecycle scripts fail with a
457+
// `spawn cmd.exe ENOENT` (the cwd no longer resolves). time+pid stays
458+
// unique across concurrent dlx processes and a process's own retries.
459+
cache_path.join(format!("{}-{}", to_base36(millis), to_base36(u128::from(pid))))
460+
}
461+
462+
/// Lowercase base36 (`0-9a-z`), matching JavaScript's
463+
/// `Number.prototype.toString(36)` used by `getPrepareDir`.
464+
fn to_base36(mut n: u128) -> String {
465+
const DIGITS: &[u8; 36] = b"0123456789abcdefghijklmnopqrstuvwxyz";
466+
if n == 0 {
467+
return "0".to_string();
468+
}
469+
let mut buf = Vec::new();
470+
while n > 0 {
471+
buf.push(DIGITS[(n % 36) as usize]);
472+
n /= 36;
473+
}
474+
buf.reverse();
475+
String::from_utf8(buf).expect("base36 digits are ASCII")
455476
}
456477

457478
/// Determine the bin to run from the first installed dependency. Ports

pacquet/crates/cli/src/cli_args/dlx/tests.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -124,11 +124,12 @@ fn create_cache_key_changes_with_supported_architectures() {
124124
}
125125

126126
#[test]
127-
fn get_prepare_dir_encodes_time_and_pid_in_hex() {
127+
fn get_prepare_dir_encodes_time_and_pid_in_base36() {
128128
let base = std::path::Path::new("/cache/dlx/key");
129-
let now = SystemTime::UNIX_EPOCH + Duration::from_millis(0x1a2b);
130-
let dir = get_prepare_dir(base, now, 0xff);
131-
assert_eq!(dir, base.join("1a2b-ff"));
129+
// 6699 = 5*36^2 + 6*36 + 3 -> "563"; 255 = 7*36 + 3 -> "73".
130+
let now = SystemTime::UNIX_EPOCH + Duration::from_millis(6699);
131+
let dir = get_prepare_dir(base, now, 255);
132+
assert_eq!(dir, base.join("563-73"));
132133
}
133134

134135
#[test]

pnpm11/exec/commands/src/dlx.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { OUTPUT_OPTIONS } from '@pnpm/cli.common-cli-options-help'
1313
import { docsUrl, readProjectManifestOnly } from '@pnpm/cli.utils'
1414
import { type Config, types } from '@pnpm/config.reader'
1515
import { getPublishedByPolicy } from '@pnpm/config.version-policy'
16-
import { createHexHash } from '@pnpm/crypto.hash'
16+
import { createShortHash } from '@pnpm/crypto.hash'
1717
import { PnpmError } from '@pnpm/error'
1818
import { createResolver, makeResolutionStrict } from '@pnpm/installing.client'
1919
import { add } from '@pnpm/installing.commands'
@@ -421,7 +421,12 @@ export function createCacheKey (opts: {
421421
}
422422
}
423423
const hashStr = JSON.stringify(args)
424-
return createHexHash(hashStr)
424+
// A short (truncated) hash keeps the dlx cache path short. The full
425+
// virtual-store path below it (`<key>/<prepare>/node_modules/.pnpm/<pkgId>/
426+
// node_modules/<pkg>`) can otherwise blow past Windows' MAX_PATH (260) and
427+
// make lifecycle scripts fail with a `spawn cmd.exe ENOENT` (the cwd no
428+
// longer resolves). 128 bits is ample collision resistance for a cache key.
429+
return createShortHash(hashStr)
425430
}
426431

427432
function getValidCacheDir (cacheLink: string, dlxCacheMaxAge: number): string | undefined {
@@ -446,7 +451,10 @@ function getValidCacheDir (cacheLink: string, dlxCacheMaxAge: number): string |
446451
}
447452

448453
function getPrepareDir (cachePath: string): string {
449-
const name = `${new Date().getTime().toString(16)}-${process.pid.toString(16)}`
454+
// base36 (vs hex) keeps this segment short — see createCacheKey for why dlx
455+
// path length matters on Windows. time+pid stays unique across concurrent
456+
// dlx processes and across a process's own retries of a failed install.
457+
const name = `${Date.now().toString(36)}-${process.pid.toString(36)}`
450458
return path.join(cachePath, name)
451459
}
452460

pnpm11/exec/commands/test/dlx.createCacheKey.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { expect, test } from '@jest/globals'
2-
import { createHexHash } from '@pnpm/crypto.hash'
2+
import { createShortHash } from '@pnpm/crypto.hash'
33

44
import { createCacheKey } from '../src/dlx.js'
55

@@ -11,7 +11,7 @@ test('creates a hash', () => {
1111
'@foo': 'https://example.com/npm-registry/foo/',
1212
},
1313
})
14-
const expected = createHexHash(JSON.stringify([['@foo/bar', 'shx'], [
14+
const expected = createShortHash(JSON.stringify([['@foo/bar', 'shx'], [
1515
['@foo', 'https://example.com/npm-registry/foo/'],
1616
['default', 'https://registry.npmjs.com/'],
1717
]]))

pnpm11/exec/commands/test/dlx.e2e.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,9 @@ function sanitizeDlxCacheComponent (cacheName: string): string {
4343
throw new Error(`Unexpected name: ${cacheName}`)
4444
}
4545
const [date, pid] = segments
46-
if (!/[0-9a-f]+/.test(date) && !/[0-9a-f]+/.test(pid)) {
47-
throw new Error(`Name ${cacheName} doesn't end with 2 hex numbers`)
46+
// getPrepareDir encodes time and pid in base36 (0-9a-z).
47+
if (!/^[0-9a-z]+$/.test(date) || !/^[0-9a-z]+$/.test(pid)) {
48+
throw new Error(`Name ${cacheName} is not a base36 "<time>-<pid>" prepare dir`)
4849
}
4950
return '***********-*****'
5051
}

0 commit comments

Comments
 (0)