Files
pmg/internal/shim/path.go
T
6546116e28 feat: migrate PMG to use PATH shims for package manager wrapper (#246)
* feat: add FilterPMGFromPath utility for PATH shim recursion prevention

* feat: add FilterPMGFromEnv to filter PATH from env slices

* feat: filter ~/.pmg/bin from PATH in proxy subprocess env

* feat: add PathExport method to Shell interface for shim PATH integration

* feat: add ShimManager for PATH shim install/remove lifecycle

* feat: wire ShimManager into setup commands with --use-aliases fallback

* refactor: add DefaultShimConfig helper to reduce setup boilerplate

* fix: resolve real binary path to prevent shim double-invocation

exec.CommandContext resolves the binary using the current process PATH,
which still contains ~/.pmg/bin. This caused pmg to launch the shim
instead of the real package manager, resulting in a second pmg instance
with its own proxy — producing duplicate error messages and wasted work.

ResolveRealBinary searches a filtered PATH (without ~/.pmg/bin) to find
the real package manager binary before execution.

* fix: resolve real binary in runner.Execute and expand path resolution tests

Ensure guard mode and proxy skip paths also resolve through
ResolveRealBinary to prevent infinite shim recursion. Add table-driven
tests covering error cases, multi-binary PATH, and PATH restoration.

* fix: handle error return values from os.Setenv and file Close calls

Address errcheck lint failures: check os.Setenv returns in
ResolveRealBinary, and check f.Close/tempFile.Close in ShimManager.

* feat: auto-migrate shell aliases to PATH shims on setup install

When running `pmg setup install`, detect existing shell aliases and
automatically remove them before installing shims. Existing users
get a seamless migration with no extra flags or commands needed.

* fix: update E2E test to verify shim installation instead of alias RC file

Replace the .pmg.rc file check with assertions that ~/.pmg/bin/ exists
and contains executable shim scripts for npm and pip.

* feat: add FilterPMGFromPath utility for PATH shim recursion prevention

* feat: add FilterPMGFromEnv to filter PATH from env slices

* feat: filter ~/.pmg/bin from PATH in proxy subprocess env

* feat: add PathExport method to Shell interface for shim PATH integration

* feat: add ShimManager for PATH shim install/remove lifecycle

* feat: wire ShimManager into setup commands with --use-aliases fallback

* refactor: add DefaultShimConfig helper to reduce setup boilerplate

* fix: resolve real binary path to prevent shim double-invocation

exec.CommandContext resolves the binary using the current process PATH,
which still contains ~/.pmg/bin. This caused pmg to launch the shim
instead of the real package manager, resulting in a second pmg instance
with its own proxy — producing duplicate error messages and wasted work.

ResolveRealBinary searches a filtered PATH (without ~/.pmg/bin) to find
the real package manager binary before execution.

* fix: resolve real binary in runner.Execute and expand path resolution tests

Ensure guard mode and proxy skip paths also resolve through
ResolveRealBinary to prevent infinite shim recursion. Add table-driven
tests covering error cases, multi-binary PATH, and PATH restoration.

* fix: handle error return values from os.Setenv and file Close calls

Address errcheck lint failures: check os.Setenv returns in
ResolveRealBinary, and check f.Close/tempFile.Close in ShimManager.

* feat: auto-migrate shell aliases to PATH shims on setup install

When running `pmg setup install`, detect existing shell aliases and
automatically remove them before installing shims. Existing users
get a seamless migration with no extra flags or commands needed.

* fix: update E2E test to verify shim installation instead of alias RC file

Replace the .pmg.rc file check with assertions that ~/.pmg/bin/ exists
and contains executable shim scripts for npm and pip.

* feat: install both aliases and shims for full coverage

Aliases win in interactive shells (including venvs), shims catch
non-interactive contexts (IDEs, CI, subprocesses). Remove --use-aliases
flag and migration logic since both are always installed together.
Update E2E to verify all shim scripts and alias RC file.

* feat: address review feedback for shim implementation

- Install both aliases and shims together for full coverage
- Move homeDir resolution into NewDefaultShimManager (internal concern)
- Add mutex to ResolveRealBinary to guard against concurrent PATH mutation
- Use filepath.SplitList for platform-correct PATH splitting
- Add ResolveRealBinary to runner.Execute and proxy flow to prevent
  shim recursion in all execution paths
- Remove print side-effects from ShimManager.Remove
- Update E2E to verify all shim scripts and alias RC file
- Expand ResolveRealBinary tests with table-driven cases

* fix: restore errcheck handling and add concurrency test for ResolveRealBinary

- Restore proper defer with log.Warnf for PATH restoration in ResolveRealBinary
- Restore errcheck handling for f.Close() and tempFile.Close() in ShimManager
- Add explanatory comment for ResolveRealBinary call in proxy_flow
- Add TestResolveRealBinaryConcurrent to verify mutex guards concurrent access

* feat: skip shell integration on Windows with informative warning

On Windows, pmg setup install now writes only the config file and
prints a warning that shell aliases and PATH shims require WSL.

* fix: PMG use pre-resolved binary path (#253)

---------

Co-authored-by: Abhisek Datta <abhisek.datta@gmail.com>
2026-05-12 22:27:05 +05:30

76 lines
1.7 KiB
Go

package shim
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"sync"
"github.com/safedep/dry/log"
)
const pmgBinSuffix = "/.pmg/bin"
var resolverMu sync.Mutex
func FilterPMGFromPath(pathEnv string) string {
if pathEnv == "" {
return ""
}
entries := filepath.SplitList(pathEnv)
filtered := make([]string, 0, len(entries))
for _, entry := range entries {
if !strings.HasSuffix(entry, pmgBinSuffix) {
filtered = append(filtered, entry)
}
}
return strings.Join(filtered, string(os.PathListSeparator))
}
// ResolveRealBinary finds the real binary path for a command by searching
// PATH with ~/.pmg/bin stripped out. This prevents exec.CommandContext from
// resolving to the shim script, which would cause infinite recursion.
func ResolveRealBinary(name string) (string, error) {
resolverMu.Lock()
defer resolverMu.Unlock()
originalPath := os.Getenv("PATH")
filteredPath := FilterPMGFromPath(originalPath)
if err := os.Setenv("PATH", filteredPath); err != nil {
return "", fmt.Errorf("failed to set filtered PATH: %w", err)
}
defer func() {
if err := os.Setenv("PATH", originalPath); err != nil {
log.Warnf("failed to restore PATH: %v", err)
}
}()
resolved, err := exec.LookPath(name)
if err != nil {
return "", fmt.Errorf("could not find %s in PATH (excluding pmg shims): %w", name, err)
}
return resolved, nil
}
func FilterPMGFromEnv(env []string) []string {
result := make([]string, 0, len(env))
for _, entry := range env {
if pathValue, ok := strings.CutPrefix(entry, "PATH="); ok {
filtered := FilterPMGFromPath(pathValue)
result = append(result, "PATH="+filtered)
} else {
result = append(result, entry)
}
}
return result
}