Skip to content

fix: stop PATH growing on every activation, guard the bash cd-hook, ship linux-aarch64 - #41

Merged
Fahl-Design merged 3 commits into
mainfrom
fix/path-growth-and-arm-release
Jul 24, 2026
Merged

fix: stop PATH growing on every activation, guard the bash cd-hook, ship linux-aarch64#41
Fahl-Design merged 3 commits into
mainfrom
fix/path-growth-and-arm-release

Conversation

@Fahl-Design

@Fahl-Design Fahl-Design commented Jul 24, 2026

Copy link
Copy Markdown
Member

Three defects found during a read of the codebase. Each is a fix: on its own commit, plus one follow-up refactor.

1. PATH grew on every activation

Shell::path emitted export PATH=<bin>:"$PATH" without removing the entry the previous activation had put there, so every switch left its predecessor behind:

run 1: .../8.9.6/bin:/usr/bin:/bin
run 2: .../8.9.6/bin:.../8.9.6/bin:/usr/bin:/bin
run 3: .../8.9.6/bin:.../8.9.6/bin:.../8.9.6/bin:/usr/bin:/bin

Combined with defect 2 this meant one extra PATH entry per shell prompt in any directory holding a .php-version file — within minutes PATH is hundreds of entries, every exec*() gets more expensive and echo $PATH is unreadable.

The fix moves PATH manipulation out of the emitted shell code. The wrapper runs pvm as a direct child, so the Rust process already sees exactly the PATH its output will be eval'd into: fs::path_without_versions() drops every $PVM_DIR/versions entry and Shell::path bakes the resulting value in literally. deactivate() uses the same list and no longer shells out to tr/grep/paste.

Two related guards came along, same root cause:

  • Bash::use_on_cd returns early unless $PWD actually changed.
  • wrapper_fn only prepends $PVM_DIR/bin when it is not already on PATH, so re-sourcing the rc file in a nested shell stops duplicating that entry too.

pvm env now prints the default-version activation before the wrapper, because the literal snapshot cannot contain the $PVM_DIR/bin entry the wrapper adds one line later.

2. The bash cd-hook ran on every prompt

_pvm_cd_hook is attached to PROMPT_COMMAND, which bash runs after every command, not just after a cd. In a project directory that meant a fork+exec of pvm, an env-file write and an eval on each prompt. Zsh (chpwd) and fish (--on-variable PWD) were already correct — only bash needed the $PWD guard.

3. linux-aarch64 was never built

get_target_triple() resolves linux-aarch64 and install.sh maps uname -m to pvm-linux-aarch64.tar.gz, but the build matrix only produced linux-x86_64 and the two macOS targets. On ARM Linux both the install script and pvm self-update fetched an asset that was never uploaded and failed with a 404. Builds natively on the ubuntu-24.04-arm runner, so no cross toolchain is needed.

Verification

  • cargo test: 29 unit + 43 integration, all green. cargo fmt --check and cargo clippy -- -D warnings clean.
  • New regression test test_use_does_not_stack_duplicate_path_entries starts from a deliberately dirty PATH (active version listed twice plus a stale second version) and asserts the emitted line lists the active version exactly once and drops the stale one.
  • Checked by hand in real bash, zsh and fish: six consecutive switches leave four PATH entries, pvm use system restores the original PATH, three rc re-sources leave one $PVM_DIR/bin entry.
  • The two shell-snippet guards were confirmed to fail with their guard removed, so they are not vacuous.
  • Test PATHs use synthetic /opt/tool-* entries — no assertion depends on how the runner lays out its directories.

Note for review

The ARM job needs ARM runners available to this repo (ubuntu-24.04-arm, free for public repos). If that runner is unavailable the build job will fail for that matrix entry only — worth confirming on the first release after merge.

Unrelated to this branch, but noticed while reading and worth separate issues: the remote-version cache has no stale fallback (any network failure kills pvm install even with a usable index on disk), .php-version is only read from the current directory with no upward walk, and status messages in network.rs / install.rs go to stdout, which pollutes pvm ls-remote | ....


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

Every activation emitted "export PATH=<bin>:$PATH", prepending its bin
directory to the live PATH without removing the entry the previous
activation had put there. Each switch therefore left its predecessor
behind, and in bash the cd hook runs from PROMPT_COMMAND -- after every
command, not just after a cd -- so a shell sitting in a directory with a
.php-version file grew PATH by one entry per prompt.

PATH manipulation moves out of the emitted shell code: the wrapper runs
pvm as a direct child, so the Rust process already sees the PATH its
output will be eval'd into. fs::path_without_versions() drops every
$PVM_DIR/versions entry and Shell::path bakes the resulting value in
literally, which also lets deactivate() stop shelling out to
tr/grep/paste. Two related guards: Bash::use_on_cd returns early unless
$PWD actually changed, and wrapper_fn only prepends $PVM_DIR/bin when it
is not already on PATH, so re-sourcing the rc file in a nested shell
stops duplicating that entry too.

