repos / dbin

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

xplshn  ·  2025-08-13

install.go

Go
  1package main
  2
  3import (
  4	"context"
  5	"fmt"
  6	"os"
  7	"path/filepath"
  8	"sync"
  9
 10	"github.com/hedzr/progressbar"
 11	"github.com/hedzr/progressbar/cursor"
 12	"github.com/urfave/cli/v3"
 13	"github.com/zeebo/errs"
 14)
 15
 16var (
 17	errInstallFailed = errs.Class("installation failed")
 18)
 19
 20func installCommand() *cli.Command {
 21	return &cli.Command{
 22		Name:    "install",
 23		Aliases: []string{"add"},
 24		Usage:   "Install binaries",
 25		Action: func(_ context.Context, c *cli.Command) error {
 26			config, err := loadConfig()
 27			if err != nil {
 28				return err
 29			}
 30			uRepoIndex, err := fetchRepoIndex(config)
 31			if err != nil {
 32				return err
 33			}
 34			return installBinaries(context.Background(), config, arrStringToArrBinaryEntry(c.Args().Slice()), uRepoIndex)
 35		},
 36	}
 37}
 38
 39func installBinaries(ctx context.Context, config *config, bEntries []binaryEntry, uRepoIndex []binaryEntry) error {
 40	cursor.Hide()
 41	defer cursor.Show()
 42
 43	// Clean up old .tmp files before installation
 44	if err := cleanInstallCache(config); err != nil {
 45		if verbosityLevel >= silentVerbosityWithErrors {
 46			fmt.Fprintf(os.Stderr, "Warning: Failed to clean up .tmp files in %s: %v\n", config.InstallDir, err)
 47		}
 48	}
 49
 50	var wg sync.WaitGroup
 51	var errors []string
 52	var errorsMu sync.Mutex
 53
 54	// Find URLs for binaries
 55	binResults, err := findURL(bEntries, uRepoIndex, config)
 56	if err != nil {
 57		return errInstallFailed.Wrap(err)
 58	}
 59
 60	filteredResults := make([]binaryEntry, 0, len(binResults))
 61	for _, result := range binResults {
 62		if result.DownloadURL != "!not_found" {
 63			filteredResults = append(filteredResults, result)
 64		}
 65	}
 66
 67	if len(filteredResults) == 0 {
 68		return errInstallFailed.New("no valid binaries found to install")
 69	}
 70
 71	var bar progressbar.MultiPB
 72	var tasks *progressbar.Tasks
 73	if verbosityLevel >= normalVerbosity {
 74		bar = progressbar.New()
 75		tasks = progressbar.NewTasks(bar)
 76		defer tasks.Close()
 77	}
 78
 79	binaryNameMaxlen := 0
 80	for _, result := range filteredResults {
 81		if binaryNameMaxlen < len(result.Name) {
 82			binaryNameMaxlen = len(result.Name)
 83		}
 84	}
 85
 86	termWidth := getTerminalWidth()
 87
 88	for _, result := range filteredResults {
 89		wg.Add(1)
 90		bEntry := result
 91		destination := filepath.Join(config.InstallDir, filepath.Base(bEntry.Name))
 92
 93		if verbosityLevel >= normalVerbosity {
 94			barTitle := fmt.Sprintf("Installing %s", bEntry.Name)
 95			pbarOpts := []progressbar.Opt{
 96				progressbar.WithBarStepper(config.ProgressbarStyle),
 97				progressbar.WithBarResumeable(true),
 98			}
 99
100			if termWidth < 120 {
101				barTitle = bEntry.Name
102				pbarOpts = append(
103					pbarOpts,
104					progressbar.WithBarTextSchema(`{{.Bar}} {{.Percent}} | <font color="green">{{.Title}}</font>`),
105					progressbar.WithBarWidth(termWidth-(binaryNameMaxlen+19)),
106				)
107			}
108
109			tasks.Add(
110				progressbar.WithTaskAddBarTitle(barTitle),
111				progressbar.WithTaskAddBarOptions(pbarOpts...),
112				progressbar.WithTaskAddOnTaskProgressing(func(bar progressbar.PB, _ <-chan struct{}) (stop bool) {
113					defer wg.Done()
114					err := fetchBinaryFromURLToDest(ctx, bar, &bEntry, destination, config)
115					if err != nil {
116						errorsMu.Lock()
117						errors = append(errors, fmt.Sprintf("error fetching binary %s: %v\n", bEntry.Name, err))
118						errorsMu.Unlock()
119						return
120					}
121
122					if err := os.Chmod(destination, 0755); err != nil {
123						errorsMu.Lock()
124						errors = append(errors, fmt.Sprintf("error making binary executable %s: %v\n", destination, err))
125						errorsMu.Unlock()
126						return
127					}
128
129					binInfo := &bEntry
130					if err := embedBEntry(destination, *binInfo); err != nil {
131						errorsMu.Lock()
132						errors = append(errors, fmt.Sprintf("failed to embed the binary's bEntry to its xattr attributes: %v\n", err))
133						errorsMu.Unlock()
134						return
135					}
136
137					if err := runIntegrationHooks(config, destination); err != nil {
138						errorsMu.Lock()
139						errors = append(errors, fmt.Sprintf("[%s] could not be handled by its default hooks: %v\n", bEntry.Name, err))
140						errorsMu.Unlock()
141						return
142					}
143
144					return
145				}),
146			)
147		} else {
148			go func(bEntry binaryEntry, destination string) {
149				defer wg.Done()
150				err := fetchBinaryFromURLToDest(ctx, nil, &bEntry, destination, config)
151				if err != nil {
152					errorsMu.Lock()
153					errors = append(errors, fmt.Sprintf("error fetching binary %s: %v", bEntry.Name, err))
154					errorsMu.Unlock()
155					return
156				}
157
158				if err := os.Chmod(destination, 0755); err != nil {
159					errorsMu.Lock()
160					errors = append(errors, fmt.Sprintf("error making binary executable %s: %v", destination, err))
161					errorsMu.Unlock()
162					return
163				}
164
165				binInfo := &bEntry
166				if err := embedBEntry(destination, *binInfo); err != nil {
167					errorsMu.Lock()
168					errors = append(errors, fmt.Sprintf("failed to embed the binary's bEntry to its xattr attributes: %v\n", err))
169					errorsMu.Unlock()
170					return
171				}
172
173				if err := runIntegrationHooks(config, destination); err != nil {
174					errorsMu.Lock()
175					errors = append(errors, fmt.Sprintf("[%s] could not be handled by its default hooks: %v", bEntry.Name, err))
176					errorsMu.Unlock()
177					return
178				}
179
180				if verbosityLevel >= normalVerbosity {
181					fmt.Printf("Successfully installed [%s]\n", binInfo.Name+"#"+binInfo.PkgID)
182				}
183			}(bEntry, destination)
184		}
185	}
186
187	wg.Wait()
188
189	if len(errors) > 0 {
190		var errN = uint8(0)
191		for _, errMsg := range errors {
192			errN++
193			fmt.Printf("%d. %v\n", errN, errMsg)
194		}
195		return errInstallFailed.New("installation completed with errors")
196	}
197
198	return nil
199}
200
201func runIntegrationHooks(config *config, binaryPath string) error {
202	if config.UseIntegrationHooks {
203		ext := filepath.Ext(binaryPath)
204		if hookCommands, exists := config.Hooks.Commands[ext]; exists {
205			if err := executeHookCommand(config, &hookCommands, ext, binaryPath, true); err != nil {
206				return errInstallFailed.Wrap(err)
207			}
208		} else if hookCommands, exists := config.Hooks.Commands["*"]; exists {
209			if err := executeHookCommand(config, &hookCommands, ext, binaryPath, true); err != nil {
210				return errInstallFailed.Wrap(err)
211			}
212		}
213	}
214	return nil
215}