@pnpm/bins.linker 1100.0.14 → 1100.0.16
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 +77 -5
- package/package.json +9 -9
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
|
}
|
|
@@ -283,6 +291,70 @@ async function linkBin(cmd, binsDir, opts) {
|
|
|
283
291
|
await ensureExecutable(cmd.path, 0o755);
|
|
284
292
|
}
|
|
285
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
|
+
}
|
|
286
358
|
// `fixBin` chmods the bin's source file (which lives inside the store) to make
|
|
287
359
|
// it executable and rewrites a Windows CRLF shebang to LF. Under the global
|
|
288
360
|
// virtual store that source is `{storeDir}/links/...`, so on a read-only store
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pnpm/bins.linker",
|
|
3
|
-
"version": "1100.0.
|
|
3
|
+
"version": "1100.0.16",
|
|
4
4
|
"description": "Link bins to node_modules/.bin",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pnpm",
|
|
@@ -11,9 +11,9 @@
|
|
|
11
11
|
"funding": "https://opencollective.com/pnpm",
|
|
12
12
|
"repository": {
|
|
13
13
|
"type": "git",
|
|
14
|
-
"url": "https://github.com/pnpm/pnpm/tree/main/bins/linker"
|
|
14
|
+
"url": "https://github.com/pnpm/pnpm/tree/main/pnpm11/bins/linker"
|
|
15
15
|
},
|
|
16
|
-
"homepage": "https://github.com/pnpm/pnpm/tree/main/bins/linker#readme",
|
|
16
|
+
"homepage": "https://github.com/pnpm/pnpm/tree/main/pnpm11/bins/linker#readme",
|
|
17
17
|
"bugs": {
|
|
18
18
|
"url": "https://github.com/pnpm/pnpm/issues"
|
|
19
19
|
},
|
|
@@ -37,13 +37,13 @@
|
|
|
37
37
|
"ramda": "npm:@pnpm/ramda@0.28.1",
|
|
38
38
|
"semver": "^7.8.4",
|
|
39
39
|
"symlink-dir": "^10.0.1",
|
|
40
|
-
"@pnpm/bins.resolver": "1100.0.8",
|
|
41
|
-
"@pnpm/pkg-manifest.utils": "1100.2.5",
|
|
42
|
-
"@pnpm/error": "1100.0.0",
|
|
43
40
|
"@pnpm/fs.read-modules-dir": "1100.0.1",
|
|
41
|
+
"@pnpm/error": "1100.0.1",
|
|
42
|
+
"@pnpm/bins.resolver": "1100.0.8",
|
|
43
|
+
"@pnpm/pkg-manifest.utils": "1100.2.6",
|
|
44
|
+
"@pnpm/pkg-manifest.reader": "1100.0.9",
|
|
44
45
|
"@pnpm/types": "1101.3.2",
|
|
45
|
-
"@pnpm/
|
|
46
|
-
"@pnpm/workspace.project-manifest-reader": "1100.0.13"
|
|
46
|
+
"@pnpm/workspace.project-manifest-reader": "1100.0.14"
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
49
49
|
"@pnpm/logger": "^1100.0.0"
|
|
@@ -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.16",
|
|
61
61
|
"@pnpm/logger": "1100.0.0",
|
|
62
62
|
"@pnpm/test-fixtures": "1100.0.0"
|
|
63
63
|
},
|