@pnpm/bins.linker 1100.0.13 → 1100.0.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/index.js +123 -7
- package/package.json +11 -11
package/lib/index.js
CHANGED
|
@@ -200,15 +200,23 @@ async function linkBin(cmd, binsDir, opts) {
|
|
|
200
200
|
catch { }
|
|
201
201
|
if (IS_WINDOWS) {
|
|
202
202
|
const exePath = path.join(binsDir, `${cmd.name}${getExeExtension()}`);
|
|
203
|
+
// node.exe is the only bin pnpm links directly as a real executable rather
|
|
204
|
+
// than through a cmd-shim, so the existing-exe handling only applies to it.
|
|
205
|
+
// We could update our own cmd shims to support node.cmd, but we can't
|
|
206
|
+
// control npm's cmd shims, which break when node resolves to node.cmd.
|
|
207
|
+
// npm's cmd shims use `IF EXIST "%~dp0\node.exe"` to find the node binary.
|
|
208
|
+
const isNodeExe = cmd.name === 'node' && cmd.path.toLowerCase().endsWith('.exe');
|
|
203
209
|
if (existsSync(exePath)) {
|
|
210
|
+
// Skip warning and re-linking when the existing node.exe already matches
|
|
211
|
+
// the target, otherwise every command that re-links node would spam the
|
|
212
|
+
// warning below on warm installs.
|
|
213
|
+
if (isNodeExe && await isSameFile(exePath, cmd.path)) {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
204
216
|
globalWarn(`The target bin directory already contains an exe called ${cmd.name}, so removing ${exePath}`);
|
|
205
217
|
await rimraf(exePath);
|
|
206
218
|
}
|
|
207
|
-
|
|
208
|
-
// We could update our own cmd shims to support node.cmd, but we can't
|
|
209
|
-
// control npm's cmd shims, which break when node resolves to node.cmd.
|
|
210
|
-
// npm's cmd shims use `IF EXIST "%~dp0\node.exe"` to find the node binary.
|
|
211
|
-
if (cmd.name === 'node' && cmd.path.toLowerCase().endsWith('.exe')) {
|
|
219
|
+
if (isNodeExe) {
|
|
212
220
|
try {
|
|
213
221
|
await fs.link(cmd.path, exePath);
|
|
214
222
|
}
|
|
@@ -231,7 +239,7 @@ async function linkBin(cmd, binsDir, opts) {
|
|
|
231
239
|
if (opts?.preferSymlinkedExecutables && !IS_WINDOWS && cmd.nodeExecPath == null) {
|
|
232
240
|
try {
|
|
233
241
|
await symlinkDir(cmd.path, externalBinPath);
|
|
234
|
-
await
|
|
242
|
+
await ensureExecutable(cmd.path, 0o755);
|
|
235
243
|
}
|
|
236
244
|
catch (err) { // eslint-disable-line
|
|
237
245
|
if (err.code !== 'ENOENT' && err.code !== 'EISDIR') {
|
|
@@ -280,7 +288,115 @@ async function linkBin(cmd, binsDir, opts) {
|
|
|
280
288
|
// ensure that bin are executable and not containing
|
|
281
289
|
// windows line-endings(CRLF) on the hashbang line
|
|
282
290
|
if (EXECUTABLE_SHEBANG_SUPPORTED) {
|
|
283
|
-
await
|
|
291
|
+
await ensureExecutable(cmd.path, 0o755);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
// Reports whether two paths refer to the same file. A matching inode/device
|
|
295
|
+
// pair (read as BigInts to avoid the precision loss of NTFS 64-bit file IDs)
|
|
296
|
+
// proves a hard link cheaply. Whenever identity can't be established that way —
|
|
297
|
+
// because the inodes genuinely differ or because Windows reports an unreliable
|
|
298
|
+
// zero inode — we fall back to comparing the file contents after a quick size
|
|
299
|
+
// check, which also treats a byte-identical copy as the same file.
|
|
300
|
+
async function isSameFile(pathA, pathB) {
|
|
301
|
+
const [statA, statB] = await Promise.all([
|
|
302
|
+
fs.stat(pathA, { bigint: true }).catch(() => null),
|
|
303
|
+
fs.stat(pathB, { bigint: true }).catch(() => null),
|
|
304
|
+
]);
|
|
305
|
+
if (statA == null || statB == null)
|
|
306
|
+
return false;
|
|
307
|
+
if (statA.ino && statB.ino && statA.ino === statB.ino && statA.dev === statB.dev) {
|
|
308
|
+
return true;
|
|
309
|
+
}
|
|
310
|
+
if (statA.size !== statB.size)
|
|
311
|
+
return false;
|
|
312
|
+
return haveEqualContents(pathA, pathB);
|
|
313
|
+
}
|
|
314
|
+
const FILE_COMPARE_CHUNK_SIZE = 64 * 1024;
|
|
315
|
+
// Compares two equally-sized files chunk by chunk, so an executable is never
|
|
316
|
+
// fully buffered in memory and a mismatch returns as early as possible.
|
|
317
|
+
async function haveEqualContents(pathA, pathB) {
|
|
318
|
+
const [fhA, fhB] = await Promise.all([
|
|
319
|
+
fs.open(pathA, 'r').catch(() => null),
|
|
320
|
+
fs.open(pathB, 'r').catch(() => null),
|
|
321
|
+
]);
|
|
322
|
+
if (fhA == null || fhB == null) {
|
|
323
|
+
await fhA?.close().catch(() => { });
|
|
324
|
+
await fhB?.close().catch(() => { });
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
try {
|
|
328
|
+
const bufA = Buffer.alloc(FILE_COMPARE_CHUNK_SIZE);
|
|
329
|
+
const bufB = Buffer.alloc(FILE_COMPARE_CHUNK_SIZE);
|
|
330
|
+
let position = 0;
|
|
331
|
+
for (;;) {
|
|
332
|
+
// Reading sequentially is intentional: each iteration compares one chunk
|
|
333
|
+
// and stops early on a mismatch or EOF.
|
|
334
|
+
const [readA, readB] = await Promise.all([
|
|
335
|
+
fhA.read(bufA, 0, FILE_COMPARE_CHUNK_SIZE, position),
|
|
336
|
+
fhB.read(bufB, 0, FILE_COMPARE_CHUNK_SIZE, position),
|
|
337
|
+
]);
|
|
338
|
+
if (readA.bytesRead !== readB.bytesRead)
|
|
339
|
+
return false;
|
|
340
|
+
if (readA.bytesRead === 0)
|
|
341
|
+
return true;
|
|
342
|
+
if (!bufA.subarray(0, readA.bytesRead).equals(bufB.subarray(0, readB.bytesRead))) {
|
|
343
|
+
return false;
|
|
344
|
+
}
|
|
345
|
+
position += readA.bytesRead;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
catch {
|
|
349
|
+
// A transient read error must not abort bin linking: treat the files as
|
|
350
|
+
// different so the caller falls back to the warn + remove + relink path.
|
|
351
|
+
return false;
|
|
352
|
+
}
|
|
353
|
+
finally {
|
|
354
|
+
await fhA.close().catch(() => { });
|
|
355
|
+
await fhB.close().catch(() => { });
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
// `fixBin` chmods the bin's source file (which lives inside the store) to make
|
|
359
|
+
// it executable and rewrites a Windows CRLF shebang to LF. Under the global
|
|
360
|
+
// virtual store that source is `{storeDir}/links/...`, so on a read-only store
|
|
361
|
+
// (e.g. `frozenStore`) the chmod is refused — with EPERM/EACCES when the file is
|
|
362
|
+
// owned but permissions forbid it, or EROFS on a genuinely read-only filesystem
|
|
363
|
+
// (Nix store, RO bind mount, OCI layer). A complete seed already ships its bins
|
|
364
|
+
// executable and shebang-normalized by the writable seed-build, so that work is
|
|
365
|
+
// redundant: treat an already-correct target as a no-op, keeping bin-linking
|
|
366
|
+
// write-free (see building/during-install: "Bin-linking reuses existing symlinks
|
|
367
|
+
// write-free"). A non-executable bin — or one still carrying a CRLF shebang that
|
|
368
|
+
// `fixBin` could not rewrite here — still throws, because that means the seed is
|
|
369
|
+
// broken and the bin would not run.
|
|
370
|
+
async function ensureExecutable(file, mode) {
|
|
371
|
+
try {
|
|
372
|
+
await fixBin(file, mode);
|
|
373
|
+
}
|
|
374
|
+
catch (err) { // eslint-disable-line
|
|
375
|
+
if (err.code === 'EPERM' || err.code === 'EACCES' || err.code === 'EROFS') {
|
|
376
|
+
const stat = await fs.stat(file).catch(() => undefined);
|
|
377
|
+
if (stat != null && (stat.mode & 0o111) !== 0 && !(await hasWindowsShebang(file)))
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
throw err;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
// Detects a `#!`-shebang line terminated by CRLF, which fails to execute on
|
|
384
|
+
// POSIX. Mirrors bin-links' own fix-bin detection so a chmod failure on a
|
|
385
|
+
// read-only store is only swallowed when the bin is genuinely already correct.
|
|
386
|
+
async function hasWindowsShebang(file) {
|
|
387
|
+
const fh = await fs.open(file, 'r').catch(() => undefined);
|
|
388
|
+
if (fh == null)
|
|
389
|
+
return false;
|
|
390
|
+
try {
|
|
391
|
+
const buf = Buffer.alloc(2048);
|
|
392
|
+
await fh.read(buf, 0, 2048, 0);
|
|
393
|
+
return buf[0] === 0x23 /* # */ && buf[1] === 0x21 /* ! */ && /^#![^\n]+\r\n/.test(buf.toString());
|
|
394
|
+
}
|
|
395
|
+
catch {
|
|
396
|
+
return false;
|
|
397
|
+
}
|
|
398
|
+
finally {
|
|
399
|
+
await fh.close().catch(() => { });
|
|
284
400
|
}
|
|
285
401
|
}
|
|
286
402
|
function getExeExtension() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pnpm/bins.linker",
|
|
3
|
-
"version": "1100.0.
|
|
3
|
+
"version": "1100.0.15",
|
|
4
4
|
"description": "Link bins to node_modules/.bin",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pnpm",
|
|
@@ -28,28 +28,28 @@
|
|
|
28
28
|
"!*.map"
|
|
29
29
|
],
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@zkochan/cmd-shim": "^9.0.
|
|
31
|
+
"@zkochan/cmd-shim": "^9.0.6",
|
|
32
32
|
"@zkochan/rimraf": "^4.0.0",
|
|
33
33
|
"bin-links": "^6.0.2",
|
|
34
34
|
"is-subdir": "^2.0.0",
|
|
35
35
|
"is-windows": "^1.0.2",
|
|
36
36
|
"normalize-path": "^3.0.0",
|
|
37
37
|
"ramda": "npm:@pnpm/ramda@0.28.1",
|
|
38
|
-
"semver": "^7.8.
|
|
38
|
+
"semver": "^7.8.4",
|
|
39
39
|
"symlink-dir": "^10.0.1",
|
|
40
|
-
"@pnpm/bins.resolver": "1100.0.7",
|
|
41
|
-
"@pnpm/pkg-manifest.reader": "1100.0.7",
|
|
42
40
|
"@pnpm/error": "1100.0.0",
|
|
41
|
+
"@pnpm/pkg-manifest.reader": "1100.0.8",
|
|
43
42
|
"@pnpm/fs.read-modules-dir": "1100.0.1",
|
|
44
|
-
"@pnpm/
|
|
45
|
-
"@pnpm/
|
|
46
|
-
"@pnpm/workspace.project-manifest-reader": "1100.0.
|
|
43
|
+
"@pnpm/bins.resolver": "1100.0.8",
|
|
44
|
+
"@pnpm/pkg-manifest.utils": "1100.2.5",
|
|
45
|
+
"@pnpm/workspace.project-manifest-reader": "1100.0.13",
|
|
46
|
+
"@pnpm/types": "1101.3.2"
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
49
|
-
"@pnpm/logger": "^
|
|
49
|
+
"@pnpm/logger": "^1100.0.0"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
|
-
"@jest/globals": "30.
|
|
52
|
+
"@jest/globals": "30.4.1",
|
|
53
53
|
"@types/is-windows": "^1.0.2",
|
|
54
54
|
"@types/node": "^22.19.19",
|
|
55
55
|
"@types/normalize-path": "^3.0.2",
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
"@types/semver": "7.7.1",
|
|
58
58
|
"cmd-extension": "^2.0.0",
|
|
59
59
|
"tempy": "3.0.0",
|
|
60
|
-
"@pnpm/bins.linker": "1100.0.
|
|
60
|
+
"@pnpm/bins.linker": "1100.0.15",
|
|
61
61
|
"@pnpm/logger": "1100.0.0",
|
|
62
62
|
"@pnpm/test-fixtures": "1100.0.0"
|
|
63
63
|
},
|