package shim import ( "fmt" "os" "os/exec" "path/filepath" "strings" "sync" "github.com/safedep/dry/log" ) const ( // pmgBinSuffix matches the legacy per-user shim dir (~/.pmg/bin). Retained // as a fallback so shims installed by older PMG versions keep working after // upgrade, until they are regenerated by a fresh `pmg setup install`. pmgBinSuffix = "/.pmg/bin" // pmgShimPathEnv is the env var the shim script exports before exec'ing // pmg. Its value is the absolute path of the shim that was invoked, which // lets FilterPMGFromPath strip exactly the dir the shim lives in — no // matter where it was placed (`~/.pmg/bin`, `/usr/local/lib/pmg/bin`, a // custom location, etc.). // // Internal: set by the shim script, consumed by pmg in the same process // tree. Setting it manually from a user shell is unsupported — it would // cause FilterPMGFromPath to strip the wrong directory from PATH lookup // and could prevent pmg from resolving the real package manager. pmgShimPathEnv = "PMG_SHIM_PATH" ) var resolverMu sync.Mutex func FilterPMGFromPath(pathEnv string) string { if pathEnv == "" { return "" } var shimDir string if shimPath := os.Getenv(pmgShimPathEnv); shimPath != "" { shimDir = filepath.Clean(filepath.Dir(shimPath)) } entries := filepath.SplitList(pathEnv) filtered := make([]string, 0, len(entries)) for _, entry := range entries { if strings.HasSuffix(entry, pmgBinSuffix) { continue } if shimDir != "" && filepath.Clean(entry) == shimDir { continue } 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) continue } // Drop PMG_SHIM_PATH so child processes don't inherit a stale marker // from the shim invocation that triggered this exec. if strings.HasPrefix(entry, pmgShimPathEnv+"=") { continue } result = append(result, entry) } return result }