mirror of
https://github.com/safedep/pmg.git
synced 2026-08-03 07:24:09 +02:00
* 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
230 lines
6.6 KiB
Go
230 lines
6.6 KiB
Go
package shim
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/safedep/pmg/internal/alias"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestShimManagerInstall(t *testing.T) {
|
|
homeDir := t.TempDir()
|
|
binDir := filepath.Join(homeDir, ".pmg", "bin")
|
|
|
|
bashrc := filepath.Join(homeDir, ".bashrc")
|
|
zshrc := filepath.Join(homeDir, ".zshrc")
|
|
fishConfig := filepath.Join(homeDir, ".config", "fish")
|
|
require.NoError(t, os.MkdirAll(fishConfig, 0o755))
|
|
require.NoError(t, os.WriteFile(bashrc, []byte("# existing bashrc\n"), 0o644))
|
|
require.NoError(t, os.WriteFile(zshrc, []byte("# existing zshrc\n"), 0o644))
|
|
require.NoError(t, os.WriteFile(filepath.Join(fishConfig, "config.fish"), []byte("# existing fish config\n"), 0o644))
|
|
|
|
pms := []string{"npm", "pip"}
|
|
pmgBin := filepath.Join(homeDir, "bin", "pmg")
|
|
shells := []alias.Shell{
|
|
&stubShell{name: "bash", path: ".bashrc", useFish: false},
|
|
&stubShell{name: "fish", path: ".config/fish/config.fish", useFish: true},
|
|
}
|
|
|
|
mgr := NewShimManager(ShimConfig{
|
|
BinDir: binDir,
|
|
HomeDir: homeDir,
|
|
PMGBin: pmgBin,
|
|
PackageManagers: pms,
|
|
Shells: shells,
|
|
})
|
|
|
|
require.NoError(t, mgr.Install())
|
|
|
|
for _, pm := range pms {
|
|
shimPath := filepath.Join(binDir, pm)
|
|
info, err := os.Stat(shimPath)
|
|
require.NoError(t, err, "shim %s should exist", pm)
|
|
assert.NotZero(t, info.Mode()&0o111, "shim %s should be executable", pm)
|
|
|
|
content, err := os.ReadFile(shimPath)
|
|
require.NoError(t, err)
|
|
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")
|
|
}
|
|
|
|
bashContent, err := os.ReadFile(bashrc)
|
|
require.NoError(t, err)
|
|
assert.Contains(t, string(bashContent), ".pmg/bin")
|
|
|
|
fishContent, err := os.ReadFile(filepath.Join(fishConfig, "config.fish"))
|
|
require.NoError(t, err)
|
|
assert.Contains(t, string(fishContent), ".pmg/bin")
|
|
}
|
|
|
|
func TestShimManagerInstallIdempotent(t *testing.T) {
|
|
homeDir := t.TempDir()
|
|
binDir := filepath.Join(homeDir, ".pmg", "bin")
|
|
|
|
bashrc := filepath.Join(homeDir, ".bashrc")
|
|
require.NoError(t, os.WriteFile(bashrc, []byte("# existing bashrc\n"), 0o644))
|
|
|
|
mgr := NewShimManager(ShimConfig{
|
|
BinDir: binDir,
|
|
HomeDir: homeDir,
|
|
PackageManagers: []string{"npm"},
|
|
Shells: []alias.Shell{&stubShell{name: "bash", path: ".bashrc", useFish: false}},
|
|
})
|
|
|
|
require.NoError(t, mgr.Install())
|
|
require.NoError(t, mgr.Install())
|
|
|
|
content, err := os.ReadFile(bashrc)
|
|
require.NoError(t, err)
|
|
|
|
count := 0
|
|
for _, line := range strings.Split(string(content), "\n") {
|
|
if strings.Contains(line, ".pmg/bin") {
|
|
count++
|
|
}
|
|
}
|
|
assert.Equal(t, 1, count, "PATH export should appear exactly once")
|
|
}
|
|
|
|
func TestShimManagerRemove(t *testing.T) {
|
|
homeDir := t.TempDir()
|
|
binDir := filepath.Join(homeDir, ".pmg", "bin")
|
|
|
|
bashrc := filepath.Join(homeDir, ".bashrc")
|
|
require.NoError(t, os.WriteFile(bashrc, []byte("# existing bashrc\n"), 0o644))
|
|
|
|
mgr := NewShimManager(ShimConfig{
|
|
BinDir: binDir,
|
|
HomeDir: homeDir,
|
|
PackageManagers: []string{"npm"},
|
|
Shells: []alias.Shell{&stubShell{name: "bash", path: ".bashrc", useFish: false}},
|
|
})
|
|
|
|
require.NoError(t, mgr.Install())
|
|
require.NoError(t, mgr.Remove())
|
|
|
|
_, err := os.Stat(binDir)
|
|
assert.True(t, os.IsNotExist(err), "bin dir should be removed")
|
|
|
|
content, err := os.ReadFile(bashrc)
|
|
require.NoError(t, err)
|
|
assert.NotContains(t, string(content), ".pmg/bin")
|
|
}
|
|
|
|
func TestShimManagerIsInstalled(t *testing.T) {
|
|
homeDir := t.TempDir()
|
|
binDir := filepath.Join(homeDir, ".pmg", "bin")
|
|
|
|
bashrc := filepath.Join(homeDir, ".bashrc")
|
|
require.NoError(t, os.WriteFile(bashrc, []byte("# existing bashrc\n"), 0o644))
|
|
|
|
mgr := NewShimManager(ShimConfig{
|
|
BinDir: binDir,
|
|
HomeDir: homeDir,
|
|
PackageManagers: []string{"npm"},
|
|
Shells: []alias.Shell{&stubShell{name: "bash", path: ".bashrc", useFish: false}},
|
|
})
|
|
|
|
installed, err := mgr.IsInstalled()
|
|
require.NoError(t, err)
|
|
assert.False(t, installed)
|
|
|
|
require.NoError(t, mgr.Install())
|
|
|
|
installed, err = mgr.IsInstalled()
|
|
require.NoError(t, err)
|
|
assert.True(t, installed)
|
|
}
|
|
|
|
func TestNewDefaultShimManager(t *testing.T) {
|
|
mgr, err := NewDefaultShimManager()
|
|
require.NoError(t, err)
|
|
|
|
assert.NotEmpty(t, mgr.GetBinDir())
|
|
assert.Contains(t, mgr.GetBinDir(), ".pmg/bin")
|
|
assert.NotEmpty(t, mgr.config.PMGBin)
|
|
assert.True(t, filepath.IsAbs(mgr.config.PMGBin))
|
|
assert.NotEmpty(t, mgr.config.PackageManagers)
|
|
assert.Contains(t, mgr.config.PackageManagers, "npm")
|
|
assert.Contains(t, mgr.config.PackageManagers, "pip")
|
|
assert.NotEmpty(t, mgr.config.Shells)
|
|
}
|
|
|
|
func TestShimManagerInstallEscapesPMGBin(t *testing.T) {
|
|
homeDir := t.TempDir()
|
|
binDir := filepath.Join(homeDir, ".pmg", "bin")
|
|
pmgBin := filepath.Join(homeDir, "PMG's bin", "pmg")
|
|
|
|
mgr := NewShimManager(ShimConfig{
|
|
BinDir: binDir,
|
|
HomeDir: homeDir,
|
|
PMGBin: pmgBin,
|
|
PackageManagers: []string{"npm"},
|
|
})
|
|
|
|
require.NoError(t, mgr.Install())
|
|
|
|
content, err := os.ReadFile(filepath.Join(binDir, "npm"))
|
|
require.NoError(t, err)
|
|
assert.Contains(t, string(content), `PMG_BIN='`+homeDir+`/PMG'\''s bin/pmg'`)
|
|
assert.NotContains(t, string(content), "command -v pmg")
|
|
}
|
|
|
|
type stubShell struct {
|
|
name string
|
|
path string
|
|
useFish bool
|
|
}
|
|
|
|
func (s *stubShell) Source(rcPath string) string {
|
|
return ""
|
|
}
|
|
|
|
func (s *stubShell) PathExport(binDir string) string {
|
|
if s.useFish {
|
|
return fmt.Sprintf("fish_add_path --prepend \"%s\" # PMG shims\n", binDir)
|
|
}
|
|
return fmt.Sprintf("export PATH=\"%s:$PATH\" # PMG shims\n", binDir)
|
|
}
|
|
|
|
func (s *stubShell) Name() string { return s.name }
|
|
|
|
func (s *stubShell) CandidateRcFiles(homeDir string) []string {
|
|
return []string{filepath.Join(homeDir, s.path)}
|
|
}
|
|
|
|
func (s *stubShell) InstallRcFiles(homeDir string, create bool) ([]string, error) {
|
|
path := filepath.Join(homeDir, s.path)
|
|
if _, err := os.Stat(path); err == nil {
|
|
return []string{path}, nil
|
|
}
|
|
|
|
if !create {
|
|
return nil, nil
|
|
}
|
|
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
|
return nil, err
|
|
}
|
|
f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0o644)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := f.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return []string{path}, nil
|
|
}
|