@pnpm/building.after-install 1101.0.18 → 1101.0.20

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.
@@ -41,6 +41,8 @@ export type StrictBuildOptions = {
41
41
  shamefullyHoist: boolean;
42
42
  deployAllFiles: boolean;
43
43
  allowBuilds?: Record<string, boolean | string>;
44
+ enableGlobalVirtualStore?: boolean;
45
+ globalVirtualStoreDir?: string;
44
46
  virtualStoreDirMaxLength: number;
45
47
  peersSuffixMaxLength: number;
46
48
  strictStorePkgContentCheck: boolean;
package/lib/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import assert from 'node:assert';
2
+ import fs from 'node:fs';
2
3
  import path from 'node:path';
3
4
  import util from 'node:util';
4
5
  import { linkBins } from '@pnpm/bins.linker';
@@ -6,7 +7,7 @@ import { pkgRequiresBuild } from '@pnpm/building.pkg-requires-build';
6
7
  import { createAllowBuildFunction } from '@pnpm/building.policy';
7
8
  import { LAYOUT_VERSION, WANTED_LOCKFILE, } from '@pnpm/constants';
8
9
  import { skippedOptionalDependencyLogger } from '@pnpm/core-loggers';
9
- import { calcDepState, findRuntimeNodeVersion, lockfileToDepGraph } from '@pnpm/deps.graph-hasher';
10
+ import { calcDepState, findRuntimeNodeVersion, iterateHashedGraphNodes, iteratePkgMeta, lockfileToDepGraph } from '@pnpm/deps.graph-hasher';
10
11
  import { graphSequencer } from '@pnpm/deps.graph-sequencer';
11
12
  import * as dp from '@pnpm/deps.path';
12
13
  import { PnpmError } from '@pnpm/error';
@@ -25,6 +26,10 @@ import pLimit from 'p-limit';
25
26
  import { runGroups } from 'run-groups';
26
27
  import semver from 'semver';
27
28
  import { extendBuildOptions, } from './extendBuildOptions.js';
29
+ // Serializes builds of a shared GVS projection across concurrent per-project
30
+ // rebuilds: the first build proceeds, concurrent ones await it and reuse the
31
+ // result. Keyed by the absolute projection directory.
32
+ const gvsBuildLocks = new Map();
28
33
  function findPackages(packages, searched, opts) {
29
34
  return Object.keys(packages)
30
35
  .filter((relativeDepPath) => {
@@ -38,15 +43,18 @@ function findPackages(packages, searched, opts) {
38
43
  });
39
44
  return false;
40
45
  }
41
- return matches(searched, pkgInfo);
46
+ return matches(searched, pkgInfo, dp.getPkgIdWithPatchHash(relativeDepPath));
42
47
  });
43
48
  }
44
49
  // TODO: move this logic to separate package as this is also used in tree-builder
