Skip to content

fix(gvs): run dependency build scripts under the global virtual store#11987

Merged
zkochan merged 6 commits into
pnpm:mainfrom
rubnogueira:feat/gvs-unavailable
Jun 4, 2026
Merged

fix(gvs): run dependency build scripts under the global virtual store#11987
zkochan merged 6 commits into
pnpm:mainfrom
rubnogueira:feat/gvs-unavailable

Conversation

@rubnogueira

@rubnogueira rubnogueira commented May 27, 2026

Copy link
Copy Markdown
Contributor

Summary

Under enableGlobalVirtualStore: true, dependency build scripts (native addons via node-gyp / prebuild-install, and any install/postinstall hooks) are never executed during a
workspace install
. The affected packages end up present but unbuilt, and crash at runtime — e.g.:

Error: Cannot find module '.../store/v11/links/@/node-expat/.../build/Release/node_expat.node'

This affects every native dependency in a workspace that uses the global virtual store (e.g. node-expat, better-sqlite3, the imagemin bins). Packages that ship prebuilt binaries in
their tarball (e.g. bcrypt via node-gyp-build) are unaffected because they need no build step.

Root cause

In a workspace install, dependency builds are intentionally deferred to a single central pass at the end (pnpm rebuildbuildProjects in @pnpm/building.after-install), so a
shared dependency is built once rather than once per project.

That pass resolved each package's location from the classic virtual-store layout:

//node_modules/

Under the global virtual store that directory does not exist — packages are projected into a hash-addressed directory in the store's links folder:

/links//node_modules/

