mirror of
https://github.com/Valkyrie00/bold-brew.git
synced 2026-03-17 23:59:59 +01:00
Rename Load* methods to Get* with forceRefresh parameter for clarity: - LoadInstalledFormulae → GetInstalledFormulae - LoadRemoteFormulae → GetRemoteFormulae - LoadTapPackages → GetTapPackages (and similar) Rename Get* methods that execute commands to Fetch* for accuracy: - GetInstalledCaskNames → FetchInstalledCaskNames - GetInstalledFormulaNames → FetchInstalledFormulaNames Replace forceDownload parameter with forceRefresh throughout.
253 lines
7.9 KiB
Go
253 lines
7.9 KiB
Go
// Package services provides Brewfile support for Bold Brew.
|
|
//
|
|
// This file handles parsing Brewfile entries (taps, formulae, casks),
|
|
// loading packages from third-party taps, and installing missing taps
|
|
// at application startup.
|
|
//
|
|
// NOTE: These methods are only active in Brewfile mode (bbrew -f <file>).
|
|
// In normal mode, these functions are not called.
|
|
//
|
|
// Execution sequence (Brewfile mode only):
|
|
//
|
|
// 1. Boot() → loadBrewfilePackages()
|
|
// Initial load using cached tap data for fast startup.
|
|
//
|
|
// 2. BuildApp() → goroutine:
|
|
// a) installBrewfileTapsAtStartup()
|
|
// Installs any missing taps from the Brewfile.
|
|
// b) updateHomeBrew() → forceRefreshResults()
|
|
// Refreshes Homebrew data and reloads packages.
|
|
//
|
|
// 3. forceRefreshResults() → fetchTapPackages() + loadBrewfilePackages()
|
|
// Fetches fresh tap package info and rebuilds the package list.
|
|
package services
|
|
|
|
import (
|
|
"bbrew/internal/models"
|
|
"fmt"
|
|
"os"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
// parseBrewfileWithTaps parses a Brewfile and returns taps and packages separately.
|
|
func parseBrewfileWithTaps(filepath string) (*models.BrewfileResult, error) {
|
|
// #nosec G304 -- filepath is user-provided via CLI flag
|
|
data, err := os.ReadFile(filepath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read Brewfile: %w", err)
|
|
}
|
|
|
|
result := &models.BrewfileResult{
|
|
Taps: []string{},
|
|
Packages: []models.BrewfileEntry{},
|
|
}
|
|
lines := strings.Split(string(data), "\n")
|
|
|
|
for _, line := range lines {
|
|
line = strings.TrimSpace(line)
|
|
|
|
// Skip empty lines and comments
|
|
if line == "" || strings.HasPrefix(line, "#") {
|
|
continue
|
|
}
|
|
|
|
// Parse tap entries: tap "user/repo"
|
|
if strings.HasPrefix(line, "tap ") {
|
|
start := strings.Index(line, "\"")
|
|
end := strings.LastIndex(line, "\"")
|
|
if start != -1 && end != -1 && start < end {
|
|
tapName := line[start+1 : end]
|
|
result.Taps = append(result.Taps, tapName)
|
|
}
|
|
}
|
|
|
|
// Parse brew entries: brew "package-name"
|
|
if strings.HasPrefix(line, "brew ") {
|
|
start := strings.Index(line, "\"")
|
|
end := strings.LastIndex(line, "\"")
|
|
if start != -1 && end != -1 && start < end {
|
|
packageName := line[start+1 : end]
|
|
result.Packages = append(result.Packages, models.BrewfileEntry{
|
|
Name: packageName,
|
|
IsCask: false,
|
|
})
|
|
}
|
|
}
|
|
|
|
// Parse cask entries: cask "package-name"
|
|
if strings.HasPrefix(line, "cask ") {
|
|
start := strings.Index(line, "\"")
|
|
end := strings.LastIndex(line, "\"")
|
|
if start != -1 && end != -1 && start < end {
|
|
packageName := line[start+1 : end]
|
|
result.Packages = append(result.Packages, models.BrewfileEntry{
|
|
Name: packageName,
|
|
IsCask: true,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// loadBrewfilePackages parses the Brewfile and creates a filtered package list.
|
|
// Uses the DataProvider to load tap packages from cache or fetch via brew info.
|
|
func (s *AppService) loadBrewfilePackages() error {
|
|
result, err := parseBrewfileWithTaps(s.brewfilePath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Store taps for later installation
|
|
s.brewfileTaps = result.Taps
|
|
|
|
// Create a map for quick lookup of Brewfile entries
|
|
packageMap := make(map[string]models.PackageType)
|
|
for _, entry := range result.Packages {
|
|
if entry.IsCask {
|
|
packageMap[entry.Name] = models.PackageTypeCask
|
|
} else {
|
|
packageMap[entry.Name] = models.PackageTypeFormula
|
|
}
|
|
}
|
|
|
|
// Track which packages were found (to avoid duplicates)
|
|
foundPackages := make(map[string]bool)
|
|
|
|
// Get actual installed packages (2 calls total, much faster than per-package checks)
|
|
installedCasks := s.dataProvider.FetchInstalledCaskNames()
|
|
installedFormulae := s.dataProvider.FetchInstalledFormulaNames()
|
|
|
|
// Filter packages to only include those in the Brewfile
|
|
*s.brewfilePackages = []models.Package{}
|
|
for _, pkg := range *s.packages {
|
|
if pkgType, exists := packageMap[pkg.Name]; exists && pkgType == pkg.Type {
|
|
// Skip if already added (prevent duplicates)
|
|
if foundPackages[pkg.Name] {
|
|
continue
|
|
}
|
|
// Verify installation status against actual installed lists
|
|
if pkgType == models.PackageTypeCask {
|
|
pkg.LocallyInstalled = installedCasks[pkg.Name]
|
|
} else {
|
|
pkg.LocallyInstalled = installedFormulae[pkg.Name]
|
|
}
|
|
*s.brewfilePackages = append(*s.brewfilePackages, pkg)
|
|
foundPackages[pkg.Name] = true
|
|
}
|
|
}
|
|
|
|
// Collect entries not found in main list (tap packages)
|
|
var tapEntries []models.BrewfileEntry
|
|
for _, entry := range result.Packages {
|
|
if !foundPackages[entry.Name] {
|
|
tapEntries = append(tapEntries, entry)
|
|
}
|
|
}
|
|
|
|
// Load tap packages from cache (fast startup)
|
|
if len(tapEntries) > 0 {
|
|
// Build existing packages map
|
|
existingPackages := make(map[string]models.Package)
|
|
for _, pkg := range *s.packages {
|
|
existingPackages[pkg.Name] = pkg
|
|
}
|
|
|
|
// Use DataProvider to load tap packages (from cache only at startup, no fetch)
|
|
tapPackages, _ := s.dataProvider.GetTapPackages(tapEntries, existingPackages, false)
|
|
|
|
// Add tap packages to brewfilePackages, updating installed status (avoid duplicates)
|
|
for _, pkg := range tapPackages {
|
|
if foundPackages[pkg.Name] {
|
|
continue // Already added
|
|
}
|
|
if pkg.Type == models.PackageTypeCask {
|
|
pkg.LocallyInstalled = installedCasks[pkg.Name]
|
|
} else {
|
|
pkg.LocallyInstalled = installedFormulae[pkg.Name]
|
|
}
|
|
*s.brewfilePackages = append(*s.brewfilePackages, pkg)
|
|
foundPackages[pkg.Name] = true
|
|
}
|
|
}
|
|
|
|
// Sort by name for consistent display
|
|
sort.Slice(*s.brewfilePackages, func(i, j int) bool {
|
|
return (*s.brewfilePackages)[i].Name < (*s.brewfilePackages)[j].Name
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
// fetchTapPackages fetches info for packages from third-party taps and adds them to s.packages.
|
|
// This is called after taps are installed so that loadBrewfilePackages can find them.
|
|
// Uses the DataProvider to fetch and cache tap package data.
|
|
func (s *AppService) fetchTapPackages() {
|
|
if !s.IsBrewfileMode() || len(s.brewfileTaps) == 0 {
|
|
return
|
|
}
|
|
|
|
result, err := parseBrewfileWithTaps(s.brewfilePath)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// Build a map of existing packages for quick lookup
|
|
existingPackages := make(map[string]models.Package)
|
|
for _, pkg := range *s.packages {
|
|
existingPackages[pkg.Name] = pkg
|
|
}
|
|
|
|
// Use DataProvider to fetch all tap packages (force download to get fresh data)
|
|
tapPackages, _ := s.dataProvider.GetTapPackages(result.Packages, existingPackages, true)
|
|
|
|
// Add tap packages to s.packages (avoiding duplicates)
|
|
for _, pkg := range tapPackages {
|
|
if _, exists := existingPackages[pkg.Name]; !exists {
|
|
*s.packages = append(*s.packages, pkg)
|
|
}
|
|
}
|
|
}
|
|
|
|
// installBrewfileTapsAtStartup installs any missing taps from the Brewfile at app startup.
|
|
// This runs before updateHomeBrew, which will then reload all data including the new taps.
|
|
func (s *AppService) installBrewfileTapsAtStartup() {
|
|
// Check which taps need to be installed
|
|
var tapsToInstall []string
|
|
for _, tap := range s.brewfileTaps {
|
|
if !s.brewService.IsTapInstalled(tap) {
|
|
tapsToInstall = append(tapsToInstall, tap)
|
|
}
|
|
}
|
|
|
|
if len(tapsToInstall) == 0 {
|
|
return // All taps already installed
|
|
}
|
|
|
|
// Install missing taps
|
|
for _, tap := range tapsToInstall {
|
|
tap := tap // Create local copy for closures
|
|
s.app.QueueUpdateDraw(func() {
|
|
s.layout.GetNotifier().ShowWarning(fmt.Sprintf("Installing tap %s...", tap))
|
|
fmt.Fprintf(s.layout.GetOutput().View(), "[TAP] Installing %s...\n", tap)
|
|
})
|
|
|
|
if err := s.brewService.InstallTap(tap, s.app, s.layout.GetOutput().View()); err != nil {
|
|
s.app.QueueUpdateDraw(func() {
|
|
s.layout.GetNotifier().ShowError(fmt.Sprintf("Failed to install tap %s", tap))
|
|
fmt.Fprintf(s.layout.GetOutput().View(), "[ERROR] Failed to install tap %s\n", tap)
|
|
})
|
|
} else {
|
|
s.app.QueueUpdateDraw(func() {
|
|
s.layout.GetNotifier().ShowSuccess(fmt.Sprintf("Tap %s installed", tap))
|
|
fmt.Fprintf(s.layout.GetOutput().View(), "[SUCCESS] tap %s installed\n", tap)
|
|
})
|
|
}
|
|
}
|
|
|
|
s.app.QueueUpdateDraw(func() {
|
|
s.layout.GetNotifier().ShowSuccess("All taps installed")
|
|
})
|
|
}
|