Skip to content

Implements Windows support - #352

Open
arcanis wants to merge 7 commits into
mainfrom
mael/windows-bis
Open

arcanis wants to merge 7 commits into
mainfrom
mael/windows-bis

Conversation

@arcanis

@arcanis arcanis commented Oct 8, 2026 •

Copy link
Copy Markdown
Member

This PR makes zpm and Yarn Switch build and run natively on Windows (x86_64-pc-windows-msvc), and adds Windows to the CI build, release, and test matrices.

What changes

Paths. On Windows, Path keeps a portable representation internally (C:\foo is stored as /C:/foo, UNC shares as /unc/server/share), so all the existing POSIX-style logic (is_absolute, relative_to, joins, …) keeps working unchanged. Conversions happen at the boundaries:

  • to_path_buf() returns native paths for the std APIs;
  • to_file_string() returns C:/foo (accepted by Win32, Node, and Git Bash, and roundtrips through from_file_string);
  • to_native_string() returns C:\foo, used for env variables (PROJECT_CWD, INIT_CWD, npm_package_json, PATH entries, …);
  • parsing accepts native, verbatim (\\?\C:\…, as returned by canonicalize), and UNC paths.

Upward directory walks stop at the drive root rather than visiting \, permission bits are no-ops, and HOME falls back to USERPROFILE.

