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}