So the rebuild looked in a non-existent path, found nothing to build, and silently did nothing. (The code even carried a note that "rebuild doesn't work for such packages at all, which
should be fixed.")

Fix

buildProjects is now global-virtual-store aware:

  1. Correct projection directory — when enableGlobalVirtualStore is set, each package's root is resolved to <globalVirtualStoreDir>/<hash>/node_modules/<name>. The base resolves
    to <storeDir>/links (in the per-project rebuild context ctx.virtualStoreDir is the project-local node_modules/.pnpm, not the shared store). The same projection directory is used
    for the post-lifecycle bin re-link pass too, so bins created by the build scripts themselves land in the projection instead of a non-existent classic path.

  2. Matching hash — the projection hash is computed with the same helpers and inputs the installer uses (iterateHashedGraphNodes + iteratePkgMeta, with the same allowBuild /
    supportedArchitectures / resolved-runtime-node-version), so it points at the exact directory the install created.

  3. Concurrency safety — because a single shared projection can be selected for rebuild by many workspace projects in parallel (unlimited workspaceConcurrency, per-project
    lockfiles), a process-wide lock keyed by the projection directory serializes builds of the same projection. The first build runs; concurrent ones wait for it and reuse the result.
    Without this, parallel builds race on the same directory (prebuild-install bus errors, node-gyp EEXIST on the python symlink, etc.).

Behavior is unchanged when enableGlobalVirtualStore is off (all new logic is gated on that flag).

Files changed

  • building/after-install/src/index.ts — the fix (GVS projection resolution, per-projection build lock, and GVS-aware post-lifecycle bin re-linking).
  • building/after-install/src/extendBuildOptions.ts — adds enableGlobalVirtualStore / globalVirtualStoreDir build options.
  • building/commands/src/build/rebuild.ts, building/commands/src/build/recursive.ts — thread enableGlobalVirtualStore through the (recursive) rebuild command options.
  • building/commands/test/build/index.ts — regression tests (single-project + multi-project shared projection).
  • .changeset/gvs-rebuild-native-deps.md — changeset (@pnpm/building.after-install + pnpm, patch).
  • cspell.json — allowlist prebuild.

Test plan

Two tests in building/commands/test/build/index.ts:

  • rebuilds dependencies in the global virtual store — installs a package with a build script under GVS with scripts deferred (--ignore-scripts), so it is projected into
    <storeDir>/links/<hash> but unbuilt; runs rebuild and asserts the build artifact now exists inside the GVS projection.
  • rebuilds a dependency shared by multiple workspace projects in the global virtual store — two-project workspace with per-project lockfiles (sharedWorkspaceLockfile: false), so
    rebuild -r runs a separate concurrent pass per project. Both projects share one GVS projection; asserts the projection is collapsed to a single directory and built once, exercising
    the per-projection build lock.

Results:

  • ✅ The single-project test fails before the fix (the rebuild targets the classic path and builds nothing), passes after.
  • ✅ Full @pnpm/building.commands build suite: 10/10 passing, no regressions (stable across repeated runs).
  • @pnpm/building.after-install type-checks.

Note: the GVS projection hash includes allowBuilds, so the install and the subsequent rebuild must agree on allowBuilds to resolve the same projection directory. The test sets it
via pnpm-workspace.yaml; real workspaces already satisfy this because allowBuilds is part of the committed config.

Coverage caveat: the shared-projection test mechanically exercises the build lock and proves multi-project GVS rebuild correctness + dedup, but does not deterministically prove the
lock prevents a hard crash — no lightweight fixture performs real node-gyp/prebuild-install work whose concurrent execution would bus-error/EEXIST. The lock's correctness rests on
its check-and-set being atomic (no await between the lookup and the set).

Related: #11385


Description updated by an agent (Claude Code, claude-opus-4-8) to reflect follow-up commits (bin re-link fix, rebuild-options threading, the multi-project test, changeset/cspell changes).

Summary by CodeRabbit

  • Bug Fixes

    • Native dependency build scripts now run correctly when Global Virtual Store is enabled; workspace package locations correctly resolve to the GVS layout and concurrent rebuilds for shared projections are serialized to avoid races.
  • Tests

    • Added tests verifying rebuilds produce postinstall artifacts in the Global Virtual Store layout and for shared-project rebuilds.
  • New Features

    • Rebuild commands and options now support the Global Virtual Store configuration.
  • Chores

    • Added a changeset and updated the spelling allowlist.

@rubnogueira rubnogueira requested a review from zkochan as a code owner May 27, 2026 12:26
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Review Summary by Qodo

Fix dependency builds in global virtual store

🐞 Bug fix

Grey Divider

Walkthroughs

Description
• Fix dependency build scripts not executing under global virtual store
• Resolve GVS projection directories using hash-based layout
• Serialize concurrent builds to prevent directory race conditions
• Add regression test for GVS rebuild functionality
Diagram
flowchart LR
  A["Rebuild Request"] --> B{"enableGlobalVirtualStore?"}
  B -->|Yes| C["Compute GVS Hash"]
  C --> D["Resolve Projection Dir"]
  D --> E["Check Build Lock"]
  E -->|Locked| F["Wait for Result"]
  E -->|Unlocked| G["Acquire Lock"]
  G --> H["Execute Build"]
  H --> I["Release Lock"]
  B -->|No| J["Use Classic Path"]
  F --> K["Complete"]
  I --> K
  J --> K

Loading

Grey Divider

File Changes

1. building/after-install/src/extendBuildOptions.ts ⚙️ Configuration changes +2/-0

Add global virtual store build options

• Add enableGlobalVirtualStore optional boolean field
• Add globalVirtualStoreDir optional string field
• Extend StrictBuildOptions type to support GVS configuration

building/after-install/src/extendBuildOptions.ts


2. building/after-install/src/index.ts 🐞 Bug fix +64/-3

Implement GVS-aware rebuild with concurrency control

• Import iterateHashedGraphNodes and iteratePkgMeta for hash computation
• Add process-wide gvsBuildLocks map to serialize concurrent builds
• Compute GVS projection directories using hash-based layout
• Implement pkgModulesDir function to resolve correct package location
• Add concurrency control logic to prevent racing on shared projections
• Use GVS projection path when enableGlobalVirtualStore is enabled

building/after-install/src/index.ts


3. building/commands/test/build/index.ts 🧪 Tests +53/-0

Add GVS rebuild regression test

• Add regression test for rebuilding dependencies in global virtual store
• Test installs package with deferred build under GVS
• Verify build artifact is created in GVS projection directory
• Validate that allowBuilds configuration is respected

building/commands/test/build/index.ts


View more (1)
4. .changeset/gvs-rebuild-native-deps.md 📝 Documentation +9/-0

Add changeset for GVS rebuild fix

• Document fix for dependency build scripts under global virtual store
• Explain root cause: classic path resolution doesn't exist under GVS
• Describe solution: hash-based projection resolution and build serialization
• Note impact on native dependencies using node-gyp and prebuild-install

.changeset/gvs-rebuild-native-deps.md


Grey Divider

Qodo Logo

@coderabbitai

coderabbitai Bot commented May 27, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 5acef30e-8304-4f58-8774-3976ace7ff77

📥 Commits

Reviewing files that changed from the base of the PR and between 934e1c0 and 57e17c2.

📒 Files selected for processing (2)
  • building/after-install/src/index.ts
  • building/commands/test/build/index.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • building/commands/test/build/index.ts
  • building/after-install/src/index.ts
📜 Recent review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Compile & Lint

📝 Walkthrough

Walkthrough

Updates build options for Global Virtual Store support, resolves rebuild targets to GVS projection directories with hashed-graph iteration, serializes concurrent rebuilds per projection via a process-wide lock map (released in finally), and adds end-to-end tests validating rebuilds inside the GVS layout.

Changes

Global Virtual Store Build Script Fix

Layer / File(s) Summary
Configuration, Type Definitions, and Concurrency Infrastructure
building/after-install/src/extendBuildOptions.ts, .changeset/gvs-rebuild-native-deps.md, building/after-install/src/index.ts
Extends StrictBuildOptions with enableGlobalVirtualStore and globalVirtualStoreDir, adds hashed-graph imports (iterateHashedGraphNodes, iteratePkgMeta), and introduces a process-wide gvsBuildLocks map keyed by GVS projection directory.
GVS-Aware Rebuild Execution and Lock Coordination
building/after-install/src/index.ts
Computes gvsDirByDepPath using hashed-graph iteration, adds pkgModulesDir() to resolve the effective node_modules for each dep, switches non-hoisted rebuild targets to use pkgModulesDir, and serializes in-flight builds per projection with lock acquire/release (release moved to finally).
GVS Rebuild Test Coverage and CLI Typing
building/commands/test/build/index.ts, building/commands/src/build/rebuild.ts, building/commands/src/build/recursive.ts, cspell.json
Adds Jest tests that enable GVS, install with scripts deferred, run rebuilds targeting GVS projections and assert postinstall artifacts; extends CLI option types to include enableGlobalVirtualStore; updates cspell.json and test imports.

Sequence Diagram

sequenceDiagram
  participant WorkspaceProject as Workspace Project
  participant RebuildTask as Rebuild Task
  participant GVSLockMap as gvsBuildLocks
  participant LifecycleScripts as Lifecycle Scripts
  WorkspaceProject->>RebuildTask: Start rebuild for dep path
  RebuildTask->>RebuildTask: Compute gvsDir from GVS projection
  RebuildTask->>GVSLockMap: Check if gvsDir lock exists
  alt Lock already held
    GVSLockMap-->>RebuildTask: Promise to ongoing build
    RebuildTask->>RebuildTask: Wait for promise
    RebuildTask-->>WorkspaceProject: Return (marked rebuilt)
  else No lock held
    RebuildTask->>GVSLockMap: Register lock for gvsDir
    RebuildTask->>LifecycleScripts: Run postinstall/build scripts
    LifecycleScripts-->>RebuildTask: Complete
    RebuildTask->>GVSLockMap: Release lock (finally)
    RebuildTask-->>WorkspaceProject: Return
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • pnpm/pnpm#11693: Related GVS hashed-graph projection path computation changes.

Suggested reviewers

  • zkochan

Poem

🐰 I hopped through graphs and locks today,
Found GVS paths where modules play.
I guarded builds from racing fray,
Now native bits can safely stay. 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix(gvs): run dependency build scripts under the global virtual store' directly and specifically describes the main change: fixing dependency build scripts to execute under the global virtual store.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed. For unrecoverable errors, disable the tool in CodeRabbit configuration.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@zkochan zkochan force-pushed the feat/gvs-unavailable branch from d0a1e86 to 13fcf0c Compare June 4, 2026 20:50
zkochan added 5 commits June 4, 2026 23:08
The post-lifecycle bin re-link pass used the classic virtualStoreDir
path even under the global virtual store, so bins created by the build
scripts this fix runs were never re-linked into the GVS projection. Use
the same pkgModulesDir helper as the rest of the rebuild path.

Also thread enableGlobalVirtualStore through the (recursive) rebuild
command opts explicitly, and list pnpm in the changeset.
…ects

Adds a multi-project recursive rebuild test under the global virtual
store with per-project lockfiles (sharedWorkspaceLockfile: false), which
routes through recursiveRebuild's per-project concurrent branch. Both
projects depend on the same package, deduped into one shared GVS
projection, so the concurrent passes select the same projection
directory and exercise the per-projection build lock. Asserts the
projection is deduped to one directory and built exactly once.
@zkochan zkochan merged commit 4e740d5 into pnpm:main Jun 4, 2026
7 of 8 checks passed
@welcome

welcome Bot commented Jun 4, 2026

Copy link
Copy Markdown

Congrats on merging your first pull request! 🎉🎉🎉

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants