xplshn
·
2025-08-13
remove.go
Go
1package main
2
3import (
4 "context"
5 "fmt"
6 "os"
7 "path/filepath"
8 "strings"
9 "sync"
10
11 "github.com/urfave/cli/v3"
12 "github.com/zeebo/errs"
13)
14
15var (
16 errRemoveFailed = errs.Class("removal failed")
17)
18
19func removeCommand() *cli.Command {
20 return &cli.Command{
21 Name: "remove",
22 Aliases: []string{"del"},
23 Usage: "Remove binaries",
24 Action: func(_ context.Context, c *cli.Command) error {
25 config, err := loadConfig()
26 if err != nil {
27 return errRemoveFailed.Wrap(err)
28 }
29 return removeBinaries(config, arrStringToArrBinaryEntry(c.Args().Slice()))
30 },
31 }
32}
33
34func removeBinaries(config *config, bEntries []binaryEntry) error {
35 var wg sync.WaitGroup
36 var removeErrors []string
37 var mutex sync.Mutex
38
39 installDir := config.InstallDir
40
41 for _, bEntry := range bEntries {
42 wg.Add(1)
43 go func(bEntry binaryEntry) {
44 defer wg.Done()
45
46 // Try to find the binary by name or by matching user.FullName
47 binaryPath, trackedBEntry, err := findBinaryByNameOrFullName(installDir, bEntry.Name)
48 if err != nil {
49 if verbosityLevel >= normalVerbosity {
50 fmt.Fprintf(os.Stderr, "Warning: '%s' does not exist or was not installed by dbin: %v\n", bEntry.Name, err)
51 }
52 return
53 }
54
55 licensePath := filepath.Join(config.LicenseDir, fmt.Sprintf("%s_LICENSE", filepath.Base(parseBinaryEntry(trackedBEntry, false))))
56
57 if !fileExists(binaryPath) {
58 if verbosityLevel >= normalVerbosity {
59 fmt.Fprintf(os.Stderr, "Warning: '%s' does not exist in %s\n", bEntry.Name, installDir)
60 }
61 return
62 }
63
64 if trackedBEntry.PkgID == "" {
65 if verbosityLevel >= normalVerbosity {
66 fmt.Fprintf(os.Stderr, "Warning: '%s' was not installed by dbin\n", bEntry.Name)
67 }
68 return
69 }
70
71 if err := runDeintegrationHooks(config, binaryPath); err != nil {
72 if verbosityLevel >= silentVerbosityWithErrors {
73 fmt.Fprintf(os.Stderr, "Error running deintegration hooks for '%s': %v\n", bEntry.Name, err)
74 }
75 mutex.Lock()
76 removeErrors = append(removeErrors, err.Error())
77 mutex.Unlock()
78 return
79 }
80
81 err = os.Remove(binaryPath)
82 if err != nil {
83 if verbosityLevel >= silentVerbosityWithErrors {
84 fmt.Fprintf(os.Stderr, "Failed to remove '%s' from %s: %v\n", bEntry.Name, installDir, err)
85 }
86 mutex.Lock()
87 removeErrors = append(removeErrors, fmt.Sprintf("failed to remove '%s' from %s: %v", bEntry.Name, installDir, err))
88 mutex.Unlock()
89 } else {
90 if verbosityLevel >= silentVerbosityWithErrors {
91 fmt.Printf("'%s' removed from %s\n", bEntry.Name, installDir)
92 }
93 // Remove corresponding license file if it exists
94 if config.CreateLicenses && fileExists(licensePath) {
95 if err := os.Remove(licensePath); err != nil {
96 if verbosityLevel >= silentVerbosityWithErrors {
97 fmt.Fprintf(os.Stderr, "Warning: Failed to remove license file %s: %v\n", licensePath, err)
98 }
99 // Non-fatal error
100 } else if verbosityLevel >= normalVerbosity {
101 fmt.Printf("Removed license file %s\n", licensePath)
102 }
103 }
104 }
105 }(bEntry)
106 }
107
108 wg.Wait()
109
110 if len(removeErrors) > 0 {
111 return errRemoveFailed.New(strings.Join(removeErrors, "\n"))
112 }
113
114 return nil
115}
116
117// findBinaryByNameOrFullName searches for a binary in installDir by its name or by matching the user.FullName xattr.
118func findBinaryByNameOrFullName(installDir, name string) (string, binaryEntry, error) {
119 // First, try direct path
120 binaryPath := filepath.Join(installDir, filepath.Base(name))
121 if fileExists(binaryPath) {
122 trackedBEntry, err := readEmbeddedBEntry(binaryPath)
123 if err == nil && trackedBEntry.Name != "" {
124 return binaryPath, trackedBEntry, nil
125 }
126 }
127
128 // If direct path fails, scan directory for matching user.FullName
129 entries, err := os.ReadDir(installDir)
130 if err != nil {
131 return "", binaryEntry{}, errFileAccess.Wrap(err)
132 }
133
134 // Normalize the input name for comparison
135 inputBEntry := stringToBinaryEntry(name)
136 inputFullName := parseBinaryEntry(inputBEntry, false)
137
138 for _, entry := range entries {
139 if entry.IsDir() {
140 continue
141 }
142 binaryPath = filepath.Join(installDir, entry.Name())
143 if !isExecutable(binaryPath) || isSymlink(binaryPath) {
144 continue
145 }
146
147 trackedBEntry, err := readEmbeddedBEntry(binaryPath)
148 if err != nil || trackedBEntry.Name == "" {
149 continue
150 }
151
152 trackedFullName := parseBinaryEntry(trackedBEntry, false)
153 if trackedFullName == inputFullName || trackedBEntry.Name == filepath.Base(name) {
154 return binaryPath, trackedBEntry, nil
155 }
156 }
157
158 return "", binaryEntry{}, errFileNotFound.New("binary '%s' not found in %s", name, installDir)
159}
160
161func runDeintegrationHooks(config *config, binaryPath string) error {
162 if config.UseIntegrationHooks {
163 ext := filepath.Ext(binaryPath)
164 if hookCommands, exists := config.Hooks.Commands[ext]; exists {
165 if err := executeHookCommand(config, &hookCommands, ext, binaryPath, false); err != nil {
166 return errRemoveFailed.Wrap(err)
167 }
168 } else if hookCommands, exists := config.Hooks.Commands["*"]; exists {
169 if err := executeHookCommand(config, &hookCommands, ext, binaryPath, false); err != nil {
170 return errRemoveFailed.Wrap(err)
171 }
172 }
173 }
174 return nil
175}