pvm env now prints the default-version activation before the wrapper,
because the literal snapshot cannot contain the $PVM_DIR/bin entry the
wrapper adds one line later.

Verified in bash, zsh and fish: six consecutive switches leave four PATH
entries, and pvm use system restores the original PATH.
get_target_triple() resolves linux-aarch64 and install.sh maps uname -m
to pvm-linux-aarch64.tar.gz, but the build matrix only produced
linux-x86_64 and the two macOS targets. On ARM Linux both the install
script and pvm self-update fetched an asset that was never uploaded and
failed with a 404.

Builds natively on the ubuntu-24.04-arm runner, so no cross toolchain is
needed.
posix and fish each had their own "drop empty entries, render them" pass,
and posix_deactivate had neither -- it joined the list raw, so an
inherited "::" survived deactivation while activation stripped it. One
path_entries() feeding a per-shell assignment renderer removes the
duplication and the asymmetry, and taking &str instead of an owned
iterator drops a String allocation per entry on the fish paths.

Test PATHs move to synthetic /opt/tool-* entries so no assertion depends
on how the host or CI runner lays out its directories, and the two
snippet guards are asserted by relative position (guard before the thing
it guards) rather than by a verbatim copy of the source line, which broke
on any reformatting. Both were confirmed to fail with their guard removed.
@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 24803dac-787b-49ff-8b61-6ab003d1664a

📥 Commits

Reviewing files that changed from the base of the PR and between 9efe094 and 8c37461.

📒 Files selected for processing (8)
  • .github/workflows/release.yml
  • CLAUDE.md
  • src/commands/env.rs
  • src/commands/install.rs
  • src/commands/use_cmd.rs
  • src/fs.rs
  • src/shell.rs
  • tests/cli.rs

📝 Walkthrough

Summary by CodeRabbit

  • New Features
    • Added ARM Linux builds for aarch64 systems.
  • Improvements
    • Improved shell environment activation and deactivation across Bash, Zsh, and Fish.
    • Prevented repeated PHP version entries from accumulating in PATH.
    • Preserved non-version PATH entries when switching PHP versions or returning to the system PHP.
  • Bug Fixes
    • Fixed shell environment updates that could produce incorrect or unavailable ARM Linux release assets.
  • Documentation
    • Expanded guidance for shell integration and automatic version switching.

Walkthrough

The PR changes shell PATH generation to explicitly preserve non-managed entries, updates activation and deactivation flows, adds regression coverage, documents the shell behavior, and adds an ARM Linux release build targeting aarch64-unknown-linux-gnu.

Changes

Shell PATH flow

Layer / File(s) Summary
PATH filtering and shell contract
src/fs.rs, src/shell.rs
Adds path_without_versions() and changes shell APIs to receive explicit non-managed PATH entries.
Shell-specific PATH and wrapper generation
src/shell.rs
Updates POSIX, Bash, Zsh, and Fish output to construct complete PATH values, clear managed state on deactivation, and guard against duplicate entries.
Activation flow and regression coverage
src/commands/..., src/shell.rs, tests/cli.rs, CLAUDE.md
Passes filtered PATH entries through activation and deactivation, orders default activation before wrappers, and tests duplicate prevention and shell output.

ARM release build

Layer / File(s) Summary
ARM Linux release matrix
.github/workflows/release.yml
Adds an ARM Linux build targeting aarch64-unknown-linux-gnu and uploads pvm-linux-aarch64 assets.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Command
  participant PathFilter
  participant Shell
  participant EnvUpdateFile
  Command->>PathFilter: obtain unmanaged PATH entries
  PathFilter-->>Command: return filtered entries
  Command->>Shell: generate activation or deactivation snippet
  Shell-->>EnvUpdateFile: write complete PATH and marker updates
Loading

Possibly related PRs

Poem

A rabbit hops through PATH anew,
No stacked bins remain in view.
Bash and Fish guard every way,
ARM builds join the release day.
Clean shell trails and targets bright—
The burrow ships its code tonight!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 78.26% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title captures the main fixes: PATH growth, Bash cd-hook guard, and ARM64 release builds.
Description check ✅ Passed The description matches the code changes and objectives, covering PATH fixes, Bash hook behavior, and ARM64 builds.
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.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/path-growth-and-arm-release

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.

@Fahl-Design
Fahl-Design merged commit 0b7b39a into main Jul 24, 2026
5 checks passed
@Fahl-Design
Fahl-Design deleted the fix/path-growth-and-arm-release branch July 24, 2026 23:00
webproject-bot Bot pushed a commit that referenced this pull request Jul 24, 2026
## [2.0.1](v2.0.0...v2.0.1) (2026-07-24)

### Bug Fixes

* stop PATH growing on every activation, guard the bash cd-hook, ship linux-aarch64 ([#41](#41)) ([0b7b39a](0b7b39a))
@webproject-bot

Copy link
Copy Markdown

🎉 This PR is included in version 2.0.1 🎉

The release is available on:

Your semantic-release bot 📦🚀

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant