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}