Skip to content

fix(arborist): retry bin-links on Windows EPERM#9028

Merged
wraithgar merged 2 commits intonpm:latestfrom
manzoorwanijk:fix/linked-strategy-windows-eperm
Mar 4, 2026
Merged

fix(arborist): retry bin-links on Windows EPERM#9028
wraithgar merged 2 commits intonpm:latestfrom
manzoorwanijk:fix/linked-strategy-windows-eperm

Conversation

@manzoorwanijk
Copy link
Contributor

@manzoorwanijk manzoorwanijk commented Feb 25, 2026

In continuation of our exploration of using install-strategy=linked in the Gutenberg monorepo, which powers the WordPress Block Editor, this is a follow up of the fixes from #8996.

On Windows, npm install with install-strategy=linked fails with EPERM: operation not permitted during the bin-linking phase. This is hitting us on Windows CI for the Gutenberg monorepo (~200 workspace packages).

Summary

During rebuild, bin-links/fix-bin.js rewrites hashbang lines in bin files using write-file-atomic, which does a temp-file write followed by fs.rename(). On Windows, antivirus (Windows Defender) and the search indexer can transiently lock files that were just written, causing the rename to fail with EPERM.

The linked strategy amplifies this because it writes all packages into .store/ in parallel, increasing the window for antivirus lock conflicts compared to the hoisted layout.

Root cause

npm already patches the global fs with graceful-fs at startup (entry.js:8), which adds Windows rename retry logic. However, graceful-fs's retry only kicks in when the destination file does not exist — it checks fs.stat(to) after EPERM, and if the target already exists (stat succeeds), it gives up immediately. In write-file-atomic's case, the destination file always exists (it's being overwritten), so the retry never fires.

Changes

  • Wrapped the binLinks() call in rebuild.js #createBinLinks with a new #binLinksWithRetry method that retries up to 5 times with 500ms–2.5s backoff on Windows when the error code is EPERM, EACCES, or EBUSY.
  • Wrapped the binLinks() call in rebuild.js #createBinLinks with @gar/promise-retry. On Windows, EPERM/EACCES/EBUSY errors trigger a retry with exponential backoff (5 retries, 500ms min timeout).
  • Added @gar/promise-retry to arborist's dependencies.
  • The retry only activates on process.platform === 'win32' — no behavior change on macOS/Linux.

Testing

We tested this approach in our fork and it resolves the issue on Windows CI for the Gutenberg monorepo.

References

Fixes #9021

@manzoorwanijk manzoorwanijk requested a review from a team as a code owner February 25, 2026 05:06
@manzoorwanijk manzoorwanijk force-pushed the fix/linked-strategy-windows-eperm branch 3 times, most recently from bff4ae0 to 0d8fa42 Compare March 3, 2026 07:14
@wraithgar
Copy link
Member

We may want to dig into the write-file-atomic PR first to see why this isn't already happening.

@wraithgar wraithgar self-assigned this Mar 3, 2026
@manzoorwanijk
Copy link
Contributor Author

We may want to dig into the write-file-atomic PR first to see why this isn't already happening.

I dug into this. As you already pointed out, npm already calls gracefulify(require('node:fs')) at startup (entry.js:8), which patches the global fs singleton - so write-file-atomic's require('fs') already gets the patched fs.rename.

The reason it doesn't help is that graceful-fs's rename retry has a condition that prevents it from retrying in this case. After getting EPERM, it does fs.stat(to) - if the destination file already exists (stat succeeds), it gives up immediately without retrying. In write-file-atomic's case, the destination file always exists (it's being overwritten), so the retry never fires.

The fix needs to be here at the arborist level. I'll update to use promiseRetry per your review.

…y install

On Windows, antivirus and search indexer can transiently lock files,
causing write-file-atomic's fs.rename to fail with EPERM during the
bin-linking phase. The linked strategy amplifies this by writing many
store entries in parallel.

Add retry with backoff (up to 5 attempts) for EPERM/EACCES/EBUSY
errors in #createBinLinks, Windows only.
@manzoorwanijk manzoorwanijk force-pushed the fix/linked-strategy-windows-eperm branch 2 times, most recently from 0c24d7c to 07a7e5c Compare March 4, 2026 06:36
@manzoorwanijk manzoorwanijk requested a review from wraithgar March 4, 2026 06:39
"version": "9.4.0",
"license": "ISC",
"dependencies": {
"@gar/promise-retry": "^1.0.0",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are making an exception for this PR on our policy accepting dependency updates. This dependency is already in the npm tree, and adding it is only a matter of adding it to the package.json and lockfile. The bundled assets are already present.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I already saw it already used in the repo and used the exact same version for the same reason.

@wraithgar
Copy link
Member

Oh yes so much cleaner w/ the retry library!

@wraithgar wraithgar merged commit a29aeee into npm:latest Mar 4, 2026
33 checks passed
@manzoorwanijk manzoorwanijk deleted the fix/linked-strategy-windows-eperm branch March 4, 2026 17:24
wraithgar pushed a commit that referenced this pull request Mar 4, 2026
This contains the changes from

- a2154cd (#8996)
- 880ecb7 (#9013)
- 26fa40e (#9041)
- 983742b (#9055)
- 10d5302 (#9051)
- a29aeee (#9028)
- 16fbe13 (#9030)
- 8614b2a (#9031)

Since Node 22 doesn't have npm 11 yet, it would be better to have this
change backported to v10


Also, we wish to use `install-strategy=linked` in the [Gutenberg
monorepo](WordPress/gutenberg#75814), which
powers the WordPress Block Editor. We are still on v10. So, these fixes
will help.
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.

[BUG] EPERM on Windows with install-strategy=linked: fs.rename fails in write-file-atomic

2 participants