mirror of
https://github.com/safedep/pmg.git
synced 2026-08-03 07:24:09 +02:00
* fix: fail fast with clear error when package manager is not installed When PMG shims intercept a package manager that isn't installed (e.g. a clean JAMF-provisioned laptop where PMG is set up before dev tooling), real-binary resolution failed with a generic "unknown" error and a bug-report link, making it look like PMG itself had crashed. Introduce a typed BinaryNotFoundError that exits with code 127 (standard "command not found") and maps to a new PackageManagerNotFound error code with an actionable message instead of the Unknown classification. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: improve missing package manager help message Use a concise, dynamic install hint instead of explaining PMG's PATH forwarding internals. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: mention PATH in missing package manager help text Covers the common case where a package manager is installed but its bin directory is not on PATH yet (e.g. after curl | bash install). Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
111 lines
3.0 KiB
Go
111 lines
3.0 KiB
Go
package shim
|
|
|
|
import (
|
|
"errors"
|
|
"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 {
|
|
if errors.Is(err, exec.ErrNotFound) {
|
|
return "", &BinaryNotFoundError{Name: name}
|
|
}
|
|
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
|
|
}
|