@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.
Files changed (2) hide show
  1. package/lib/index.js +77 -5
  2. 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
- // node.exe must exist as a real executable, not a cmd-shim wrapper.
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.14",
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/pkg-manifest.reader": "1100.0.8",
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.14",
60
+ "@pnpm/bins.linker": "1100.0.16",
61
61
  "@pnpm/logger": "1100.0.0",
62
62
  "@pnpm/test-fixtures": "1100.0.0"
63
63
  },