Links. winLinkType (which existed but wasn't read) is now honored: directory links are real symlinks with relative targets by default, falling back to NTFS junctions when the user can't create symlinks (Developer Mode disabled), or junctions when configured explicitly. node_modules/.bin entries are .cmd + sh shims, like npm does, since Windows ignores shebangs.

Scripts. Scripts run through the bash shipped with Git for Windows (or any bash.exe in the PATH, except the WSL launcher in System32), with a clear error when none is found. A few things needed care:

  • the MSYS runtime doesn't parse its command line with the MSVC rules Rust uses (it treats \\ as an escaped backslash inside quotes), so arguments given to bash are quoted accordingly — otherwise any script containing \\ got corrupted;
  • PATH shims are generated both as .cmd files (for cmd, PowerShell, and child_process with shell: true) and sh scripts (for Git Bash);
  • programs are resolved through PATHEXT, since Rust only looks for .exe files (which missed npm, pnpm, and our shims);
  • binaries with a shebang are spawned through their interpreter;
  • the PnP ESM loader is registered through a file:// URL (Node rejects C: as a URL scheme), and NODE_OPTIONS paths are quoted when they contain spaces.

Processes.

  • Liveness checks, tree termination (taskkill /T), and Ctrl-C delegation (SetConsoleCtrlHandler) through windows-sys; this replaces the previous winapi code, which was never compiled.
  • The standard handles aren't inherited implicitly anymore. Windows hands every inheritable handle to child processes, so the daemon kept its caller's stdout pipe open, and anything waiting for EOF (child_process.execFile, CI runners, IDE tasks) hung until the daemon died.
  • The daemon moves out of its project folder after loading it (Windows refuses to delete a process' working directory), and shuts down when the folder is removed (creation time replaces the inode check).
  • The main thread reserves 8 MiB of stack like on Linux; Windows defaults to 1 MiB, which yarn workspace <name> <cmd> overflowed.

CRLF. Git for Windows checks files out with CRLF by default, which the JSON document scanner rejected (it didn't treat \r as whitespace, contrary to the spec). Edits now preserve CRLF line endings in both JSON and YAML documents.

Yarn Switch. Uses yarn-bin.exe, skips the POSIX shell profiles in postinstall (a C:/… entry would break Git Bash's PATH), and yarn switch update runs the new PowerShell installer. install-script.ps1 installs into ~/.yarn/switch/bin and adds it to the user PATH through the registry (preserving REG_EXPAND_SZ entries).

Builtin Node.js. Adds the win-x64 and win-arm64 variants, fetched from the .zip distributions.

CI. Builds and releases x86_64-pc-windows-msvc (natively with cargo, archived with Compress-Archive, published to npm with os: win32), and runs the acceptance tests on windows-latest.

Things to be aware of

  • install-script.ps1 must be served as https://repo.yarnpkg.com/install.ps1. That's the URL used by the install docs and by yarn switch update on Windows.
  • Lockfile compatibility. Releases that don't know a builtin Node.js variant fail to install lockfiles listing it ("Unsupported code path") instead of skipping it. Lockfiles re-resolved by this version list the Windows variants, so they can't be installed by older releases — including the one pinned by this repository, which is why yarn.lock isn't updated here and why the Windows test job refreshes it at install time (install-args: --refresh-lockfile). Making unknown variants non-fatal would prevent this from happening again when other platforms get added.
  • Absolute paths written in files use C:/… rather than Berry's /C:/… (e.g. portal: resolutions added by yarn link). Both are parsed correctly; the native form is the one other tools understand.

Testing

All of this was exercised on Windows 11 with Git for Windows:

  • installs with the PnP, node-modules, and pnpm linkers, scripts, package and workspace binaries (from bash, cmd, and PowerShell), nested yarn calls, builtin Node.js, Yarn Switch (link, proxying, postinstall), the daemon, and tasks;
  • installing this repository from scratch fetches 1435 packages with checksums identical to the ones in the lockfile generated on Linux/macOS;
  • the acceptance suite completes with 1644 / 1760 tests passing. Compared to the Linux report of the base commit, the 56 Windows-only failures are:
    • tests relying on Unix conventions (47): Git Bash's pwd printing /tmp/… paths, the portal:/C:/… and backslash spellings, .bin entries being symlinks, executable bits, POSIX signals and process groups, the fake yarn-bin bash script served by the test registry, the OS error message for missing files, a multiline template literal picking up the CRLF line endings of its test file, and a checksum snapshot of tarballs generated on Windows;
    • environment (8): no privilege to create symlinks (Developer Mode disabled), pnpm not installed;
    • the Python venv linker, whose bin entries don't work on Windows yet (1).

cargo test passes for the touched crates on Windows, except for three path_iterators tests that fail on every platform already.


Note

Medium Risk
Broad platform port touching path resolution, linkers, script spawning, and daemon lifecycle; regressions could affect installs and CI on all OSes, though changes are largely gated behind Windows cfg.

Overview
Adds native Windows support for Yarn Switch and zpm (x86_64-pc-windows-msvc), including CI build/test/release matrix entries, a PowerShell installer (install-script.ps1), and npm packages tagged os: win32.

Paths and I/O: Path stores a portable form on Windows (/C:/…, /unc/…) while exposing to_native_string() / to_path_buf() for subprocesses and env vars; link creation honors winLinkType (symlinks with junction fallback) via fs_symlink_with and LinkType.

Install/linking/scripts: Node-modules .bin entries become .cmd + shell shims; package scripts run through Git Bash (or PATH bash), with PATHEXT-aware program resolution, shebang handling, and MSYS-safe quoting. JSON/YAML editors and lockfile writes preserve CRLF on Windows.

Processes: Daemon/switch changes cover hidden detached children, taskkill trees, Ctrl-C handling, non-inherited std handles, larger main-thread stack, and project-folder lifecycle checks without Unix inodes.

Other: Builtin Node adds win-x64 / win-arm64 zip dists; formats/sync/linker code drops Unix-only permission APIs in favor of cross-platform helpers.

Reviewed by Cursor Bugbot for commit 6d21abc. Bugbot is set up for automated code reviews on this repo. Configure here.

Git for Windows checks files out with CRLF line endings by default
(core.autocrlf=true), which made the JSON document scanner reject every
manifest and lockfile, since it didn't treat `\r` as whitespace.

- The JSON scanner now follows the spec and accepts `\r` as whitespace;
  the content it injects into CRLF documents uses CRLF as well.
- `DataDocument::update_document_field` normalizes CRLF documents to LF
  before editing them (the YAML editor only understands LF), then
  restores their original line endings.
Makes zpm and Yarn Switch build and run natively on Windows
(x86_64-pc-windows-msvc).

Paths
- `Path` keeps a portable representation internally on Windows (drives
  are exposed as `/C:/…`, UNC shares as `/unc/server/share/…`), so the
  existing POSIX-style logic keeps working. Paths are converted at the OS
  boundary: `to_path_buf` returns native paths, `to_file_string` returns
  `C:/…` (valid everywhere, roundtrips), and `to_native_string` returns
  `C:\…` for environment variables. Native, verbatim (`\\?\`), and UNC
  inputs are all accepted when parsing.
- Upward directory walks stop at the drive root instead of `\`.
- Permission bits are no-ops on Windows; `HOME` falls back to
  `USERPROFILE`.

Links
- Directory links honor `winLinkType`: real symlinks (with relative
  targets) by default, falling back to NTFS junctions when the user lacks
  the privilege to create symlinks (Developer Mode disabled), or
  junctions when configured explicitly.
- `node_modules/.bin` entries are `.cmd` + sh shims rather than symlinks,
  like npm does, since Windows ignores shebangs.

Scripts
- Scripts run through the bash shipped with Git for Windows (or any bash
  in the PATH except the WSL launcher), with its arguments quoted the
  way the MSYS runtime expects (it treats `\\` as an escaped backslash inside quotes).
- The PATH shims are generated as both `.cmd` files and sh scripts, and
  programs are resolved through PATHEXT so `.cmd` tools (npm, pnpm, our
  shims) can be spawned.
- Binaries with a shebang are run through their interpreter.
- NODE_OPTIONS uses a `file://` URL for the ESM loader, and quotes paths
  containing spaces.

Processes
- Process liveness, tree termination (`taskkill /T`), and Ctrl-C
  delegation are implemented through windows-sys.
- Standard handles aren't inherited implicitly anymore, so detached
  processes (the daemon) don't keep the caller's pipes open.
- The daemon doesn't lock its project folder (Windows can't delete a
  process' working directory) and shuts down when the folder is removed.
- The main thread reserves 8 MiB of stack, like on Linux (Windows
  defaults to 1 MiB, which deeply nested futures overflowed).

Misc
- Yarn Switch installs and runs `yarn-bin.exe`, skips the POSIX shell
  profiles on Windows, and `yarn switch update` uses the PowerShell
  installer.
- The builtin Node.js provisioning supports the win-x64 and win-arm64
  distributions.
- Build x86_64-pc-windows-msvc on windows-latest (natively, without
  cross) in the Build and Releases workflows; the release archive is
  generated with Compress-Archive since the runners lack `zip`, and the
  npm package declares `os: win32`.
- Add install-script.ps1, the Windows counterpart of install-script.sh.
  It installs Yarn Switch into ~/.yarn/switch/bin and adds it to the user
  PATH through the registry, preserving REG_EXPAND_SZ entries. It's
  meant to be served as https://repo.yarnpkg.com/install.ps1, which is
  what `yarn switch update` fetches on Windows.
- Document the Windows install and the Git Bash requirement.
Adds x86_64-pc-windows-msvc to the test matrix, using the Windows
binaries built by the build job.

The pinned Yarn release doesn't know about the Windows builds of the
builtin Node.js and fails to install lockfiles listing them, so the
committed lockfile can't include them yet; the Windows job refreshes the
lockfile at install time instead, through a new `install-args` input of
the prepare-node action. The job is also bounded to an hour, so that a
hanging test can't hold a Windows runner for the default six.
@netlify

netlify Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

❌ Deploy Preview for yarn-v6 failed. Why did it fail? →

Name Link
🔨 Latest commit 6d21abc
🔍 Latest deploy log https://app.netlify.com/projects/yarn-v6/deploys/6ac8b3fcbd0966000898cfc8

@arcanis arcanis changed the title Mael/windows bis Implements Windows support Oct 8, 2026

@cursor cursor Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Release unlinks yarn without .exe
    • Added the ext variable to the unlinkSync call so it correctly deletes yarn.exe on Windows instead of yarn, matching the subsequent renameSync and chmodSync operations.

Create PR

Or push these changes by commenting:

@cursor push 5b517ba983
Preview (5b517ba983)
diff --git a/.github/workflows/releases.yml b/.github/workflows/releases.yml
--- a/.github/workflows/releases.yml
+++ b/.github/workflows/releases.yml
@@ -184,7 +184,7 @@
               const ext = includes(targetName, `windows`) ? `.exe` : ``;
 
               // We don't need to distribute Yarn Switch itself
-              fs.unlinkSync(path.join(destinationPath, `yarn`));
+              fs.unlinkSync(path.join(destinationPath, `yarn${ext}`));
 
               // Rename the binary to `yarn`
               fs.renameSync(path.join(destinationPath, `yarn-bin${ext}`), path.join(destinationPath, `yarn${ext}`));

You can send follow-ups to the cloud agent here.

Comment thread .github/workflows/releases.yml
@github-actions

github-actions Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

⏱️ Benchmark Results

gatsby install-full-cold

Metric Base Head Difference
Mean 3.771s 3.810s +1.03% ⚠️
Median 3.747s 3.812s +1.76% ⚠️
Min 3.637s 3.715s
Max 3.919s 3.905s
Std Dev 0.070s 0.041s
📊 Raw benchmark data (gatsby install-full-cold)

Base times: 3.785s, 3.832s, 3.789s, 3.875s, 3.847s, 3.791s, 3.732s, 3.887s, 3.857s, 3.891s, 3.785s, 3.919s, 3.741s, 3.843s, 3.722s, 3.785s, 3.725s, 3.712s, 3.697s, 3.725s, 3.697s, 3.741s, 3.744s, 3.745s, 3.748s, 3.637s, 3.679s, 3.715s, 3.768s, 3.713s

Head times: 3.715s, 3.792s, 3.761s, 3.812s, 3.848s, 3.902s, 3.813s, 3.783s, 3.809s, 3.846s, 3.792s, 3.814s, 3.822s, 3.822s, 3.824s, 3.750s, 3.772s, 3.796s, 3.795s, 3.775s, 3.816s, 3.905s, 3.788s, 3.858s, 3.846s, 3.844s, 3.774s, 3.826s, 3.813s, 3.781s


gatsby install-cache-only

Metric Base Head Difference
Mean 1.112s 1.164s +4.72% ⚠️
Median 1.099s 1.114s +1.42% ⚠️
Min 1.067s 1.069s
Max 1.384s 1.697s
Std Dev 0.059s 0.151s
📊 Raw benchmark data (gatsby install-cache-only)

Base times: 1.070s, 1.119s, 1.067s, 1.075s, 1.100s, 1.067s, 1.148s, 1.147s, 1.144s, 1.090s, 1.112s, 1.079s, 1.121s, 1.093s, 1.107s, 1.116s, 1.108s, 1.097s, 1.084s, 1.075s, 1.094s, 1.384s, 1.193s, 1.074s, 1.084s, 1.104s, 1.097s, 1.101s, 1.096s, 1.111s

Head times: 1.135s, 1.166s, 1.114s, 1.150s, 1.095s, 1.098s, 1.105s, 1.125s, 1.115s, 1.115s, 1.113s, 1.111s, 1.118s, 1.137s, 1.069s, 1.079s, 1.087s, 1.098s, 1.164s, 1.094s, 1.155s, 1.697s, 1.607s, 1.480s, 1.168s, 1.111s, 1.095s, 1.105s, 1.102s, 1.126s


gatsby install-cache-and-lock (warm, with lockfile)

Metric Base Head Difference
Mean 0.288s 0.338s +17.39% ⚠️
Median 0.289s 0.298s +3.08% ⚠️
Min 0.267s 0.285s
Max 0.321s 0.773s
Std Dev 0.012s 0.119s
📊 Raw benchmark data (gatsby install-cache-and-lock (warm, with lockfile))

Base times: 0.288s, 0.285s, 0.280s, 0.273s, 0.273s, 0.267s, 0.267s, 0.283s, 0.284s, 0.289s, 0.284s, 0.283s, 0.278s, 0.321s, 0.306s, 0.282s, 0.290s, 0.290s, 0.290s, 0.283s, 0.289s, 0.289s, 0.289s, 0.291s, 0.298s, 0.293s, 0.290s, 0.295s, 0.300s, 0.312s

Head times: 0.299s, 0.297s, 0.296s, 0.301s, 0.298s, 0.298s, 0.296s, 0.300s, 0.299s, 0.303s, 0.676s, 0.514s, 0.505s, 0.773s, 0.300s, 0.297s, 0.300s, 0.293s, 0.295s, 0.289s, 0.298s, 0.299s, 0.300s, 0.291s, 0.288s, 0.285s, 0.291s, 0.286s, 0.286s, 0.293s

The import was dropped from the top of the file when the OsStr
conversions became portable, but `fs_clonefile` still relies on it on
macOS.
Lockfiles checked out on Windows use CRLF line endings by default
(core.autocrlf=true). Since the lockfile was always serialized with LF,
immutable installs (and hardened mode, which enables them on public pull
requests) reported an unmodified lockfile as changed, and regular
installs rewrote it on every run. The existing line endings are now kept.

This commit also adds the Windows variants of the builtin Node.js to the
lockfile, which the Windows CI jobs need; the Windows test job doesn't
have to refresh the lockfile anymore. Note that the Yarn release pinned
by `packageManager` can't install this lockfile, as it doesn't know these
variants; a follow-up commit will point it to a build of this branch.

@cursor cursor Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Lockfile breaks pinned Yarn installs
    • Removed Windows Node variants from yarn.lock and restored --refresh-lockfile flag for Windows CI until packageManager is upgraded to a version supporting these variants.

Create PR

Or push these changes by commenting:

@cursor push 0ac3b74969
Preview (0ac3b74969)
diff --git a/.github/actions/prepare-node/action.yml b/.github/actions/prepare-node/action.yml
--- a/.github/actions/prepare-node/action.yml
+++ b/.github/actions/prepare-node/action.yml
@@ -8,6 +8,10 @@
   switch:
     description: 'Path to a locally built yarn-switch binary to use in place of the released one'
     required: false
+  install-args:
+    description: 'Extra arguments to pass to yarn install'
+    required: false
+    default: ''
 
 runs:
   using: composite
@@ -42,4 +46,4 @@
     - name: Install dependencies
       shell: bash
       run: |
-        yarn install
+        yarn install ${{inputs.install-args}}

diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -98,6 +98,9 @@
           - target: x86_64-pc-windows-msvc
             os: windows-latest
             ext: .exe
+            # The lockfile can't list the Windows builds of the builtin Node.js
+            # yet, as the Yarn release we pin doesn't know about them
+            install-args: --refresh-lockfile
             timeout: 60
 
     name: "Testing ${{matrix.target}}"
@@ -122,6 +125,7 @@
         with:
           link: artifacts/yarn-bin${{matrix.ext}}
           switch: artifacts/yarn${{matrix.ext}}
+          install-args: ${{matrix.install-args}}
 
       - name: Generate the test report
         run: |

diff --git a/yarn.lock b/yarn.lock
--- a/yarn.lock
+++ b/yarn.lock
@@ -6781,9 +6781,7 @@
           "@yarnpkg/node-linux-x64@builtin:24.12.0",
           "@yarnpkg/node-linux-arm64@builtin:24.12.0",
           "@yarnpkg/node-darwin-x64@builtin:24.12.0",
-          "@yarnpkg/node-darwin-arm64@builtin:24.12.0",
-          "@yarnpkg/node-win-x64@builtin:24.12.0",
-          "@yarnpkg/node-win-arm64@builtin:24.12.0"
+          "@yarnpkg/node-darwin-arm64@builtin:24.12.0"
         ]
       }
     },
@@ -6847,36 +6845,6 @@
         }
       }
     },
