fix(shim): recognize shims at arbitrary paths via PMG_SHIM_PATH (#323)

* fix(shim): recognize shims at arbitrary paths via PMG_SHIM_PATH

The recursion guard in FilterPMGFromPath hardcoded the `/.pmg/bin`
suffix, so shims placed anywhere else (e.g. `/usr/local/lib/pmg/bin`,
`/shims`, or any future system-wide location) would not be stripped
from PATH when PMG resolved the real package manager. The shim would
resolve back to itself and PMG would re-exec it in an infinite loop.

This blocks moving shims out of `~/.pmg/bin` — needed for a future
`pmg setup install --system` (#317) — and also any user attempt to
relocate shims manually.

Have the shim export its own path before exec'ing pmg, and let the
filter use that to strip the exact dir at runtime. Keep the legacy
suffix check as a fallback so already-installed shims keep working
until they are regenerated.

Also drop `PMG_SHIM_PATH` from the env passed to the real package
manager so child processes don't inherit a stale marker.

* docs(shim): clarify PMG_SHIM_PATH is internal and unsupported to set manually

* remove comment

* update comment
This commit is contained in:
Sahil Bansal
2026-06-09 20:33:14 +05:30
committed by GitHub
parent 872c5d663c
commit 141894ed8f
5 changed files with 89 additions and 6 deletions
+36 -5
View File
@@ -11,7 +11,24 @@ import (
"github.com/safedep/dry/log"
)
const pmgBinSuffix = "/.pmg/bin"
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
@@ -20,13 +37,22 @@ func FilterPMGFromPath(pathEnv string) string {
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) {
filtered = append(filtered, entry)
if strings.HasSuffix(entry, pmgBinSuffix) {
continue
}
if shimDir != "" && filepath.Clean(entry) == shimDir {
continue
}
filtered = append(filtered, entry)
}
return strings.Join(filtered, string(os.PathListSeparator))
@@ -66,9 +92,14 @@ func FilterPMGFromEnv(env []string) []string {
if pathValue, ok := strings.CutPrefix(entry, "PATH="); ok {
filtered := FilterPMGFromPath(pathValue)
result = append(result, "PATH="+filtered)
} else {
result = append(result, entry)
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
+48
View File
@@ -14,6 +14,7 @@ func TestFilterPMGFromPath(t *testing.T) {
tests := []struct {
name string
path string
shimEnv string
expected string
}{
{
@@ -51,10 +52,45 @@ func TestFilterPMGFromPath(t *testing.T) {
path: "/usr/local/bin:/home/user/.pmg/binaries:/usr/bin",
expected: "/usr/local/bin:/home/user/.pmg/binaries:/usr/bin",
},
{
name: "env var strips non-legacy shim dir",
path: "/usr/local/lib/pmg/bin:/usr/local/bin:/usr/bin",
shimEnv: "/usr/local/lib/pmg/bin/npm",
expected: "/usr/local/bin:/usr/bin",
},
{
name: "env var strips arbitrary shim dir",
path: "/shims:/usr/local/bin:/usr/bin",
shimEnv: "/shims/npm",
expected: "/usr/local/bin:/usr/bin",
},
{
name: "env var and legacy suffix both strip",
path: "/shims:/home/user/.pmg/bin:/usr/bin",
shimEnv: "/shims/npm",
expected: "/usr/bin",
},
{
name: "env var matches even with trailing slash in PATH entry",
path: "/shims/:/usr/bin",
shimEnv: "/shims/npm",
expected: "/usr/bin",
},
{
name: "env var unset falls back to legacy suffix only",
path: "/shims:/home/user/.pmg/bin:/usr/bin",
shimEnv: "",
expected: "/shims:/usr/bin",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
if tc.shimEnv != "" {
t.Setenv(pmgShimPathEnv, tc.shimEnv)
} else {
t.Setenv(pmgShimPathEnv, "")
}
result := FilterPMGFromPath(tc.path)
assert.Equal(t, tc.expected, result)
})
@@ -245,6 +281,18 @@ func TestFilterPMGFromEnv(t *testing.T) {
env: []string{},
expected: []string{},
},
{
name: "drops PMG_SHIM_PATH from child env",
env: []string{
"HOME=/home/user",
"PMG_SHIM_PATH=/home/user/.pmg/bin/npm",
"PATH=/home/user/.pmg/bin:/usr/bin",
},
expected: []string{
"HOME=/home/user",
"PATH=/usr/bin",
},
},
}
for _, tc := range tests {
+2
View File
@@ -124,6 +124,8 @@ if [ ! -x "$PMG_BIN" ]; then
echo "[pmg] error: run 'pmg setup install' again or remove shims with 'pmg setup remove'" >&2
exit 127
fi
PMG_SHIM_PATH=$(cd -- "$(dirname -- "$0")" && pwd)/$(basename -- "$0")
export PMG_SHIM_PATH
exec "$PMG_BIN" %s "$@"
`, pmgBin, pm)
+2
View File
@@ -52,6 +52,8 @@ func TestShimManagerInstall(t *testing.T) {
assert.Contains(t, string(content), "#!/bin/sh")
assert.Contains(t, string(content), "PMG_BIN='"+pmgBin+"'")
assert.Contains(t, string(content), `exec "$PMG_BIN" `+pm+` "$@"`)
assert.Contains(t, string(content), `PMG_SHIM_PATH=$(cd -- "$(dirname -- "$0")" && pwd)/$(basename -- "$0")`)
assert.Contains(t, string(content), "export PMG_SHIM_PATH")
assert.NotContains(t, string(content), "command -v pmg")
assert.NotContains(t, string(content), "exec pmg")
assert.NotContains(t, string(content), "falling back to native")
+1 -1
View File
@@ -90,7 +90,7 @@ func main() {
ui.ErrorExit(err)
}
// Initialize event logging (silently fail if it can't be initialized)
// Initialize event logging
var eventlogErr error
if logFile != "" {
// If a custom log file is specified, use it for event logging too