feat: add Linux system-wide setup

Install shared shims, managed configuration, and login-shell PATH integration so golden images and multi-user hosts can protect package installs for every user.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Sahilb315
2026-07-11 02:19:59 +05:30
co-authored by Cursor
parent d3e656edcd
commit 8f6d0fcce0
19 changed files with 915 additions and 66 deletions
+35
View File
@@ -18,6 +18,11 @@ type ShimConfig struct {
PMGBin string
PackageManagers []string
Shells []alias.Shell
// SkipShellRc skips per-user shell rc PATH edits. Used by system install,
// which relies on /etc/profile.d or ENV PATH instead.
SkipShellRc bool
// ManageProfile writes and removes /etc/profile.d/pmg.sh with Install/Remove.
ManageProfile bool
}
type ShimManager struct {
@@ -68,6 +73,16 @@ func (m *ShimManager) Install() error {
}
}
if m.config.ManageProfile {
if err := writeSystemProfile(); err != nil {
return fmt.Errorf("failed to write system profile: %w", err)
}
}
if m.config.SkipShellRc {
return nil
}
if err := m.addPathToShells(); err != nil {
return fmt.Errorf("failed to update shell configs: %w", err)
}
@@ -80,6 +95,16 @@ func (m *ShimManager) Remove() error {
log.Warnf("Warning: failed to remove shim directory: %v", err)
}
if m.config.ManageProfile {
if err := removeSystemProfile(); err != nil {
return fmt.Errorf("failed to remove system profile: %w", err)
}
}
if m.config.SkipShellRc {
return nil
}
if err := m.removePathFromShells(); err != nil {
return fmt.Errorf("failed to clean shell configs: %w", err)
}
@@ -207,3 +232,13 @@ func (m *ShimManager) removePathFromShells() error {
return nil
}
// UserShimsInstalled reports whether the per-user shim directory (~/.pmg/bin)
// contains at least one shim script.
func UserShimsInstalled() bool {
homeDir, err := os.UserHomeDir()
if err != nil {
return false
}
return shimsPresent(filepath.Join(homeDir, ".pmg", "bin"))
}
+119
View File
@@ -0,0 +1,119 @@
package shim
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/safedep/pmg/internal/alias"
)
const (
defaultSystemBinDir = "/usr/local/lib/pmg/bin"
defaultSystemProfilePath = "/etc/profile.d/pmg.sh"
systemProfileMarker = "PMG system shims"
)
// systemBinDirOverride and systemProfilePathOverride replace the OS-level
// system install paths. They exist only for tests within this package. There
// is intentionally no env var or flag for them.
var (
systemBinDirOverride string
systemProfilePathOverride string
)
// SystemBinDir returns the directory for system-wide PMG shims.
func SystemBinDir() string {
if systemBinDirOverride != "" {
return systemBinDirOverride
}
return defaultSystemBinDir
}
// SystemProfilePath returns the path of the system profile.d snippet.
func SystemProfilePath() string {
if systemProfilePathOverride != "" {
return systemProfilePathOverride
}
return defaultSystemProfilePath
}
// NewSystemShimManager creates a shim manager for system-wide install: shims
// under SystemBinDir, no per-user rc edits, and /etc/profile.d management.
func NewSystemShimManager() (*ShimManager, error) {
aliasCfg := alias.DefaultConfig()
pmgBin, err := currentExecutable()
if err != nil {
return nil, err
}
return &ShimManager{
config: ShimConfig{
BinDir: SystemBinDir(),
PMGBin: pmgBin,
PackageManagers: aliasCfg.PackageManagers,
SkipShellRc: true,
ManageProfile: true,
},
}, nil
}
// SystemShimsInstalled reports whether the system shim directory contains at
// least one shim script.
func SystemShimsInstalled() bool {
return shimsPresent(SystemBinDir())
}
func shimsPresent(dir string) bool {
entries, err := os.ReadDir(dir)
if err != nil {
return false
}
for _, e := range entries {
if !e.IsDir() {
return true
}
}
return false
}
// SystemProfileInstalled reports whether the system profile snippet exists and
// contains the PMG marker.
func SystemProfileInstalled() bool {
data, err := os.ReadFile(SystemProfilePath())
if err != nil {
return false
}
return strings.Contains(string(data), systemProfileMarker)
}
func writeSystemProfile() error {
binDir := SystemBinDir()
path := SystemProfilePath()
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
return fmt.Errorf("failed to create profile.d directory: %w", err)
}
if data, err := os.ReadFile(path); err == nil && strings.Contains(string(data), systemProfileMarker) {
return nil
}
content := fmt.Sprintf(`# %s - managed by pmg setup install --system
# remove by running: pmg setup remove --system
export PATH="%s:$PATH"
`, systemProfileMarker, binDir)
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
return fmt.Errorf("failed to write system profile %s: %w", path, err)
}
return nil
}
func removeSystemProfile() error {
path := SystemProfilePath()
if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("failed to remove system profile %s: %w", path, err)
}
return nil
}
+73
View File
@@ -0,0 +1,73 @@
package shim
import (
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func useSystemPaths(t *testing.T, dir string) {
t.Helper()
systemBinDirOverride = filepath.Join(dir, "bin")
systemProfilePathOverride = filepath.Join(dir, "profile.d", "pmg.sh")
t.Cleanup(func() {
systemBinDirOverride = ""
systemProfilePathOverride = ""
})
}
func TestSystemShimManagerInstallAndRemove(t *testing.T) {
root := t.TempDir()
useSystemPaths(t, root)
mgr, err := NewSystemShimManager()
require.NoError(t, err)
assert.True(t, mgr.config.SkipShellRc)
assert.True(t, mgr.config.ManageProfile)
assert.Equal(t, SystemBinDir(), mgr.GetBinDir())
require.NoError(t, mgr.Install())
assert.True(t, SystemShimsInstalled())
assert.True(t, SystemProfileInstalled())
npmShim := filepath.Join(SystemBinDir(), "npm")
content, err := os.ReadFile(npmShim)
require.NoError(t, err)
assert.Contains(t, string(content), "export PMG_SHIM_PATH")
profile, err := os.ReadFile(SystemProfilePath())
require.NoError(t, err)
assert.Contains(t, string(profile), mgr.GetBinDir())
assert.Contains(t, string(profile), systemProfileMarker)
require.NoError(t, mgr.Install())
profile2, err := os.ReadFile(SystemProfilePath())
require.NoError(t, err)
assert.Equal(t, string(profile), string(profile2))
require.NoError(t, mgr.Remove())
assert.False(t, SystemShimsInstalled())
assert.False(t, SystemProfileInstalled())
require.NoError(t, mgr.Remove())
}
func TestSystemShimManagerDoesNotTouchUserRc(t *testing.T) {
root := t.TempDir()
useSystemPaths(t, root)
home := t.TempDir()
bashrc := filepath.Join(home, ".bashrc")
require.NoError(t, os.WriteFile(bashrc, []byte("# user bashrc\n"), 0o644))
mgr, err := NewSystemShimManager()
require.NoError(t, err)
mgr.config.HomeDir = home
require.NoError(t, mgr.Install())
content, err := os.ReadFile(bashrc)
require.NoError(t, err)
assert.Equal(t, "# user bashrc\n", string(content))
}