From 1ad305040909ad67f0dbb32eca34a525f5fe3055 Mon Sep 17 00:00:00 2001 From: jdholtz Date: Sat, 29 Nov 2025 00:31:00 -0600 Subject: [PATCH 1/6] Add print and print-format support for sync operations Currently, AUR upgrades during sync operations are not handled. Also, -U and -R operations are not handled as well. --- cmd.go | 3 +- pkg/settings/args.go | 3 + pkg/settings/config.go | 8 ++- print.go | 142 +++++++++++++++++++++++++++++++++++++++++ query.go | 36 ++++++----- sync.go | 117 +++++++++++++++++++++++++++++---- 6 files changed, 277 insertions(+), 32 deletions(-) diff --git a/cmd.go b/cmd.go index 3149e1e04..3de4d2001 100644 --- a/cmd.go +++ b/cmd.go @@ -342,8 +342,7 @@ func handleSync(ctx context.Context, run *runtime.Runtime, cmdArgs *parser.Argum case cmdArgs.ExistsArg("s", "search"): return syncSearch(ctx, targets, dbExecutor, run.QueryBuilder, !cmdArgs.ExistsArg("q", "quiet")) case cmdArgs.ExistsArg("p", "print", "print-format"): - return run.CmdBuilder.Show(run.CmdBuilder.BuildPacmanCmd(ctx, - cmdArgs, run.Cfg.Mode, settings.NoConfirm)) + return syncPrint(ctx, run, cmdArgs, dbExecutor) case cmdArgs.ExistsArg("c", "clean"): return syncClean(ctx, run, cmdArgs, dbExecutor) case cmdArgs.ExistsArg("l", "list"): diff --git a/pkg/settings/args.go b/pkg/settings/args.go index 974125941..ac96b8df1 100644 --- a/pkg/settings/args.go +++ b/pkg/settings/args.go @@ -82,6 +82,9 @@ func (c *Configuration) handleOption(option, value string) bool { c.SortBy = value case "searchby": c.SearchBy = value + case "print-format": + c.PrintFormat = value + return false case "noconfirm": NoConfirm = boolValue case "config": diff --git a/pkg/settings/config.go b/pkg/settings/config.go index 20b57ee95..1a562622a 100644 --- a/pkg/settings/config.go +++ b/pkg/settings/config.go @@ -74,9 +74,10 @@ type Configuration struct { CompletionPath string `json:"-"` VCSFilePath string `json:"-"` // ConfigPath string `json:"-"` - SaveConfig bool `json:"-"` - Mode parser.TargetMode `json:"-"` - ReBuild parser.RebuildMode `json:"rebuild"` + SaveConfig bool `json:"-"` + Mode parser.TargetMode `json:"-"` + ReBuild parser.RebuildMode `json:"rebuild"` + PrintFormat string `json:"-"` } // SaveConfig writes yay config to file. @@ -238,6 +239,7 @@ func DefaultConfig(version string) *Configuration { UseRPC: true, DoubleConfirm: true, Mode: parser.ModeAny, + PrintFormat: "%l", } } diff --git a/print.go b/print.go index 08b0ebefa..661d49e4a 100644 --- a/print.go +++ b/print.go @@ -5,12 +5,14 @@ import ( "fmt" "io" "os" + "path/filepath" "strconv" "strings" "syscall" "unicode" aur "github.com/Jguer/aur" + alpm "github.com/Jguer/go-alpm/v2" mapset "github.com/deckarep/golang-set/v2" "github.com/leonelquinteros/gotext" "golang.org/x/sys/unix" @@ -265,3 +267,143 @@ func getColumnCount() int { return 80 } + +// printLocalPackages prints installed AUR packages according to the user-defined format. Mimics pacman's +// print format. +func printLocalPackages(config *settings.Configuration, pkgs []alpm.IPackage) { + format := config.PrintFormat + + for _, pkg := range pkgs { + printString := format + + // %a : arch + printString = strings.ReplaceAll(printString, "%a", pkg.Architecture()) + // %b : build date + buildDate := pkg.BuildDate().Local().Unix() + printString = strings.ReplaceAll(printString, "%b", text.FormatTimeQuery(int(buildDate))) + // %d : description + printString = strings.ReplaceAll(printString, "%d", pkg.Description()) + // %e : pkgbase + printString = strings.ReplaceAll(printString, "%e", pkg.Base()) + // %f : filename + printString = strings.ReplaceAll(printString, "%f", pkg.FileName()) + // %g : base64 encoded PGP signature + printString = strings.ReplaceAll(printString, "%g", pkg.Base64Signature()) + // %h : sha256sum + printString = strings.ReplaceAll(printString, "%h", pkg.SHA256Sum()) + // %n : pkgname + printString = strings.ReplaceAll(printString, "%n", pkg.Name()) + // %p : packager + printString = strings.ReplaceAll(printString, "%p", pkg.Packager()) + // %v : pkgver + printString = strings.ReplaceAll(printString, "%v", pkg.Version()) + // %l : location + printString = strings.ReplaceAll(printString, "%l", getLocalPkgLocation(config, pkg)) + // %r : repo + printString = strings.ReplaceAll(printString, "%r", "aur") + // %s : size + // TODO: Different per op + sizeStr := fmt.Sprintf("%d", pkg.Size()) + printString = strings.ReplaceAll(printString, "%s", sizeStr) + // %u : URL + printString = strings.ReplaceAll(printString, "%u", pkg.URL()) + // %C : checkdepends + printString = strings.ReplaceAll(printString, "%C", dependsListToString(pkg.CheckDepends())) + // %D : depends + printString = strings.ReplaceAll(printString, "%D", dependsListToString(pkg.Depends())) + // %G : groups + printString = strings.ReplaceAll(printString, "%G", strings.Join(pkg.Groups().Slice(), " ")) + // %H : conflicts + printString = strings.ReplaceAll(printString, "%H", dependsListToString(pkg.Conflicts())) + // %M : makedepends + printString = strings.ReplaceAll(printString, "%M", dependsListToString(pkg.MakeDepends())) + // %O : optdepends + printString = strings.ReplaceAll(printString, "%O", dependsListToString(pkg.OptionalDepends())) + // %P : provides + printString = strings.ReplaceAll(printString, "%P", dependsListToString(pkg.Provides())) + // %R : replaces + printString = strings.ReplaceAll(printString, "%R", dependsListToString(pkg.Replaces())) + // %L : licenses + printString = strings.ReplaceAll(printString, "%L", strings.Join(pkg.Licenses().Slice(), " ")) + + fmt.Println(printString) + } +} + +// getLocalPkgLocation returns the local package file location if it exists in the build directory. +// Otherwise, it falls back to the AUR snapshot URL. +func getLocalPkgLocation(config *settings.Configuration, pkg alpm.IPackage) string { + pkgFileName := fmt.Sprintf("%s-%s-%s.pkg.tar.zst", pkg.Name(), pkg.Version(), pkg.Architecture()) + pkgLocation := filepath.Join(config.BuildDir, pkg.Name(), pkgFileName) + if _, err := os.Stat(pkgLocation); err == nil { + return fmt.Sprintf("file://%s", pkgLocation) + } + + // Fallback to AUR snapshot URL + return fmt.Sprintf("%s/cgit/aur.git/snapshot/%s.tar.gz", config.AURURL, pkg.Name()) +} + +func dependsListToString(depList alpm.IDependList) string { + strList := []string{} + _ = depList.ForEach(func(dep *alpm.Depend) error { + strList = append(strList, dep.Name) + return nil + }) + + return strings.Join(strList, " ") +} + +// printAurPackages prints remote AUR packages according to the user-defined format. Mimics pacman's +// print format. All format options with information not available from the AUR are removed. +func printAurPackages(config *settings.Configuration, pkgs []aur.Pkg) { + // Remove all unhandled format options so they don't appear in the output. + unusedFormats := strings.NewReplacer( + "%a", "", // arch + "%b", "", // build date + "%f", "", // filename + "%g", "", // base64 encoded PGP signature + "%h", "", // sha256sum + "%p", "", // packager + "%s", "", // size + ) + format := unusedFormats.Replace(config.PrintFormat) + + for _, pkg := range pkgs { + printString := format + + // %d : description + printString = strings.ReplaceAll(printString, "%d", pkg.Description) + // %e : pkgbase + printString = strings.ReplaceAll(printString, "%e", pkg.PackageBase) + // %n : pkgname + printString = strings.ReplaceAll(printString, "%n", pkg.Name) + // %v : pkgver + printString = strings.ReplaceAll(printString, "%v", pkg.Version) + // %l : location + printString = strings.ReplaceAll(printString, "%l", config.AURURL+pkg.URLPath) + // %r : repo + printString = strings.ReplaceAll(printString, "%r", "aur") + // %u : URL + printString = strings.ReplaceAll(printString, "%u", pkg.URL) + // %C : checkdepends + printString = strings.ReplaceAll(printString, "%C", strings.Join(pkg.CheckDepends, " ")) + // %D : depends + printString = strings.ReplaceAll(printString, "%D", strings.Join(pkg.Depends, " ")) + // %G : groups + printString = strings.ReplaceAll(printString, "%G", strings.Join(pkg.Groups, " ")) + // %H : conflicts + printString = strings.ReplaceAll(printString, "%H", strings.Join(pkg.Conflicts, " ")) + // %M : makedepends + printString = strings.ReplaceAll(printString, "%M", strings.Join(pkg.MakeDepends, " ")) + // %O : optdepends + printString = strings.ReplaceAll(printString, "%O", strings.Join(pkg.OptDepends, " ")) + // %P : provides + printString = strings.ReplaceAll(printString, "%P", strings.Join(pkg.Provides, " ")) + // %R : replaces + printString = strings.ReplaceAll(printString, "%R", strings.Join(pkg.Replaces, " ")) + // %L : licenses + printString = strings.ReplaceAll(printString, "%L", strings.Join(pkg.License, " ")) + + fmt.Println(printString) + } +} diff --git a/query.go b/query.go index 10000baad..e329ca7d0 100644 --- a/query.go +++ b/query.go @@ -43,21 +43,8 @@ func syncInfo(ctx context.Context, run *runtime.Runtime, ) 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) + pkgS = ExpandPackages(pkgS, dbExecutor) + aurS, repoS := PackageSlices(pkgS, run.Cfg, dbExecutor) if len(repoS) == 0 && len(aurS) == 0 { if run.Cfg.Mode != parser.ModeRepo { @@ -115,8 +102,25 @@ func syncInfo(ctx context.Context, run *runtime.Runtime, return err } +// ExpandPackages expands group names into the packages they contain. +func ExpandPackages(packages []string, dbExecutor db.Executor) []string { + expandedPackages := []string{} + for _, pkg := range packages { + groupPackages := dbExecutor.PackagesFromGroup(pkg) + if len(groupPackages) > 0 { + for _, p := range groupPackages { + expandedPackages = append(expandedPackages, p.Name()) + } + } else { + expandedPackages = append(expandedPackages, pkg) + } + } + + return expandedPackages +} + // PackageSlices separates an input slice into aur and repo slices. -func packageSlices(toCheck []string, config *settings.Configuration, dbExecutor db.Executor) (aurNames, repoNames []string) { +func PackageSlices(toCheck []string, config *settings.Configuration, dbExecutor db.Executor) (aurNames, repoNames []string) { for _, _pkg := range toCheck { dbName, name := text.SplitDBFromName(_pkg) diff --git a/sync.go b/sync.go index 53a501375..793ab2596 100644 --- a/sync.go +++ b/sync.go @@ -5,16 +5,20 @@ import ( "fmt" "strings" + aur "github.com/Jguer/aur" + alpm "github.com/Jguer/go-alpm/v2" "github.com/leonelquinteros/gotext" "github.com/Jguer/yay/v12/pkg/db" "github.com/Jguer/yay/v12/pkg/dep" "github.com/Jguer/yay/v12/pkg/multierror" + "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/exe" "github.com/Jguer/yay/v12/pkg/settings/parser" "github.com/Jguer/yay/v12/pkg/sync" + "github.com/Jguer/yay/v12/pkg/text" "github.com/Jguer/yay/v12/pkg/upgrade" ) @@ -24,23 +28,14 @@ func syncInstall(ctx context.Context, dbExecutor db.Executor, ) error { aurCache := run.AURClient - refreshArg := cmdArgs.ExistsArg("y", "refresh") noDeps := cmdArgs.ExistsArg("d", "nodeps") noCheck := strings.Contains(run.Cfg.MFlags, "--nocheck") if noDeps { run.CmdBuilder.AddMakepkgFlag("-d") } - if refreshArg && run.Cfg.Mode.AtLeastRepo() { - if errR := earlyRefresh(ctx, run.Cfg, run.CmdBuilder, cmdArgs); errR != nil { - return fmt.Errorf("%s - %w", gotext.Get("error refreshing databases"), errR) - } - - // we may have done -Sy, our handle now has an old - // database. - if errRefresh := dbExecutor.RefreshHandle(); errRefresh != nil { - return errRefresh - } + if err := earlyRefreshIfNeeded(ctx, run, cmdArgs, dbExecutor); err != nil { + return err } grapher := dep.NewGrapher(dbExecutor, aurCache, false, settings.NoConfirm, @@ -90,6 +85,106 @@ func syncInstall(ctx context.Context, return opService.Run(ctx, run, cmdArgs, targets, excluded) } +func syncPrint(ctx context.Context, run *runtime.Runtime, cmdArgs *parser.Arguments, + dbExecutor db.Executor, +) error { + var ( + remoteAurPkgs []aur.Pkg + localAurPkgs []alpm.IPackage + err error + ) + + if err := earlyRefreshIfNeeded(ctx, run, cmdArgs, dbExecutor); err != nil { + return err + } + + pkgS := query.RemoveInvalidTargets(run.Logger, cmdArgs.Targets, run.Cfg.Mode) + pkgS = ExpandPackages(pkgS, dbExecutor) + aurS, repoS := PackageSlices(pkgS, run.Cfg, dbExecutor) + + if len(aurS) != 0 { + // Use the AUR client to search for AUR packages not currently installed + + noDB := make([]string, 0, len(aurS)) + + for _, pkg := range aurS { + _, name := text.SplitDBFromName(pkg) + + localPkg := dbExecutor.LocalPackage(name) + if localPkg != nil { + localAurPkgs = append(localAurPkgs, localPkg) + } else { + 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, print errors for any not found + found := make(map[string]struct{}, len(remoteAurPkgs)) + for i := range remoteAurPkgs { + found[remoteAurPkgs[i].Name] = struct{}{} + } + + missing := false + for _, name := range noDB { + if _, ok := found[name]; !ok { + missing = true + run.Logger.Errorln(gotext.Get("No AUR package found for"), " ", name) + } + } + + // Mimic pacman's behavior by exiting if any packages are missing. + if missing { + return nil + } + } + + if len(repoS) > 0 { + // Use pacman to print repo packages + + arguments := cmdArgs.Copy() + // If this argument is present, we already refreshed the databases. Remove so pacman doesn't + // do it again. + arguments.DelArg("y", "refresh") + arguments.ClearTargets() + arguments.AddTarget(repoS...) + + if err := run.CmdBuilder.Show(run.CmdBuilder.BuildPacmanCmd(ctx, arguments, + run.Cfg.Mode, settings.NoConfirm)); err != nil { + return err + } + } + + printLocalPackages(run.Cfg, localAurPkgs) + printAurPackages(run.Cfg, remoteAurPkgs) + + return nil +} + +func earlyRefreshIfNeeded(ctx context.Context, run *runtime.Runtime, cmdArgs *parser.Arguments, + dbExecutor db.Executor, +) error { + refreshArg := cmdArgs.ExistsArg("y", "refresh") + if !refreshArg || !run.Cfg.Mode.AtLeastRepo() { + return nil + } + + if errR := earlyRefresh(ctx, run.Cfg, run.CmdBuilder, cmdArgs); errR != nil { + return fmt.Errorf("%s - %w", gotext.Get("error refreshing databases"), errR) + } + + // we may have done -Sy, our handle now has an old + // database. + return dbExecutor.RefreshHandle() +} + func earlyRefresh(ctx context.Context, cfg *settings.Configuration, cmdBuilder exe.ICmdBuilder, cmdArgs *parser.Arguments) error { arguments := cmdArgs.Copy() if cfg.CombinedUpgrade { From 234e88b781741c61c75ae392cf22d299d9d855d3 Mon Sep 17 00:00:00 2001 From: jdholtz Date: Sat, 29 Nov 2025 00:47:39 -0600 Subject: [PATCH 2/6] Disable needing root when printing with -U --- pkg/settings/parser/parser.go | 4 ++++ print.go | 5 +++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/pkg/settings/parser/parser.go b/pkg/settings/parser/parser.go index d14e9209f..dec448bd7 100644 --- a/pkg/settings/parser/parser.go +++ b/pkg/settings/parser/parser.go @@ -152,6 +152,10 @@ func (a *Arguments) NeedRoot(mode TargetMode) bool { return true case "U", "upgrade": + if a.ExistsArg("p", "print", "print-format") { + return false + } + return true default: return false diff --git a/print.go b/print.go index 661d49e4a..f9f912f8c 100644 --- a/print.go +++ b/print.go @@ -302,8 +302,9 @@ func printLocalPackages(config *settings.Configuration, pkgs []alpm.IPackage) { // %r : repo printString = strings.ReplaceAll(printString, "%r", "aur") // %s : size - // TODO: Different per op - sizeStr := fmt.Sprintf("%d", pkg.Size()) + // pacman uses download size, but no download size info is available for AUR packages, so we + // use installed size instead. + sizeStr := fmt.Sprintf("%d", pkg.ISize()) printString = strings.ReplaceAll(printString, "%s", sizeStr) // %u : URL printString = strings.ReplaceAll(printString, "%u", pkg.URL()) From c7b487c1a3faed2db988b77489dfbf3fd457b79e Mon Sep 17 00:00:00 2001 From: jdholtz Date: Sat, 29 Nov 2025 02:00:45 -0600 Subject: [PATCH 3/6] Add support for print upgrades when -u is specified with the sync operation --- pkg/upgrade/service.go | 76 +++++++++++++++++++++++------------------- sync.go | 53 ++++++++++++++++++++++++++--- 2 files changed, 91 insertions(+), 38 deletions(-) diff --git a/pkg/upgrade/service.go b/pkg/upgrade/service.go index 839dec500..1656452b8 100644 --- a/pkg/upgrade/service.go +++ b/pkg/upgrade/service.go @@ -57,42 +57,11 @@ func (u *UpgradeService) upGraph(ctx context.Context, graph *topo.Graph[string, enableDowngrade bool, filter Filter, ) (err error) { - var ( - develUp UpSlice - errs multierror.MultiError - aurdata = make(map[string]*aur.Pkg) - aurUp UpSlice - ) - - remote := u.dbExecutor.InstalledRemotePackages() - remoteNames := u.dbExecutor.InstalledRemotePackageNames() - - if u.cfg.Mode.AtLeastAUR() { - u.log.OperationInfoln(gotext.Get("Searching AUR for updates...")) - - _aurdata, err := u.aurCache.Get(ctx, &aur.Query{Needles: remoteNames, By: aur.Name}) + var errs multierror.MultiError + aurdata, aurUp, develUp, err := u.GetAURUpgrades(ctx, enableDowngrade) + if err != nil { errs.Add(err) - - if err == nil { - for i := range _aurdata { - pkg := &_aurdata[i] - aurdata[pkg.Name] = pkg - u.AURWarnings.AddToWarnings(remote, pkg) - } - - u.AURWarnings.CalculateMissing(remoteNames, remote, aurdata) - - aurUp = UpAUR(u.log, remote, aurdata, u.cfg.TimeUpdate, enableDowngrade) - - if u.cfg.Devel { - u.log.OperationInfoln(gotext.Get("Checking development packages...")) - - develUp = UpDevel(ctx, u.log, remote, aurdata, u.vcsStore) - - u.vcsStore.CleanOrphans(remote) - } - } } aurPkgsAdded := []*aur.Pkg{} @@ -233,6 +202,45 @@ func (u *UpgradeService) graphToUpSlice(graph *topo.Graph[string, *dep.InstallIn return aurUp, repoUp } +func (u *UpgradeService) GetAURUpgrades(ctx context.Context, enableDowngrade bool) ( + aurdata map[string]*aur.Pkg, aurUp, develUp UpSlice, err error, +) { + aurdata = make(map[string]*aur.Pkg) + remote := u.dbExecutor.InstalledRemotePackages() + remoteNames := u.dbExecutor.InstalledRemotePackageNames() + + if !u.cfg.Mode.AtLeastAUR() { + return aurdata, aurUp, develUp, err + } + + u.log.OperationInfoln(gotext.Get("Searching AUR for updates...")) + + _aurdata, err := u.aurCache.Get(ctx, &aur.Query{Needles: remoteNames, By: aur.Name}) + if err != nil { + return aurdata, aurUp, develUp, err + } + + for i := range _aurdata { + pkg := &_aurdata[i] + aurdata[pkg.Name] = pkg + u.AURWarnings.AddToWarnings(remote, pkg) + } + + u.AURWarnings.CalculateMissing(remoteNames, remote, aurdata) + + aurUp = UpAUR(u.log, remote, aurdata, u.cfg.TimeUpdate, enableDowngrade) + + if u.cfg.Devel { + u.log.OperationInfoln(gotext.Get("Checking development packages...")) + + develUp = UpDevel(ctx, u.log, remote, aurdata, u.vcsStore) + + u.vcsStore.CleanOrphans(remote) + } + + return aurdata, aurUp, develUp, nil +} + func (u *UpgradeService) GraphUpgrades(ctx context.Context, graph *topo.Graph[string, *dep.InstallInfo], enableDowngrade bool, filter Filter, diff --git a/sync.go b/sync.go index 793ab2596..e9baea50a 100644 --- a/sync.go +++ b/sync.go @@ -7,6 +7,7 @@ import ( aur "github.com/Jguer/aur" alpm "github.com/Jguer/go-alpm/v2" + mapset "github.com/deckarep/golang-set/v2" "github.com/leonelquinteros/gotext" "github.com/Jguer/yay/v12/pkg/db" @@ -102,6 +103,7 @@ func syncPrint(ctx context.Context, run *runtime.Runtime, cmdArgs *parser.Argume pkgS = ExpandPackages(pkgS, dbExecutor) aurS, repoS := PackageSlices(pkgS, run.Cfg, dbExecutor) + aurNames := mapset.NewThreadUnsafeSet[string]() if len(aurS) != 0 { // Use the AUR client to search for AUR packages not currently installed @@ -110,6 +112,12 @@ func syncPrint(ctx context.Context, run *runtime.Runtime, cmdArgs *parser.Argume for _, pkg := range aurS { _, name := text.SplitDBFromName(pkg) + if aurNames.Contains(name) { + // This package has already been specified + continue + } + + aurNames.Add(name) localPkg := dbExecutor.LocalPackage(name) if localPkg != nil { localAurPkgs = append(localAurPkgs, localPkg) @@ -126,15 +134,15 @@ func syncPrint(ctx context.Context, run *runtime.Runtime, cmdArgs *parser.Argume run.Logger.Errorln(err) } - // Check for any missing packages, print errors for any not found - found := make(map[string]struct{}, len(remoteAurPkgs)) + // Check for any missing packages and print errors for any not found + found := mapset.NewThreadUnsafeSet[string]() for i := range remoteAurPkgs { - found[remoteAurPkgs[i].Name] = struct{}{} + found.Add(remoteAurPkgs[i].Name) } missing := false for _, name := range noDB { - if _, ok := found[name]; !ok { + if !found.Contains(name) { missing = true run.Logger.Errorln(gotext.Get("No AUR package found for"), " ", name) } @@ -146,6 +154,43 @@ func syncPrint(ctx context.Context, run *runtime.Runtime, cmdArgs *parser.Argume } } + // Include any pending AUR upgrades if requested + if cmdArgs.ExistsArg("u", "sysupgrade") && run.Cfg.Mode.AtLeastAUR() { + grapher := dep.NewGrapher(dbExecutor, run.AURClient, false, settings.NoConfirm, + true, true, cmdArgs.ExistsArg("needed"), run.Logger.Child("grapher")) + upService := upgrade.NewUpgradeService( + grapher, run.AURClient, dbExecutor, run.VCSStore, + run.Cfg, settings.NoConfirm, run.Logger.Child("upgrade")) + + aurData, aurUp, develUp, err := upService.GetAURUpgrades(ctx, false) + + if err == nil { + for i := range develUp.Up { + up := &develUp.Up[i] + // don't duplicate entries + if aurNames.Contains(up.Name) { + continue + } + + aurPkg := aurData[up.Name] + remoteAurPkgs = append(remoteAurPkgs, *aurPkg) + } + + for i := range aurUp.Up { + up := &aurUp.Up[i] + // don't duplicate entries + if aurNames.Contains(up.Name) { + continue + } + + aurPkg := aurData[up.Name] + remoteAurPkgs = append(remoteAurPkgs, *aurPkg) + } + } else { + run.Logger.Errorln(err) + } + } + if len(repoS) > 0 { // Use pacman to print repo packages From 5ccaa1986644bab796af6e605a1f02b22bd8c848 Mon Sep 17 00:00:00 2001 From: jdholtz Date: Mon, 1 Dec 2025 12:04:14 -0800 Subject: [PATCH 4/6] Add tests for GetAURUpgrades and syncPrint --- pkg/upgrade/service_test.go | 131 +++++++++++++++++++++++++++ sync_test.go | 176 ++++++++++++++++++++++++++++++++++++ 2 files changed, 307 insertions(+) diff --git a/pkg/upgrade/service_test.go b/pkg/upgrade/service_test.go index d3800162d..690d5c28b 100644 --- a/pkg/upgrade/service_test.go +++ b/pkg/upgrade/service_test.go @@ -32,6 +32,137 @@ func ptrString(s string) *string { return &s } +func TestUpgradeService_GetAURUpgrades(t *testing.T) { + t.Parallel() + + remoteNames := []string{"yay", "example-git"} + remotePackages := func() map[string]mock.IPackage { + return map[string]mock.IPackage{ + "yay": &mock.Package{ + PName: "yay", + PBase: "yay", + PVersion: "10.2.3", + PReason: alpm.PkgReasonExplicit, + }, + "example-git": &mock.Package{ + PName: "example-git", + PBase: "example", + PVersion: "2.2.1", + PReason: alpm.PkgReasonDepend, + }, + } + } + + tests := []struct { + name string + mode parser.TargetMode + aurPkgs []aur.Pkg + vcsToUpgrade []string + wantAURDataKeys []string + wantAurUp []db.Upgrade + wantDevelUp []db.Upgrade + wantErr error + }{ + { + name: "repo mode only skips aur upgrade checks", + mode: parser.ModeRepo, + wantAURDataKeys: []string{}, + }, + { + name: "aur and devel upgrades", + mode: parser.ModeAny, + vcsToUpgrade: []string{"example-git"}, + aurPkgs: []aur.Pkg{ + {Name: "yay", Version: "10.2.4", PackageBase: "yay"}, + {Name: "example-git", Version: "2.3.0", PackageBase: "example"}, + }, + wantAURDataKeys: []string{"yay", "example-git"}, + wantAurUp: []db.Upgrade{ + { + Name: "yay", + Base: "yay", + Repository: "aur", + LocalVersion: "10.2.3", + RemoteVersion: "10.2.4", + Reason: alpm.PkgReasonExplicit, + }, + { + Name: "example-git", + Base: "example", + Repository: "aur", + LocalVersion: "2.2.1", + RemoteVersion: "2.3.0", + Reason: alpm.PkgReasonDepend, + }, + }, + wantDevelUp: []db.Upgrade{ + { + Name: "example-git", + Base: "example", + Repository: "devel", + LocalVersion: "2.2.1", + RemoteVersion: "latest-commit", + Reason: alpm.PkgReasonDepend, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dbExe := &mock.DBExecutor{ + InstalledRemotePackageNamesFn: func() []string { + return append([]string(nil), remoteNames...) + }, + InstalledRemotePackagesFn: func() map[string]mock.IPackage { + return remotePackages() + }, + } + + mockAUR := &mockaur.MockAUR{ + GetFn: func(ctx context.Context, query *aur.Query) ([]aur.Pkg, error) { + require.Equal(t, remoteNames, query.Needles) + require.Equal(t, aur.Name, query.By) + return append([]aur.Pkg(nil), tt.aurPkgs...), nil + }, + } + + vcsStore := &vcs.Mock{ + ToUpgradeReturn: tt.vcsToUpgrade, + } + + logger := text.NewLogger(io.Discard, os.Stderr, + strings.NewReader(""), true, "test") + + u := &UpgradeService{ + log: logger, + aurCache: mockAUR, + dbExecutor: dbExe, + vcsStore: vcsStore, + cfg: &settings.Configuration{Mode: tt.mode, Devel: true}, + AURWarnings: query.NewWarnings(logger), + } + + aurdata, aurUp, develUp, err := u.GetAURUpgrades(context.Background(), false) + + if tt.wantErr != nil { + require.ErrorIs(t, err, tt.wantErr) + } else { + require.NoError(t, err) + } + + keys := make([]string, 0, len(aurdata)) + for k := range aurdata { + keys = append(keys, k) + } + assert.ElementsMatch(t, tt.wantAURDataKeys, keys) + + assert.ElementsMatch(t, tt.wantAurUp, aurUp.Up) + assert.ElementsMatch(t, tt.wantDevelUp, develUp.Up) + }) + } +} + func TestUpgradeService_GraphUpgrades(t *testing.T) { t.Parallel() linuxDepInfo := &dep.InstallInfo{ diff --git a/sync_test.go b/sync_test.go index e1c6ca327..1537ed321 100644 --- a/sync_test.go +++ b/sync_test.go @@ -31,6 +31,182 @@ import ( "github.com/Jguer/yay/v12/pkg/vcs" ) +type syncPrintTestEnv struct { + ctx context.Context + run *runtime.Runtime + args *parser.Arguments + db *mock.DBExecutor + runner *exe.MockRunner + aur *mockaur.MockAUR +} + +func newSyncPrintTestEnv(t *testing.T) *syncPrintTestEnv { + t.Helper() + + mockRunner := &exe.MockRunner{} + mockBuilder := &exe.MockBuilder{ + Runner: mockRunner, + BuildPacmanCmdFn: func(ctx context.Context, args *parser.Arguments, mode parser.TargetMode, noConfirm bool) *exec.Cmd { + return exec.CommandContext(ctx, "pacman") + }, + } + + mockAUR := &mockaur.MockAUR{} + + run := &runtime.Runtime{ + Cfg: &settings.Configuration{}, + Logger: text.NewLogger(io.Discard, os.Stderr, strings.NewReader("\n"), true, "test"), + CmdBuilder: mockBuilder, + VCSStore: &vcs.Mock{}, + AURClient: mockAUR, + } + + dbExec := &mock.DBExecutor{ + PackagesFromGroupFn: func(string) []mock.IPackage { return nil }, + SyncSatisfierFn: func(string) mock.IPackage { return nil }, + LocalPackageFn: func(string) mock.IPackage { return nil }, + InstalledRemotePackagesFn: func() map[string]alpm.IPackage { + return map[string]alpm.IPackage{} + }, + InstalledRemotePackageNamesFn: func() []string { return []string{} }, + RefreshHandleFn: func() error { return nil }, + } + + args := parser.MakeArguments() + require.NoError(t, args.AddArg("S")) + + return &syncPrintTestEnv{ + ctx: context.Background(), + run: run, + args: args, + db: dbExec, + runner: mockRunner, + aur: mockAUR, + } +} + +func TestSyncPrint_RepoTargetsUsePacmanOnly(t *testing.T) { + t.Parallel() + + env := newSyncPrintTestEnv(t) + env.db.SyncSatisfierFn = func(name string) mock.IPackage { + if name == "vim" { + return &mock.Package{PName: name} + } + + return nil + } + env.args.AddTarget("vim") + + aurCalls := 0 + env.aur.GetFn = func(ctx context.Context, query *aur.Query) ([]aur.Pkg, error) { + aurCalls++ + return []aur.Pkg{}, nil + } + + err := syncPrint(env.ctx, env.run, env.args, env.db) + require.NoError(t, err) + // Make sure pacman was called and AUR was not + require.Len(t, env.runner.ShowCalls, 1) + assert.Equal(t, 0, aurCalls) +} + +func TestSyncPrint_AURTargetsSkipPacman(t *testing.T) { + t.Parallel() + + env := newSyncPrintTestEnv(t) + env.args.AddTarget("yay-bin") + + var queried []string + env.aur.GetFn = func(ctx context.Context, query *aur.Query) ([]aur.Pkg, error) { + queried = append([]string(nil), query.Needles...) + return []aur.Pkg{ + { + Name: query.Needles[0], + PackageBase: query.Needles[0], + Version: "1.0.0", + }, + }, nil + } + + err := syncPrint(env.ctx, env.run, env.args, env.db) + require.NoError(t, err) + // Make sure pacman was not called and AUR was + assert.Len(t, env.runner.ShowCalls, 0) + require.Equal(t, []string{"yay-bin"}, queried) +} + +func TestSyncPrint_MixedTargetsCallPacmanAndAUR(t *testing.T) { + t.Parallel() + + env := newSyncPrintTestEnv(t) + env.db.SyncSatisfierFn = func(name string) mock.IPackage { + if name == "vim" { + return &mock.Package{PName: name} + } + + return nil + } + env.args.AddTarget("vim", "yay-bin") + + var queried []string + env.aur.GetFn = func(ctx context.Context, query *aur.Query) ([]aur.Pkg, error) { + queried = append([]string(nil), query.Needles...) + return []aur.Pkg{ + { + Name: query.Needles[0], + PackageBase: query.Needles[0], + Version: "2.0.0", + }, + }, nil + } + + err := syncPrint(env.ctx, env.run, env.args, env.db) + require.NoError(t, err) + // Make sure both pacman and AUR were called + require.Len(t, env.runner.ShowCalls, 1) + require.Equal(t, []string{"yay-bin"}, queried) +} + +func TestSyncPrint_UpgradeChecksAUR(t *testing.T) { + t.Parallel() + + env := newSyncPrintTestEnv(t) + require.NoError(t, env.args.AddArg("u")) + + env.db.InstalledRemotePackagesFn = func() map[string]alpm.IPackage { + return map[string]alpm.IPackage{ + "yay-bin": &mock.Package{ + PName: "yay-bin", + PVersion: "1.0.0", + PBase: "yay-bin", + PReason: alpm.PkgReasonExplicit, + }, + } + } + env.db.InstalledRemotePackageNamesFn = func() []string { + return []string{"yay-bin"} + } + + var queried []string + env.aur.GetFn = func(ctx context.Context, query *aur.Query) ([]aur.Pkg, error) { + queried = append([]string(nil), query.Needles...) + return []aur.Pkg{ + { + Name: "yay-bin", + PackageBase: "yay-bin", + Version: "2.0.0", + }, + }, nil + } + + err := syncPrint(env.ctx, env.run, env.args, env.db) + require.NoError(t, err) + // Make sure pacman was not called and AUR was + assert.Len(t, env.runner.ShowCalls, 0) + require.Equal(t, []string{"yay-bin"}, queried) +} + func TestSyncUpgrade(t *testing.T) { t.Parallel() makepkgBin := t.TempDir() + "/makepkg" From 1198e44e39b1a938a4d9c74baac5e442fbed5afd Mon Sep 17 00:00:00 2001 From: jdholtz Date: Mon, 1 Dec 2025 12:11:41 -0800 Subject: [PATCH 5/6] Ensure no duplicates show up in devel packages --- sync.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sync.go b/sync.go index e9baea50a..0bae3695e 100644 --- a/sync.go +++ b/sync.go @@ -172,6 +172,7 @@ func syncPrint(ctx context.Context, run *runtime.Runtime, cmdArgs *parser.Argume continue } + aurNames.Add(up.Name) aurPkg := aurData[up.Name] remoteAurPkgs = append(remoteAurPkgs, *aurPkg) } @@ -183,6 +184,7 @@ func syncPrint(ctx context.Context, run *runtime.Runtime, cmdArgs *parser.Argume continue } + aurNames.Add(up.Name) aurPkg := aurData[up.Name] remoteAurPkgs = append(remoteAurPkgs, *aurPkg) } From 93a10d0412b12c4b7d8d603665a3cb49b6a6f9e0 Mon Sep 17 00:00:00 2001 From: jdholtz Date: Mon, 1 Dec 2025 12:29:22 -0800 Subject: [PATCH 6/6] Ensure Pacman is called when upgrading is specified Also simplify the AUR upgrade code in syncPrint a bit --- sync.go | 45 +++++++++++++++++++++------------------------ sync_test.go | 6 +++--- 2 files changed, 24 insertions(+), 27 deletions(-) diff --git a/sync.go b/sync.go index 0bae3695e..8a681e399 100644 --- a/sync.go +++ b/sync.go @@ -118,8 +118,7 @@ func syncPrint(ctx context.Context, run *runtime.Runtime, cmdArgs *parser.Argume } aurNames.Add(name) - localPkg := dbExecutor.LocalPackage(name) - if localPkg != nil { + if localPkg := dbExecutor.LocalPackage(name); localPkg != nil { localAurPkgs = append(localAurPkgs, localPkg) } else { noDB = append(noDB, name) @@ -165,36 +164,34 @@ func syncPrint(ctx context.Context, run *runtime.Runtime, cmdArgs *parser.Argume aurData, aurUp, develUp, err := upService.GetAURUpgrades(ctx, false) if err == nil { - for i := range develUp.Up { - up := &develUp.Up[i] - // don't duplicate entries - if aurNames.Contains(up.Name) { - continue + processUpgrade := func(slice upgrade.UpSlice) { + for i := range slice.Up { + up := &slice.Up[i] + // Ensure we don't add duplicates + if aurNames.Contains(up.Name) { + continue + } + + aurNames.Add(up.Name) + // Since these are upgrades, all packages should be local. Check just in case + if localPkg := dbExecutor.LocalPackage(up.Name); localPkg != nil { + localAurPkgs = append(localAurPkgs, localPkg) + } else { + aurPkg := aurData[up.Name] + remoteAurPkgs = append(remoteAurPkgs, *aurPkg) + } } - - aurNames.Add(up.Name) - aurPkg := aurData[up.Name] - remoteAurPkgs = append(remoteAurPkgs, *aurPkg) } - for i := range aurUp.Up { - up := &aurUp.Up[i] - // don't duplicate entries - if aurNames.Contains(up.Name) { - continue - } - - aurNames.Add(up.Name) - aurPkg := aurData[up.Name] - remoteAurPkgs = append(remoteAurPkgs, *aurPkg) - } + processUpgrade(develUp) + processUpgrade(aurUp) } else { run.Logger.Errorln(err) } } - if len(repoS) > 0 { - // Use pacman to print repo packages + if len(repoS) > 0 || (cmdArgs.ExistsArg("u", "sysupgrade") && run.Cfg.Mode.AtLeastRepo()) { + // Use pacman to print repo packages and upgrades arguments := cmdArgs.Copy() // If this argument is present, we already refreshed the databases. Remove so pacman doesn't diff --git a/sync_test.go b/sync_test.go index 1537ed321..5e5ebcd13 100644 --- a/sync_test.go +++ b/sync_test.go @@ -168,7 +168,7 @@ func TestSyncPrint_MixedTargetsCallPacmanAndAUR(t *testing.T) { require.Equal(t, []string{"yay-bin"}, queried) } -func TestSyncPrint_UpgradeChecksAUR(t *testing.T) { +func TestSyncPrint_UpgradeChecksPacmanAndAUR(t *testing.T) { t.Parallel() env := newSyncPrintTestEnv(t) @@ -202,8 +202,8 @@ func TestSyncPrint_UpgradeChecksAUR(t *testing.T) { err := syncPrint(env.ctx, env.run, env.args, env.db) require.NoError(t, err) - // Make sure pacman was not called and AUR was - assert.Len(t, env.runner.ShowCalls, 0) + // Make sure pacman and AUR were both called + require.Len(t, env.runner.ShowCalls, 1) require.Equal(t, []string{"yay-bin"}, queried) }