Skip to content
This repository was archived by the owner on May 14, 2026. It is now read-only.

feat(executor): port makeEnv, extendPath, and shell selection for lifecycle scripts (#397)#418

Merged
zkochan merged 12 commits into
mainfrom
fix/lifecycle-env-path-shell-397
May 12, 2026
Merged

feat(executor): port makeEnv, extendPath, and shell selection for lifecycle scripts (#397)#418
zkochan merged 12 commits into
mainfrom
fix/lifecycle-env-path-shell-397

Conversation

@zkochan

@zkochan zkochan commented May 12, 2026

Copy link
Copy Markdown
Member

Summary

Closes three of the critical items in #397 by porting the corresponding
behaviors from @pnpm/npm-lifecycle@d2d8e790 and pnpm/pnpm@b4f8f47ac2:

  • add package json module #1 — Lifecycle env vars. New make_env module ports makeEnv and
    the surrounding env block in lifecycle() (npm-lifecycle/index.js:73-104, :354-414) plus the pnpm wrapper's extraEnv additions (runLifecycleHook.ts:119-124). Lifecycle scripts now see npm_lifecycle_event, npm_lifecycle_script, npm_node_execpath/NODE, npm_package_json, npm_execpath, npm_package_* (name, version, and recursive config/engines/bin), npm_config_node_gyp, npm_config_user_agent, INIT_CWD, PNPM_SCRIPT_SRC_DIR, and TMPDIR (when unsafe_perm is false). The spawn now strips inherited env (env_clear()) so leftover npm_* keys from a wrapping invocation cannot leak through.
  • Some feedback #2 — PATH ancestor walk. New extend_path module ports extendPath (npm-lifecycle/lib/extendPath.js:5-27) plus the tri-state scriptsPrependNodePath gating (:29-61). For a dep at <root>/node_modules/.pnpm/<slot>/node_modules/<pkg>, PATH now contains the dep's own .bin, the .pnpm slot's .bin, the project root's .bin, the bundled node-gyp-bin (when supplied), extra_bin_paths, and finally the inherited PATH.
  • add version-pin #4 — Shell selection. New shell module ports the shell-selection block and the pnpm-side .bat/.cmd guard. cmd /d /s /c on Windows, custom scriptShell on either platform, otherwise sh -c. The Windows-batch-file scriptShell case surfaces as ERR_PNPM_INVALID_SCRIPT_SHELL_WINDOWS (matching upstream's error code).

RunPostinstallHooks grows seven new fields to surface these knobs; BuildModules passes safe defaults (None / true / Never) for all of them — full config plumbing for user-agent, unsafe-perm, scripts-prepend-node-path, node-gyp bundling, and script-shell are tracked as separate items in #397 (#14, #15) or follow-ups.

Explicit non-goals in this PR

Three caveats called out in #397 that are deliberately deferred:

  • windowsVerbatimArguments (Rust equivalent: CommandExt::raw_arg) is signalled by SelectedShell.windows_verbatim_args but not yet applied to the spawned Command.
  • @yarnpkg/shell / shellEmulator: true has no clean Rust port; pacquet ignores the flag for now.
  • unsafe_perm uid/gid drop (Broken windows tests #14) — BuildModules passes true, which keeps current behavior (no TMPDIR creation, no privilege drop).

Test parity

Per the project guide ("port the relevant pnpm tests too whenever they translate"), this branch ports:

Plus fills the gaps where upstream coverage is thin: tri-state scriptsPrependNodePath, two-level pnpm virtual-store PATH walks, extra_bin_paths ordering, TMPDIR gating on unsafe_perm, and extra_env precedence vs npm_lifecycle_script.

Test plan

  • just ready (466 tests + clippy clean)
  • taplo format --check (clean — just ready doesn't run this, CI does)
  • CI green on Linux / macOS / Windows
  • Verify against alot7-style real-install fixtures in a follow-up perf run if reviewers want to confirm no warm-install regression

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

Summary by CodeRabbit

  • New Features
    • Improved lifecycle script execution: deterministic PATH ordering for tooling bins, optional inclusion of the node executable directory, configurable node-gyp placement, richer lifecycle environment stamping (npm_* vars, INIT_CWD, PNPM_SCRIPT_SRC_DIR), and platform-aware script-shell selection with safer defaults.
  • Tests
    • Extensive unit and integration tests covering PATH ordering, environment stamping, TMPDIR behavior, and shell selection.

Review Change Stack

zkochan added 4 commits May 12, 2026 10:08
Ports `makeEnv` and the surrounding env block from `@pnpm/npm-lifecycle`
at `d2d8e790` (`index.js:73-104`, `:354-414`) plus the pnpm-side
wrapper's `extraEnv` additions at `pnpm/exec/lifecycle/src/runLifecycleHook.ts:119-124`
(pinned to `b4f8f47ac2`).

Lifecycle scripts now see:

- `npm_lifecycle_event`, `npm_lifecycle_script`
- `npm_node_execpath` / `NODE` (resolved against `PATH` when not supplied)
- `npm_package_json`, `npm_execpath`
- `npm_package_*` for `name`, `version`, and every key under `config`,
  `engines`, `bin` (filter mirrors `index.js:380-385`; multi-line
  values JSON-encode at `:406-408`)
- `npm_config_node_gyp`, `npm_config_user_agent` when supplied
- `INIT_CWD`, `PNPM_SCRIPT_SRC_DIR`
- `TMPDIR` under `<wd>/node_modules/.tmp` when `unsafe_perm` is false
  (created on disk; `EEXIST` is tolerated)

The spawn now strips inherited env via `env_clear()` so leftover
`npm_*` / `pnpm_*` keys from a wrapping invocation cannot leak
through, matching `makeEnv`'s `!i.match(/^npm_/)` filter at
`index.js:358-362`.

`RunPostinstallHooks` grows `node_execpath`, `npm_execpath`,
`node_gyp_path`, `user_agent`, and `unsafe_perm` fields. The
`BuildModules` caller passes `None`/`true` for all of them — the
`unsafePerm` plumbing is item #14 in #397.

Tests port the upstream `test('makeEnv')` invariants and add an
end-to-end check that a spawned child sees the stamped variables
and does NOT see leaked `npm_config_*` keys (adapted from
`pnpm/exec/lifecycle/test/index.ts:65-77` using a file-dump model
rather than an IPC fixture).
Ports `extendPath` from `@pnpm/npm-lifecycled2d8e790` at
`lib/extendPath.js:5-27` (the walk) and `:29-61` (the tri-state
`scriptsPrependNodePath` gating).

The lifecycle hook's `PATH` now matches upstream's `wd.split(/[\\\\/]node_modules[\\\\/]/)`
scheme: every ancestor `node_modules/.bin` from `wd` upward is
prepended, deepest first, followed by the bundled `node-gyp-bin`
slot, `extra_bin_paths`, and (when `scriptsPrependNodePath` is
`Always`) `dirname(node)`. The inherited PATH stays last.

The shape this unblocks for real installs is a dep at
`<root>/node_modules/.pnpm/<slot>/node_modules/<pkg>` whose
postinstall calls `node-gyp` — the previous PATH only prepended
the dep's own (almost always empty) `.bin`, so the tool stayed
unreachable. The new walk picks it up from the project root.

`RunPostinstallHooks` grows `node_gyp_bin: Option<&Path>` and
`scripts_prepend_node_path: ScriptsPrependNodePath`. `BuildModules`
passes `None` / `Never` for both — bundling node-gyp and surfacing
the `scriptsPrependNodePath` config are separate items in #397
(#15) and a follow-up.

Tests port the upstream ordering test from
`pnpm/npm-lifecycle/test/extendPath.test.js` and add coverage for
the gaps upstream tests don't exercise: the no-ancestor case,
two-level pnpm virtual-store layout, `extra_bin_paths` ordering,
`original_path` placement, and all three `ScriptsPrependNodePath`
variants.
Ports the shell-selection block from `@pnpm/npm-lifecycled2d8e790`
at `index.js:241-252` and the `.bat` / `.cmd` guard from
`pnpm/exec/lifecycle/src/runLifecycleHook.ts:63-71` (pinned to
`b4f8f47ac2`).

Priority of resolution:

1. `scriptShell` ending in `.cmd` or `.bat` on Windows is rejected
   with `ERR_PNPM_INVALID_SCRIPT_SHELL_WINDOWS` (the same diagnostic
   code upstream uses).
2. A non-batch `scriptShell` value wins on both platforms, invoked
   with `-c`.
3. On Windows: `%ComSpec%` (or literal `cmd`) with `/d /s /c` and
   the `windowsVerbatimArguments` intent flag set.
4. Otherwise: `sh -c` as before.

`select_shell` takes an explicit `is_windows: bool` so tests drive
both branches without `#[cfg(windows)]` gating; the production call
in `run_lifecycle_hook` passes `cfg!(windows)`.

`RunPostinstallHooks` grows `script_shell: Option<&Path>`;
`BuildModules` passes `None` (config plumbing for `script-shell` /
`shell-emulator` is a follow-up). The `shellEmulator` /
`@yarnpkg/shell` branch from `index.js:258-280` has no
straightforward Rust port and remains unimplemented — pacquet
silently ignores `shellEmulator: true` for now.

`windowsVerbatimArguments` (Rust equivalent:
`std::os::windows::process::CommandExt::raw_arg`) is signalled by
`SelectedShell.windows_verbatim_args` but not yet applied to the
spawned `Command`; the field is in place so a follow-up can wire
it without churn.

Tests port the upstream
`onlyOnWindows('pnpm shows error if script-shell is .cmd')` case
(`pnpm/exec/commands/test/index.ts:509-542`) and adapt the
custom-shell path from the same file (`:478-508`), plus add
coverage for the platform-default branches.
`just ready` rewrapped a few long argument lists and conditionals
in the three new modules introduced by this branch (make_env,
extend_path, shell) plus their test files. Pure formatting — no
behavior change.
Copilot AI review requested due to automatic review settings May 12, 2026 08:17
@coderabbitai

coderabbitai Bot commented May 12, 2026

Copy link
Copy Markdown

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
📝 Walkthrough

Walkthrough

Adds modules to build child envs (make_env), compute PATH (extend_path), and select shells (shell); integrates them into the lifecycle runner to clear inherited env, compute PATH, optionally create TMPDIR, and spawn lifecycle scripts with manifest-derived npm package stamping.

Changes

Lifecycle Script Spawning with Environment and Shell Selection

Layer / File(s) Summary
Shell selection for script execution
crates/executor/src/shell.rs, crates/executor/src/shell/tests.rs
ScriptShellError and SelectedShell represent shell selection. select_shell prefers a custom scriptShell with -c, falls back to Windows cmd /d /s /c (verbatim args) or POSIX sh -c, and rejects .bat/.cmd on Windows. Tests cover defaults, custom shell, and rejection cases.
PATH construction with node_modules/.bin walking
crates/executor/src/extend_path.rs, crates/executor/src/extend_path/tests.rs
ScriptsPrependNodePath controls prepending dirname(node). extend_path aggregates deepest-first ancestor node_modules/.bin, optional node-gyp-bin, extra_bin_paths, optionally dirname(node_execpath) when Always, then the original PATH. Tests assert ordering and platform normalization.
Environment variable construction from manifest and options
crates/executor/src/make_env.rs, crates/executor/src/make_env/tests.rs
EnvOptions and EnvBuild model inputs/outputs. build_env filters parent env, recursively stamps npm_package_* from manifest (sanitizing/escaping), stamps lifecycle vars and execpaths, applies extra_env last, conditionally creates TMPDIR when unsafe_perm is false, and final-stamps npm_lifecycle_script. Tests validate stamping, filtering, tmpdir gating, and helpers.
Lifecycle script spawning with integrated env, PATH, and shell
crates/executor/src/lifecycle.rs, crates/executor/src/lifecycle/tests.rs
RunPostinstallHooks gains execpath/user-agent/unsafe-perm/node-gyp and scripts_prepend_node_path fields. LifecycleScriptError adds ScriptShell. run_lifecycle_hook now builds env via build_env, constructs PATH via extend_path, selects shell via select_shell, clears inherited env, injects built env and PATH, and spawns the process with piped I/O. Tests updated and new e2e env-stamping/stripping test added.
Public API exports and consumer configuration
crates/executor/src/lib.rs, crates/package-manager/src/build_modules.rs
lib.rs declares and re-exports extend_path, make_env, and shell APIs. build_modules.rs updates imports to include ScriptsPrependNodePath and configures RunPostinstallHooks with explicit execution options (scripts_prepend_node_path: Never, unsafe_perm: true, execpaths set to None).

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related issues

Poem

🐰 I nibble paths and stitch them neat,
deepest bins first, then node's own street.
Shell is chosen, env is clean,
TMPDIR set, lifecycle scene.
Hop goes the rabbit — tests all green!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title accurately describes the main changes: porting makeEnv, extendPath, and shell selection from upstream pnpm for lifecycle scripts, matching the PR's primary objectives.
Description check ✅ Passed The description provides a comprehensive summary covering all three major components (lifecycle env, PATH extension, shell selection), includes upstream references with commit SHAs, explains test coverage and deferred items, and follows the template structure.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/lifecycle-env-path-shell-397

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

CI surfaced two classes of issue on the new lifecycle modules:

- `dylint` (perfectionist macro-trailing-comma) flagged five
  `assert*!`/`format!` sites where `cargo fmt`'s rewraps left the
  invocations in the disallowed shape: trailing comma on a
  single-line macro, missing trailing comma on a multi-line one.
- `rustdoc -D warnings` rejected a Windows-only intra-doc link to
  `std::os::windows::process::CommandExt::raw_arg` (the path
  doesn't resolve on non-Windows targets where docs build) and a
  redundant explicit `[`build_env`](crate::build_env)` target.

Pure lint fixes — no behavior or test-coverage change. Tests
still pass and `RUSTDOCFLAGS=-D warnings cargo doc` is clean for
`pacquet-executor`.

Copilot AI 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.

Pull request overview

Ports additional pnpm lifecycle-script behavior into pacquet_executor so dependency build hooks run with upstream-compatible env/PATH/shell semantics.

Changes:

  • Add make_env env builder to stamp npm_lifecycle_*, npm_package_*, TMPDIR gating, and strip inherited npm_*/pnpm_* leakage.
  • Add extend_path PATH construction to walk node_modules/.bin ancestors and support tri-state scriptsPrependNodePath.
  • Add shell selection to use cmd /d /s /c on Windows (and reject .bat/.cmd custom shells), otherwise sh -c / scriptShell.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
crates/package-manager/src/build_modules.rs Wires new lifecycle options into RunPostinstallHooks with safe defaults.
crates/executor/src/lib.rs Exposes new executor modules (extend_path, make_env, shell) via re-exports.
crates/executor/src/lifecycle.rs Uses build_env, extend_path, and select_shell; clears inherited env before spawning.
crates/executor/src/lifecycle/tests.rs Extends lifecycle tests and adds an end-to-end env-dump assertion.
crates/executor/src/make_env.rs Implements pnpm-compatible env stamping/filtering for lifecycle spawns.
crates/executor/src/make_env/tests.rs Adds unit tests for env stamping/filtering/TMPDIR behavior.
crates/executor/src/extend_path.rs Implements pnpm-compatible PATH ancestor walk and tri-state node-dir prepend.
crates/executor/src/extend_path/tests.rs Adds ordering and virtual-store layout tests for PATH construction.
crates/executor/src/shell.rs Implements platform/default/custom shell selection + Windows batch-shell guard.
crates/executor/src/shell/tests.rs Adds tests for POSIX/Windows defaults and batch-shell rejection.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread crates/executor/src/make_env.rs Outdated
Comment thread crates/executor/src/make_env.rs Outdated
Comment thread crates/executor/src/lifecycle.rs Outdated
Comment thread crates/executor/src/lifecycle/tests.rs Outdated
Comment thread crates/executor/src/lifecycle/tests.rs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/executor/src/lifecycle.rs`:
- Around line 242-250: The code uses built.env.get("PATH") which fails on
Windows because env keys preserve system casing (e.g., "Path"); change the
lookup to perform a case-insensitive search for the PATH key before calling
extend_path: replace built.env.get("PATH").map(...) with logic that iterates
built.env (or uses built.env.keys()) to find a key where
key.eq_ignore_ascii_case("PATH") and then map that value to OsString (preserving
existing mapping code), so original_path gets the inherited PATH on Windows;
keep all other arguments to extend_path (opts.pkg_root, opts.node_gyp_bin,
opts.extra_bin_paths, opts.scripts_prepend_node_path, opts.node_execpath)
unchanged.

In `@crates/executor/src/lifecycle/tests.rs`:
- Around line 256-258: Remove the unnecessary unsafe blocks and accompanying
SAFETY comment around calls to std::env::set_var and std::env::remove_var in the
tests (the blocks wrapping env::set_var("npm_config_should_be_stripped", "leak")
and env::remove_var("npm_config_should_be_stripped")). Since these functions
have been safe since Rust 1.62, delete the unsafe { ... } wrappers and the
outdated SAFETY comment at both locations (around the set_var and remove_var
calls) so the calls are plain safe calls to std::env::set_var and
std::env::remove_var.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 4f711f2c-294c-48f3-b369-2f56a59f6458

📥 Commits

Reviewing files that changed from the base of the PR and between 617e614 and 6c21f25.

📒 Files selected for processing (10)
  • crates/executor/src/extend_path.rs
  • crates/executor/src/extend_path/tests.rs
  • crates/executor/src/lib.rs
  • crates/executor/src/lifecycle.rs
  • crates/executor/src/lifecycle/tests.rs
  • crates/executor/src/make_env.rs
  • crates/executor/src/make_env/tests.rs
  • crates/executor/src/shell.rs
  • crates/executor/src/shell/tests.rs
  • crates/package-manager/src/build_modules.rs
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: Agent
  • GitHub Check: Lint and Test (macos-latest)
  • GitHub Check: Lint and Test (windows-latest)
  • GitHub Check: Run benchmark on ubuntu-latest
  • GitHub Check: Run benchmark on ubuntu-latest
  • GitHub Check: Code Coverage
🧰 Additional context used
📓 Path-based instructions (1)
**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

**/*.rs: Log emissions are part of 'match pnpm'—when porting a function that fires pnpm:<channel> events through globalLogger, logger.debug(...), or streamParser.write(...), mirror the call site, payload, and ordering so @pnpm/cli.default-reporter parses pacquet's NDJSON the same way
Declare a newtype wrapper for branded string types instead of collapsing into plain String or &str to preserve the distinction between validating and non-validating brands
If upstream always validates before construction of a branded string type, implement validation via TryFrom<String> and/or FromStr without providing an infallible public constructor
If upstream never validates a branded string type, expose an infallible From<String> constructor to provide type-safety without runtime validation
When upstream occasionally constructs a branded string type without validation via bare as assertion, expose a from_str_unchecked constructor so callers can opt into the same unchecked path explicitly
Match upstream serde behavior for branded types—use #[serde(try_from = "String")] for deserialization to go through the validator and #[serde(into = "String")] for serialization
Use #[derive(derive_more::From)] and #[derive(derive_more::Into)] for mechanical conversion impls on branded types rather than handwriting impl blocks
Model string-literal unions (like 'auto' | 'always' | 'never') as Rust enums instead of newtype wrappers
Template literal types (like ${string}@${string}) should be treated as branded string types with the same validation discipline applied
Follow the code style guide in CODE_STYLE_GUIDE.md covering code-level conventions not enforced by tooling: imports, modules, naming, ownership and borrowing, parameter type selection, trait bounds, pattern matching, pipe-trait, error handling, test layout, and cloning of Arc and Rc
Warnings are errors in cargo clippy --locked -- --deny warnings, and should not be silenced with `#[allow(....

Files:

  • crates/executor/src/shell.rs
  • crates/executor/src/lib.rs
  • crates/executor/src/shell/tests.rs
  • crates/executor/src/extend_path.rs
  • crates/executor/src/make_env.rs
  • crates/executor/src/make_env/tests.rs
  • crates/package-manager/src/build_modules.rs
  • crates/executor/src/extend_path/tests.rs
  • crates/executor/src/lifecycle/tests.rs
  • crates/executor/src/lifecycle.rs
🧠 Learnings (2)
📚 Learning: 2026-05-07T23:19:08.272Z
Learnt from: KSXGitHub
Repo: pnpm/pacquet PR: 401
File: tasks/integrated-benchmark/src/work_env.rs:343-344
Timestamp: 2026-05-07T23:19:08.272Z
Learning: When reviewing Rust code in pnpm/pacquet for deprecated API usage, do not automatically treat `serde_saphyr::to_string` as deprecated. In `serde-saphyr` v0.0.25, `serde_saphyr::to_string` has no `#[deprecated]` attribute (the `#[deprecated]` later in `serde-saphyr-0.0.25/src/lib.rs` applies to a different function). Only flag `serde_saphyr::to_string` as deprecated if the resolved dependency version’s source shows `#[deprecated]` on that specific function.

Applied to files:

  • crates/executor/src/shell.rs
  • crates/executor/src/lib.rs
  • crates/executor/src/shell/tests.rs
  • crates/executor/src/extend_path.rs
  • crates/executor/src/make_env.rs
  • crates/executor/src/make_env/tests.rs
  • crates/package-manager/src/build_modules.rs
  • crates/executor/src/extend_path/tests.rs
  • crates/executor/src/lifecycle/tests.rs
  • crates/executor/src/lifecycle.rs
📚 Learning: 2026-05-01T10:01:33.766Z
Learnt from: zkochan
Repo: pnpm/pacquet PR: 349
File: crates/reporter/src/tests.rs:121-121
Timestamp: 2026-05-01T10:01:33.766Z
Learning: In Rust test code, follow the repo’s CODE_STYLE_GUIDE test-logging rule: add logging (e.g., `eprintln!`/`eprintln!(...)`) so that useful diagnostic values are printed when a test fails, unless the assertion is `assert_eq!` (where the differing values are already included). Concretely, if you use assertions like `assert!`, `assert_ne!`, etc., ensure the test logs the relevant actual/expected values (or context) before/around the assertion so failures can be diagnosed without rerunning.

Applied to files:

  • crates/executor/src/shell/tests.rs
  • crates/executor/src/make_env/tests.rs
  • crates/executor/src/extend_path/tests.rs
  • crates/executor/src/lifecycle/tests.rs
🔇 Additional comments (4)
crates/executor/src/make_env.rs (1)

1-209: Well-structured port of makeEnv with clear documentation.

The environment builder correctly implements the upstream logic:

  • Parent env filtering strips npm_*/pnpm_* stamping keys
  • Recursive stamp_package handles the top-level keep-list (name/version/config/engines/bin) and nested descent correctly
  • Multi-line string escaping via JSON encoding matches the JS JSON.stringify behavior
  • npm_lifecycle_script is correctly stamped last to prevent extra_env from clobbering it

The find_node_in_path() fallback using env::var_os("PATH") rather than the passed parent_env is acceptable since callers can provide an explicit node_execpath when test isolation is needed.

crates/executor/src/lifecycle.rs (3)

59-67: LGTM: New ScriptShell error variant.

Clean addition with proper miette diagnostic code and #[error(source)] for the wrapped ScriptShellError.


210-237: LGTM: Environment construction and TMPDIR handling.

The EnvOptions construction and build_env call are well-wired. The TMPDIR creation with AlreadyExists handling mirrors the upstream index.js:97-102 defensive check.


264-279: LGTM: Shell selection and command setup.

The .env_clear() followed by explicit .envs(&built.env) correctly isolates the child from inherited npm_* keys. The let _ = shell.windows_verbatim_args; acknowledges the deferred TODO cleanly.

Comment thread crates/executor/src/lifecycle.rs Outdated
Comment thread crates/executor/src/lifecycle/tests.rs Outdated
@codecov

codecov Bot commented May 12, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 87.45520% with 35 lines in your changes missing coverage. Please review.
✅ Project coverage is 84.72%. Comparing base (6862e70) to head (c3b32b1).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
crates/executor/src/lifecycle.rs 75.47% 13 Missing ⚠️
crates/package-manager/src/build_modules.rs 0.00% 12 Missing ⚠️
crates/executor/src/make_env.rs 92.10% 9 Missing ⚠️
crates/executor/src/extend_path.rs 98.36% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #418      +/-   ##
==========================================
+ Coverage   84.55%   84.72%   +0.17%     
==========================================
  Files          75       78       +3     
  Lines        5159     5415     +256     
==========================================
+ Hits         4362     4588     +226     
- Misses        797      827      +30     

☔ View full report in Codecov by Sentry.
📢 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 May 12, 2026

Copy link
Copy Markdown

Micro-Benchmark Results

Linux

group                          main                                   pr
-----                          ----                                   --
tarball/download_dependency    1.02     15.9±0.61ms   273.0 KB/sec    1.00     15.6±0.04ms   277.8 KB/sec

The Windows CI runner caught two test-only Windows regressions
introduced by the shell-selection change earlier in this branch:

- `extend_path::tests::pnpm_virtual_store_layout_yields_three_bins_deepest_first`
  hard-coded `/proj/...` expected values. `path::absolute("/proj")`
  on Windows resolves against the current drive (`C:\proj`), making
  those expectations racy with the runner. Split into a Unix-gated
  test with literal expectations and a new platform-neutral test
  that asserts the structural invariants (count + deepest-first
  ordering) without anchoring to any absolute root.
- `lifecycle::tests::lifecycle_emits_script_stdio_and_exit_in_order`
  and the new `child_sees_stamped_npm_package_and_no_leaked_npm_config`
  both use POSIX-only script syntax (`;`, `1>&2`, `printf`, `$VAR`).
  Before this branch the spawn used a hard-coded `sh -c` so Git
  Bash on the Windows runner satisfied them; once the shell
  selector started picking `cmd /d /s /c` on Windows (per item #4
  / parity with pnpm) those tests can no longer run there. Gate
  them on `cfg(unix)`. The cmd-path shell selection itself is
  covered by `crate::shell` unit tests; env stamping by
  `crate::make_env` units.

No production-code change.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
crates/executor/src/lifecycle/tests.rs (1)

264-317: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Unsafe env mutation lacks enforced isolation and panic-safe cleanup.

At Line 264-271 and Line 315-317, the test mutates process-global env, but the safety comment itself acknowledges possible concurrent env reads. That does not satisfy the unsafe contract. Also, if run_postinstall_hooks panics, cleanup is skipped and the var leaks into later tests.

#!/bin/bash
set -euo pipefail

# 1) Check whether test execution is explicitly serialized anywhere.
rg -n --hidden -S 'nextest|test-threads|RUST_TEST_THREADS|serial_test|harness\s*=\s*false'

# 2) Check whether other tests may read/write process env concurrently.
rg -n --type rust 'std::env::(var|vars|set_var|remove_var)\('

Expected verification outcome: if execution is not forced to single-thread/single-process for this test context, and other env reads/writes exist, this unsafe usage is unsound. Consider moving this case to an isolated subprocess-based test and adding a drop guard so cleanup always runs.

As per coding guidelines: "Do not introduce unsafe without a clear justification and review".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/executor/src/lifecycle/tests.rs` around lines 264 - 317, The test
unsafely mutates process-global env ("npm_config_should_be_stripped") without
serialization and with panic-unsafe cleanup; replace the direct unsafe
std::env::set_var/remove_var usage in this test by running the scenario in an
isolated subprocess (spawn Command that sets the env and executes the
postinstall logic or a small test helper) or, if you must keep it in-process,
wrap the set/remove in a panic-safe guard (e.g., use std::panic::catch_unwind or
a Drop-based scope guard) so remove_var always runs; locate the mutation around
npm_config_should_be_stripped and the call to
run_postinstall_hooks::<SilentReporter> and move the env mutation into the child
or protect it with the guard to ensure deterministic isolation and no leak to
other tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/executor/src/lifecycle/tests.rs`:
- Around line 312-314: The test currently does a bare assert!(ran) after calling
run_postinstall_hooks::<SilentReporter>(opts); replace that with an assertion
that includes context so failures show the actual value: use assert!(ran,
"run_postinstall_hooks::<SilentReporter>(...) returned false: ran = {:?}", ran)
or equivalent; reference the call/run_postinstall_hooks::<SilentReporter> and
the ran variable so the failure message clearly shows the returned value in CI
logs.

---

Duplicate comments:
In `@crates/executor/src/lifecycle/tests.rs`:
- Around line 264-317: The test unsafely mutates process-global env
("npm_config_should_be_stripped") without serialization and with panic-unsafe
cleanup; replace the direct unsafe std::env::set_var/remove_var usage in this
test by running the scenario in an isolated subprocess (spawn Command that sets
the env and executes the postinstall logic or a small test helper) or, if you
must keep it in-process, wrap the set/remove in a panic-safe guard (e.g., use
std::panic::catch_unwind or a Drop-based scope guard) so remove_var always runs;
locate the mutation around npm_config_should_be_stripped and the call to
run_postinstall_hooks::<SilentReporter> and move the env mutation into the child
or protect it with the guard to ensure deterministic isolation and no leak to
other tests.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: d4ecfe87-e65c-40bd-8191-8af720b703f4

📥 Commits

Reviewing files that changed from the base of the PR and between e724ced and ec5c208.

📒 Files selected for processing (2)
  • crates/executor/src/extend_path/tests.rs
  • crates/executor/src/lifecycle/tests.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/executor/src/extend_path/tests.rs
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Run benchmark on ubuntu-latest
  • GitHub Check: Lint and Test (windows-latest)
  • GitHub Check: Code Coverage
  • GitHub Check: Run benchmark on ubuntu-latest
🧰 Additional context used
📓 Path-based instructions (1)
**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

**/*.rs: Log emissions are part of 'match pnpm'—when porting a function that fires pnpm:<channel> events through globalLogger, logger.debug(...), or streamParser.write(...), mirror the call site, payload, and ordering so @pnpm/cli.default-reporter parses pacquet's NDJSON the same way
Declare a newtype wrapper for branded string types instead of collapsing into plain String or &str to preserve the distinction between validating and non-validating brands
If upstream always validates before construction of a branded string type, implement validation via TryFrom<String> and/or FromStr without providing an infallible public constructor
If upstream never validates a branded string type, expose an infallible From<String> constructor to provide type-safety without runtime validation
When upstream occasionally constructs a branded string type without validation via bare as assertion, expose a from_str_unchecked constructor so callers can opt into the same unchecked path explicitly
Match upstream serde behavior for branded types—use #[serde(try_from = "String")] for deserialization to go through the validator and #[serde(into = "String")] for serialization
Use #[derive(derive_more::From)] and #[derive(derive_more::Into)] for mechanical conversion impls on branded types rather than handwriting impl blocks
Model string-literal unions (like 'auto' | 'always' | 'never') as Rust enums instead of newtype wrappers
Template literal types (like ${string}@${string}) should be treated as branded string types with the same validation discipline applied
Follow the code style guide in CODE_STYLE_GUIDE.md covering code-level conventions not enforced by tooling: imports, modules, naming, ownership and borrowing, parameter type selection, trait bounds, pattern matching, pipe-trait, error handling, test layout, and cloning of Arc and Rc
Warnings are errors in cargo clippy --locked -- --deny warnings, and should not be silenced with `#[allow(....

Files:

  • crates/executor/src/lifecycle/tests.rs
🧠 Learnings (2)
📚 Learning: 2026-05-01T10:01:33.766Z
Learnt from: zkochan
Repo: pnpm/pacquet PR: 349
File: crates/reporter/src/tests.rs:121-121
Timestamp: 2026-05-01T10:01:33.766Z
Learning: In Rust test code, follow the repo’s CODE_STYLE_GUIDE test-logging rule: add logging (e.g., `eprintln!`/`eprintln!(...)`) so that useful diagnostic values are printed when a test fails, unless the assertion is `assert_eq!` (where the differing values are already included). Concretely, if you use assertions like `assert!`, `assert_ne!`, etc., ensure the test logs the relevant actual/expected values (or context) before/around the assertion so failures can be diagnosed without rerunning.

Applied to files:

  • crates/executor/src/lifecycle/tests.rs
📚 Learning: 2026-05-07T23:19:08.272Z
Learnt from: KSXGitHub
Repo: pnpm/pacquet PR: 401
File: tasks/integrated-benchmark/src/work_env.rs:343-344
Timestamp: 2026-05-07T23:19:08.272Z
Learning: When reviewing Rust code in pnpm/pacquet for deprecated API usage, do not automatically treat `serde_saphyr::to_string` as deprecated. In `serde-saphyr` v0.0.25, `serde_saphyr::to_string` has no `#[deprecated]` attribute (the `#[deprecated]` later in `serde-saphyr-0.0.25/src/lib.rs` applies to a different function). Only flag `serde_saphyr::to_string` as deprecated if the resolved dependency version’s source shows `#[deprecated]` on that specific function.

Applied to files:

  • crates/executor/src/lifecycle/tests.rs

Comment thread crates/executor/src/lifecycle/tests.rs Outdated
Two more Windows-only test regressions slipped through the previous
pass:

- `make_env::tests::make_env_stamps_lifecycle_specific_keys` and
  `make_env::tests::make_env_tmpdir_gating_mirrors_unsafe_perm`
  hard-coded forward-slash expected values for `npm_package_json`,
  `INIT_CWD`, `PNPM_SCRIPT_SRC_DIR`, and `TMPDIR`. On Windows
  `Path::join` emits `\` separators, so the assertions break. Build
  expected values through the same `Path::join` path so separator
  handling stays `std`'s problem, not the test's.
- `extend_path::tests::virtual_store_walk_orders_deepest_first`'s
  multi-line `assert!` collapsed to single-line-plus-trailing-comma
  under `cargo fmt`, which the dylint `perfectionist` rule rejects.
  Drop the trailing comma so fmt and dylint agree.
Copilot AI review requested due to automatic review settings May 12, 2026 08:32

Copilot AI 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.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 4 comments.

Comment thread crates/executor/src/make_env.rs
Comment thread crates/executor/src/lifecycle.rs Outdated
Comment thread crates/executor/src/lifecycle.rs Outdated
Comment thread crates/executor/src/lifecycle/tests.rs
Five production-code fixes and three test-quality fixes from
Copilot + coderabbit on PR #418:

- `is_stamping_key` narrowed from `npm_* | pnpm_*` to just `npm_*`,
  matching upstream's `!i.match(/^npm_/)` at `npm-lifecycle/index.js:359`.
  Dropping every `pnpm_*` key would have stripped `PNPM_HOME` and
  feature flags upstream preserves.
- `find_node_in_path` no longer reads the process-global `PATH` via
  `env::var_os`. It now takes the parent env's PATH as a parameter,
  so `build_env` is fully deterministic given its inputs (matching
  the docstring contract).
- Look up `PATH` from `parent_env` case-insensitively via a new
  `path_value` helper. Windows preserves the system casing
  (typically `Path`) on env keys, so the previous `built.env.get("PATH")`
  call returned `None` there and `extend_path` lost the inherited
  system PATH.
- `run_postinstall_hooks` captures the parent env once and threads it
  through to each lifecycle hook, instead of calling
  `env::vars().collect()` from inside `run_lifecycle_hook` on every
  hook spawn. The three hooks for one package now observe a
  consistent snapshot.
- TMPDIR creation drops the `AlreadyExists`-swallow special case.
  `fs::create_dir_all` returns `Ok(())` when the path already exists
  *as a directory*; `AlreadyExists` from it specifically signals a
  non-directory file at that path, which is a real error that
  should surface as a spawn failure.

Test fixes:
- `child_sees_stamped_npm_package_and_no_leaked_npm_config` now
  installs a `Drop`-based env guard around the `set_var` call, so
  an assertion failure cleans up the seed instead of leaking it
  into sibling tests. Stray `dbg!(dump)` removed; bare `assert!(ran)`
  picks up a diagnostic message.
- `make_env_stamps_top_level_keys_and_strips_npm_config_leakage`
  swapped the `pnpm_config_user_agent`-must-be-stripped assertion
  for a `PNPM_HOME`-must-pass-through assertion. Mirrors the
  narrowed filter and the upstream behavior.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/executor/src/make_env.rs`:
- Around line 63-124: is_stamping_key and filter_parent_env perform
case-sensitive env-key checks which on Windows lets duplicated keys (e.g.,
NPM_CONFIG_FOO vs npm_config_foo, Node vs NODE) pass through; update both
functions to normalize keys using the same case-insensitive normalization used
by path_value (e.g., map keys to a canonical lowercase or use path_value's
normalization helper) before calling starts_with("npm_") or matches! so
comparisons are Windows-safe, and ensure filter_parent_env de-duplicates using
that normalized form so inserted keys from stamp_package,
npm_node_execpath/NODE, npm_execpath, INIT_CWD, TMPDIR, etc. won't create
conflicting duplicates on Windows.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 908a1771-63c5-4ae2-bcd2-3809e5a656c1

📥 Commits

Reviewing files that changed from the base of the PR and between 6cd63e7 and 6a57a2b.

📒 Files selected for processing (4)
  • crates/executor/src/lifecycle.rs
  • crates/executor/src/lifecycle/tests.rs
  • crates/executor/src/make_env.rs
  • crates/executor/src/make_env/tests.rs
🚧 Files skipped from review as they are similar to previous changes (3)
  • crates/executor/src/make_env/tests.rs
  • crates/executor/src/lifecycle.rs
  • crates/executor/src/lifecycle/tests.rs
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Run benchmark on ubuntu-latest
🧰 Additional context used
📓 Path-based instructions (1)
**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

**/*.rs: Log emissions are part of 'match pnpm'—when porting a function that fires pnpm:<channel> events through globalLogger, logger.debug(...), or streamParser.write(...), mirror the call site, payload, and ordering so @pnpm/cli.default-reporter parses pacquet's NDJSON the same way
Declare a newtype wrapper for branded string types instead of collapsing into plain String or &str to preserve the distinction between validating and non-validating brands
If upstream always validates before construction of a branded string type, implement validation via TryFrom<String> and/or FromStr without providing an infallible public constructor
If upstream never validates a branded string type, expose an infallible From<String> constructor to provide type-safety without runtime validation
When upstream occasionally constructs a branded string type without validation via bare as assertion, expose a from_str_unchecked constructor so callers can opt into the same unchecked path explicitly
Match upstream serde behavior for branded types—use #[serde(try_from = "String")] for deserialization to go through the validator and #[serde(into = "String")] for serialization
Use #[derive(derive_more::From)] and #[derive(derive_more::Into)] for mechanical conversion impls on branded types rather than handwriting impl blocks
Model string-literal unions (like 'auto' | 'always' | 'never') as Rust enums instead of newtype wrappers
Template literal types (like ${string}@${string}) should be treated as branded string types with the same validation discipline applied
Follow the code style guide in CODE_STYLE_GUIDE.md covering code-level conventions not enforced by tooling: imports, modules, naming, ownership and borrowing, parameter type selection, trait bounds, pattern matching, pipe-trait, error handling, test layout, and cloning of Arc and Rc
Warnings are errors in cargo clippy --locked -- --deny warnings, and should not be silenced with `#[allow(....

Files:

  • crates/executor/src/make_env.rs

Comment thread crates/executor/src/make_env.rs
Rust's `Command::env` treats env keys case-insensitively on Windows.
Combined with our case-sensitive filter, a parent env containing
e.g. `NPM_CONFIG_FOO` (uppercase) would pass through unfiltered;
when we then inserted `npm_lifecycle_event` and friends, the spawn-
side env block would carry both casings of the same Windows-logical
variable, with the surviving value picked unpredictably from
HashMap iteration order.

Make `is_stamping_key` take an explicit `is_windows: bool` and use
`eq_ignore_ascii_case` for the prefix and exact-match arms on
Windows. Production callers pass `cfg!(windows)`; tests cover both
branches.

Coverage:
- POSIX path: matches `/^npm_/` literally; `Node`, `NPM_CONFIG_*`,
  etc. all survive (they're distinct variables on POSIX).
- Windows path: strips any-case `npm_` prefix and `NODE` / `TMPDIR` /
  `INIT_CWD` / `PNPM_SCRIPT_SRC_DIR` exact-matches in any case.
- `PNPM_HOME` and other `pnpm_*` keys still pass through on both
  platforms — they're not in upstream's strip set.
Copilot AI review requested due to automatic review settings May 12, 2026 08:56

Copilot AI 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.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 3 comments.

Comment thread crates/executor/src/make_env.rs
Comment thread crates/executor/src/lifecycle.rs
Comment thread crates/executor/src/extend_path.rs Outdated
The `// Item #14 will replace this …` / `// Item #15 in #397 …`
comments referenced specific items in the audit issue this PR
partially closes. Per `CLAUDE.md`:

  Don't reference the current task, fix, or callers ("used by X",
  "added for the Y flow", "handles the case from issue #123"), since
  those belong in the PR description and rot as the codebase evolves.

Replaced the substantive WHY (placeholder default until config is
plumbed) without naming the issue, and dropped the comments that
were just restating the field name. Same applies to the
`RunPostinstallHooks.unsafe_perm` doc comment.
@zkochan zkochan changed the title fix(executor): port makeEnv, extendPath, and shell selection for lifecycle scripts (#397) feat(executor): port makeEnv, extendPath, and shell selection for lifecycle scripts (#397) May 12, 2026
Three issues raised by Copilot on the case-insensitive filter
commit:

- `is_stamping_key` previously did `key[..4].eq_ignore_ascii_case(...)`
  on the Windows branch. That can panic when `key` is non-ASCII and
  byte 4 lands inside a multi-byte codepoint (e.g. `"x\u{10000}"`
  is 5 bytes with a 4-byte codepoint starting at byte 1). Switched
  to a byte-level prefix check via `key.as_bytes().get(..4)`, which
  short-circuits on non-ASCII keys without panicking. Added a
  regression test covering the panic-y inputs.
- `run_lifecycle_hook` previously called `.envs(&built.env).env("PATH", ...)`,
  relying on `Command::env`'s case-insensitive dedup on Windows to
  beat the inherited `Path` key. The dedup behavior is real (and
  documented), but relying on it leaves an unspecified-winner edge
  case visible to the spawned process. Strip any PATH-like key
  from the env map case-insensitively before inserting our
  computed `PATH`, so the spawn sees exactly one PATH slot.
- `ancestor_node_modules_bins` previously fell back to an empty
  `PathBuf` when `head` was empty (i.e. `wd` starts with
  `node_modules/`). Upstream's `path.resolve("")` returns the
  process cwd; switch to `env::current_dir()` for that case to
  match. Added a code comment so the cwd-dependence isn't
  surprising — pacquet's production call sites always pass an
  absolute `pkg_root`, so the dependency is a non-issue in
  practice, but matching upstream removes the divergence.
Copilot AI review requested due to automatic review settings May 12, 2026 09:11

Copilot AI 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.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.

Comment on lines +18 to +24
#[display(
"Cannot spawn .bat or .cmd as a script shell. \
The pnpm-workspace.yaml scriptShell option was configured to a .bat or .cmd file. \
These cannot be used as a script shell reliably. \
Please unset the scriptShell option, or configure it to a .exe instead. \
(scriptShell={path})"
)]

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Pushing back on this one. The text is a verbatim port of pnpm v11's hint at exec/lifecycle/src/runLifecycleHook.ts:64-69 (pinned to b4f8f47ac2):

The pnpm-workspace.yaml scriptShell option was configured to a .bat or .cmd file. These cannot be used as a script shell reliably.

Please unset the scriptShell option, or configure it to a .exe instead.

Per this repo's CLAUDE.md:

Do not invent behavior that pnpm does not have. Do not 'fix' pnpm quirks unless the same fix has landed upstream.

User-facing diagnostic strings are part of the contract (ERR_PNPM_INVALID_SCRIPT_SHELL_WINDOWS is in pnpm's public error catalog), so I'd rather mirror upstream and let the source-agnostic phrasing be a pnpm upstream PR that we then port. Happy to revisit if @zkochan wants to diverge for pacquet specifically.


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

Comment thread crates/executor/src/extend_path.rs Outdated
Copilot flagged that `env::join_paths` errors when any path
component contains the platform separator, and my fallback
discarded every computed entry — `.bin` walk, node-gyp shim,
extra paths — leaving lifecycle scripts with only the inherited
PATH (or empty). Upstream's
`pathArr.join(process.platform === 'win32' ? ';' : ':')` at
`lib/extendPath.js:26` does an unvalidated string join; if a
component contains the separator, the spawned shell sees an
embedded extra entry, which is the same outcome upstream
produces.

Replaced `env::join_paths` with a tiny `join_paths_lossy` helper
that does the literal string join, matching upstream exactly.
Added a POSIX-gated regression test (`separator_in_path_component_does_not_drop_other_entries`)
that exercises an `extra_bin_paths` component containing `:` and
asserts both the wd `.bin` and the weird extra path survive.
@github-actions

Copy link
Copy Markdown

Integrated-Benchmark Report (Linux)

Scenario: Frozen Lockfile

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 2.538 ± 0.076 2.448 2.650 1.01 ± 0.04
pacquet@main 2.514 ± 0.078 2.420 2.687 1.00
pnpm 6.274 ± 0.108 6.088 6.414 2.50 ± 0.09
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 2.53834279366,
      "stddev": 0.07609073078478107,
      "median": 2.5049310393599997,
      "user": 2.51874312,
      "system": 3.493900959999999,
      "min": 2.44785918036,
      "max": 2.65009997636,
      "times": [
        2.61830636036,
        2.4907442353600002,
        2.49625183136,
        2.65009997636,
        2.46426930736,
        2.64221253536,
        2.51361024736,
        2.57461473936,
        2.44785918036,
        2.48545952336
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 2.51423552276,
      "stddev": 0.07753500308971267,
      "median": 2.49482211086,
      "user": 2.54900032,
      "system": 3.4978371599999996,
      "min": 2.42030591736,
      "max": 2.68710937036,
      "times": [
        2.49481209636,
        2.59565837336,
        2.49067148036,
        2.42030591736,
        2.49278487736,
        2.68710937036,
        2.43086092536,
        2.49483212536,
        2.51424402636,
        2.52107603536
      ]
    },
    {
      "command": "pnpm",
      "mean": 6.27363785366,
      "stddev": 0.10829208953007195,
      "median": 6.2943365903599995,
      "user": 9.07003992,
      "system": 4.59820476,
      "min": 6.0884925033599995,
      "max": 6.41395913436,
      "times": [
        6.17161222536,
        6.38708013436,
        6.41395913436,
        6.3701903913599995,
        6.0884925033599995,
        6.151726733359999,
        6.3142717383599996,
        6.274401442359999,
        6.316308181359999,
        6.24833605236
      ]
    }
  ]
}

Scenario: Frozen Lockfile (Hot Cache)

Command Mean [ms] Min [ms] Max [ms] Relative
pacquet@HEAD 669.4 ± 52.6 635.1 813.4 1.00
pacquet@main 675.2 ± 28.7 652.6 748.1 1.01 ± 0.09
pnpm 2643.4 ± 106.9 2544.3 2903.5 3.95 ± 0.35
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 0.6693595670600001,
      "stddev": 0.052606689818215986,
      "median": 0.6569874342600001,
      "user": 0.35250017999999994,
      "system": 1.4662264799999998,
      "min": 0.63507351176,
      "max": 0.8133982727600001,
      "times": [
        0.8133982727600001,
        0.6660129837600001,
        0.6374919117600001,
        0.6632978767600001,
        0.6720206817600001,
        0.6724918647600001,
        0.65067699176,
        0.64381089976,
        0.6393206757600001,
        0.63507351176
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 0.6751510434599999,
      "stddev": 0.028650265776925546,
      "median": 0.6666921652600001,
      "user": 0.3591970799999999,
      "system": 1.4955684799999998,
      "min": 0.65255358476,
      "max": 0.7480579487600001,
      "times": [
        0.7480579487600001,
        0.67562661276,
        0.65255358476,
        0.66278960576,
        0.6637614997600001,
        0.6952363547600001,
        0.65401501476,
        0.6696228307600001,
        0.65520736176,
        0.6746396207600001
      ]
    },
    {
      "command": "pnpm",
      "mean": 2.6433685097599997,
      "stddev": 0.10692195525631905,
      "median": 2.60408245226,
      "user": 3.17180188,
      "system": 2.2786787800000003,
      "min": 2.5442516727599997,
      "max": 2.90346864276,
      "times": [
        2.60493870076,
        2.5442516727599997,
        2.70287111176,
        2.7098375037599998,
        2.59759767276,
        2.5569915527599996,
        2.57438244376,
        2.63611959276,
        2.90346864276,
        2.60322620376
      ]
    }
  ]
}

@zkochan zkochan merged commit ff6edea into main May 12, 2026
16 checks passed
@zkochan zkochan deleted the fix/lifecycle-env-path-shell-397 branch May 12, 2026 09:42
zkochan added a commit that referenced this pull request May 12, 2026
Dylint blocker + seven Copilot threads:

- `crates/reporter/src/tests.rs:636` — `assert!` collapsed to a
  single-line trailing-comma form by `cargo fmt`, which dylint's
  `perfectionist::macro-trailing-comma` rejects. Remove the
  trailing comma. (Same conflict we hit on #418.)
- `pnpm:lifecycle` events now carry the dep's `optional` flag.
  Previously `LifecycleMessage::Script` / `Exit` were hard-coded
  to `optional: false`, so an optional dep's lifecycle records
  diverged from upstream's `lifecycleLogger.debug({ optional, … })`
  payloads at `pnpm/exec/lifecycle/src/runLifecycleHook.ts:102`
  and `:165`. Added `RunPostinstallHooks.optional`, threaded
  through to both emit sites, and added a `cfg(unix)` test
  asserting the flag survives the spawn-and-capture round-trip.
  `BuildModules` reads the value from `SnapshotEntry.optional`
  (already in the lockfile, now in the wire too).
- Tightened the `SkippedOptionalDependencyLog` doc comment to
  explain that `ResolutionFailure` has a distinct `package` shape
  upstream (`bareSpecifier`, no `id`) and will need a sibling
  struct / untagged enum when an emit site lands — not just a new
  `reason` value. Refactoring deferred until then per YAGNI.
- `crates/lockfile/src/snapshot_entry/tests.rs` — `!out.contains("optional")`
  would falsely fire on a future fixture containing
  `optionalDependencies:`. Tightened to check the exact key
  spelling (`optional: ` or `optional:\n`).
- `crates/package-manager/src/build_modules/tests.rs` — stale
  comment pointed at `crate::executor::lifecycle::tests` which
  is not a valid path from this crate. Now references
  `pacquet_executor::select_shell` and the executor crate's
  `shell::tests` module.
- `tasks/registry-mock/launch.mjs` — signal-forwarding logic
  installed `process.on('SIGINT'|'SIGTERM'|'SIGHUP', …)` handlers
  that overrode Node's default termination behavior, while the
  child-exit handler tried to re-raise via
  `process.kill(process.pid, signal)` — that hits our own
  listener in a loop. Switched to forwarding signals to the
  child only, and exiting with the shell-convention `128 + signo`
  when the child reports it was killed by a signal.
- `tasks/registry-mock/launch.mjs` — stale path
  `crates/tasks/registry-mock/...` → `tasks/registry-mock/...`.
- `tasks/registry-mock/pnpm-workspace.yaml` — comment said "Rust
  calls the CLI binary" but the Rust launcher now spawns
  `node launch.mjs`. Updated.
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants