Browse Source

feat(v12): add group install (#1835)

v12engine: add group install
Jo 2 years ago
parent
commit
9f67d10d5c

+ 28 - 4
aur_install.go

@@ -4,6 +4,7 @@ import (
 	"context"
 	"fmt"
 	"os"
+	"sync"
 
 	"github.com/Jguer/yay/v11/pkg/db"
 	"github.com/Jguer/yay/v11/pkg/dep"
@@ -12,6 +13,7 @@ import (
 	"github.com/Jguer/yay/v11/pkg/settings/parser"
 	"github.com/Jguer/yay/v11/pkg/text"
 
+	gosrc "github.com/Morganamilo/go-srcinfo"
 	mapset "github.com/deckarep/golang-set/v2"
 	"github.com/leonelquinteros/gotext"
 )
@@ -48,10 +50,11 @@ func (installer *Installer) Install(ctx context.Context,
 	cmdArgs *parser.Arguments,
 	targets []map[string]*dep.InstallInfo,
 	pkgBuildDirs map[string]string,
+	srcinfos map[string]*gosrc.Srcinfo,
 ) error {
 	// Reorganize targets into layers of dependencies
 	for i := len(targets) - 1; i >= 0; i-- {
-		err := installer.handleLayer(ctx, cmdArgs, targets[i], pkgBuildDirs)
+		err := installer.handleLayer(ctx, cmdArgs, targets[i], pkgBuildDirs, srcinfos)
 		if err != nil {
 			// rollback
 			return err
@@ -62,7 +65,10 @@ func (installer *Installer) Install(ctx context.Context,
 }
 
 func (installer *Installer) handleLayer(ctx context.Context,
-	cmdArgs *parser.Arguments, layer map[string]*dep.InstallInfo, pkgBuildDirs map[string]string,
+	cmdArgs *parser.Arguments,
+	layer map[string]*dep.InstallInfo,
+	pkgBuildDirs map[string]string,
+	srcinfos map[string]*gosrc.Srcinfo,
 ) error {
 	// Install layer
 	nameToBaseMap := make(map[string]string, 0)
@@ -107,7 +113,8 @@ func (installer *Installer) handleLayer(ctx context.Context,
 		return ErrInstallRepoPkgs
 	}
 
-	errAur := installer.installAURPackages(ctx, cmdArgs, aurDeps, aurExp, nameToBaseMap, pkgBuildDirs, false)
+	errAur := installer.installAURPackages(ctx, cmdArgs, aurDeps, aurExp,
+		nameToBaseMap, pkgBuildDirs, true, srcinfos)
 
 	return errAur
 }
@@ -117,11 +124,22 @@ func (installer *Installer) installAURPackages(ctx context.Context,
 	aurDepNames, aurExpNames mapset.Set[string],
 	nameToBase, pkgBuildDirsByBase map[string]string,
 	installIncompatible bool,
+	srcinfos map[string]*gosrc.Srcinfo,
 ) error {
+	all := aurDepNames.Union(aurExpNames).ToSlice()
+	if len(all) == 0 {
+		return nil
+	}
+
 	deps, exps := make([]string, 0, aurDepNames.Cardinality()), make([]string, 0, aurExpNames.Cardinality())
 	pkgArchives := make([]string, 0, len(exps)+len(deps))
 
-	for _, name := range aurDepNames.Union(aurExpNames).ToSlice() {
+	var (
+		mux sync.Mutex
+		wg  sync.WaitGroup
+	)
+
+	for _, name := range all {
 		base := nameToBase[name]
 		dir := pkgBuildDirsByBase[base]
 		args := []string{"--nobuild", "-fC"}
@@ -169,8 +187,14 @@ func (installer *Installer) installAURPackages(ctx context.Context,
 		if hasDebug {
 			deps = append(deps, name+"-debug")
 		}
+
+		srcinfo := srcinfos[base]
+		wg.Add(1)
+		go config.Runtime.VCSStore.Update(ctx, name, srcinfo.Source, &mux, &wg)
 	}
 
+	wg.Wait()
+
 	if err := installPkgArchive(ctx, cmdArgs, pkgArchives); err != nil {
 		return fmt.Errorf("%s - %w", fmt.Sprintf(gotext.Get("error installing:")+" %v", pkgArchives), err)
 	}

+ 37 - 48
install.go

@@ -75,7 +75,6 @@ func asexp(ctx context.Context, cmdArgs *parser.Arguments, pkgs []string) error
 // Install handles package installs.
 func install(ctx context.Context, cmdArgs *parser.Arguments, dbExecutor db.Executor, ignoreProviders bool) error {
 	var (
-		incompatible    stringset.StringSet
 		do              *dep.Order
 		srcinfos        map[string]*gosrc.Srcinfo
 		noDeps          = cmdArgs.ExistsDouble("d", "nodeps")
@@ -270,11 +269,11 @@ func install(ctx context.Context, cmdArgs *parser.Arguments, dbExecutor db.Execu
 		text.Errorln(errDiffMenu)
 	}
 
-	if errM := mergePkgbuilds(ctx, do.Aur); errM != nil {
+	if errM := mergePkgbuilds(ctx, pkgbuildDirs); errM != nil {
 		return errM
 	}
 
-	srcinfos, err = parseSrcinfoFiles(do.Aur, true)
+	srcinfos, err = parseSrcinfoFiles(pkgbuildDirs, true)
 	if err != nil {
 		return err
 	}
@@ -289,13 +288,12 @@ func install(ctx context.Context, cmdArgs *parser.Arguments, dbExecutor db.Execu
 		text.Errorln(errEditMenu)
 	}
 
-	incompatible, err = getIncompatible(do.Aur, srcinfos, dbExecutor)
-	if err != nil {
-		return err
+	if errI := confirmIncompatibleInstall(srcinfos, dbExecutor); errI != nil {
+		return errI
 	}
 
 	if config.PGPFetch {
-		if errCPK := pgp.CheckPgpKeys(do.Aur, srcinfos, config.GpgBin, config.GpgFlags, settings.NoConfirm); errCPK != nil {
+		if _, errCPK := pgp.CheckPgpKeys(pkgbuildDirs, srcinfos, config.GpgBin, config.GpgFlags, settings.NoConfirm); errCPK != nil {
 			return errCPK
 		}
 	}
@@ -341,19 +339,14 @@ func install(ctx context.Context, cmdArgs *parser.Arguments, dbExecutor db.Execu
 			config.AURURL, config.Runtime.CompletionPath, config.CompletionInterval, false)
 	}()
 
-	pkgBuildDirs := make(map[string]string, len(do.Aur))
-	for _, base := range do.Aur {
-		pkgBuildDirs[base.Pkgbase()] = filepath.Join(config.BuildDir, base.Pkgbase())
-	}
-
 	if errP := downloadPKGBUILDSourceFanout(ctx,
 		config.Runtime.CmdBuilder,
-		pkgBuildDirs,
-		len(incompatible) > 0, config.MaxConcurrentDownloads); errP != nil {
+		pkgbuildDirs,
+		true, config.MaxConcurrentDownloads); errP != nil {
 		text.Errorln(errP)
 	}
 
-	if errB := buildInstallPkgbuilds(ctx, cmdArgs, dbExecutor, dp, do, srcinfos, incompatible, conflicts, noDeps, noCheck); errB != nil {
+	if errB := buildInstallPkgbuilds(ctx, cmdArgs, dbExecutor, dp, do, srcinfos, true, conflicts, noDeps, noCheck); errB != nil {
 		return errB
 	}
 
@@ -461,42 +454,39 @@ func earlyRefresh(ctx context.Context, cmdArgs *parser.Arguments) error {
 		arguments, config.Runtime.Mode, settings.NoConfirm))
 }
 
-func getIncompatible(bases []dep.Base, srcinfos map[string]*gosrc.Srcinfo, dbExecutor db.Executor) (stringset.StringSet, error) {
-	incompatible := make(stringset.StringSet)
-	basesMap := make(map[string]dep.Base)
+func confirmIncompatibleInstall(srcinfos map[string]*gosrc.Srcinfo, dbExecutor db.Executor) error {
+	incompatible := []string{}
 
 	alpmArch, err := dbExecutor.AlpmArchitectures()
 	if err != nil {
-		return nil, err
+		return err
 	}
 
 nextpkg:
-	for _, base := range bases {
-		for _, arch := range srcinfos[base.Pkgbase()].Arch {
+	for base, srcinfo := range srcinfos {
+		for _, arch := range srcinfo.Arch {
 			if db.ArchIsSupported(alpmArch, arch) {
 				continue nextpkg
 			}
 		}
-
-		incompatible.Set(base.Pkgbase())
-		basesMap[base.Pkgbase()] = base
+		incompatible = append(incompatible, base)
 	}
 
 	if len(incompatible) > 0 {
 		text.Warnln(gotext.Get("The following packages are not compatible with your architecture:"))
 
-		for pkg := range incompatible {
-			fmt.Print("  " + text.Cyan(basesMap[pkg].String()))
+		for _, pkg := range incompatible {
+			fmt.Print("  " + text.Cyan(pkg))
 		}
 
 		fmt.Println()
 
 		if !text.ContinueTask(os.Stdin, gotext.Get("Try to build them anyway?"), true, settings.NoConfirm) {
-			return nil, &settings.ErrUserAbort{}
+			return &settings.ErrUserAbort{}
 		}
 	}
 
-	return incompatible, nil
+	return nil
 }
 
 func parsePackageList(ctx context.Context, dir string) (pkgdests map[string]string, pkgVersion string, err error) {
@@ -532,26 +522,25 @@ func parsePackageList(ctx context.Context, dir string) (pkgdests map[string]stri
 	return pkgdests, pkgVersion, nil
 }
 
-func parseSrcinfoFiles(bases []dep.Base, errIsFatal bool) (map[string]*gosrc.Srcinfo, error) {
+func parseSrcinfoFiles(pkgBuildDirs map[string]string, errIsFatal bool) (map[string]*gosrc.Srcinfo, error) {
 	srcinfos := make(map[string]*gosrc.Srcinfo)
 
-	for k, base := range bases {
-		pkg := base.Pkgbase()
-		dir := filepath.Join(config.BuildDir, pkg)
-
-		text.OperationInfoln(gotext.Get("(%d/%d) Parsing SRCINFO: %s", k+1, len(bases), text.Cyan(base.String())))
+	k := 0
+	for base, dir := range pkgBuildDirs {
+		text.OperationInfoln(gotext.Get("(%d/%d) Parsing SRCINFO: %s", k+1, len(pkgBuildDirs), text.Cyan(base)))
 
 		pkgbuild, err := gosrc.ParseFile(filepath.Join(dir, ".SRCINFO"))
 		if err != nil {
 			if !errIsFatal {
-				text.Warnln(gotext.Get("failed to parse %s -- skipping: %s", base.String(), err))
+				text.Warnln(gotext.Get("failed to parse %s -- skipping: %s", base, err))
 				continue
 			}
 
-			return nil, errors.New(gotext.Get("failed to parse %s: %s", base.String(), err))
+			return nil, errors.New(gotext.Get("failed to parse %s: %s", base, err))
 		}
 
-		srcinfos[pkg] = pkgbuild
+		srcinfos[base] = pkgbuild
+		k++
 	}
 
 	return srcinfos, nil
@@ -583,27 +572,27 @@ func pkgbuildsToSkip(bases []dep.Base, targets stringset.StringSet) stringset.St
 	return toSkip
 }
 
-func gitMerge(ctx context.Context, path, name string) error {
+func gitMerge(ctx context.Context, dir string) error {
 	_, stderr, err := config.Runtime.CmdBuilder.Capture(
 		config.Runtime.CmdBuilder.BuildGitCmd(ctx,
-			filepath.Join(path, name), "reset", "--hard", "HEAD"))
+			dir, "reset", "--hard", "HEAD"))
 	if err != nil {
-		return errors.New(gotext.Get("error resetting %s: %s", name, stderr))
+		return errors.New(gotext.Get("error resetting %s: %s", dir, stderr))
 	}
 
 	_, stderr, err = config.Runtime.CmdBuilder.Capture(
 		config.Runtime.CmdBuilder.BuildGitCmd(ctx,
-			filepath.Join(path, name), "merge", "--no-edit", "--ff"))
+			dir, "merge", "--no-edit", "--ff"))
 	if err != nil {
-		return errors.New(gotext.Get("error merging %s: %s", name, stderr))
+		return errors.New(gotext.Get("error merging %s: %s", dir, stderr))
 	}
 
 	return nil
 }
 
-func mergePkgbuilds(ctx context.Context, bases []dep.Base) error {
-	for _, base := range bases {
-		err := gitMerge(ctx, config.BuildDir, base.Pkgbase())
+func mergePkgbuilds(ctx context.Context, pkgbuildDirs map[string]string) error {
+	for _, dir := range pkgbuildDirs {
+		err := gitMerge(ctx, dir)
 		if err != nil {
 			return err
 		}
@@ -619,7 +608,7 @@ func buildInstallPkgbuilds(
 	dp *dep.Pool,
 	do *dep.Order,
 	srcinfos map[string]*gosrc.Srcinfo,
-	incompatible stringset.StringSet,
+	incompatible bool,
 	conflicts stringset.MapStringSet, noDeps, noCheck bool,
 ) error {
 	deps := make([]string, 0)
@@ -680,7 +669,7 @@ func buildInstallPkgbuilds(
 
 		args := []string{"--nobuild", "-fC"}
 
-		if incompatible.Get(pkg) {
+		if incompatible {
 			args = append(args, "--ignorearch")
 		}
 
@@ -749,7 +738,7 @@ func buildInstallPkgbuilds(
 		} else {
 			args := []string{"-cf", "--noconfirm", "--noextract", "--noprepare", "--holdver"}
 
-			if incompatible.Get(pkg) {
+			if incompatible {
 				args = append(args, "--ignorearch")
 			}
 

+ 11 - 8
local_install.go

@@ -58,24 +58,27 @@ func installLocalPKGBUILD(
 	}
 	installer := &Installer{dbExecutor: dbExecutor}
 
-	if errP := preparer.Present(os.Stdout, topoSorted); errP != nil {
-		return errP
+	pkgBuildDirs, err := preparer.Run(ctx, os.Stdout, topoSorted)
+	if err != nil {
+		return err
 	}
 
 	if cleanFunc := preparer.ShouldCleanMakeDeps(); cleanFunc != nil {
 		installer.AddPostInstallHook(cleanFunc)
 	}
 
-	pkgBuildDirs, err := preparer.PrepareWorkspace(ctx, topoSorted)
-	if err != nil {
-		return err
-	}
-
 	if cleanAURDirsFunc := preparer.ShouldCleanAURDirs(pkgBuildDirs); cleanAURDirsFunc != nil {
 		installer.AddPostInstallHook(cleanAURDirsFunc)
 	}
 
-	if err = installer.Install(ctx, cmdArgs, topoSorted, pkgBuildDirs); err != nil {
+	srcinfoOp := srcinfoOperator{dbExecutor: dbExecutor}
+
+	srcinfos, err := srcinfoOp.Run(pkgBuildDirs)
+	if err != nil {
+		return err
+	}
+
+	if err = installer.Install(ctx, cmdArgs, topoSorted, pkgBuildDirs, srcinfos); err != nil {
 		if errHook := installer.RunPostInstallHooks(ctx); errHook != nil {
 			text.Errorln(errHook)
 		}

+ 19 - 1
pkg/dep/depGraph.go

@@ -130,7 +130,7 @@ func (g *Grapher) GraphFromTargets(ctx context.Context,
 			if pkg := g.dbExecutor.SyncPackage(target.Name); pkg != nil {
 				dbName := pkg.DB().Name()
 				graph.AddNode(pkg.Name())
-				g.ValidateAndSetNodeInfo(graph, target.Name, &topo.NodeInfo[*InstallInfo]{
+				g.ValidateAndSetNodeInfo(graph, pkg.Name(), &topo.NodeInfo[*InstallInfo]{
 					Color:      colorMap[Explicit],
 					Background: bgColorMap[Sync],
 					Value: &InstallInfo{
@@ -144,6 +144,24 @@ func (g *Grapher) GraphFromTargets(ctx context.Context,
 				continue
 			}
 
+			groupPackages := g.dbExecutor.PackagesFromGroup(target.Name)
+			if len(groupPackages) > 0 {
+				dbName := groupPackages[0].DB().Name()
+				graph.AddNode(target.Name)
+				g.ValidateAndSetNodeInfo(graph, target.Name, &topo.NodeInfo[*InstallInfo]{
+					Color:      colorMap[Explicit],
+					Background: bgColorMap[Sync],
+					Value: &InstallInfo{
+						Source:     Sync,
+						Reason:     Explicit,
+						Version:    "",
+						SyncDBName: &dbName,
+					},
+				})
+
+				continue
+			}
+
 			fallthrough
 		case "aur":
 			graph, err = g.GraphFromAURCache(ctx, graph, []string{target.Name})

+ 4 - 0
pkg/menus/clean_menu.go

@@ -25,6 +25,10 @@ func anyExistInCache(pkgbuildDirs map[string]string) bool {
 }
 
 func CleanFn(ctx context.Context, config *settings.Configuration, w io.Writer, pkgbuildDirsByBase map[string]string) error {
+	if len(pkgbuildDirsByBase) == 0 {
+		return nil // no work to do
+	}
+
 	if !anyExistInCache(pkgbuildDirsByBase) {
 		return nil
 	}

+ 4 - 0
pkg/menus/diff_menu.go

@@ -182,6 +182,10 @@ func Diff(ctx context.Context, cmdBuilder exe.ICmdBuilder, w io.Writer,
 }
 
 func DiffFn(ctx context.Context, config *settings.Configuration, w io.Writer, pkgbuildDirsByBase map[string]string) error {
+	if len(pkgbuildDirsByBase) == 0 {
+		return nil // no work to do
+	}
+
 	bases := make([]string, 0, len(pkgbuildDirsByBase))
 	for base := range pkgbuildDirsByBase {
 		bases = append(bases, base)

+ 4 - 0
pkg/menus/edit_menu.go

@@ -150,6 +150,10 @@ func Edit(w io.Writer, editMenuOption bool, pkgbuildDirs map[string]string, edit
 func EditFn(ctx context.Context, config *settings.Configuration, w io.Writer,
 	pkgbuildDirsByBase map[string]string,
 ) error {
+	if len(pkgbuildDirsByBase) == 0 {
+		return nil // no work to do
+	}
+
 	bases := make([]string, 0, len(pkgbuildDirsByBase))
 	for pkg := range pkgbuildDirsByBase {
 		bases = append(bases, pkg)

+ 0 - 5
pkg/pgp/.snapshots/TestCheckPgpKeys-Two_dummy_packages_requiring_the_same_key

@@ -1,5 +0,0 @@
-
-
- -> ABAF11C65A2970B130ABE3C479BE3E4300411886, required by: dummy-1 (dummy-1 dummy-2)
-:: Importing keys with gpg...
-:: PGP keys need importing:

+ 0 - 5
pkg/pgp/.snapshots/TestCheckPgpKeys-_one_valid_key_not_yet_in_the_keyring

@@ -1,5 +0,0 @@
-
-
- -> 487EACC08557AD082088DABA1EB2638FF56C0C53, required by: cower
-:: Importing keys with gpg...
-:: PGP keys need importing:

+ 0 - 5
pkg/pgp/.snapshots/TestCheckPgpKeys-one_already_in_keyring

@@ -1,5 +0,0 @@
-
-
- -> C52048C0C0748FEE227D47A2702353E0F7E48EDB, required by: dummy-3
-:: Importing keys with gpg...
-:: PGP keys need importing:

+ 0 - 1
pkg/pgp/.snapshots/TestCheckPgpKeys-two_existing

@@ -1 +0,0 @@
-

+ 0 - 6
pkg/pgp/.snapshots/TestCheckPgpKeys-two_valid_keys_not_yet_in_the_keyring

@@ -1,6 +0,0 @@
-
-
- -> 11E521D646982372EB577A1F8F0871F202119294, required by: libc++
- -> B6C8F98282B944E3B0D5C2530FC3042E345AD05D, required by: libc++
-:: Importing keys with gpg...
-:: PGP keys need importing:

+ 12 - 14
pkg/pgp/keys.go

@@ -11,13 +11,12 @@ import (
 	gosrc "github.com/Morganamilo/go-srcinfo"
 	"github.com/leonelquinteros/gotext"
 
-	"github.com/Jguer/yay/v11/pkg/dep"
 	"github.com/Jguer/yay/v11/pkg/text"
 )
 
 // pgpKeySet maps a PGP key with a list of PKGBUILDs that require it.
 // This is similar to stringSet, used throughout the code.
-type pgpKeySet map[string][]dep.Base
+type pgpKeySet map[string][]string
 
 func (set pgpKeySet) toSlice() []string {
 	slice := make([]string, 0, len(set))
@@ -28,7 +27,7 @@ func (set pgpKeySet) toSlice() []string {
 	return slice
 }
 
-func (set pgpKeySet) set(key string, p dep.Base) {
+func (set pgpKeySet) set(key, p string) {
 	// Using ToUpper to make sure keys with a different case will be
 	// considered the same.
 	upperKey := strings.ToUpper(key)
@@ -44,9 +43,9 @@ func (set pgpKeySet) get(key string) bool {
 
 // CheckPgpKeys iterates through the keys listed in the PKGBUILDs and if needed,
 // asks the user whether yay should try to import them.
-func CheckPgpKeys(bases []dep.Base, srcinfos map[string]*gosrc.Srcinfo,
+func CheckPgpKeys(pkgbuildDirsByBase map[string]string, srcinfos map[string]*gosrc.Srcinfo,
 	gpgBin, gpgFlags string, noConfirm bool,
-) error {
+) ([]string, error) {
 	// Let's check the keys individually, and then we can offer to import
 	// the problematic ones.
 	problematic := make(pgpKeySet)
@@ -54,43 +53,42 @@ func CheckPgpKeys(bases []dep.Base, srcinfos map[string]*gosrc.Srcinfo,
 	args := append(strings.Fields(gpgFlags), "--list-keys")
 
 	// Mapping all the keys.
-	for _, base := range bases {
-		pkg := base.Pkgbase()
+	for pkg := range pkgbuildDirsByBase {
 		srcinfo := srcinfos[pkg]
 
 		for _, key := range srcinfo.ValidPGPKeys {
 			// If key already marked as problematic, indicate the current
 			// PKGBUILD requires it.
 			if problematic.get(key) {
-				problematic.set(key, base)
+				problematic.set(key, pkg)
 				continue
 			}
 
 			cmd := exec.Command(gpgBin, append(args, key)...)
 			if err := cmd.Run(); err != nil {
-				problematic.set(key, base)
+				problematic.set(key, pkg)
 			}
 		}
 	}
 
 	// No key issues!
 	if len(problematic) == 0 {
-		return nil
+		return []string{}, nil
 	}
 
 	str, err := formatKeysToImport(problematic)
 	if err != nil {
-		return err
+		return nil, err
 	}
 
 	fmt.Println()
 	fmt.Println(str)
 
 	if text.ContinueTask(os.Stdin, gotext.Get("Import?"), true, noConfirm) {
-		return importKeys(problematic.toSlice(), gpgBin, gpgFlags)
+		return problematic.toSlice(), importKeys(problematic.toSlice(), gpgBin, gpgFlags)
 	}
 
-	return nil
+	return problematic.toSlice(), nil
 }
 
 // importKeys tries to import the list of keys specified in its argument.
@@ -122,7 +120,7 @@ func formatKeysToImport(keys pgpKeySet) (string, error) {
 	for key, bases := range keys {
 		pkglist := ""
 		for _, base := range bases {
-			pkglist += base.String() + "  "
+			pkglist += base + "  "
 		}
 
 		pkglist = strings.TrimRight(pkglist, " ")

+ 24 - 40
pkg/pgp/keys_test.go

@@ -4,21 +4,16 @@ import (
 	"bytes"
 	"context"
 	"fmt"
-	"io"
 	"net/http"
 	"os"
 	"path"
 	"regexp"
-	"sort"
-	"strings"
 	"testing"
 	"time"
 
-	aur "github.com/Jguer/aur"
 	gosrc "github.com/Morganamilo/go-srcinfo"
-	"github.com/bradleyjkemp/cupaloy"
-
-	"github.com/Jguer/yay/v11/pkg/dep"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 )
 
 const (
@@ -42,10 +37,6 @@ func init() {
 	})
 }
 
-func newPkg(basename string) *aur.Pkg {
-	return &aur.Pkg{Name: basename, PackageBase: basename}
-}
-
 func getPgpKey(key string) string {
 	var buffer bytes.Buffer
 
@@ -159,40 +150,44 @@ func TestCheckPgpKeys(t *testing.T) {
 
 	casetests := []struct {
 		name      string
-		pkgs      dep.Base
+		pkgs      map[string]string
 		srcinfos  map[string]*gosrc.Srcinfo
 		wantError bool
+		expected  []string
 	}{
 		// cower: single package, one valid key not yet in the keyring.
 		// 487EACC08557AD082088DABA1EB2638FF56C0C53: Dave Reisner.
 		{
 			name:      " one valid key not yet in the keyring",
-			pkgs:      dep.Base{newPkg("cower")},
+			pkgs:      map[string]string{"cower": ""},
 			srcinfos:  map[string]*gosrc.Srcinfo{"cower": makeSrcinfo("cower", "487EACC08557AD082088DABA1EB2638FF56C0C53")},
 			wantError: false,
+			expected:  []string{"487EACC08557AD082088DABA1EB2638FF56C0C53"},
 		},
 		// libc++: single package, two valid keys not yet in the keyring.
 		// 11E521D646982372EB577A1F8F0871F202119294: Tom Stellard.
 		// B6C8F98282B944E3B0D5C2530FC3042E345AD05D: Hans Wennborg.
 		{
 			name: "two valid keys not yet in the keyring",
-			pkgs: dep.Base{newPkg("libc++")},
+			pkgs: map[string]string{"libc++": ""},
 			srcinfos: map[string]*gosrc.Srcinfo{
 				"libc++": makeSrcinfo("libc++", "11E521D646982372EB577A1F8F0871F202119294", "B6C8F98282B944E3B0D5C2530FC3042E345AD05D"),
 			},
 			wantError: false,
+			expected:  []string{"11E521D646982372EB577A1F8F0871F202119294", "B6C8F98282B944E3B0D5C2530FC3042E345AD05D"},
 		},
 		// Two dummy packages requiring the same key.
 		// ABAF11C65A2970B130ABE3C479BE3E4300411886: Linus Torvalds.
 		{
 			name: "Two dummy packages requiring the same key",
-			pkgs: dep.Base{newPkg("dummy-1"), newPkg("dummy-2")},
+			pkgs: map[string]string{"dummy-1": "", "dummy-2": ""},
 			srcinfos: map[string]*gosrc.Srcinfo{
 				"dummy-1": makeSrcinfo("dummy-1",
 					"ABAF11C65A2970B130ABE3C479BE3E4300411886"),
 				"dummy-2": makeSrcinfo("dummy-2", "ABAF11C65A2970B130ABE3C479BE3E4300411886"),
 			},
 			wantError: false,
+			expected:  []string{"ABAF11C65A2970B130ABE3C479BE3E4300411886"},
 		},
 		// dummy package: single package, two valid keys, one of them already
 		// in the keyring.
@@ -200,26 +195,28 @@ func TestCheckPgpKeys(t *testing.T) {
 		// C52048C0C0748FEE227D47A2702353E0F7E48EDB: Thomas Dickey.
 		{
 			name: "one already in keyring",
-			pkgs: dep.Base{newPkg("dummy-3")},
+			pkgs: map[string]string{"dummy-3": ""},
 			srcinfos: map[string]*gosrc.Srcinfo{
 				"dummy-3": makeSrcinfo("dummy-3", "11E521D646982372EB577A1F8F0871F202119294", "C52048C0C0748FEE227D47A2702353E0F7E48EDB"),
 			},
 			wantError: false,
+			expected:  []string{"C52048C0C0748FEE227D47A2702353E0F7E48EDB"},
 		},
 		// Two dummy packages with existing keys.
 		{
 			name: "two existing",
-			pkgs: dep.Base{newPkg("dummy-4"), newPkg("dummy-5")},
+			pkgs: map[string]string{"dummy-4": "", "dummy-5": ""},
 			srcinfos: map[string]*gosrc.Srcinfo{
 				"dummy-4": makeSrcinfo("dummy-4", "11E521D646982372EB577A1F8F0871F202119294"),
 				"dummy-5": makeSrcinfo("dummy-5", "C52048C0C0748FEE227D47A2702353E0F7E48EDB"),
 			},
 			wantError: false,
+			expected:  []string{},
 		},
 		// Dummy package with invalid key, should fail.
 		{
 			name:      "one invalid",
-			pkgs:      dep.Base{newPkg("dummy-7")},
+			pkgs:      map[string]string{"dummy-7": ""},
 			srcinfos:  map[string]*gosrc.Srcinfo{"dummy-7": makeSrcinfo("dummy-7", "THIS-SHOULD-FAIL")},
 			wantError: true,
 		},
@@ -227,40 +224,27 @@ func TestCheckPgpKeys(t *testing.T) {
 		// A314827C4E4250A204CE6E13284FC34C8E4B1A25: Thomas Bächler.
 		{
 			name:      "one invalid, one valid",
-			pkgs:      dep.Base{newPkg("dummy-8")},
+			pkgs:      map[string]string{"dummy-8": ""},
 			srcinfos:  map[string]*gosrc.Srcinfo{"dummy-8": makeSrcinfo("dummy-8", "A314827C4E4250A204CE6E13284FC34C8E4B1A25", "THIS-SHOULD-FAIL")},
 			wantError: true,
+			expected:  []string{},
 		},
 	}
 
 	for _, tt := range casetests {
 		tt := tt
 		t.Run(tt.name, func(t *testing.T) {
-			rescueStdout := os.Stdout
-			r, w, _ := os.Pipe()
-			os.Stdout = w
-
-			err := CheckPgpKeys([]dep.Base{tt.pkgs}, tt.srcinfos, "gpg",
+			problematic, err := CheckPgpKeys(tt.pkgs, tt.srcinfos, "gpg",
 				fmt.Sprintf("--homedir %s --keyserver 127.0.0.1", keyringDir), true)
-			if !tt.wantError {
-				if err != nil {
-					t.Fatalf("Got error %q, want no error", err)
-				}
-
-				w.Close()
-				out, _ := io.ReadAll(r)
-				os.Stdout = rescueStdout
 
-				splitLines := strings.Split(string(out), "\n")
-				sort.Strings(splitLines)
-
-				cupaloy.SnapshotT(t, strings.Join(splitLines, "\n"))
+			if tt.wantError {
+				require.Error(t, err)
 				return
 			}
-			// Here, we want to see the error.
-			if err == nil {
-				t.Fatalf("Got no error; want error")
-			}
+
+			require.NoError(t, err)
+
+			assert.ElementsMatch(t, tt.expected, problematic, fmt.Sprintf("%#v", problematic))
 		})
 	}
 }

+ 34 - 8
preparer.go

@@ -21,13 +21,14 @@ import (
 	"github.com/leonelquinteros/gotext"
 )
 
-type PostDownloadHookFunc func(ctx context.Context, config *settings.Configuration, w io.Writer, pkgbuildDirsByBase map[string]string) error
+type PreparerHookFunc func(ctx context.Context, config *settings.Configuration, w io.Writer, pkgbuildDirsByBase map[string]string) error
 
 type Preparer struct {
 	dbExecutor        db.Executor
 	cmdBuilder        exe.ICmdBuilder
 	config            *settings.Configuration
-	postDownloadHooks []PostDownloadHookFunc
+	postDownloadHooks []PreparerHookFunc
+	postMergeHooks    []PreparerHookFunc
 
 	makeDeps []string
 }
@@ -37,7 +38,8 @@ func NewPreparer(dbExecutor db.Executor, cmdBuilder exe.ICmdBuilder, config *set
 		dbExecutor:        dbExecutor,
 		cmdBuilder:        cmdBuilder,
 		config:            config,
-		postDownloadHooks: []PostDownloadHookFunc{},
+		postDownloadHooks: []PreparerHookFunc{},
+		postMergeHooks:    []PreparerHookFunc{},
 	}
 
 	if config.CleanMenu {
@@ -45,11 +47,11 @@ func NewPreparer(dbExecutor db.Executor, cmdBuilder exe.ICmdBuilder, config *set
 	}
 
 	if config.DiffMenu {
-		preper.postDownloadHooks = append(preper.postDownloadHooks, menus.DiffFn)
+		preper.postMergeHooks = append(preper.postMergeHooks, menus.DiffFn)
 	}
 
 	if config.EditMenu {
-		preper.postDownloadHooks = append(preper.postDownloadHooks, menus.EditFn)
+		preper.postMergeHooks = append(preper.postMergeHooks, menus.EditFn)
 	}
 
 	return preper
@@ -91,7 +93,20 @@ func (preper *Preparer) ShouldCleanMakeDeps() PostInstallHookFunc {
 	}
 }
 
-func (preper *Preparer) Present(w io.Writer, targets []map[string]*dep.InstallInfo) error {
+func (preper *Preparer) Run(ctx context.Context,
+	w io.Writer, targets []map[string]*dep.InstallInfo,
+) (pkgbuildDirsByBase map[string]string, err error) {
+	preper.Present(w, targets)
+
+	pkgBuildDirs, err := preper.PrepareWorkspace(ctx, targets)
+	if err != nil {
+		return nil, err
+	}
+
+	return pkgBuildDirs, nil
+}
+
+func (preper *Preparer) Present(w io.Writer, targets []map[string]*dep.InstallInfo) {
 	pkgsBySourceAndReason := map[string]map[string][]string{}
 
 	for _, layer := range targets {
@@ -127,8 +142,6 @@ func (preper *Preparer) Present(w io.Writer, targets []map[string]*dep.InstallIn
 				strings.Join(pkgs, ", "))
 		}
 	}
-
-	return nil
 }
 
 func (preper *Preparer) PrepareWorkspace(ctx context.Context, targets []map[string]*dep.InstallInfo) (map[string]string, error) {
@@ -168,6 +181,16 @@ func (preper *Preparer) PrepareWorkspace(ctx context.Context, targets []map[stri
 		}
 	}
 
+	if err := mergePkgbuilds(ctx, pkgBuildDirsByBase); err != nil {
+		return nil, err
+	}
+
+	for _, hookFn := range preper.postMergeHooks {
+		if err := hookFn(ctx, preper.config, os.Stdout, pkgBuildDirsByBase); err != nil {
+			return nil, err
+		}
+	}
+
 	return pkgBuildDirsByBase, nil
 }
 
@@ -179,6 +202,9 @@ func (preper *Preparer) needToCloneAURBase(installInfo *dep.InstallInfo, pkgbuil
 	srcinfoFile := filepath.Join(pkgbuildDir, ".SRCINFO")
 	if pkgbuild, err := gosrc.ParseFile(srcinfoFile); err == nil {
 		if db.VerCmp(pkgbuild.Version(), installInfo.Version) >= 0 {
+			text.OperationInfoln(
+				gotext.Get("PKGBUILD up to date, skipping download: %s",
+					text.Cyan(*installInfo.AURBase)))
 			return false
 		}
 	}

+ 32 - 0
srcinfo.go

@@ -0,0 +1,32 @@
+package main
+
+import (
+	"github.com/Jguer/yay/v11/pkg/db"
+	"github.com/Jguer/yay/v11/pkg/pgp"
+	"github.com/Jguer/yay/v11/pkg/settings"
+
+	gosrc "github.com/Morganamilo/go-srcinfo"
+)
+
+type srcinfoOperator struct {
+	dbExecutor db.Executor
+}
+
+func (s *srcinfoOperator) Run(pkgbuildDirs map[string]string) (map[string]*gosrc.Srcinfo, error) {
+	srcinfos, err := parseSrcinfoFiles(pkgbuildDirs, true)
+	if err != nil {
+		return nil, err
+	}
+
+	if err := confirmIncompatibleInstall(srcinfos, s.dbExecutor); err != nil {
+		return nil, err
+	}
+
+	if config.PGPFetch {
+		if _, errCPK := pgp.CheckPgpKeys(pkgbuildDirs, srcinfos, config.GpgBin, config.GpgFlags, settings.NoConfirm); errCPK != nil {
+			return nil, errCPK
+		}
+	}
+
+	return srcinfos, nil
+}

+ 16 - 7
sync.go

@@ -5,6 +5,7 @@ import (
 	"fmt"
 	"os"
 
+	"github.com/Jguer/yay/v11/pkg/completion"
 	"github.com/Jguer/yay/v11/pkg/db"
 	"github.com/Jguer/yay/v11/pkg/dep"
 	"github.com/Jguer/yay/v11/pkg/settings"
@@ -55,8 +56,9 @@ func syncInstall(ctx context.Context,
 	preparer := NewPreparer(dbExecutor, config.Runtime.CmdBuilder, config)
 	installer := &Installer{dbExecutor: dbExecutor}
 
-	if errP := preparer.Present(os.Stdout, topoSorted); errP != nil {
-		return errP
+	pkgBuildDirs, err := preparer.Run(ctx, os.Stdout, topoSorted)
+	if err != nil {
+		return err
 	}
 
 	cleanFunc := preparer.ShouldCleanMakeDeps()
@@ -64,16 +66,23 @@ func syncInstall(ctx context.Context,
 		installer.AddPostInstallHook(cleanFunc)
 	}
 
-	pkgBuildDirs, err := preparer.PrepareWorkspace(ctx, topoSorted)
+	if cleanAURDirsFunc := preparer.ShouldCleanAURDirs(pkgBuildDirs); cleanAURDirsFunc != nil {
+		installer.AddPostInstallHook(cleanAURDirsFunc)
+	}
+
+	srcinfoOp := srcinfoOperator{dbExecutor: dbExecutor}
+
+	srcinfos, err := srcinfoOp.Run(pkgBuildDirs)
 	if err != nil {
 		return err
 	}
 
-	if cleanAURDirsFunc := preparer.ShouldCleanAURDirs(pkgBuildDirs); cleanAURDirsFunc != nil {
-		installer.AddPostInstallHook(cleanAURDirsFunc)
-	}
+	go func() {
+		_ = completion.Update(ctx, config.Runtime.HTTPClient, dbExecutor,
+			config.AURURL, config.Runtime.CompletionPath, config.CompletionInterval, false)
+	}()
 
-	err = installer.Install(ctx, cmdArgs, topoSorted, pkgBuildDirs)
+	err = installer.Install(ctx, cmdArgs, topoSorted, pkgBuildDirs, srcinfos)
 	if err != nil {
 		if errHook := installer.RunPostInstallHooks(ctx); errHook != nil {
 			text.Errorln(errHook)

+ 5 - 1
vcs.go

@@ -2,6 +2,7 @@ package main
 
 import (
 	"context"
+	"path/filepath"
 	"strings"
 	"sync"
 
@@ -33,11 +34,14 @@ func createDevelDB(ctx context.Context, config *settings.Configuration, dbExecut
 	toSkip := pkgbuildsToSkip(bases, stringset.FromSlice(remoteNames))
 
 	targets := make([]string, 0, len(bases))
+	pkgBuildDirsByBase := make(map[string]string, len(bases))
 
 	for _, base := range bases {
 		if !toSkip.Get(base.Pkgbase()) {
 			targets = append(targets, base.Pkgbase())
 		}
+
+		pkgBuildDirsByBase[base.Pkgbase()] = filepath.Join(config.BuildDir, base.Pkgbase())
 	}
 
 	toSkipSlice := toSkip.ToSlice()
@@ -52,7 +56,7 @@ func createDevelDB(ctx context.Context, config *settings.Configuration, dbExecut
 		return err
 	}
 
-	srcinfos, err := parseSrcinfoFiles(bases, false)
+	srcinfos, err := parseSrcinfoFiles(pkgBuildDirsByBase, false)
 	if err != nil {
 		return err
 	}