-    "@yarnpkg/node-win-arm64@builtin:24.12.0": {
-      "checksum": null,
-      "resolution": {
-        "resolution": "@yarnpkg/node-win-arm64@builtin:24.12.0",
-        "version": "24.12.0",
-        "requirements": {
-          "cpu": [
-            "arm64"
-          ],
-          "os": [
-            "win32"
-          ]
-        }
-      }
-    },
-    "@yarnpkg/node-win-x64@builtin:24.12.0": {
-      "checksum": null,
-      "resolution": {
-        "resolution": "@yarnpkg/node-win-x64@builtin:24.12.0",
-        "version": "24.12.0",
-        "requirements": {
-          "cpu": [
-            "x64"
-          ],
-          "os": [
-            "win32"
-          ]
-        }
-      }
-    },
     "@yarnpkg/parsers@npm:^3.0.3": {
       "checksum": "df317636a26d25b2c9ee6829252dc20b4d978cf20de08be59439e4e8ed8afad240fca0d4d091dd9e89eb786bea49d6ac1b241ecfdc892c160c7a735388d1683a",
       "resolution": {

You can send follow-ups to the cloud agent here.

Reviewed by Cursor Bugbot for commit d42d00b. Configure here.

Comment thread yarn.lock
"@yarnpkg/node-darwin-arm64@builtin:24.12.0"
"@yarnpkg/node-darwin-arm64@builtin:24.12.0",
"@yarnpkg/node-win-x64@builtin:24.12.0",
"@yarnpkg/node-win-arm64@builtin:24.12.0"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Lockfile breaks pinned Yarn installs

High Severity

The lockfile now lists @yarnpkg/node-win-x64 and @yarnpkg/node-win-arm64, while this repo still pins yarn@6.0.0-rc.20. That release treats unknown builtin variants as Unsupported code path instead of skipping them, so yarn install fails for the pinned manager. Dropping --refresh-lockfile also means CI jobs that use setup-action rather than the freshly built binary hit the same failure.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit d42d00b. Configure here.

Windows runners default to PowerShell, which doesn't understand the
step's script. The test matrix also stops failing fast, so that a
failure on one platform doesn't cancel the report of the others.

This branch was successfully deployed

1 active deployment
test-reports — 6d21abc6 Deployed Oct 9, 2026 by arcanis via Reporting test results #1408
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.

1 participant