update_unix.go

  1//go:build !windows && !darwin
  2
  3package update
  4
  5import (
  6	"bytes"
  7	"context"
  8	"os"
  9	"os/exec"
 10	"path/filepath"
 11	"strings"
 12	"time"
 13)
 14
 15// detectInstallMethod determines how Crush was installed on Linux/BSD.
 16func detectInstallMethod(exePath string) InstallMethod {
 17	// Check for Nix installation first (works on any platform with Nix).
 18	if strings.HasPrefix(exePath, "/nix/store/") {
 19		return InstallMethodNix
 20	}
 21
 22	// Check for Homebrew on Linux.
 23	if strings.Contains(exePath, "/Cellar/") || strings.Contains(exePath, "linuxbrew") {
 24		return InstallMethodHomebrew
 25	}
 26
 27	// Check for npm global installation.
 28	if strings.Contains(exePath, "node_modules") {
 29		return InstallMethodNPM
 30	}
 31
 32	// Check for go install.
 33	gopath := os.Getenv("GOPATH")
 34	if gopath == "" {
 35		home, _ := os.UserHomeDir()
 36		gopath = filepath.Join(home, "go")
 37	}
 38	if strings.HasPrefix(exePath, filepath.Join(gopath, "bin")) {
 39		return InstallMethodGoInstall
 40	}
 41
 42	// Use package manager queries for accurate detection.
 43	// These are more reliable than path heuristics.
 44
 45	// Check dpkg (Debian/Ubuntu).
 46	if isInstalledViaDpkg(exePath) {
 47		return InstallMethodApt
 48	}
 49
 50	// Check rpm (Fedora/RHEL/CentOS).
 51	if isInstalledViaRpm(exePath) {
 52		return InstallMethodYum
 53	}
 54
 55	// Check pacman (Arch Linux / AUR).
 56	if isInstalledViaPacman(exePath) {
 57		return InstallMethodAUR
 58	}
 59
 60	return InstallMethodUnknown
 61}
 62
 63// isInstalledViaDpkg checks if the executable was installed via dpkg/apt.
 64func isInstalledViaDpkg(exePath string) bool {
 65	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
 66	defer cancel()
 67
 68	cmd := exec.CommandContext(ctx, "dpkg", "-S", exePath)
 69	var stdout bytes.Buffer
 70	cmd.Stdout = &stdout
 71	if err := cmd.Run(); err != nil {
 72		return false
 73	}
 74	// dpkg -S outputs "package: /path/to/file" if the file belongs to a package.
 75	return strings.Contains(stdout.String(), ":")
 76}
 77
 78// isInstalledViaRpm checks if the executable was installed via rpm/yum/dnf.
 79func isInstalledViaRpm(exePath string) bool {
 80	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
 81	defer cancel()
 82
 83	cmd := exec.CommandContext(ctx, "rpm", "-qf", exePath)
 84	var stdout bytes.Buffer
 85	cmd.Stdout = &stdout
 86	if err := cmd.Run(); err != nil {
 87		return false
 88	}
 89	// rpm -qf outputs the package name if the file belongs to a package.
 90	// It exits with error if file is not owned by any package.
 91	output := strings.TrimSpace(stdout.String())
 92	return output != "" && !strings.Contains(output, "not owned by any package")
 93}
 94
 95// isInstalledViaPacman checks if the executable was installed via pacman (Arch/AUR).
 96func isInstalledViaPacman(exePath string) bool {
 97	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
 98	defer cancel()
 99
100	cmd := exec.CommandContext(ctx, "pacman", "-Qo", exePath)
101	var stdout bytes.Buffer
102	cmd.Stdout = &stdout
103	if err := cmd.Run(); err != nil {
104		return false
105	}
106	// pacman -Qo outputs "file is owned by package" if the file belongs to a package.
107	return strings.Contains(stdout.String(), "is owned by")
108}