repos / dbin

📦 Poor man's package manager.
git clone https://github.com/xplshn/dbin.git

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}