45
- function matches(searched, manifest) {
50
+ function matches(searched, manifest, pkgIdWithPatchHash) {
46
51
  return searched.some((searchedPkg) => {
47
52
  if (typeof searchedPkg === 'string') {
48
53
  return manifest.name === searchedPkg;
49
54
  }
55
+ if ('pkgIdWithPatchHash' in searchedPkg) {
56
+ return searchedPkg.pkgIdWithPatchHash === pkgIdWithPatchHash;
57
+ }
50
58
  return searchedPkg.name === manifest.name && !!manifest.version &&
51
59
  semver.satisfies(manifest.version, searchedPkg.range);
52
60
  });
@@ -62,6 +70,9 @@ export async function buildSelectedPkgs(projects, pkgSpecs, maybeOpts) {
62
70
  return {};
63
71
  const packages = ctx.currentLockfile.packages;
64
72
  const searched = pkgSpecs.map((arg) => {
73
+ if (matchesDepPath(packages, arg)) {
74
+ return { pkgIdWithPatchHash: dp.removePeersSuffix(arg) };
75
+ }
65
76
  const { fetchSpec, name, raw, type } = npa(arg);
66
77
  if (raw === name) {
67
78
  return name;
@@ -107,6 +118,10 @@ export async function buildSelectedPkgs(projects, pkgSpecs, maybeOpts) {
107
118
  ignoredBuilds: ignoredPkgs,
108
119
  };
109
120
  }
121
+ function matchesDepPath(packages, pkgSpec) {
122
+ const normalizedPkgSpec = dp.removePeersSuffix(pkgSpec);
123
+ return Object.keys(packages).some((depPath) => dp.removePeersSuffix(depPath) === normalizedPkgSpec);
124
+ }
110
125
  export async function buildProjects(projects, maybeOpts) {
111
126
  const reporter = maybeOpts?.reporter;
112
127
  if ((reporter != null) && typeof reporter === 'function') {
@@ -217,8 +232,8 @@ async function _rebuild(ctx, opts) {
217
232
  };
218
233
  const ignoredPkgs = new Set();
219
234
  const _allowBuild = createAllowBuildFunction(opts) ?? (() => undefined);
220
- const allowBuild = (pkgName, version, depPath) => {
221
- switch (_allowBuild(pkgName, version)) {
235
+ const allowBuild = (depPath) => {
236
+ switch (_allowBuild(depPath)) {
222
237
  case true: return true;
223
238
  case undefined: {
224
239
  ignoredPkgs.add(depPath);
@@ -229,12 +244,28 @@ async function _rebuild(ctx, opts) {
229
244
  };
230
245
  const builtDepPaths = new Set();
231
246
  const storeIndex = opts.skipIfHasSideEffectsCache ? new StoreIndex(opts.storeDir) : undefined;
247
+ // Under GVS, packages live at `<globalVirtualStoreDir>/<hash>/node_modules/<name>`,
248
+ // not the classic virtualStoreDir layout. The hash is computed with the same inputs
249
+ // as the installer so rebuild resolves the exact directory the install created.
250
+ const gvsDirByDepPath = new Map();
251
+ if (opts.enableGlobalVirtualStore) {
252
+ const globalVirtualStoreDir = opts.globalVirtualStoreDir ?? path.join(opts.storeDir, 'links');
253
+ for (const { hash, pkgMeta } of iterateHashedGraphNodes(depGraph, iteratePkgMeta(ctx.currentLockfile, depGraph), _allowBuild, opts.supportedArchitectures, nodeVersion)) {
254
+ const preferredGvsDir = path.join(globalVirtualStoreDir, hash);
255
+ gvsDirByDepPath.set(pkgMeta.depPath, fs.existsSync(preferredGvsDir)
256
+ ? preferredGvsDir
257
+ : findLinkedGvsDir(pkgMeta.name, Object.values(ctx.projects), globalVirtualStoreDir) ?? preferredGvsDir);
258
+ }
259
+ }
260
+ const pkgModulesDir = (depPath) => gvsDirByDepPath.has(depPath)
261
+ ? path.join(gvsDirByDepPath.get(depPath), 'node_modules')
262
+ : path.join(ctx.virtualStoreDir, dp.depPathToFilename(depPath, opts.virtualStoreDirMaxLength), 'node_modules');
232
263
  const groups = chunks.map((chunk) => chunk.filter((depPath) => ctx.pkgsToRebuild.has(depPath) && !ctx.skipped.has(depPath)).map((depPath) => async () => {
233
264
  const pkgSnapshot = pkgSnapshots[depPath];
234
265
  const pkgInfo = nameVerFromPkgSnapshot(depPath, pkgSnapshot);
235
266
  const pkgRoots = opts.nodeLinker === 'hoisted'
236
267
  ? (ctx.modulesFile?.hoistedLocations?.[depPath] ?? []).map((hoistedLocation) => path.join(opts.lockfileDir, hoistedLocation))
237
- : [path.join(ctx.virtualStoreDir, dp.depPathToFilename(depPath, opts.virtualStoreDirMaxLength), 'node_modules', pkgInfo.name)];
268
+ : [path.join(pkgModulesDir(depPath), pkgInfo.name)];
238
269
  if (pkgRoots.length === 0) {
239
270
  if (pkgSnapshot.optional)
240
271
  return;
@@ -243,10 +274,32 @@ async function _rebuild(ctx, opts) {
243
274
  });
244
275
  }
245
276
  const pkgRoot = pkgRoots[0];
277
+ // If another project is already building this shared projection, wait for it
278
+ // and reuse the result instead of racing on the same directory.
279
+ const gvsDir = gvsDirByDepPath.get(depPath);
280
+ if (gvsDir != null) {
281
+ const inFlight = gvsBuildLocks.get(gvsDir);
282
+ if (inFlight != null) {
283
+ await inFlight.catch(() => { });
284
+ pkgsThatWereRebuilt.add(depPath);
285
+ return;
286
+ }
287
+ }
288
+ let releaseGvsLock;
289
+ if (gvsDir != null) {
290
+ let resolveLock;
291
+ gvsBuildLocks.set(gvsDir, new Promise((resolve) => {
292
+ resolveLock = resolve;
293
+ }));
294
+ releaseGvsLock = () => {
295
+ gvsBuildLocks.delete(gvsDir);
296
+ resolveLock();
297
+ };
298
+ }
246
299
  try {
247
300
  const extraBinPaths = ctx.extraBinPaths;
248
301
  if (opts.nodeLinker !== 'hoisted') {
249
- const modules = path.join(ctx.virtualStoreDir, dp.depPathToFilename(depPath, opts.virtualStoreDirMaxLength), 'node_modules');
302
+ const modules = pkgModulesDir(depPath);
250
303
  const binPath = path.join(pkgRoot, 'node_modules', '.bin');
251
304
  await linkBins(modules, binPath, { extraNodePaths: ctx.extraNodePaths, warn });
252
305
  }
@@ -281,7 +334,7 @@ async function _rebuild(ctx, opts) {
281
334
  // However, currently rebuild doesn't work for such packages at all, which should be fixed.
282
335
  requiresBuild = pkgRequiresBuild(pgkManifest, new Map());
283
336
  }
284
- const hasSideEffects = requiresBuild && allowBuild(pkgInfo.name, pkgInfo.version, depPath) && await runPostinstallHooks({
337
+ const hasSideEffects = requiresBuild && allowBuild(depPath) && await runPostinstallHooks({
285
338
  depPath,
286
339
  extraBinPaths,
287
340
  extraEnv: opts.extraEnv,
@@ -337,6 +390,9 @@ async function _rebuild(ctx, opts) {
337
390
  }
338
391
  throw err;
339
392
  }
393
+ finally {
394
+ releaseGvsLock?.();
395
+ }
340
396
  if (pkgRoots.length > 1) {
341
397
  await hardLinkDir(pkgRoot, pkgRoots.slice(1));
342
398
  }
@@ -351,7 +407,7 @@ async function _rebuild(ctx, opts) {
351
407
  .map(async (depPath) => limitLinking(async () => {
352
408
  const pkgSnapshot = pkgSnapshots[depPath];
353
409
  const pkgInfo = nameVerFromPkgSnapshot(depPath, pkgSnapshot);
354
- const modules = path.join(ctx.virtualStoreDir, dp.depPathToFilename(depPath, opts.virtualStoreDirMaxLength), 'node_modules');
410
+ const modules = pkgModulesDir(depPath);
355
411
  const binPath = path.join(modules, pkgInfo.name, 'node_modules', '.bin');
356
412
  return linkBins(modules, binPath, { warn });
357
413
  })));
@@ -366,6 +422,35 @@ async function _rebuild(ctx, opts) {
366
422
  }
367
423
  return { pkgsThatWereRebuilt, ignoredPkgs };
368
424
  }
425
+ // TODO: delete once rebuild relocates GVS projections to the newly computed
426
+ // hash instead of building in place (https://github.com/pnpm/pnpm/issues/12302).
427
+ function findLinkedGvsDir(pkgName, projects, globalVirtualStoreDir) {
428
+ const normalizedGvsRoot = `${path.resolve(globalVirtualStoreDir)}${path.sep}`;
429
+ for (const { rootDir } of projects) {
430
+ const pkgLink = path.join(rootDir, 'node_modules', pkgName);
431
+ try {
432
+ const target = fs.readlinkSync(pkgLink);
433
+ const pkgRoot = path.resolve(path.dirname(pkgLink), target);
434
+ if (!pkgRoot.startsWith(normalizedGvsRoot))
435
+ continue;
436
+ return nthAncestorDir(pkgRoot, pkgName.split('/').length + 1);
437
+ }
438
+ catch (err) {
439
+ // EINVAL: pkgLink exists but is not a symlink.
440
+ if (util.types.isNativeError(err) && 'code' in err && (err.code === 'EINVAL' || err.code === 'ENOENT'))
441
+ continue;
442
+ throw err;
443
+ }
444
+ }
445
+ return undefined;
446
+ }
447
+ function nthAncestorDir(dir, levels) {
448
+ let result = dir;
449
+ for (let i = 0; i < levels; i++) {
450
+ result = path.dirname(result);
451
+ }
452
+ return result;
453
+ }
369
454
  function binDirsInAllParentDirs(pkgRoot, lockfileDir) {
370
455
  const binDirs = [];
371
456
  let dir = pkgRoot;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pnpm/building.after-install",
3
- "version": "1101.0.18",
3
+ "version": "1101.0.20",
4
4
  "description": "Rebuild packages that are already installed by running their lifecycle scripts",
5
5
  "keywords": [
6
6
  "pnpm",
@@ -9,7 +9,10 @@
9
9
  ],
10
10
  "license": "MIT",
11
11
  "funding": "https://opencollective.com/pnpm",
12
- "repository": "https://github.com/pnpm/pnpm/tree/main/building/after-install",
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "https://github.com/pnpm/pnpm/tree/main/building/after-install"
15
+ },
13
16
  "homepage": "https://github.com/pnpm/pnpm/tree/main/building/after-install#readme",
14
17
  "bugs": {
15
18
  "url": "https://github.com/pnpm/pnpm/issues"
@@ -30,37 +33,37 @@
30
33
  "p-limit": "^7.3.0",
31
34
  "run-groups": "^5.0.0",
32
35
  "semver": "^7.8.1",
33
- "@pnpm/building.pkg-requires-build": "1100.0.5",
34
- "@pnpm/building.policy": "1100.0.7",
35
- "@pnpm/bins.linker": "1100.0.10",
36
- "@pnpm/config.normalize-registries": "1100.0.5",
37
- "@pnpm/config.reader": "1101.5.0",
38
- "@pnpm/constants": "1100.0.0",
39
- "@pnpm/core-loggers": "1100.1.2",
40
- "@pnpm/deps.graph-hasher": "1100.2.2",
36
+ "@pnpm/bins.linker": "1100.0.12",
37
+ "@pnpm/building.policy": "1100.0.9",
38
+ "@pnpm/config.reader": "1101.7.0",
39
+ "@pnpm/core-loggers": "1100.1.4",
40
+ "@pnpm/deps.graph-hasher": "1100.2.4",
41
41
  "@pnpm/deps.graph-sequencer": "1100.0.0",
42
- "@pnpm/deps.path": "1100.0.5",
42
+ "@pnpm/constants": "1100.0.0",
43
+ "@pnpm/exec.lifecycle": "1100.0.16",
43
44
  "@pnpm/error": "1100.0.0",
44
- "@pnpm/exec.lifecycle": "1100.0.14",
45
- "@pnpm/installing.context": "1100.0.14",
46
- "@pnpm/installing.modules-yaml": "1100.0.6",
47
- "@pnpm/lockfile.types": "1100.0.8",
48
- "@pnpm/lockfile.walker": "1100.0.8",
49
- "@pnpm/pkg-manifest.reader": "1100.0.5",
50
- "@pnpm/lockfile.utils": "1100.0.10",
51
- "@pnpm/store.connection-manager": "1100.2.5",
52
- "@pnpm/store.controller-types": "1100.1.2",
53
- "@pnpm/store.cafs": "1100.1.7",
45
+ "@pnpm/deps.path": "1100.0.7",
46
+ "@pnpm/installing.context": "1100.0.16",
47
+ "@pnpm/lockfile.utils": "1100.0.12",
48
+ "@pnpm/lockfile.types": "1100.0.10",
49
+ "@pnpm/pkg-manifest.reader": "1100.0.7",
50
+ "@pnpm/config.normalize-registries": "1100.0.7",
51
+ "@pnpm/store.connection-manager": "1100.2.7",
52
+ "@pnpm/store.cafs": "1100.1.9",
53
+ "@pnpm/store.controller-types": "1100.1.4",
54
+ "@pnpm/types": "1101.3.1",
55
+ "@pnpm/installing.modules-yaml": "1100.0.8",
54
56
  "@pnpm/store.index": "1100.1.0",
55
- "@pnpm/types": "1101.2.0"
57
+ "@pnpm/building.pkg-requires-build": "1100.0.7",
58
+ "@pnpm/lockfile.walker": "1100.0.10"
56
59
  },
57
60
  "peerDependencies": {
58
61
  "@pnpm/logger": "^1001.0.1",
59
- "@pnpm/worker": "^1100.1.8"
62
+ "@pnpm/worker": "^1100.1.10"
60
63
  },
61
64
  "devDependencies": {
62
65
  "@types/semver": "7.7.1",
63
- "@pnpm/building.after-install": "1101.0.18"
66
+ "@pnpm/building.after-install": "1101.0.20"
64
67
  },
65
68
  "engines": {
66
69
  "node": ">=22.13"