Skip to content

fix(executor): run lifecycle scripts from long global virtual store slots on Windows - #15147

Open
Ayush442842q wants to merge 18 commits into
pnpm:mainfrom
Ayush442842q:fix-pnpm-issue-15111
Open

Ayush442842q wants to merge 18 commits into
pnpm:mainfrom
Ayush442842q:fix-pnpm-issue-15111

Conversation

@Ayush442842q

@Ayush442842q Ayush442842q commented Sep 19, 2026

Copy link
Copy Markdown
Contributor

Summary

Under enableGlobalVirtualStore, a dependency's lifecycle script failed to spawn on Windows with The directory name is invalid. (os error 267). Fixes #15111.

Windows keeps a process's working directory in a MAX_PATH field and refuses a longer one with ERROR_DIRECTORY — the same answer it gives for a directory that is not there. A global virtual store slot reaches that length readily, because its path spells out the scope, the name, the version and a 64-character graph digest beneath the configured store directory. The directory CI was refused is 272 characters:

C:\Users\runneradmin\AppData\Local\Temp\pacquet-test-mBtuIA\workspace\../pacquet-store\v11\links\@pnpm.e2e\pre-and-postinstall-scripts-example\1.0.0\<64 hex>\node_modules\@pnpm.e2e\pre-and-postinstall-scripts-example

The bound Windows actually applies sits below MAX_PATH and is not documented — a normalized 259-character spelling of that same slot is refused too — so pnpm does not predict it. The spawn goes to the directory the install computed, and only once the refusal comes back are shorter spellings of it tried, in order:

  1. The lexically normalized path, which drops the .. a relative storeDir leaves behind. Waiting for the refusal is what makes this safe as well as cheap: a working directory the OS was willing to open is never second-guessed, because .. through a symlinked directory does not lead where resolving it lexically says it does.
  2. Its 8.3 short form, which a volume that still generates short names answers with.

When no spelling works, the first refusal is the one reported, since it names the directory the install computed.

ERR_PNPM_EXECUTOR_SPAWN_LIFECYCLE now carries that directory. The operating system reports only that it refused one, and under the global virtual store nobody typed the path — this is what identified the length as the cause.

The seven global_virtual_store::builds tests that reach a lifecycle script no longer skip on Windows. Only one of them installs a package whose slot stays under the limit, so six of them could not have run there before.

Separately, the slot's node_modules/<name> was joined as a single path component, leaving the / of a scoped name inside it. That is not what CreateProcessW rejected, but it is what CreateSymbolicLinkW rejects — the reason pnpm_fs::to_native_separators exists. pnpm_fs::join_slash_separated_path and pnpm_fs::push_slash_separated_path now make that join in every producer of a slot or modules path: deps-restorer (safe_join_modules_dir, build_modules/slots, virtual_store_layout, package_map), graph-hasher, env-installer, and the executor's PATH walk.

Squash Commit Body

Under enableGlobalVirtualStore a dependency's lifecycle script failed to
spawn on Windows with `The directory name is invalid. (os error 267)`.

Windows keeps a process's working directory in a MAX_PATH field and
refuses a longer one with ERROR_DIRECTORY, the same answer it gives for
a directory that is not there. A global virtual store slot reaches that
length readily: its path spells out the scope, the name, the version
and a 64-character graph digest under whatever store directory the user
configured.

The bound Windows applies sits below MAX_PATH and is undocumented, and
the error does not say which of the two conditions it meant, so neither
is predicted. The spawn goes to the directory the install computed, and
shorter spellings of it are tried only once the refusal has come back:
first the lexically normalized path, which drops the `..` a relative
storeDir leaves behind, then its 8.3 short form, which a volume that
still generates short names answers with. Waiting for the refusal is
what makes the first of those safe as well as cheap, since `..` through
a symlinked directory does not lead where resolving it lexically says
it does. When no spelling works the first refusal is reported, naming
the directory the install computed.

ERR_PNPM_EXECUTOR_SPAWN_LIFECYCLE carries that directory, which is what
identified the length as the cause.

Separately, the slot's `node_modules/<name>` was joined as one path
component, leaving the `/` of a scoped name inside it. CreateProcessW
takes that in its stride but CreateSymbolicLinkW does not, which is why
pnpm_fs::to_native_separators exists. pnpm_fs::join_slash_separated_path
and push_slash_separated_path now make that join in every producer of a
slot or modules path, and carry the rationale in one place.

Fixes pnpm/pnpm#15111.

Checklist

  • I checked the referenced issue and verified that none of the PRs
    already linked to it solves it.
  • New features are implemented only in the Rust pnpm v12 CLI. Bug fixes
    are implemented in every affected version. The working-directory limit is
    the operating system's, and pnpm 11 formats the same 64-hex slot segment,
    so it can reach it too; this PR fixes pnpm v12 only.
  • Added a changeset (pnpm changeset) if this PR changes any published
    package. Keep it short and written for pnpm users — it becomes a release note.
  • Added or updated tests.
  • Updated the documentation if needed.

Written by an agent (Claude Code, claude-opus-5).

@qodo-code-review

Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@coderabbitai

coderabbitai Bot commented Sep 19, 2026

Copy link
Copy Markdown

Review Change StackReview 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: Repository: pnpm/pnpm/.coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: e87e9f00-4018-4aad-a6fa-a5ca5875651a

📥 Commits

Reviewing files that changed from the base of the PR and between 7d63802 and eaa8e74.

📒 Files selected for processing (1)
  • pnpm/crates/deps-restorer/src/build_modules/slots.rs

Included review availability: Your plan provides up to 10 included reviews per hour; 8 remain after this review.


📝 Walkthrough

Walkthrough

The change adds shared slash-separated path helpers, uses them for scoped package paths in GVS and executor code, enables GVS lifecycle tests on Windows, and adds a patch changeset.

Changes

Global virtual store lifecycle scripts

Layer / File(s) Summary
Scoped path construction
pnpm/crates/fs/src/relative_path.rs, pnpm/crates/fs/src/lib.rs, pnpm/crates/graph-hasher/..., pnpm/crates/deps-restorer/src/{build_modules/slots.rs,safe_join_modules_dir.rs}, pnpm/crates/cli/tests/suite/global_virtual_store.rs
The shared helpers split /-separated paths into platform-native components. GVS and dependency-restoration code uses the helpers for scoped package paths.
Injected target path handling
pnpm/crates/deps-restorer/src/virtual_store_layout.rs
Injected targets now add scoped package-name segments as nested node_modules path components.
Windows executor path resolution
pnpm/crates/executor/Cargo.toml, pnpm/crates/executor/src/extend_path.rs
The executor converts Windows path separators before resolving the lifecycle-script working directory. Ancestor paths use the shared segment-pushing helper.
Windows lifecycle validation
pnpm/crates/deps-restorer/src/safe_join_modules_dir/tests.rs, pnpm/crates/cli/tests/suite/global_virtual_store/builds.rs
Alias path expectations use segment-by-segment construction. GVS lifecycle tests no longer skip on Windows.
Release declaration
.changeset/fix-gvs-lifecycle-script-cwd-windows.md
A patch release for pacquet documents the Windows scoped-dependency fix.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Bug fix · Severity of issue fixed: Medium

Assessment against linked issues

