repo_description: Yet another Yogurt - An AUR Helper written in Go
repo_topics: archlinux aur aur-completions aur-helper aur-packages pacaur pacman pkgbuild yaourt yay
code: package main
  
  import (
  	"context"
  	"fmt"
  	"io/fs"
  	"path/filepath"
  
  	aur "github.com/Jguer/aur"
  	alpm "github.com/Jguer/dyalpm"
  	mapset "github.com/deckarep/golang-set/v2"
  	"github.com/leonelquinteros/gotext"
  
  	"github.com/Jguer/yay/v12/pkg/db"
  	"github.com/Jguer/yay/v12/pkg/query"
  	"github.com/Jguer/yay/v12/pkg/runtime"
  	"github.com/Jguer/yay/v12/pkg/settings"
  	"github.com/Jguer/yay/v12/pkg/settings/parser"
  	"github.com/Jguer/yay/v12/pkg/text"
  )
  
  // SyncSearch presents a query to the local repos and to the AUR.
  func syncSearch(ctx context.Context, pkgS []string,
  	dbExecutor db.Executor, queryBuilder query.Builder, verbose bool,
  ) error {
  	queryBuilder.Execute(ctx, dbExecutor, pkgS)
  
  	searchMode := query.Minimal
  	if verbose {
  		searchMode = query.Detailed
  	}
  
  	return queryBuilder.Results(dbExecutor, searchMode)
  }
  
  // SyncInfo serves as a pacman -Si for repo packages and AUR packages.
  func syncInfo(ctx context.Context, run *runtime.Runtime,
  	cmdArgs *parser.Arguments, pkgS []string, dbExecutor db.Executor,
  ) error {
  	var (
  		remoteAurPkgs []aur.Pkg
  		err           error
  		missing       = false
  	)
  
  	pkgS = query.RemoveInvalidTargets(run.Logger, pkgS, run.Cfg.Mode)
  
  	expandedPackages := []string{}
  	for _, pkg := range pkgS {
  		groupPackages := dbExecutor.PackagesFromGroup(pkg)
  		if len(groupPackages) > 0 {
  			for _, p := range groupPackages {
  				expandedPackages = append(expandedPackages, p.Name())
  			}
  		} else {
  			expandedPackages = append(expandedPackages, pkg)
  		}
  	}
  	pkgS = expandedPackages
  
  	aurS, repoS := packageSlices(pkgS, run.Cfg, dbExecutor)
  
  	if len(repoS) == 0 && len(aurS) == 0 {
  		if run.Cfg.Mode != parser.ModeRepo {
  			aurS = dbExecutor.InstalledRemotePackageNames()
  		}
  
  		if run.Cfg.Mode != parser.ModeAUR {
  			repoS = dbExecutor.InstalledSyncPackageNames()
  		}
  	}
  
  	if len(aurS) != 0 {
  		noDB := make([]string, 0, len(aurS))
  
  		for _, pkg := range aurS {
  			_, name := text.SplitDBFromName(pkg)
  			noDB = append(noDB, name)
  		}
  
  		remoteAurPkgs, err = run.AURClient.Get(ctx, &aur.Query{
  			Needles: noDB,
  			By:      aur.Name,
  		})
  		if err != nil {
  			run.Logger.Errorln(err)
  		}
  
  		// Check for any missing packages and print errors for any not found
  		found := mapset.NewThreadUnsafeSet[string]()
  		for i := range remoteAurPkgs {
  			found.Add(remoteAurPkgs[i].Name)
  		}
  
  		for _, name := range noDB {
  			if !found.Contains(name) {
  				missing = true
  				run.Logger.Errorln(gotext.Get("No AUR package found for"), " ", name)
  			}
  		}
  	}
  
  	if len(repoS) != 0 || (len(aurS) == 0 && len(repoS) == 0) {
  		arguments := cmdArgs.Copy()
  		arguments.ClearTargets()
  		arguments.AddTarget(repoS...)
  
  		err = run.CmdBuilder.Show(run.CmdBuilder.BuildPacmanCmd(ctx,
  			arguments, run.Cfg.Mode, settings.NoConfirm))
  		if err != nil {
  			return err
  		}
  	}
  
  	for i := range remoteAurPkgs {
  		printInfo(run.Logger, run.Cfg, &remoteAurPkgs[i], cmdArgs.ExistsDouble("i"))
  	}
  
  	if missing {
  		err = fmt.Errorf("")
  	}
  
  	return err
  }
  
  // PackageSlices separates an input slice into aur and repo slices.
  func packageSlices(toCheck []string, config *settings.Configuration, dbExecutor db.Executor) (aurNames, repoNames []string) {
  	for _, _pkg := range toCheck {
  		dbName, name := text.SplitDBFromName(_pkg)
  
  		if dbName == "aur" || config.Mode == parser.ModeAUR {
  			aurNames = append(aurNames, _pkg)
  			continue
  		} else if dbName != "" || config.Mode == parser.ModeRepo {
  			repoNames = append(repoNames, _pkg)
  			continue
  		}
  
  		if dbExecutor.SyncSatisfierExists(name) ||
  			len(dbExecutor.PackagesFromGroup(name)) != 0 {
  			repoNames = append(repoNames, _pkg)
  		} else {
  			aurNames = append(aurNames, _pkg)
  		}
  	}
  
  	return aurNames, repoNames
  }
  
  // MapSetMap is a Map of Sets.
  type mapSetMap[T comparable] map[T]mapset.Set[T]
  
  // Add adds a new value to the Map.
  // If n is already in the map, then v is appended to the StringSet under that key.
  // Otherwise a new Set is created containing v.
  func (mss mapSetMap[T]) Add(n, v T) {
  	if _, ok := mss[n]; !ok {
  		mss[n] = mapset.NewSet[T]()
  	}
  
  	mss[n].Add(v)
  }
  
  // HangingPackages returns a list of packages installed as deps
  // and unneeded by the system
  // removeOptional decides whether optional dependencies are counted or not.
  func hangingPackages(removeOptional bool, dbExecutor db.Executor) (hanging []string) {
  	// safePackages represents every package in the system in one of 3 states
  	// State = 0 - Remove package from the system
  	// State = 1 - Keep package in the system; need to iterate over dependencies
  	// State = 2 - Keep package and have iterated over dependencies
  	safePackages := make(map[string]uint8)
  	// provides stores a mapping from the provides name back to the original package name
  	provides := make(mapSetMap[string])
  
  	packages := dbExecutor.LocalPackages()
  	// Mark explicit dependencies and enumerate the provides list
  	for _, pkg := range packages {
  		if pkg.Reason() == alpm.PkgReasonExplicit {
  			safePackages[pkg.Name()] = 1
  		} else {
  			safePackages[pkg.Name()] = 0
  		}
  
  		for _, dep := range dbExecutor.PackageProvides(pkg) {
  			provides.Add(dep.Name, pkg.Name())
  		}
  	}
  
  	iterateAgain := true
  
  	for iterateAgain {
  		iterateAgain = false
  
  		for _, pkg := range packages {
  			if state := safePackages[pkg.Name()]; state == 0 || state == 2 {
  				continue
  			}
  
  			safePackages[pkg.Name()] = 2
  			deps := dbExecutor.PackageDepends(pkg)
  
  			if !removeOptional {
  				deps = append(deps, dbExecutor.PackageOptionalDepends(pkg)...)
  			}
  
  			// Update state for dependencies
  			for _, dep := range deps {
  				// Don't assume a dependency is installed
  				state, ok := safePackages[dep.Name]
  				if !ok {
  					// Check if dep is a provides rather than actual package name
  					if pset, ok2 := provides[dep.Name]; ok2 {
  						for p := range pset.Iter() {
  							if safePackages[p] == 0 {
  								iterateAgain = true
  								safePackages[p] = 1
  							}
  						}
  					}
  
  					continue
  				}
  
  				if state == 0 {
  					iterateAgain = true
  					safePackages[dep.Name] = 1
  				}
  			}
  		}
  	}
  
  	// Build list of packages to be removed
  	for _, pkg := range packages {
  		if safePackages[pkg.Name()] == 0 {
  			hanging = append(hanging, pkg.Name())
  		}
  	}
  
  	return hanging
  }
  
  func getFolderSize(path string) (size int64) {
  	_ = filepath.WalkDir(path, func(p string, entry fs.DirEntry, err error) error {
  		if err != nil || entry == nil {
  			return nil
  		}
  		info, infoErr := entry.Info()
  		if infoErr != nil || info == nil {
  			return nil
  		}
  		size += info.Size()
  		return nil
  	})
  
  	return size
  }
  
  // Statistics returns statistics about packages installed in system.
  func statistics(run *runtime.Runtime, dbExecutor db.Executor) (res struct {
  	Totaln       int
  	Expln        int
  	TotalSize    int64
  	pacmanCaches map[string]int64
  	yayCache     int64
  },
  ) {
  	for _, pkg := range dbExecutor.LocalPackages() {
  		res.TotalSize += pkg.ISize()
  		res.Totaln++
  
  		if pkg.Reason() == alpm.PkgReasonExplicit {
  			res.Expln++
  		}
  	}
  
  	res.pacmanCaches = make(map[string]int64)
  	for _, path := range run.PacmanConf.CacheDir {
  		res.pacmanCaches[path] = getFolderSize(path)
  	}
  
  	res.yayCache = getFolderSize(run.Cfg.BuildDir)
  
  	return
  }
  