@@ -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 }
137141func (b * BrewInstaller ) Source () string { return b .Formula }
138142
139143func (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
165281func 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
173289func 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+
194316func 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.
203333func getBrewInstalledVersion (formula string ) (string , error ) {
204334 out , err := osexec .Command ("brew" , "list" , "--versions" , formula ).Output ()
0 commit comments