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

fix: resolve symlinks in sh shims before computing basedir - #59

Merged
zkochan merged 3 commits into
mainfrom
fix/resolve-symlinks-in-sh-shim
Aug 7, 2026
Merged

zkochan merged 3 commits into
mainfrom
fix/resolve-symlinks-in-sh-shim

Conversation

@zkochan

@zkochan zkochan commented Aug 6, 2026 •

Copy link
Copy Markdown
Member

Summary

POSIX shells set $0 to the path of the invoked symlink, not the shim it points at. When a generated sh shim is reached through an external symlink (for example one placed on PATH), basedir resolved to the symlink's directory, so the relative target path into node_modules broke and the shim failed to launch its target.

This follows the symlink chain with a portable readlink loop before deriving basedir. Note that npm's cmd-shim does not need this because npm links POSIX bins as plain symlinks rather than sh shims; pnpm relies on sh shims on POSIX, so the shim itself has to resolve its physical location.

Fixes pnpm/pnpm#13405. The same change already ships in pnpm via a patchedDependencies patch on @zkochan/cmd-shim@9.0.6 (pnpm/pnpm#13411, mirroring the Rust pacquet-cmd-shim template); releasing this upstream lets that patch be dropped.

Tests

  • New POSIX e2e test that executes a generated shim through a two-hop external symlink chain; it fails without the fix.
  • Shell-shim snapshots regenerated. The three Windows-recorded linking to another drive on Windows entries were restored by hand (Node's snapshot updater drops entries not exercised on the running platform) with the same header change applied.

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

Summary by CodeRabbit

  • Bug Fixes
    • Fixed POSIX command shims invoked through symbolic links so they correctly locate and execute their target scripts.
    • Improved handling of both absolute and relative symbolic-link paths, including multiple linked paths.
    • Preserved platform detection, environment handling, executable selection, and argument forwarding.

POSIX shells set $0 to the path of the invoked symlink, not the shim it
points at. When a shim was reached through an external symlink (for
example one placed on PATH), basedir resolved to the symlink's directory
and the relative target path into node_modules broke. Follow the symlink
chain with readlink before deriving basedir.

Fixes pnpm/pnpm#13405

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 6, 2026 •

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@zkochan, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 40 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 180e8d8a-0890-42f5-b056-16f20d5eaa75

📥 Commits

Reviewing files that changed from the base of the PR and between 77feda8 and 7c7ba04.

📒 Files selected for processing (4)
  • src/index.ts
  • test/e2e.test.js
  • test/e2e.test.js.snapshot
  • test/test.js.snapshot
📝 Walkthrough

Walkthrough

POSIX shim generation now follows absolute and relative symbolic links before deriving basedir. Snapshots cover the generated variants. An end-to-end test verifies execution through a two-link symlink chain.

Changes

POSIX shim resolution

Layer / File(s) Summary
Implement symlink-aware shim paths
src/index.ts
Shell shim templates now resolve $0 through symbolic links and derive basedir from the resolved path.
Validate generated shim variants
test/e2e.test.js, test/test.js.snapshot, test/e2e.test.js.snapshot
Snapshots cover the updated POSIX shims. The end-to-end test invokes a shim through two symlink hops and checks successful execution.

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

Possibly related issues

  • #13405 — Fixes the POSIX shim basedir failure when invoked through external or multi-hop symbolic links.

Poem

A rabbit follows links in a line,
While readlink resolves each sign.
The shim finds its basedir true,
Then runs the target as it should do.
Two hops, one launch—success! 🐇

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: resolving symlinks before computing the shell shim basedir.
Linked Issues check ✅ Passed The changes resolve symlink chains before deriving basedir and add a regression test for external multi-hop symlinks, satisfying issue #13405.
Out of Scope Changes check ✅ Passed The implementation, regression test, and regenerated snapshots are directly related to the linked issue and PR objectives.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/resolve-symlinks-in-sh-shim

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.

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Fix sh shim basedir by resolving $0 symlink chain before launching target

🐞 Bug fix 🧪 Tests 🕐 20-40 Minutes

Grey Divider

AI Description

• Resolve POSIX sh shim location by following $0 through symlinks before computing basedir.
• Prevent failures when shims are invoked via external PATH symlinks (pnpm/pnpm#13405).
• Add POSIX e2e coverage and refresh shim snapshots to match new shim header.
Diagram

graph TD
  A["generateShShim (TS)"] --> B[["Generated sh shim"]] --> C(("External symlink chain")) --> D["readlink loop"] --> E["basedir from real path"] --> F[["node_modules target bin"]] --> G["exec target"]
  H["POSIX e2e test"] --> I["cmdShim() writes shim"] --> B
  H --> C
  subgraph Legend
    direction LR
    _p["Process"] ~~~ _f[["File/Script"]] ~~~ _s(("Symlink"))
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Use realpath/readlink -f in shim
  • ➕ Much simpler shim code (single command)
  • ➕ Less shell logic to maintain
  • ➖ Not portable across POSIX environments (readlink -f and realpath vary by platform)
  • ➖ Would regress compatibility on minimal shells/distros where these utilities/flags are absent
2. Avoid sh shims by using plain symlinks on POSIX
  • ➕ Symlink invocation naturally resolves to the real target path
  • ➕ Eliminates basedir logic entirely
  • ➖ Not compatible with pnpm’s POSIX shim strategy (needs sh wrapper behavior)
  • ➖ Would be a broader behavioral/packaging change beyond this targeted fix

Recommendation: Keep the PR’s portable readlink-walking loop. It directly addresses the POSIX $0 behavior across shells without relying on non-standard utilities, and it confines the fix to basedir derivation while preserving existing shim semantics.

Files changed (3) +203 / -17

Bug fix (1) +20 / -2
index.tsResolve $0 symlink chain before computing sh shim basedir +20/-2

Resolve $0 symlink chain before computing sh shim basedir

• Updates the POSIX sh shim template to walk the symlink chain starting from $0 using readlink and dirname. The shim then derives basedir from the resolved physical shim location, preventing incorrect relative target resolution when invoked via external symlinks.

src/index.ts

Tests (2) +183 / -15
e2e.test.jsAdd POSIX e2e for sh shim invoked via two-hop external symlink chain +33/-0

Add POSIX e2e for sh shim invoked via two-hop external symlink chain

• Adds a new POSIX-only e2e test that generates a shim, creates a two-hop symlink chain to it, and executes the final link. Verifies the shim exits successfully and runs the expected target output, covering the regression scenario from pnpm/pnpm#13405.

test/e2e.test.js

test.js.snapshotRegenerate sh shim snapshots to include symlink-resolution prologue +150/-15

Regenerate sh shim snapshots to include symlink-resolution prologue

• Updates all stored shim snapshots to reflect the new symlink-resolution block preceding basedir computation. This keeps snapshot expectations aligned with the newly generated sh shim content.

test/test.js.snapshot

zkochan and others added 2 commits August 7, 2026 11:09
The .exe-target sh shim snapshot is only exercised on Windows, so the
Linux snapshot regeneration could not update it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The cap makes a symlink cycle rewritten mid-execution terminate instead
of looping forever. New e2e tests cover relative-target hops (the
dirname-composition branch), directories with spaces, and a node-shebang
bin invoked through a chain.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
zkochan added a commit to Ayush442842q/pnpm that referenced this pull request Aug 7, 2026
The cap makes a symlink cycle rewritten mid-execution terminate instead
of looping forever, mirroring the kernel's ELOOP limit. The regression
tests now route the chain through a relative symlink in another
directory, covering the shim's dirname-composition branch that the
absolute-only chain missed. Applied identically to the Rust template and
the `@zkochan/cmd-shim` patch, matching upstream
pnpm/cmd-shim#59.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@zkochan
zkochan merged commit c39e955 into main Aug 7, 2026
8 of 10 checks passed
@zkochan
zkochan deleted the fix/resolve-symlinks-in-sh-shim branch August 7, 2026 09:46
zkochan added a commit to Ayush442842q/pnpm that referenced this pull request Aug 7, 2026
The symlink-resolution fix is released upstream
(pnpm/cmd-shim#59), so the patchedDependencies
entry is no longer needed and library consumers of the TypeScript
packages get the fixed shims too.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.

Bin shim's basedir=$(dirname "$0") breaks when reached via an external symlink

1 participant