Objective Addressed Explanation
Fix invalid lifecycle-script working directories for scoped dependencies under the global virtual store on Windows [#15111]

Suggested labels: product: pacquet

✨ Finishing Touches 💡 1
🧪 Generate unit tests (beta)
  • Create a new PR
🛠️ Fix failing CI checks 💡
  • Commit to this branch
  • Create a new PR

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.

@greptile-apps

greptile-apps Bot commented Sep 19, 2026

Copy link
Copy Markdown

RetriggerConfidence Score: 5/5

The PR appears safe to merge; no outstanding correctness or repository-rule violations remain.

Reviews (15) · Last reviewed commit: "docs(deps-restorer): state the peer-stri..."

Comment thread pnpm/crates/deps-restorer/src/safe_join_modules_dir.rs Outdated
@github-actions github-actions Bot added the reviewed: coderabbit CodeRabbit submitted an approving review label Sep 19, 2026
@codecov-commenter

codecov-commenter commented Sep 19, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 79.76190% with 17 lines in your changes missing coverage. Please review.
✅ Project coverage is 91.38%. Comparing base (8d220e5) to head (0cc1fbe).
⚠️ Report is 9 commits behind head on main.

Files with missing lines Patch % Lines
pnpm/crates/executor/src/lifecycle.rs 59.09% 9 Missing ⚠️
pnpm/crates/executor/src/script_working_dir.rs 85.71% 4 Missing ⚠️
pnpm/crates/deps-restorer/src/package_map.rs 50.00% 3 Missing ⚠️
pnpm/crates/executor/src/extend_path.rs 80.00% 1 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main   #15147   +/-   ##
=======================================
  Coverage   91.38%   91.38%           
=======================================
  Files        1362     1363    +1     
  Lines      182893   182969   +76     
=======================================
+ Hits       167131   167203   +72     
- Misses      15762    15766    +4     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions

github-actions Bot commented Sep 19, 2026

Copy link
Copy Markdown
Contributor

Integrated-Benchmark Report (Linux)

Commit: 0cc1fbe4185b

Regular scenarios report direct and pnpr installs. The peer-heavy resolver scenario compares current Rust, main Rust, and TypeScript pnpm; the linked-workspace scenario compares current Rust against main Rust. Bencher consumes pacquet@HEAD and pnpr@HEAD.

The tables below show mean ± σ; Bencher thresholds on the minimum latency, which is far less perturbed by shared-runner contention (noise only adds time).

Scenario: Isolated linker: fresh restore, cold cache + cold store

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 2.322 ± 0.085 2.256 2.512 1.65 ± 0.15
pacquet@main 2.414 ± 0.139 2.239 2.640 1.72 ± 0.17
pnpr@HEAD 1.406 ± 0.116 1.309 1.680 1.00
pnpr@main 1.445 ± 0.133 1.302 1.682 1.03 ± 0.13
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 2.3219387332000005,
      "stddev": 0.08492086791398075,
      "median": 2.2943252024,
      "user": 1.31137784,
      "system": 1.5462546999999998,
      "min": 2.2561456624,
      "max": 2.5121137284
    },
    {
      "command": "pacquet@main",
      "mean": 2.4135974695,
      "stddev": 0.13880031023501363,
      "median": 2.3784772474,
      "user": 1.2811995399999998,
      "system": 1.4732870999999998,
      "min": 2.2392873474,
      "max": 2.6397291914
    },
    {
      "command": "pnpr@HEAD",
      "mean": 1.405793864,
      "stddev": 0.11573652875313035,
      "median": 1.3763974194,
      "user": 1.23288984,
      "system": 1.5430451,
      "min": 1.3086155204,
      "max": 1.6799619114
    },
    {
      "command": "pnpr@main",
      "mean": 1.4451156814000001,
      "stddev": 0.13287892104204538,
      "median": 1.4036565879,
      "user": 1.1986563400000003,
      "system": 1.5416415999999997,
      "min": 1.3017060404,
      "max": 1.6821186004
    }
  ]
}

Scenario: Isolated linker: fresh restore, hot cache + hot store

Command Mean [ms] Min [ms] Max [ms] Relative
pacquet@HEAD 209.1 ± 10.1 192.7 225.4 1.00
pacquet@main 249.3 ± 50.4 193.3 313.2 1.19 ± 0.25
pnpr@HEAD 214.7 ± 10.0 200.8 229.8 1.03 ± 0.07
pnpr@main 231.4 ± 19.7 221.5 286.2 1.11 ± 0.11
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 0.20912745776000002,
      "stddev": 0.01007548289180315,
      "median": 0.20781688476000004,
      "user": 0.2378024,
      "system": 0.51110268,
      "min": 0.19273416626000003,
      "max": 0.22542754026000003
    },
    {
      "command": "pacquet@main",
      "mean": 0.24930626246000004,
      "stddev": 0.05035951427078881,
      "median": 0.22807414976,
      "user": 0.2610717,
      "system": 0.6025672799999999,
      "min": 0.19328491026000003,
      "max": 0.31320519626000004
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.21474436136000002,
      "stddev": 0.010044261452065944,
      "median": 0.21690889876000002,
      "user": 0.2116786,
      "system": 0.50613778,
      "min": 0.20078544526000003,
      "max": 0.22977535426000004
    },
    {
      "command": "pnpr@main",
      "mean": 0.23139130516,
      "stddev": 0.01973944072665529,
      "median": 0.22360860126000004,
      "user": 0.24645720000000004,
      "system": 0.53947468,
      "min": 0.22151082426000002,
      "max": 0.28615411826000003
    }
  ]
}

Scenario: Isolated linker: repeat install, hot cache + hot store

Command Mean [ms] Min [ms] Max [ms] Relative
pacquet@HEAD 11.1 ± 2.8 7.1 17.7 1.41 ± 0.37
pacquet@main 8.8 ± 2.7 6.5 17.0 1.12 ± 0.36
pnpr@HEAD 8.3 ± 0.7 7.0 10.9 1.06 ± 0.12
pnpr@main 7.9 ± 0.6 6.6 11.5 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 0.01110597286767123,
      "stddev": 0.0027643467473086867,
      "median": 0.01081197358,
      "user": 0.006776100273972604,
      "system": 0.005089645296803655,
      "min": 0.00710942858,
      "max": 0.01773239258
    },
    {
      "command": "pacquet@main",
      "mean": 0.008802641896831682,
      "stddev": 0.002733719275776084,
      "median": 0.007586212080000001,
      "user": 0.005308556633663364,
      "system": 0.004115963762376235,
      "min": 0.00653110858,
      "max": 0.01695950258
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.008299663487185633,
      "stddev": 0.0006964647378836183,
      "median": 0.00814389708,
      "user": 0.004808610059880241,
      "system": 0.004141361676646705,
      "min": 0.0070206595800000006,
      "max": 0.01090004158
    },
    {
      "command": "pnpr@main",
      "mean": 0.007852818583095979,
      "stddev": 0.0005673868128441512,
      "median": 0.00781527758,
      "user": 0.004427757151702787,
      "system": 0.004042514551083591,
      "min": 0.006590418580000001,
      "max": 0.01152690158
    }
  ]
}

Scenario: Isolated linker: repeat install, cold cache + hot store

Command Mean [ms] Min [ms] Max [ms] Relative
pacquet@HEAD 7.3 ± 0.5 6.3 8.7 1.04 ± 0.10
pacquet@main 7.0 ± 0.5 5.8 8.9 1.00
pnpr@HEAD 7.2 ± 0.5 6.2 9.7 1.03 ± 0.10
pnpr@main 7.0 ± 0.4 6.1 8.8 1.00 ± 0.09
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 0.007264892248391607,
      "stddev": 0.00045524928233674135,
      "median": 0.00723802764,
      "user": 0.0040403883916083905,
      "system": 0.0036872998601398597,
      "min": 0.00625148214,
      "max": 0.008747487140000001
    },
    {
      "command": "pacquet@main",
      "mean": 0.006966632214766354,
      "stddev": 0.0004937983337145535,
      "median": 0.0069637981400000005,
      "user": 0.004347429532710278,
      "system": 0.003076677570093459,
      "min": 0.00580029514,
      "max": 0.00887571814
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.007190832302650603,
      "stddev": 0.00048360664048917903,
      "median": 0.007168137640000001,
      "user": 0.004058502891566265,
      "system": 0.003605713373493977,
      "min": 0.00620606614,
      "max": 0.00974869714
    },
    {
      "command": "pnpr@main",
      "mean": 0.0069814202322190134,
      "stddev": 0.0004028820328247432,
      "median": 0.0069594691400000006,
      "user": 0.004294138789625357,
      "system": 0.003106775734870315,
      "min": 0.00610092914,
      "max": 0.00875862814
    }
  ]
}

Scenario: Isolated linker: fresh install, cold cache + cold store

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 2.483 ± 0.115 2.418 2.768 1.67 ± 0.15
pacquet@main 2.498 ± 0.069 2.422 2.650 1.68 ± 0.13
pnpr@HEAD 1.521 ± 0.129 1.390 1.723 1.02 ± 0.12
pnpr@main 1.490 ± 0.111 1.381 1.760 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 2.48334781736,
      "stddev": 0.1152900664753419,
      "median": 2.4397476880599998,
      "user": 1.6943682199999999,
      "system": 1.6347579999999997,
      "min": 2.41830000706,
      "max": 2.76843342906
    },
    {
      "command": "pacquet@main",
      "mean": 2.49849890396,
      "stddev": 0.0689259531840303,
      "median": 2.4776401025599997,
      "user": 1.8011657199999997,
      "system": 1.6489116,
      "min": 2.42244445306,
      "max": 2.65035694006
    },
    {
      "command": "pnpr@HEAD",
      "mean": 1.5210571385600002,
      "stddev": 0.12874919718008124,
      "median": 1.50151966406,
      "user": 1.08108732,
      "system": 1.2890732000000003,
      "min": 1.38964363506,
      "max": 1.7231126490600002
    },
    {
      "command": "pnpr@main",
      "mean": 1.49036879826,
      "stddev": 0.11107998351302861,
      "median": 1.4652117805600002,
      "user": 1.11655412,
      "system": 1.2272353999999999,
      "min": 1.3814650910600001,
      "max": 1.75951381806
    }
  ]
}

Scenario: Isolated linker: fresh install, hot cache + hot store

Command Mean [ms] Min [ms] Max [ms] Relative
pacquet@HEAD 360.8 ± 28.1 326.3 416.3 1.35 ± 0.13
pacquet@main 355.9 ± 29.3 324.4 426.0 1.33 ± 0.13
pnpr@HEAD 267.5 ± 13.5 250.5 298.6 1.00
pnpr@main 297.0 ± 39.1 244.6 359.8 1.11 ± 0.16
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 0.3608288499,
      "stddev": 0.028137507978301567,
      "median": 0.3480492998,
      "user": 0.56910202,
      "system": 0.7987947000000001,
      "min": 0.3262722983,
      "max": 0.4162866383
    },
    {
      "command": "pacquet@main",
      "mean": 0.3559148458,
      "stddev": 0.029288846973360858,
      "median": 0.34578162830000003,
      "user": 0.53919812,
      "system": 0.8089073000000001,
      "min": 0.3244195583,
      "max": 0.4260144173
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.26750146350000004,
      "stddev": 0.013468699536830312,
      "median": 0.2674865388,
      "user": 0.21047822,
      "system": 0.5058583,
      "min": 0.2505037103,
      "max": 0.2985661183
    },
    {
      "command": "pnpr@main",
      "mean": 0.2969718627,
      "stddev": 0.03905801272908955,
      "median": 0.2895868053,
      "user": 0.24091312,
      "system": 0.5712501,
      "min": 0.2446094723,
      "max": 0.3597510333
    }
  ]
}

Scenario: Isolated linker: fresh install, cold cache + hot store

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 1.573 ± 0.069 1.525 1.706 5.82 ± 0.30
pacquet@main 1.557 ± 0.056 1.524 1.708 5.76 ± 0.26
pnpr@HEAD 0.270 ± 0.008 0.264 0.288 1.00
pnpr@main 0.271 ± 0.007 0.262 0.284 1.00 ± 0.04
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 1.5730497537199994,
      "stddev": 0.06925034273605457,
      "median": 1.5420333523199998,
      "user": 0.8646922,
      "system": 0.9710522000000001,
      "min": 1.52510224132,
      "max": 1.70553497432
    },
    {
      "command": "pacquet@main",
      "mean": 1.5574768671199999,
      "stddev": 0.0558017190383619,
      "median": 1.53518523432,
      "user": 0.8292456999999999,
      "system": 0.9454223,
      "min": 1.52396945632,
      "max": 1.70813202932
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.27034319312000005,
      "stddev": 0.007500097304318636,
      "median": 0.26834484782,
      "user": 0.2084689,
      "system": 0.5272427,
      "min": 0.26361965932000003,
      "max": 0.28828399832
    },
    {
      "command": "pnpr@main",
      "mean": 0.27128234792000006,
      "stddev": 0.006769226453506438,
      "median": 0.27116527481999997,
      "user": 0.229005,
      "system": 0.5058005,
      "min": 0.26184290132000004,
      "max": 0.28360913832
    }
  ]
}

Scenario: Isolated linker: fresh resolve, hot cache, offline

Command Mean [ms] Min [ms] Max [ms] Relative
pacquet@HEAD 195.0 ± 12.1 181.3 226.7 2.19 ± 0.15
pacquet@main 196.7 ± 14.0 179.4 230.0 2.21 ± 0.17
pnpr@HEAD 90.2 ± 2.8 86.1 95.8 1.02 ± 0.04
pnpr@main 88.9 ± 2.1 85.8 94.9 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 0.19495686479230773,
      "stddev": 0.01208623192400324,
      "median": 0.1906657391,
      "user": 0.20204680923076923,
      "system": 0.0704655923076923,
      "min": 0.18129951610000003,
      "max": 0.22673459610000002
    },
    {
      "command": "pacquet@main",
      "mean": 0.1967323866,
      "stddev": 0.013967027311822248,
      "median": 0.1948142216,
      "user": 0.19884418285714286,
      "system": 0.0734111857142857,
      "min": 0.1794425611,
      "max": 0.23001950610000002
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.09023274134242422,
      "stddev": 0.0028120837891492272,
      "median": 0.09023748010000002,
      "user": 0.03373549454545454,
      "system": 0.013218572727272731,
      "min": 0.08609099710000001,
      "max": 0.09575614610000001
    },
    {
      "command": "pnpr@main",
      "mean": 0.08887259456875002,
      "stddev": 0.002146724509003531,
      "median": 0.0884474251,
      "user": 0.03150729,
      "system": 0.013253403125,
      "min": 0.08575047310000002,
      "max": 0.09492807610000001
    }
  ]
}

Scenario: Isolated linker: peer-heavy resolve, hot cache, offline

Command Mean [ms] Min [ms] Max [ms] Relative
pacquet@HEAD 636.5 ± 59.3 565.2 717.6 1.05 ± 0.12
pacquet@main 604.7 ± 44.2 553.4 676.4 1.00
pnpm@HEAD 2903.7 ± 92.1 2783.9 3051.4 4.80 ± 0.38
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 0.636452921408889,
      "stddev": 0.05931568312357954,
      "median": 0.6117806825200001,
      "user": 0.6692096511111111,
      "system": 0.14597464,
      "min": 0.5651720115200001,
      "max": 0.7175928395200001
    },
    {
      "command": "pacquet@main",
      "mean": 0.6047473920755556,
      "stddev": 0.044192464103604404,
      "median": 0.6107808035200001,
      "user": 0.6290084288888887,
      "system": 0.14099186222222224,
      "min": 0.5533779925200001,
      "max": 0.6763702735200001
    },
    {
      "command": "pnpm@HEAD",
      "mean": 2.903734755408889,
      "stddev": 0.09207016522725738,
      "median": 2.9226788805200004,
      "user": 4.585938762222222,
      "system": 0.2184438622222222,
      "min": 2.78388671952,
      "max": 3.05143950252
    }
  ]
}

Scenario: Isolated linker: linked-workspace resolve, hot cache, offline

Command Mean [ms] Min [ms] Max [ms] Relative
pacquet@HEAD 158.6 ± 2.0 156.7 165.5 1.00
pacquet@main 160.7 ± 3.6 156.3 167.9 1.01 ± 0.03
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 0.15858164354222223,
      "stddev": 0.0020128493349129703,
      "median": 0.15810925582000002,
      "user": 0.11760723111111109,
      "system": 0.07321674222222221,
      "min": 0.15668169632,
      "max": 0.16554984832
    },
    {
      "command": "pacquet@main",
      "mean": 0.16074132926444448,
      "stddev": 0.0036163238758597804,
      "median": 0.15975233332,
      "user": 0.11558861999999998,
      "system": 0.08186229777777776,
      "min": 0.15628042332,
      "max": 0.16788189332
    }
  ]
}

Scenario: Isolated linker: fresh restore, cold cache + cold store + cold pnpr

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 4.430 ± 0.120 4.328 4.657 1.27 ± 0.05
pacquet@main 4.435 ± 0.124 4.333 4.725 1.27 ± 0.05
pnpr@HEAD 3.533 ± 0.137 3.403 3.733 1.02 ± 0.05
pnpr@main 3.479 ± 0.099 3.393 3.666 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 4.429545742019999,
      "stddev": 0.11958130615549659,
      "median": 4.38717327332,
      "user": 1.5321806999999998,
      "system": 1.9729548399999999,
      "min": 4.32806994282,
      "max": 4.65696982282
    },
    {
      "command": "pacquet@main",
      "mean": 4.43463527332,
      "stddev": 0.12367424754740686,
      "median": 4.370414201819999,
      "user": 1.5404839000000001,
      "system": 1.99417884,
      "min": 4.33325006582,
      "max": 4.72460105182
    },
    {
      "command": "pnpr@HEAD",
      "mean": 3.53279855342,
      "stddev": 0.13710294503617218,
      "median": 3.4662356793200004,
      "user": 1.4392002000000002,
      "system": 1.8414586400000001,
      "min": 3.4033191988200002,
      "max": 3.73313429882
    },
    {
      "command": "pnpr@main",
      "mean": 3.4794367573200007,
      "stddev": 0.09936141279405686,
      "median": 3.41998431632,
      "user": 1.4334073,
      "system": 1.8601518400000003,
      "min": 3.39282430782,
      "max": 3.66591809682
    }
  ]
}

Scenario: GVS linker: fresh restore, hot cache + hot store

Same install as the isolated fresh-restore hot/hot scenario, into the shared virtual store, the layout pnpm 12 installs into by default. Scenarios run on separate machines, so compare main and branch within each table.

Command Mean [ms] Min [ms] Max [ms] Relative
pacquet@HEAD 111.0 ± 3.0 107.0 118.3 1.00
pacquet@main 115.4 ± 5.2 104.8 122.3 1.04 ± 0.05
pnpr@HEAD 129.3 ± 4.0 122.6 136.9 1.17 ± 0.05
pnpr@main 129.4 ± 4.6 123.1 143.7 1.17 ± 0.05
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 0.11098435537391306,
      "stddev": 0.003030635440105974,
      "median": 0.1103800232,
      "user": 0.13904435304347823,
      "system": 0.11622053478260867,
      "min": 0.1069637882,
      "max": 0.1182829822
    },
    {
      "command": "pacquet@main",
      "mean": 0.11543729749166666,
      "stddev": 0.0052204562723426814,
      "median": 0.1167610182,
      "user": 0.15647194,
      "system": 0.11461701666666667,
      "min": 0.1048493692,
      "max": 0.1223294982
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.1293357304105263,
      "stddev": 0.00399233663098175,
      "median": 0.1295059122,
      "user": 0.15895943999999998,
      "system": 0.13228336315789474,
      "min": 0.12255108420000001,
      "max": 0.1369289792
    },
    {
      "command": "pnpr@main",
      "mean": 0.12942880535000004,
      "stddev": 0.0045512081887046025,
      "median": 0.12978929820000001,
      "user": 0.15991408999999998,
      "system": 0.13077299999999997,
      "min": 0.1230599502,
      "max": 0.1436773952
    }
  ]
}

@github-actions

github-actions Bot commented Sep 19, 2026

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Projectpnpm's project
Branchpr/15147
Testbedpacquet
Click to view all benchmark results
BenchmarkLatencyBenchmark Result
milliseconds (ms)
(Result Δ%)
Upper Boundary
milliseconds (ms)
(Limit %)
gvs-linker.fresh-restore.hot-cache.hot-store📈 view plot
🚷 view threshold
106.96 ms
(+2.51%)Baseline: 104.35 ms
125.21 ms
(85.42%)
isolated-linker.fresh-install.cold-cache.cold-store📈 view plot
🚷 view threshold
2,418.30 ms
(-0.81%)Baseline: 2,438.01 ms
2,925.62 ms
(82.66%)
isolated-linker.fresh-install.cold-cache.hot-store📈 view plot
🚷 view threshold
1,525.10 ms
(-0.03%)Baseline: 1,525.53 ms
1,830.64 ms
(83.31%)
isolated-linker.fresh-install.hot-cache.hot-store📈 view plot
🚷 view threshold
326.27 ms
(-5.83%)Baseline: 346.46 ms
415.75 ms
(78.48%)
isolated-linker.fresh-resolve.hot-cache.offline📈 view plot
🚷 view threshold
181.30 ms
(-4.44%)Baseline: 189.72 ms
227.67 ms
(79.63%)
isolated-linker.fresh-restore.cold-cache.cold-store📈 view plot
🚷 view threshold
2,256.15 ms
(+0.49%)Baseline: 2,245.23 ms
2,694.28 ms
(83.74%)
isolated-linker.fresh-restore.cold-cache.cold-store.cold-pnpr📈 view plot
🚷 view threshold
4,328.07 ms
(+1.00%)Baseline: 4,285.10 ms
5,142.12 ms
(84.17%)
isolated-linker.fresh-restore.hot-cache.hot-store📈 view plot
🚷 view threshold
192.73 ms
(-6.13%)Baseline: 205.31 ms
246.38 ms
(78.23%)
isolated-linker.linked-workspace-resolve.hot-cache.offline📈 view plot
🚷 view threshold
156.68 ms
(-2.70%)Baseline: 161.03 ms
193.23 ms
(81.08%)
isolated-linker.peer-heavy-resolve.hot-cache.offline📈 view plot
🚷 view threshold
565.17 ms
(+10.73%)Baseline: 510.41 ms
612.50 ms
(92.27%)
isolated-linker.repeat-install.cold-cache.hot-store📈 view plot
🚷 view threshold
6.25 ms
(+2.95%)Baseline: 6.07 ms
7.29 ms
(85.79%)
isolated-linker.repeat-install.hot-cache.hot-store📈 view plot
🚷 view threshold
7.11 ms
(+14.88%)Baseline: 6.19 ms
7.43 ms
(95.73%)
🐰 View full continuous benchmarking report in Bencher

Comment thread pnpm/crates/fs/src/relative_path.rs Outdated
@github-actions

github-actions Bot commented Sep 19, 2026

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Projectpnpm's project
Branchpr/15147
Testbedpnpr

⚠️ WARNING: No Threshold found!

Without a Threshold, no Alerts will ever be generated.

Click here to create a new Threshold
For more information, see the Threshold documentation.
To only post results if a Threshold exists, set the --ci-only-thresholds flag.

Click to view all benchmark results
BenchmarkLatencymilliseconds (ms)
gvs-linker.fresh-restore.hot-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
122.55 ms
isolated-linker.fresh-install.cold-cache.cold-store📈 view plot
⚠️ NO THRESHOLD
1,389.64 ms
isolated-linker.fresh-install.cold-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
263.62 ms
isolated-linker.fresh-install.hot-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
250.50 ms
isolated-linker.fresh-resolve.hot-cache.offline📈 view plot
⚠️ NO THRESHOLD
86.09 ms
isolated-linker.fresh-restore.cold-cache.cold-store📈 view plot
⚠️ NO THRESHOLD
1,308.62 ms
isolated-linker.fresh-restore.cold-cache.cold-store.cold-pnpr📈 view plot
⚠️ NO THRESHOLD
3,403.32 ms
isolated-linker.fresh-restore.hot-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
200.79 ms
isolated-linker.repeat-install.cold-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
6.21 ms
isolated-linker.repeat-install.hot-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
7.02 ms
🐰 View full continuous benchmarking report in Bencher

greptile-apps[bot]
greptile-apps Bot previously approved these changes Sep 20, 2026
@greptile-apps
greptile-apps Bot dismissed their stale review September 20, 2026 13:53

Dismissed because a newer commit was pushed; Greptile will re-review the current head.

greptile-apps[bot]
greptile-apps Bot previously approved these changes Sep 20, 2026
@greptile-apps
greptile-apps Bot dismissed their stale review September 20, 2026 15:28

Dismissed because a newer commit was pushed; Greptile will re-review the current head.

greptile-apps[bot]
greptile-apps Bot previously approved these changes Sep 20, 2026
…es dir under GVS

When joining scoped package aliases (such as `@scope/name`) onto node_modules,
Path::join on Windows appends the string as-is, leaving forward slashes inside
the package name component. When spawning lifecycle scripts under the global
virtual store, this resulted in an invalid working directory path with mixed
separators, causing CreateProcess to fail with os error 267 (ERROR_DIRECTORY).

Split dependency aliases and package names on '/' when building node_modules
paths in safe_join_modules_dir and virtual_store_dir_for_key to ensure native
path separators are used on Windows.

Fixes pnpm#15111.
Normalize head and tail path components in ancestor_node_modules_bins
so that PATH components on Windows do not retain forward slashes.
Use key.name.to_string() in virtual_store_dir_for_key to avoid manual
string slicing and keep it aligned with safe_join_modules_dir.
…le script spawn

Ensure working directory paths pass through dunce::simplified when spawning
lifecycle scripts and shell commands.

On Windows, dunce::canonicalize prepends UNC prefixes (\?\), which causes
cmd.exe to fail with OS error 267 (ERROR_DIRECTORY) when set as the working
directory. Using dunce::simplified strips UNC prefixes while maintaining native
path separators.

Fixes pnpm#15111.
@zkochan
zkochan force-pushed the fix-pnpm-issue-15111 branch from 4c0a747 to 11f5faa Compare September 20, 2026 21:25
@qodo-code-review

qodo-code-review Bot commented Sep 20, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Windows regression tests never prove scripts start ✓ Resolved 📜 Skill insight ▣ Testability
Description
every_spelling_offered_for_a_refused_slot_is_shorter only checks that fallback spellings are
shorter and resolve to the same directory, without spawning a lifecycle process through the fallback
path. The test can pass while every candidate still fails as a working directory, so the reported
Windows lifecycle-script fix is not directly regression-tested.
Code

pnpm/crates/executor/src/script_working_dir/tests.rs[R58-80]

