Skip to content

Commit 1a44a55

Browse files
committed
Fix homebrew install failure issue
1 parent 5c89378 commit 1a44a55

2 files changed

Lines changed: 312 additions & 21 deletions

File tree

internal/installer/brew.go

Lines changed: 150 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ type BrewInstaller struct {
1717
runBrewUpdate func(formula string) error
1818
runBrewUninstall func(formula string) error
1919
brewBinDir func() (string, error)
20+
formulaBinDir func(formula string) (string, error)
2021
pluginBinDir func() (string, error)
2122
getVersion func(formula string) (string, error)
2223
lookPath func(name string) (string, error)
@@ -30,6 +31,7 @@ func NewBrewInstaller(formula string) *BrewInstaller {
3031
runBrewUpdate: runBrewUpdate,
3132
runBrewUninstall: runBrewUninstall,
3233
brewBinDir: brewBinDir,
34+
formulaBinDir: brewFormulaBinDir,
3335
pluginBinDir: plugin.PluginBinDir,
3436
getVersion: getBrewInstalledVersion,
3537
lookPath: osexec.LookPath,
@@ -41,13 +43,15 @@ func (b *BrewInstaller) Install(name string) (string, error) {
4143
return "", fmt.Errorf("homebrew is not installed or not on PATH: %w", err)
4244
}
4345

44-
if err := b.runBrewInstall(b.Formula); err != nil {
45-
return "", fmt.Errorf("installing formula %q: %w", b.Formula, err)
46-
}
47-
48-
binaryPath, err := b.resolveInstalledBinary(name)
49-
if err != nil {
50-
return "", err
46+
installErr := b.runBrewInstall(b.Formula)
47+
binaryPath, resolveErr := b.resolveInstalledBinary(name)
48+
if installErr != nil {
49+
// If brew install fails but an executable is already available, link it anyway.
50+
if resolveErr != nil {
51+
return "", fmt.Errorf("installing formula %q: %w", b.Formula, installErr)
52+
}
53+
} else if resolveErr != nil {
54+
return "", resolveErr
5155
}
5256

5357
installDir, err := b.pluginBinDir()
@@ -137,41 +141,153 @@ func (b *BrewInstaller) PluginType() string { return plugin.SourceTypeBrew }
137141
func (b *BrewInstaller) Source() string { return b.Formula }
138142

139143
func (b *BrewInstaller) resolveInstalledBinary(name string) (string, error) {
140-
binName := plugin.BinPrefix + name
144+
candidates := preferredBinaryNames(name, b.Formula)
145+
binName := candidates[0]
146+
147+
if b.brewBinDir != nil {
148+
if binDir, err := b.brewBinDir(); err == nil {
149+
if path, ok := findFirstExistingBinary(binDir, candidates); ok {
150+
return path, nil
151+
}
152+
}
153+
}
154+
155+
lookPath := b.lookPath
156+
if lookPath == nil {
157+
lookPath = osexec.LookPath
158+
}
141159

142-
if binDir, err := b.brewBinDir(); err == nil {
143-
path := filepath.Join(binDir, binName)
144-
if _, statErr := os.Stat(path); statErr == nil {
160+
for _, candidate := range candidates {
161+
if found, err := lookPath(candidate); err == nil {
162+
return found, nil
163+
}
164+
}
165+
166+
formulaBinDir := b.formulaBinDir
167+
if formulaBinDir == nil {
168+
formulaBinDir = brewFormulaBinDir
169+
}
170+
if binDir, err := formulaBinDir(b.Formula); err == nil {
171+
if path, ok := findFirstExistingBinary(binDir, candidates); ok {
145172
return path, nil
146173
}
147-
path = filepath.Join(binDir, name)
148-
if _, statErr := os.Stat(path); statErr == nil {
174+
if path, ok := findLikelyBinaryInDir(binDir, candidates); ok {
149175
return path, nil
150176
}
151177
}
152178

153-
if found, err := b.lookPath(binName); err == nil {
154-
return found, nil
179+
return "", fmt.Errorf("binary %q or %q not found after brew install %q", binName, name, b.Formula)
180+
}
181+
182+
func preferredBinaryNames(name, formula string) []string {
183+
var candidates []string
184+
addCandidateWithCLIVariants(&candidates, plugin.BinPrefix+name)
185+
addCandidateWithCLIVariants(&candidates, name)
186+
187+
formulaName := filepath.Base(strings.TrimSpace(formula))
188+
if formulaName != "" {
189+
addCandidateWithCLIVariants(&candidates, formulaName)
190+
if strings.HasPrefix(formulaName, plugin.BinPrefix) {
191+
addCandidateWithCLIVariants(&candidates, strings.TrimPrefix(formulaName, plugin.BinPrefix))
192+
} else {
193+
addCandidateWithCLIVariants(&candidates, plugin.BinPrefix+formulaName)
194+
}
155195
}
156-
if found, err := b.lookPath(name); err == nil {
157-
return found, nil
196+
197+
deduped := make([]string, 0, len(candidates))
198+
seen := make(map[string]struct{}, len(candidates))
199+
for _, candidate := range candidates {
200+
if candidate == "" {
201+
continue
202+
}
203+
if _, exists := seen[candidate]; exists {
204+
continue
205+
}
206+
seen[candidate] = struct{}{}
207+
deduped = append(deduped, candidate)
158208
}
209+
return deduped
210+
}
159211

160-
return "", fmt.Errorf("binary %q or %q not found after brew install %q", binName, name, b.Formula)
212+
func addCandidateWithCLIVariants(candidates *[]string, name string) {
213+
name = strings.TrimSpace(name)
214+
if name == "" {
215+
return
216+
}
217+
*candidates = append(*candidates, name)
218+
219+
if strings.HasSuffix(name, "-cli") {
220+
trimmed := strings.TrimSuffix(name, "-cli")
221+
trimmed = strings.TrimSuffix(trimmed, "-")
222+
if trimmed != "" {
223+
*candidates = append(*candidates, trimmed)
224+
}
225+
}
226+
}
227+
228+
func findFirstExistingBinary(dir string, names []string) (string, bool) {
229+
for _, name := range names {
230+
path := filepath.Join(dir, name)
231+
if _, err := os.Stat(path); err == nil {
232+
return path, true
233+
}
234+
}
235+
return "", false
236+
}
237+
238+
func findLikelyBinaryInDir(dir string, preferred []string) (string, bool) {
239+
entries, err := os.ReadDir(dir)
240+
if err != nil {
241+
return "", false
242+
}
243+
244+
executables := make([]string, 0, len(entries))
245+
for _, entry := range entries {
246+
if entry.IsDir() {
247+
continue
248+
}
249+
info, err := entry.Info()
250+
if err != nil {
251+
continue
252+
}
253+
if !info.Mode().IsRegular() || info.Mode()&0111 == 0 {
254+
continue
255+
}
256+
executables = append(executables, filepath.Join(dir, entry.Name()))
257+
}
258+
259+
if len(executables) == 1 {
260+
return executables[0], true
261+
}
262+
263+
matches := make([]string, 0, len(executables))
264+
for _, path := range executables {
265+
base := filepath.Base(path)
266+
for _, token := range preferred {
267+
if strings.Contains(base, token) {
268+
matches = append(matches, path)
269+
break
270+
}
271+
}
272+
}
273+
if len(matches) == 1 {
274+
return matches[0], true
275+
}
276+
return "", false
161277
}
162278

163279
// brew helper functions
164280

165281
func runBrewInstall(formula string) error {
166-
cmd := osexec.Command("brew", "install", formula)
282+
cmd := brewInstallOrUpgradeCmd("install", formula)
167283
if output, err := cmd.CombinedOutput(); err != nil {
168284
return fmt.Errorf("brew install failed: %w\n%s", err, string(output))
169285
}
170286
return nil
171287
}
172288

173289
func runBrewUpdate(formula string) error {
174-
cmd := osexec.Command("brew", "upgrade", formula)
290+
cmd := brewInstallOrUpgradeCmd("upgrade", formula)
175291
output, err := cmd.CombinedOutput()
176292
if err != nil {
177293
// brew upgrade exits non-zero when the formula is already at the latest version.
@@ -191,6 +307,12 @@ func runBrewUninstall(formula string) error {
191307
return nil
192308
}
193309

310+
func brewInstallOrUpgradeCmd(action, formula string) *osexec.Cmd {
311+
cmd := osexec.Command("brew", action, formula)
312+
cmd.Env = append(os.Environ(), "HOMEBREW_NO_INSTALL_CLEANUP=1")
313+
return cmd
314+
}
315+
194316
func brewBinDir() (string, error) {
195317
out, err := osexec.Command("brew", "--prefix").Output()
196318
if err != nil {
@@ -199,6 +321,14 @@ func brewBinDir() (string, error) {
199321
return filepath.Join(strings.TrimSpace(string(out)), "bin"), nil
200322
}
201323

324+
func brewFormulaBinDir(formula string) (string, error) {
325+
out, err := osexec.Command("brew", "--prefix", formula).Output()
326+
if err != nil {
327+
return "", fmt.Errorf("failed to get brew formula prefix: %w", err)
328+
}
329+
return filepath.Join(strings.TrimSpace(string(out)), "bin"), nil
330+
}
331+
202332
// getBrewInstalledVersion returns the latest listed installed version for a formula.
203333
func getBrewInstalledVersion(formula string) (string, error) {
204334
out, err := osexec.Command("brew", "list", "--versions", formula).Output()

0 commit comments

Comments
 (0)