+fn every_spelling_offered_for_a_refused_slot_is_shorter() {
+    let root = tempfile::Builder::new()
+        .prefix("pnpm-wd-")
+        .tempdir()
+        .expect("create temporary directory");
+    let slot = slot_dir(root.path());
+    std::fs::create_dir_all(&slot).expect("create the deep slot");
+
+    let refusal = spawn_in(&slot).expect_err("Windows must refuse a working directory this long");
+    assert!(is_refused_directory(&refusal), "expected ERROR_DIRECTORY, got {refusal:?}");
+
+    for spelling in shorter_working_dirs(&slot) {
+        assert_eq!(
+            std::fs::canonicalize(&spelling).expect("canonicalize a shorter spelling"),
+            std::fs::canonicalize(&slot).expect("canonicalize the slot"),
+            "{spelling:?} must name the same directory as the slot",
+        );
+        assert!(
+            spelling.as_os_str().len() < slot.as_os_str().len(),
+            "{spelling:?} is not shorter than the {}-character slot it stands in for",
+            slot.as_os_str().len(),
+        );
+    }
Relevance

●●● Strong

Recent history accepts strengthening regression tests to exercise the actual failure mode, not
merely indirect invariants.

PR-#15182
PR-#15153

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The added Windows test creates a refused long directory and asserts only that each candidate
canonicalizes to the same directory and is shorter. No assertion starts a process with a candidate
working directory, while the production behavior being fixed is process creation from the lifecycle
directory.

pnpm/crates/executor/src/script_working_dir/tests.rs[58-80]
Skill: review-code

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The Windows regression test verifies only path length and directory identity. It does not prove that the fallback working directory can actually start a child process, so the lifecycle-script failure can remain undetected.

## Fix Focus Areas
- pnpm/crates/executor/src/script_working_dir/tests.rs[58-80]

## Recommended Fix
After generating each shorter spelling, invoke a child process with that spelling as its current directory and assert that spawning succeeds. Prefer exercising the same spawn helper or lifecycle spawn path used by production so the test fails when the fallback does not solve the original working-directory refusal.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Release note overstates Windows fix ✓ Resolved 📜 Skill insight ≡ Correctness
Description
.changeset/fix-gvs-lifecycle-script-cwd-windows.md says dependencies with build scripts now
install on Windows under enableGlobalVirtualStore, but shorten_to_working_dir_limit returns an
over-limit path unchanged when normalization and the 8.3 conversion cannot shorten it. Stores on
volumes without short-name generation and long network-share paths therefore still reach process
spawning unchanged, so the unqualified release-note outcome extends beyond the implemented cases.
Code

.changeset/fix-gvs-lifecycle-script-cwd-windows.md[5]

+Dependencies that run build scripts now install on Windows under `enableGlobalVirtualStore`. A global virtual store slot can be longer than the working directory Windows allows a process. The install used to fail there with `The directory name is invalid. (os error 267)` [#15111](https://github.com/pnpm/pnpm/issues/15111).
Relevance

●●● Strong

Recent precedents accept release-note qualifications when implementation supports only narrower
conditions than the claim.

PR-#15073
PR-#14963
PR-#15132

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Rule 3365343 requires changeset claims to match the implemented user-visible behavior. The changeset
makes an unconditional Windows success claim, while the implementation documents and returns the
original over-limit path when no shortening method succeeds and excludes network-share paths from
8.3 conversion.

.changeset/fix-gvs-lifecycle-script-cwd-windows.md[5-5]
pnpm/crates/executor/src/script_working_dir.rs[39-51]
pnpm/crates/executor/src/script_working_dir.rs[76-82]
pnpm/crates/executor/src/script_working_dir.rs[98-107]
Skill: review-code

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The changeset promises that global-virtual-store build scripts now install on Windows without disclosing that overlong paths still fail when they cannot be normalized or converted to an 8.3 path.

## Fix Focus Areas
- .changeset/fix-gvs-lifecycle-script-cwd-windows.md[5-5]

## Recommended Fix
Revise the release note to state that the fix applies when pnpm can shorten the working directory, without claiming success for every Windows global virtual store path.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Lifecycle scripts use the wrong folder ✓ Resolved 📜 Skill insight ⛨ Security
Description
shorten_to_working_dir_limit applies pnpm_fs::lexical_normalize to over-limit paths, removing
.. without preserving symlink traversal semantics and potentially selecting a different package
root. On Windows, a repository-relative storeDir containing symlink/.. triggers this once the
slot exceeds 259 UTF-16 units, so the manifest, lifecycle environment, and PATH can refer to the
original root while shell and emulated lifecycle commands execute from the rewritten root.
Code

pnpm/crates/executor/src/script_working_dir.rs[R47-49]

+    let normalized = pnpm_fs::lexical_normalize(path);
+    if utf16_len(&normalized) <= MAX_WORKING_DIR_CHARS {
+        return Cow::Owned(normalized);
Relevance

●● Moderate

Potential symlink-sensitive path mismatch is plausible, but history lacks a closely matching
accepted or rejected precedent.

PR-#13403
PR-#13445

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The shortening function uses filesystem-free lexical normalization for every over-limit path when
the normalized spelling fits, and that helper removes preceding components without inspecting
symlinks. Repository-controlled relative store paths can contain .., so a symlink can make the
normalized destination differ from the installed package root; manifest selection remains based on
the original root, while the changed shell and emulator call sites use the rewritten root as their
working directory, demonstrating the mismatched execution path and the failure to keep path
resolution within its intended destination.

pnpm/crates/executor/src/script_working_dir.rs[26-51]
pnpm/crates/executor/src/lifecycle.rs[399-410]
pnpm/crates/executor/src/lifecycle.rs[460-466]
pnpm/crates/config/src/workspace_yaml/apply.rs[130-154]
pnpm/crates/fs/src/lexical_normalize.rs[6-17]
pnpm/crates/fs/src/lexical_normalize.rs[90-101]
pnpm/crates/executor/src/lifecycle.rs[176-184]
pnpm/crates/executor/src/lifecycle.rs[402-410]
pnpm/crates/executor/src/lifecycle.rs[463-466]
Skill: review-code

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Long Windows lifecycle working directories are lexically normalized before execution, even though removing `..` after a symlink can resolve to a different directory. This can run a lifecycle script from a working directory other than the package root whose manifest supplied the script, while the lifecycle environment and `PATH` still reference the original root.

## Fix Focus Areas
- pnpm/crates/executor/src/script_working_dir.rs[43-51]
- pnpm/crates/executor/src/lifecycle.rs[402-410]
- pnpm/crates/executor/src/lifecycle.rs[463-466]
- pnpm/crates/executor/src/script_working_dir/tests.rs[23-46]

## Recommended Fix
Replace filesystem-free lexical normalization with filesystem-aware resolution that preserves the original path's symlink semantics before obtaining a short path. Use the resolved working directory consistently for process cwd, lifecycle environment, and PATH construction; if equivalent resolution cannot be completed, retain the original path and fail rather than executing in a potentially different directory. Add a Windows regression test using an over-limit path containing a symlink followed by `..`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

4. Windows test requires short names ✓ Resolved 🐞 Bug ☼ Reliability
Description
a_slot_windows_refuses_still_spawns_by_its_short_name unconditionally unwraps the result of
shorter_working_dirs, although that function explicitly returns no candidate when the volume does
not generate 8.3 names. Running the Windows suite on such a volume reaches the .expect(...) after
the long-path refusal and fails the test rather than treating the unavailable fallback as an unmet
precondition.
Code

pnpm/crates/executor/src/script_working_dir/tests.rs[66]

+    let shortest = shorter_working_dirs(&slot).pop().expect("a shorter spelling of the slot");
Relevance

●●● Strong

Test unconditionally unwraps a documented optional fallback; handling absent 8.3 names is a
deterministic reliability fix.

PR-#13445
PR-#14853

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The test requires .pop() to produce a path, but the production helper documents and implements an
empty result when 8.3 generation is disabled. Because the constructed test path has no lexical
normalization opportunity, a volume without short names leaves the vector empty and triggers the
added expectation.

pnpm/crates/executor/src/script_working_dir/tests.rs[63-72]
pnpm/crates/executor/src/script_working_dir.rs[48-61]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The Windows regression test assumes every test volume generates 8.3 short names, while the production helper explicitly supports volumes where no short spelling exists. This makes the suite fail due to host filesystem configuration rather than a product regression.

## Fix Focus Areas
- pnpm/crates/executor/src/script_working_dir/tests.rs[63-72]
- pnpm/crates/executor/src/script_working_dir.rs[48-61]

## Recommended Fix
Handle an empty `shorter_working_dirs` result as an unsupported test precondition, returning from the test with a diagnostic message. Keep the canonical-path and successful-spawn assertions when a short spelling is available.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Release note mixes old and new behavior ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
The changeset sentence combines the prior lifecycle-script failure with the claim that it is fixed
instead of stating the corrected behavior separately. Readers encountering this Windows bug-fix
entry must infer what now succeeds, and the previous failure is not documented in its own past-tense
sentence.
Code

.changeset/fix-gvs-lifecycle-script-cwd-windows.md[5]

+Fixed lifecycle scripts failing to spawn under `enableGlobalVirtualStore` on Windows when running scoped dependencies [pnpm/pnpm#15111](https://github.com/pnpm/pnpm/issues/15111).
Relevance

●●● Strong

Recent changeset reviews accepted separating prior failures from corrected behavior, matching this
exact repository rule.

PR-#15029
PR-#15121

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Compliance rule 3058498 requires previous buggy behavior to appear in a separate past-tense
sentence. The changed changeset instead uses one sentence, `Fixed lifecycle scripts failing to
spawn...`, to express both the defect and its resolution.

Rule 3058498: Describe previous buggy behavior in a separate past-tense sentence
.changeset/fix-gvs-lifecycle-script-cwd-windows.md[5-5]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The changeset combines the previous failure and its resolution in one sentence rather than describing the current behavior and then the old behavior separately.

## Fix Focus Areas
- .changeset/fix-gvs-lifecycle-script-cwd-windows.md[5-5]

## Recommended Fix
Replace the entry with two sentences. State first that lifecycle scripts for scoped dependencies now spawn successfully on Windows when `enableGlobalVirtualStore` is enabled, then state in a separate past-tense sentence that they previously failed to spawn.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. One helper comment repeats its name ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
The doc comment on push_slash_separated_path merely says that slash-separated segments are pushed
onto path, repeating the function name and its one-line body. It provides no additional rationale
or contract concerning empty segments, traversal components, or the expected relative-path invariant
for later callers.
Code

pnpm/crates/fs/src/relative_path.rs[71]

+/// Push each `/`-separated segment of `rel` onto `path`.
Relevance

●●● Strong

Recent Rust reviews accepted removing or rewriting comments that merely restate helper behavior or
names.

PR-#15119
PR-#15132

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Compliance rule 713467 rejects comments that only mirror an identifier or simple expression. The
comment directly restates both push_slash_separated_path and path.extend(rel.split('/')) without
adding intent or constraints.

Rule 713467: Avoid redundant comments that restate the code
pnpm/crates/fs/src/relative_path.rs[71-73]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new helper's doc comment repeats its name and implementation without documenting useful constraints or rationale.

## Fix Focus Areas
- pnpm/crates/fs/src/relative_path.rs[71-73]

## Recommended Fix
Rewrite the comment to document the expected relative input and behavior for empty or traversal segments, or remove it if the repository does not require public-item documentation.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View medium (1)
7. Symlink safety fix lacks coverage 🐞 Bug ⚙ Maintainability
Description
shorter_working_dirs relies on names_the_same_dir to reject lexical normalization when ..
crosses a symlink, but the added tests only construct ordinary directories where both spellings are
equivalent. Removing the new filter would therefore leave the tests green while allowing lifecycle
scripts to execute from a different directory again.
Code

pnpm/crates/executor/src/script_working_dir.rs[67]

+    spellings.retain(|spelling| names_the_same_dir(spelling, pkg_root));
Relevance

●● Moderate

Coverage-strengthening findings are often accepted, but a recent symlink-specific test request was
rejected in similar normalization code.

PR-#14853
PR-#15140

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The production comments explicitly identify symlink traversal as the reason every candidate must be
checked, and line 67 implements that check. The revised normalization tests create only normal
directories through workspace/.., so they cannot distinguish safe lexical normalization from the
symlink case this change is intended to reject.

pnpm/crates/executor/src/script_working_dir.rs[46-52]
pnpm/crates/executor/src/script_working_dir.rs[67-78]
pnpm/crates/executor/src/script_working_dir/tests.rs[7-30]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new directory-identity check protects a security-sensitive invariant, but no regression test exercises a `..` path whose lexical normalization changes meaning because an ancestor is a symlink or junction.

## Fix Focus Areas
- pnpm/crates/executor/src/script_working_dir.rs[46-52]
- pnpm/crates/executor/src/script_working_dir/tests.rs[7-24]

## Recommended Fix
Add a platform-appropriate symlink or junction test that constructs a package path containing `symlink/..` where the normalized spelling resolves to a different directory, then assert that `shorter_working_dirs` does not offer that spelling. Ensure the test fails if the `names_the_same_dir` filtering is removed.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
✅ Compliance rules (platform): 64 rules
✅ Skills: review-code
Review mode: ⏭️ Skipped: The latest push only revises comments/documentation in one source file and introduces no behavioral or semantic changes.

Grey Divider

Tip of the day
💡 Did you know, you can add REVIEW.md to your repo root and Qodo follows it on every PR

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Previous reviews

Review updated until commit cf64719 ⏭️ Skipped

Results up to commit 11f5faa ⚖️ Balanced


🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)


Remediation recommended
1. Release note mixes old and new behavior ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
The changeset sentence combines the prior lifecycle-script failure with the claim that it is fixed
instead of stating the corrected behavior separately. Readers encountering this Windows bug-fix
entry must infer what now succeeds, and the previous failure is not documented in its own past-tense
sentence.
Code

.changeset/fix-gvs-lifecycle-script-cwd-windows.md[5]

+Fixed lifecycle scripts failing to spawn under `enableGlobalVirtualStore` on Windows when running scoped dependencies [pnpm/pnpm#15111](https://github.com/pnpm/pnpm/issues/15111).
Relevance

●●● Strong

Recent changeset reviews accepted separating prior failures from corrected behavior, matching this
exact repository rule.

PR-#15029
PR-#15121

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Compliance rule 3058498 requires previous buggy behavior to appear in a separate past-tense
sentence. The changed changeset instead uses one sentence, `Fixed lifecycle scripts failing to
spawn...`, to express both the defect and its resolution.

Rule 3058498: Describe previous buggy behavior in a separate past-tense sentence
.changeset/fix-gvs-lifecycle-script-cwd-windows.md[5-5]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The changeset combines the previous failure and its resolution in one sentence rather than describing the current behavior and then the old behavior separately.

## Fix Focus Areas
- .changeset/fix-gvs-lifecycle-script-cwd-windows.md[5-5]

## Recommended Fix
Replace the entry with two sentences. State first that lifecycle scripts for scoped dependencies now spawn successfully on Windows when `enableGlobalVirtualStore` is enabled, then state in a separate past-tense sentence that they previously failed to spawn.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. One helper comment repeats its name ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
The doc comment on push_slash_separated_path merely says that slash-separated segments are pushed
onto path, repeating the function name and its one-line body. It provides no additional rationale
or contract concerning empty segments, traversal components, or the expected relative-path invariant
for later callers.
Code

pnpm/crates/fs/src/relative_path.rs[71]

+/// Push each `/`-separated segment of `rel` onto `path`.
Relevance

●●● Strong

Recent Rust reviews accepted removing or rewriting comments that merely restate helper behavior or
names.

PR-#15119
PR-#15132

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Compliance rule 713467 rejects comments that only mirror an identifier or simple expression. The
comment directly restates both push_slash_separated_path and path.extend(rel.split('/')) without
adding intent or constraints.

Rule 713467: Avoid redundant comments that restate the code
pnpm/crates/fs/src/relative_path.rs[71-73]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new helper's doc comment repeats its name and implementation without documenting useful constraints or rationale.

## Fix Focus Areas
- pnpm/crates/fs/src/relative_path.rs[71-73]

## Recommended Fix
Rewrite the comment to document the expected relative input and behavior for empty or traversal segments, or remove it if the repository does not require public-item documentation.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Results up to commit 5dfdeca 🚀 Fast


No changes from previous review

Results up to commit 533335e 🚀 Fast


No changes from previous review

Results up to commit 3153db6 ⚖️ Balanced


🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)


Action required
1. Release note overstates Windows fix ✓ Resolved 📜 Skill insight ≡ Correctness
Description
.changeset/fix-gvs-lifecycle-script-cwd-windows.md says dependencies with build scripts now
install on Windows under enableGlobalVirtualStore, but shorten_to_working_dir_limit returns an
over-limit path unchanged when normalization and the 8.3 conversion cannot shorten it. Stores on
volumes without short-name generation and long network-share paths therefore still reach process
spawning unchanged, so the unqualified release-note outcome extends beyond the implemented cases.
Code

.changeset/fix-gvs-lifecycle-script-cwd-windows.md[5]

+Dependencies that run build scripts now install on Windows under `enableGlobalVirtualStore`. A global virtual store slot can be longer than the working directory Windows allows a process. The install used to fail there with `The directory name is invalid. (os error 267)` [#15111](https://github.com/pnpm/pnpm/issues/15111).
Relevance

●●● Strong

Recent precedents accept release-note qualifications when implementation supports only narrower
conditions than the claim.

PR-#15073
PR-#14963
PR-#15132

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Rule 3365343 requires changeset claims to match the implemented user-visible behavior. The changeset
makes an unconditional Windows success claim, while the implementation documents and returns the
original over-limit path when no shortening method succeeds and excludes network-share paths from
8.3 conversion.

.changeset/fix-gvs-lifecycle-script-cwd-windows.md[5-5]
pnpm/crates/executor/src/script_working_dir.rs[39-51]
pnpm/crates/executor/src/script_working_dir.rs[76-82]
pnpm/crates/executor/src/script_working_dir.rs[98-107]
Skill: review-code

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The changeset promises that global-virtual-store build scripts now install on Windows without disclosing that overlong paths still fail when they cannot be normalized or converted to an 8.3 path.

## Fix Focus Areas
- .changeset/fix-gvs-lifecycle-script-cwd-windows.md[5-5]

## Recommended Fix
Revise the release note to state that the fix applies when pnpm can shorten the working directory, without claiming success for every Windows global virtual store path.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Lifecycle scripts use the wrong folder ✓ Resolved 📜 Skill insight ⛨ Security
Description
shorten_to_working_dir_limit applies pnpm_fs::lexical_normalize to over-limit paths, removing
.. without preserving symlink traversal semantics and potentially selecting a different package
root. On Windows, a repository-relative storeDir containing symlink/.. triggers this once the
slot exceeds 259 UTF-16 units, so the manifest, lifecycle environment, and PATH can refer to the
original root while shell and emulated lifecycle commands execute from the rewritten root.
Code

pnpm/crates/executor/src/script_working_dir.rs[R47-49]

+    let normalized = pnpm_fs::lexical_normalize(path);
+    if utf16_len(&normalized) <= MAX_WORKING_DIR_CHARS {
+        return Cow::Owned(normalized);
Relevance

●● Moderate

Potential symlink-sensitive path mismatch is plausible, but history lacks a closely matching
accepted or rejected precedent.

PR-#13403
PR-#13445

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The shortening function uses filesystem-free lexical normalization for every over-limit path when
the normalized spelling fits, and that helper removes preceding components without inspecting
symlinks. Repository-controlled relative store paths can contain .., so a symlink can make the
normalized destination differ from the installed package root; manifest selection remains based on
the original root, while the changed shell and emulator call sites use the rewritten root as their
working directory, demonstrating the mismatched execution path and the failure to keep path
resolution within its intended destination.

pnpm/crates/executor/src/script_working_dir.rs[26-51]
pnpm/crates/executor/src/lifecycle.rs[399-410]
pnpm/crates/executor/src/lifecycle.rs[460-466]
pnpm/crates/config/src/workspace_yaml/apply.rs[130-154]
pnpm/crates/fs/src/lexical_normalize.rs[6-17]
pnpm/crates/fs/src/lexical_normalize.rs[90-101]
pnpm/crates/executor/src/lifecycle.rs[176-184]
pnpm/crates/executor/src/lifecycle.rs[402-410]
pnpm/crates/executor/src/lifecycle.rs[463-466]
Skill: review-code

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Long Windows lifecycle working directories are lexically normalized before execution, even though removing `..` after a symlink can resolve to a different directory. This can run a lifecycle script from a working directory other than the package root whose manifest supplied the script, while the lifecycle environment and `PATH` still reference the original root.

## Fix Focus Areas
- pnpm/crates/executor/src/script_working_dir.rs[43-51]
- pnpm/crates/executor/src/lifecycle.rs[402-410]
- pnpm/crates/executor/src/lifecycle.rs[463-466]
- pnpm/crates/executor/src/script_working_dir/tests.rs[23-46]

## Recommended Fix
Replace filesystem-free lexical normalization with filesystem-aware resolution that preserves the original path's symlink semantics before obtaining a short path. Use the resolved working directory consistently for process cwd, lifecycle environment, and PATH construction; if equivalent resolution cannot be completed, retain the original path and fail rather than executing in a potentially different directory. Add a Windows regression test using an over-limit path containing a symlink followed by `..`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Results up to commit fbd1604 ⚖️ Balanced


🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)


Remediation recommended
1. Windows test requires short names ✓ Resolved 🐞 Bug ☼ Reliability
Description
a_slot_windows_refuses_still_spawns_by_its_short_name unconditionally unwraps the result of
shorter_working_dirs, although that function explicitly returns no candidate when the volume does
not generate 8.3 names. Running the Windows suite on such a volume reaches the .expect(...) after
the long-path refusal and fails the test rather than treating the unavailable fallback as an unmet
precondition.
Code

pnpm/crates/executor/src/script_working_dir/tests.rs[66]

+    let shortest = shorter_working_dirs(&slot).pop().expect("a shorter spelling of the slot");
Relevance

●●● Strong

Test unconditionally unwraps a documented optional fallback; handling absent 8.3 names is a
deterministic reliability fix.

PR-#13445
PR-#14853

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The test requires .pop() to produce a path, but the production helper documents and implements an
empty result when 8.3 generation is disabled. Because the constructed test path has no lexical
normalization opportunity, a volume without short names leaves the vector empty and triggers the
added expectation.

pnpm/crates/executor/src/script_working_dir/tests.rs[63-72]
pnpm/crates/executor/src/script_working_dir.rs[48-61]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The Windows regression test assumes every test volume generates 8.3 short names, while the production helper explicitly supports volumes where no short spelling exists. This makes the suite fail due to host filesystem configuration rather than a product regression.

## Fix Focus Areas
- pnpm/crates/executor/src/script_working_dir/tests.rs[63-72]
- pnpm/crates/executor/src/script_working_dir.rs[48-61]

## Recommended Fix
Handle an empty `shorter_working_dirs` result as an unsupported test precondition, returning from the test with a diagnostic message. Keep the canonical-path and successful-spawn assertions when a short spelling is available.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Results up to commit ff52c3d 🚀 Fast


No changes from previous review

Results up to commit 8968c3b 🚀 Fast


🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)


Action required
1. Windows regression tests never prove scripts start ✓ Resolved 📜 Skill insight ▣ Testability
Description
every_spelling_offered_for_a_refused_slot_is_shorter only checks that fallback spellings are
shorter and resolve to the same directory, without spawning a lifecycle process through the fallback
path. The test can pass while every candidate still fails as a working directory, so the reported
Windows lifecycle-script fix is not directly regression-tested.
Code

pnpm/crates/executor/src/script_working_dir/tests.rs[R58-80]

+fn every_spelling_offered_for_a_refused_slot_is_shorter() {
+    let root = tempfile::Builder::new()
+        .prefix("pnpm-wd-")
+        .tempdir()
+        .expect("create temporary directory");
+    let slot = slot_dir(root.path());
+    std::fs::create_dir_all(&slot).expect("create the deep slot");
+
+    let refusal = spawn_in(&slot).expect_err("Windows must refuse a working directory this long");
+    assert!(is_refused_directory(&refusal), "expected ERROR_DIRECTORY, got {refusal:?}");
+
+    for spelling in shorter_working_dirs(&slot) {
+        assert_eq!(
+            std::fs::canonicalize(&spelling).expect("canonicalize a shorter spelling"),
+            std::fs::canonicalize(&slot).expect("canonicalize the slot"),
+            "{spelling:?} must name the same directory as the slot",
+        );
+        assert!(
+            spelling.as_os_str().len() < slot.as_os_str().len(),
+            "{spelling:?} is not shorter than the {}-character slot it stands in for",
+            slot.as_os_str().len(),
+        );
+    }
Relevance

●●● Strong

Recent history accepts strengthening regression tests to exercise the actual failure mode, not
merely indirect invariants.

PR-#15182
PR-#15153

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The added Windows test creates a refused long directory and asserts only that each candidate
canonicalizes to the same directory and is shorter. No assertion starts a process with a candidate
working directory, while the production behavior being fixed is process creation from the lifecycle
directory.

pnpm/crates/executor/src/script_working_dir/tests.rs[58-80]
Skill: review-code

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The Windows regression test verifies only path length and directory identity. It does not prove that the fallback working directory can actually start a child process, so the lifecycle-script failure can remain undetected.

## Fix Focus Areas
- pnpm/crates/executor/src/script_working_dir/tests.rs[58-80]

## Recommended Fix
After generating each shorter spelling, invoke a child process with that spelling as its current directory and assert that spawning succeeds. Prefer exercising the same spawn helper or lifecycle spawn path used by production so the test fails when the fallback does not solve the original working-directory refusal.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Results up to commit 0cc1fbe ⚖️ Balanced


🐞 Bugs (1) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)


Remediation recommended
1. Symlink safety fix lacks coverage 🐞 Bug ⚙ Maintainability
Description
shorter_working_dirs relies on names_the_same_dir to reject lexical normalization when ..
crosses a symlink, but the added tests only construct ordinary directories where both spellings are
equivalent. Removing the new filter would therefore leave the tests green while allowing lifecycle
scripts to execute from a different directory again.
Code

pnpm/crates/executor/src/script_working_dir.rs[67]

+    spellings.retain(|spelling| names_the_same_dir(spelling, pkg_root));
Relevance

●● Moderate

Coverage-strengthening findings are often accepted, but a recent symlink-specific test request was
rejected in similar normalization code.

PR-#14853
PR-#15140

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The production comments explicitly identify symlink traversal as the reason every candidate must be
checked, and line 67 implements that check. The revised normalization tests create only normal
directories through workspace/.., so they cannot distinguish safe lexical normalization from the
symlink case this change is intended to reject.

pnpm/crates/executor/src/script_working_dir.rs[46-52]
pnpm/crates/executor/src/script_working_dir.rs[67-78]
pnpm/crates/executor/src/script_working_dir/tests.rs[7-30]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new directory-identity check protects a security-sensitive invariant, but no regression test exercises a `..` path whose lexical normalization changes meaning because an ancestor is a symlink or junction.

## Fix Focus Areas
- pnpm/crates/executor/src/script_working_dir.rs[46-52]
- pnpm/crates/executor/src/script_working_dir/tests.rs[7-24]

## Recommended Fix
Add a platform-appropriate symlink or junction test that constructs a package path containing `symlink/..` where the normalized spelling resolves to a different directory, then assert that `shorter_working_dirs` does not offer that spelling. Ensure the test fails if the `names_the_same_dir` filtering is removed.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

@qodo-code-review

Copy link
Copy Markdown

PR Code Suggestions ✨

Warning

/improve is deprecated. Use /agentic_review instead (removal date not yet scheduled).

No code suggestions found for the PR.

zkochan and others added 3 commits September 20, 2026 23:31
push_slash_separated_path is now the common home of the Windows
separator argument, and join_global_virtual_store_path points at it
instead of restating it. virtual_store_dir_for_key names the field it
reads, and the builds module drops its note about a per-test Windows
ignore it no longer carries.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A scoped package pushed as one component leaves its `/` inside the
Windows path string, which is invisible on Unix. The assertion is
written against the foreign separator so it runs on every platform and
only Windows can fail it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Restores the blank line before the dependency table too.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@qodo-code-review

Copy link
Copy Markdown

PR Code Suggestions ✨

Warning

/improve is deprecated. Use /agentic_review instead (removal date not yet scheduled).

No code suggestions found for the PR.

The package map's slot paths and the env installer's config-dependency
paths still passed a whole `@scope/name` to Path::join, so on Windows
the `/` stayed inside one path component. Route them through
pnpm_fs::join_slash_separated_path like every other producer of a
global-virtual-store package directory.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@zkochan zkochan changed the title fix(executor): spawn lifecycle scripts from global virtual store slots on Windows fix(executor): run lifecycle scripts from long global virtual store slots on Windows Sep 20, 2026
Comment thread pnpm/crates/executor/src/script_working_dir.rs Outdated
Comment thread pnpm/crates/executor/src/script_working_dir.rs
Comment thread pnpm/crates/executor/src/script_working_dir/tests.rs Outdated
Comment thread pnpm/crates/executor/src/script_working_dir.rs Outdated
Comment thread .changeset/fix-gvs-lifecycle-script-cwd-windows.md Outdated
@qodo-code-review

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 3153db6

@qodo-code-review

Copy link
Copy Markdown

PR Code Suggestions ✨

Warning

/improve is deprecated. Use /agentic_review instead (removal date not yet scheduled).

No code suggestions found for the PR.

zkochan and others added 2 commits September 21, 2026 00:50
Under enableGlobalVirtualStore a dependency's lifecycle script failed to
spawn on Windows with `The directory name is invalid. (os error 267)`.

Windows keeps a process's working directory in a MAX_PATH field and
refuses a longer one with ERROR_DIRECTORY, the same answer it gives for
a directory that is not there. A global virtual store slot reaches that
length readily: its path spells out the scope, the name, the version
and a 64-character graph digest under whatever store directory the user
configured.

The bound it applies sits below MAX_PATH and is undocumented, and the
error does not say which of the two conditions it meant, so neither is
predicted. The spawn goes to the directory the install computed, and
shorter spellings of it are tried only once the refusal has come back:
first the lexically normalized path, which drops the `..` a relative
storeDir leaves behind, then its 8.3 short form, which a volume that
still generates short names answers with. Waiting for the refusal is
what makes the first of those safe as well as cheap, since `..` through
a symlinked directory does not lead where resolving it lexically says
it does. When no spelling works the first refusal is reported, naming
the directory the install computed.

The global-virtual-store build tests no longer skip on Windows. Only one
of them installs a package whose slot path stays under the limit, so
before this the other six could not run there.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@zkochan
zkochan force-pushed the fix-pnpm-issue-15111 branch from 3153db6 to fbd1604 Compare September 20, 2026 22:55
@qodo-code-review

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit fbd1604

@qodo-code-review

Copy link
Copy Markdown

PR Code Suggestions ✨

Warning

/improve is deprecated. Use /agentic_review instead (removal date not yet scheduled).

No code suggestions found for the PR.

Comment thread pnpm/crates/executor/src/lifecycle.rs
@qodo-code-review

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit ff52c3d

@qodo-code-review

Copy link
Copy Markdown

PR Code Suggestions ✨

Warning

/improve is deprecated. Use /agentic_review instead (removal date not yet scheduled).

No code suggestions found for the PR.

GetShortPathNameW has to be asked in the verbatim `\\?\` form, which is
what lets it reach a path over MAX_PATH, and a volume with 8.3 name
generation turned off then answers with the very path it was given. Over
MAX_PATH `dunce::simplified` cannot shed that prefix either, so pnpm
offered as its shorter spelling a path four characters longer than the
one Windows had just refused, in a form fewer programs accept.

Only a spelling that comes back strictly shorter, and not still
verbatim, is worth the second attempt.

The Windows test asserts that contract instead of asserting that some
spelling exists: whether one does is the volume's business, and the
global-virtual-store build tests already cover the fallback end to end.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@zkochan
zkochan force-pushed the fix-pnpm-issue-15111 branch from ff52c3d to 8968c3b Compare September 21, 2026 00:31
Comment thread pnpm/crates/executor/src/script_working_dir/tests.rs Outdated
Comment thread pnpm/crates/executor/src/script_working_dir/tests.rs Outdated
@qodo-code-review

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 8968c3b

@qodo-code-review

Copy link
Copy Markdown

PR Code Suggestions ✨

Warning

/improve is deprecated. Use /agentic_review instead (removal date not yet scheduled).

No code suggestions found for the PR.

…slot

Lexical normalization resolves `..` the way the path text reads, and a
`..` that steps through a symlinked directory does not lead there. A
spelling that named a different directory would run the build script in
it, which is worse than not running it, so each one is now checked
against the slot with `canonicalize` before it is offered.

The Windows test shapes its slot like the one the issue reports, `..`
and all, so a shorter spelling exists on any volume rather than only on
one that still generates 8.3 names, and asserts that a child starts in
it. It used to assert only that the spellings were shorter, which held
vacuously where no spelling was offered.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@qodo-code-review

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 0cc1fbe

@qodo-code-review

Copy link
Copy Markdown

PR Code Suggestions ✨

Warning

/improve is deprecated. Use /agentic_review instead (removal date not yet scheduled).

No code suggestions found for the PR.

@greptile-apps

This comment has been minimized.

…sent

The rustdoc named a spelling the code no longer has and described what
it used to do. The hazard is still worth the warning, so it now says
what a lookup by the peer-stripped key does rather than what one did.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@zkochan

zkochan commented Sep 21, 2026

Copy link
Copy Markdown
Member

Fixed the outside-diff finding in cf64719.

virtual_store_dir_for_key's rustdoc named a spelling the code no longer has and said what it used to do. The hazard is worth keeping — a lookup by the peer-stripped key really does land on a directory nothing created, and drops the lifecycle scripts of every peer-resolved snapshot without a word — so it now says that in the present tense instead of as history. The line predates this PR, but it sits in the same doc block this PR already rewrote, so it came along.

The UNC finding on script_working_dir.rs I am not acting on; the reasoning is on that thread.


Written by an agent (Claude Code, claude-opus-5).

@qodo-code-review

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit cf64719

@qodo-code-review

Copy link
Copy Markdown

PR Code Suggestions ✨

Warning

/improve is deprecated. Use /agentic_review instead (removal date not yet scheduled).

No code suggestions found for the PR.

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

Labels

product: pacquet reviewed: coderabbit CodeRabbit submitted an approving review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Lifecycle scripts fail to spawn under the global virtual store on Windows: invalid working directory

3 participants