Browse Source

feat(new engine): skip built and respect --needed (#1852)

* add built package check

* respect --needed for new engine

* add needed check and test

* add test for not built
Jo 2 years ago
parent
commit
7da9f4869d
9 changed files with 480 additions and 71 deletions
  1. 90 39
      aur_install.go
  2. 179 0
      aur_install_test.go
  3. 8 0
      errors.go
  4. 52 28
      install.go
  5. 4 0
      pkg/db/mock/executor.go
  6. 109 0
      pkg/settings/exe/mock.go
  7. 25 0
      pkg/vcs/mock.go
  8. 9 0
      pkg/vcs/vcs.go
  9. 4 4
      sync.go

+ 90 - 39
aur_install.go

@@ -10,8 +10,10 @@ import (
 	"github.com/Jguer/yay/v11/pkg/dep"
 	"github.com/Jguer/yay/v11/pkg/multierror"
 	"github.com/Jguer/yay/v11/pkg/settings"
+	"github.com/Jguer/yay/v11/pkg/settings/exe"
 	"github.com/Jguer/yay/v11/pkg/settings/parser"
 	"github.com/Jguer/yay/v11/pkg/text"
+	"github.com/Jguer/yay/v11/pkg/vcs"
 
 	gosrc "github.com/Morganamilo/go-srcinfo"
 	mapset "github.com/deckarep/golang-set/v2"
@@ -24,14 +26,20 @@ type (
 		dbExecutor        db.Executor
 		postInstallHooks  []PostInstallHookFunc
 		failedAndIngnored map[string]error
+		exeCmd            exe.ICmdBuilder
+		vcsStore          vcs.Store
+		targetMode        parser.TargetMode
 	}
 )
 
-func NewInstaller(dbExecutor db.Executor) *Installer {
+func NewInstaller(dbExecutor db.Executor, exeCmd exe.ICmdBuilder, vcsStore vcs.Store, targetMode parser.TargetMode) *Installer {
 	return &Installer{
 		dbExecutor:        dbExecutor,
 		postInstallHooks:  []PostInstallHookFunc{},
 		failedAndIngnored: map[string]error{},
+		exeCmd:            exeCmd,
+		vcsStore:          vcsStore,
+		targetMode:        targetMode,
 	}
 }
 
@@ -163,38 +171,9 @@ func (installer *Installer) installAURPackages(ctx context.Context,
 	for _, name := range all {
 		base := nameToBase[name]
 		dir := pkgBuildDirsByBase[base]
-		args := []string{"--nobuild", "-fC"}
 
-		if installIncompatible {
-			args = append(args, "--ignorearch")
-		}
-
-		// pkgver bump
-		if err := config.Runtime.CmdBuilder.Show(
-			config.Runtime.CmdBuilder.BuildMakepkgCmd(ctx, dir, args...)); err != nil {
-			if !lastLayer {
-				return fmt.Errorf("%s - %w", gotext.Get("error making: %s", base), err)
-			}
-
-			installer.failedAndIngnored[name] = err
-			text.Errorln(gotext.Get("error making: %s", base), "-", err)
-			continue
-		}
-
-		pkgdests, _, errList := parsePackageList(ctx, dir)
-		if errList != nil {
-			return errList
-		}
-
-		args = []string{"-cf", "--noconfirm", "--noextract", "--noprepare", "--holdver"}
-
-		if installIncompatible {
-			args = append(args, "--ignorearch")
-		}
-
-		if errMake := config.Runtime.CmdBuilder.Show(
-			config.Runtime.CmdBuilder.BuildMakepkgCmd(ctx,
-				dir, args...)); errMake != nil {
+		pkgdests, errMake := installer.buildPkg(ctx, dir, base, installIncompatible, cmdArgs.ExistsArg("needed"))
+		if errMake != nil {
 			if !lastLayer {
 				return fmt.Errorf("%s - %w", gotext.Get("error making: %s", base), errMake)
 			}
@@ -204,6 +183,11 @@ func (installer *Installer) installAURPackages(ctx context.Context,
 			continue
 		}
 
+		if len(pkgdests) == 0 {
+			text.Warnln(gotext.Get("nothing to install for %s", text.Cyan(base)))
+			continue
+		}
+
 		newPKGArchives, hasDebug, err := installer.getNewTargets(pkgdests, name)
 		if err != nil {
 			return err
@@ -223,22 +207,89 @@ func (installer *Installer) installAURPackages(ctx context.Context,
 
 		srcinfo := srcinfos[base]
 		wg.Add(1)
-		go config.Runtime.VCSStore.Update(ctx, name, srcinfo.Source, &mux, &wg)
+		go installer.vcsStore.Update(ctx, name, srcinfo.Source, &mux, &wg)
 	}
 
 	wg.Wait()
 
-	if err := installPkgArchive(ctx, cmdArgs, pkgArchives); err != nil {
+	if err := installPkgArchive(ctx, installer.exeCmd, installer.targetMode, installer.vcsStore, cmdArgs, pkgArchives); err != nil {
 		return fmt.Errorf("%s - %w", fmt.Sprintf(gotext.Get("error installing:")+" %v", pkgArchives), err)
 	}
 
-	if err := setInstallReason(ctx, cmdArgs, deps, exps); err != nil {
+	if err := setInstallReason(ctx, installer.exeCmd, installer.targetMode, cmdArgs, deps, exps); err != nil {
 		return fmt.Errorf("%s - %w", fmt.Sprintf(gotext.Get("error installing:")+" %v", pkgArchives), err)
 	}
 
 	return nil
 }
 
+func (installer *Installer) buildPkg(ctx context.Context,
+	dir, base string,
+	installIncompatible, needed bool,
+) (map[string]string, error) {
+	args := []string{"--nobuild", "-fC"}
+
+	if installIncompatible {
+		args = append(args, "--ignorearch")
+	}
+
+	// pkgver bump
+	if err := installer.exeCmd.Show(
+		installer.exeCmd.BuildMakepkgCmd(ctx, dir, args...)); err != nil {
+		return nil, err
+	}
+
+	pkgdests, pkgVersion, errList := parsePackageList(ctx, installer.exeCmd, dir)
+	if errList != nil {
+		return nil, errList
+	}
+
+	switch {
+	case needed && installer.pkgsAreAlreadyInstalled(pkgdests, pkgVersion):
+		args = []string{"-c", "--nobuild", "--noextract", "--ignorearch"}
+		pkgdests = map[string]string{}
+		text.Warnln(gotext.Get("%s is up to date -- skipping", text.Cyan(base+"-"+pkgVersion)))
+	case pkgsAreBuilt(pkgdests):
+		args = []string{"-c", "--nobuild", "--noextract", "--ignorearch"}
+		text.Warnln(gotext.Get("%s already made -- skipping build", text.Cyan(base+"-"+pkgVersion)))
+	default:
+		args = []string{"-cf", "--noconfirm", "--noextract", "--noprepare", "--holdver"}
+		if installIncompatible {
+			args = append(args, "--ignorearch")
+		}
+	}
+
+	errMake := installer.exeCmd.Show(
+		installer.exeCmd.BuildMakepkgCmd(ctx,
+			dir, args...))
+	if errMake != nil {
+		return nil, errMake
+	}
+
+	return pkgdests, nil
+}
+
+func (installer *Installer) pkgsAreAlreadyInstalled(pkgdests map[string]string, pkgVersion string) bool {
+	for pkgName := range pkgdests {
+		if !installer.dbExecutor.IsCorrectVersionInstalled(pkgName, pkgVersion) {
+			return false
+		}
+	}
+
+	return true
+}
+
+func pkgsAreBuilt(pkgdests map[string]string) bool {
+	for _, pkgdest := range pkgdests {
+		if _, err := os.Stat(pkgdest); err != nil {
+			text.Debugln("pkgIsBuilt:", pkgdest, "does not exist")
+			return false
+		}
+	}
+
+	return true
+}
+
 func (*Installer) isDep(cmdArgs *parser.Arguments, aurExpNames mapset.Set[string], name string) bool {
 	switch {
 	case cmdArgs.ExistsArg("asdeps", "asdep"):
@@ -279,7 +330,7 @@ func (installer *Installer) getNewTargets(pkgdests map[string]string, name strin
 	return pkgArchives, ok, nil
 }
 
-func (*Installer) installSyncPackages(ctx context.Context, cmdArgs *parser.Arguments,
+func (installer *Installer) installSyncPackages(ctx context.Context, cmdArgs *parser.Arguments,
 	syncDeps, // repo targets that are deps
 	syncExp mapset.Set[string], // repo targets that are exp
 ) error {
@@ -297,14 +348,14 @@ func (*Installer) installSyncPackages(ctx context.Context, cmdArgs *parser.Argum
 	arguments.ClearTargets()
 	arguments.AddTarget(repoTargets...)
 
-	errShow := config.Runtime.CmdBuilder.Show(config.Runtime.CmdBuilder.BuildPacmanCmd(ctx,
+	errShow := installer.exeCmd.Show(installer.exeCmd.BuildPacmanCmd(ctx,
 		arguments, config.Runtime.Mode, settings.NoConfirm))
 
-	if errD := asdeps(ctx, cmdArgs, syncDeps.ToSlice()); errD != nil {
+	if errD := asdeps(ctx, installer.exeCmd, installer.targetMode, cmdArgs, syncDeps.ToSlice()); errD != nil {
 		return errD
 	}
 
-	if errE := asexp(ctx, cmdArgs, syncExp.ToSlice()); errE != nil {
+	if errE := asexp(ctx, installer.exeCmd, installer.targetMode, cmdArgs, syncExp.ToSlice()); errE != nil {
 		return errE
 	}
 

+ 179 - 0
aur_install_test.go

@@ -0,0 +1,179 @@
+package main
+
+import (
+	"context"
+	"os"
+	"os/exec"
+	"strings"
+	"testing"
+
+	gosrc "github.com/Morganamilo/go-srcinfo"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
+	"github.com/Jguer/yay/v11/pkg/db/mock"
+	"github.com/Jguer/yay/v11/pkg/dep"
+	"github.com/Jguer/yay/v11/pkg/settings/exe"
+	"github.com/Jguer/yay/v11/pkg/settings/parser"
+	"github.com/Jguer/yay/v11/pkg/vcs"
+)
+
+func ptrString(s string) *string {
+	return &s
+}
+
+func TestInstaller_InstallNeeded(t *testing.T) {
+	t.Parallel()
+
+	makepkgBin := t.TempDir() + "/makepkg"
+	pacmanBin := t.TempDir() + "/pacman"
+	f, err := os.OpenFile(makepkgBin, os.O_RDONLY|os.O_CREATE, 0o755)
+	require.NoError(t, err)
+	require.NoError(t, f.Close())
+
+	f, err = os.OpenFile(pacmanBin, os.O_RDONLY|os.O_CREATE, 0o755)
+	require.NoError(t, err)
+	require.NoError(t, f.Close())
+
+	type testCase struct {
+		desc        string
+		isInstalled bool
+		isBuilt     bool
+		wantShow    []string
+		wantCapture []string
+	}
+
+	testCases := []testCase{
+		{
+			desc:        "not installed and not built",
+			isInstalled: false,
+			isBuilt:     false,
+			wantShow: []string{
+				"makepkg --nobuild -fC --ignorearch",
+				"makepkg -cf --noconfirm --noextract --noprepare --holdver --ignorearch",
+				"pacman -U --needed --config  -- /testdir/yay-91.0.0-1-x86_64.pkg.tar.zst",
+				"pacman -D -q --asexplicit --config  -- yay",
+			},
+			wantCapture: []string{"makepkg --packagelist"},
+		},
+		{
+			desc:        "not installed and built",
+			isInstalled: false,
+			isBuilt:     true,
+			wantShow: []string{
+				"makepkg --nobuild -fC --ignorearch",
+				"makepkg -c --nobuild --noextract --ignorearch",
+				"pacman -U --needed --config  -- /testdir/yay-91.0.0-1-x86_64.pkg.tar.zst",
+				"pacman -D -q --asexplicit --config  -- yay",
+			},
+			wantCapture: []string{"makepkg --packagelist"},
+		},
+		{
+			desc:        "installed",
+			isInstalled: true,
+			isBuilt:     false,
+			wantShow: []string{
+				"makepkg --nobuild -fC --ignorearch",
+				"makepkg -c --nobuild --noextract --ignorearch",
+			},
+			wantCapture: []string{"makepkg --packagelist"},
+		},
+	}
+
+	for _, tc := range testCases {
+		tc := tc
+		t.Run(tc.desc, func(td *testing.T) {
+			tmpDir := td.TempDir()
+			pkgTar := tmpDir + "/yay-91.0.0-1-x86_64.pkg.tar.zst"
+
+			captureOverride := func(cmd *exec.Cmd) (stdout string, stderr string, err error) {
+				return pkgTar, "", nil
+			}
+
+			i := 0
+			showOverride := func(cmd *exec.Cmd) error {
+				i++
+				if i == 2 {
+					if !tc.isBuilt {
+						f, err := os.OpenFile(pkgTar, os.O_RDONLY|os.O_CREATE, 0o666)
+						require.NoError(td, err)
+						require.NoError(td, f.Close())
+					}
+				}
+				return nil
+			}
+
+			// create a mock file
+			if tc.isBuilt {
+				f, err := os.OpenFile(pkgTar, os.O_RDONLY|os.O_CREATE, 0o666)
+				require.NoError(td, err)
+				require.NoError(td, f.Close())
+			}
+
+			isCorrectInstalledOverride := func(string, string) bool {
+				return tc.isInstalled
+			}
+
+			mockDB := &mock.DBExecutor{IsCorrectVersionInstalledFunc: isCorrectInstalledOverride}
+			mockRunner := &exe.MockRunner{CaptureFn: captureOverride, ShowFn: showOverride}
+			cmdBuilder := &exe.CmdBuilder{
+				MakepkgBin:      makepkgBin,
+				SudoBin:         "su",
+				PacmanBin:       pacmanBin,
+				Runner:          mockRunner,
+				SudoLoopEnabled: false,
+			}
+
+			cmdBuilder.Runner = mockRunner
+
+			installer := NewInstaller(mockDB, cmdBuilder, &vcs.Mock{}, parser.ModeAny)
+
+			cmdArgs := parser.MakeArguments()
+			cmdArgs.AddArg("needed")
+			cmdArgs.AddTarget("yay")
+
+			pkgBuildDirs := map[string]string{
+				"yay": tmpDir,
+			}
+
+			srcInfos := map[string]*gosrc.Srcinfo{"yay": {}}
+
+			targets := []map[string]*dep.InstallInfo{
+				{
+					"yay": {
+						Source:      dep.AUR,
+						Reason:      dep.Explicit,
+						Version:     "91.0.0-1",
+						SrcinfoPath: ptrString(tmpDir + "/.SRCINFO"),
+						AURBase:     ptrString("yay"),
+						SyncDBName:  nil,
+					},
+				},
+			}
+
+			errI := installer.Install(context.Background(), cmdArgs, targets, pkgBuildDirs, srcInfos)
+			require.NoError(td, errI)
+
+			require.Len(td, mockRunner.ShowCalls, len(tc.wantShow))
+			require.Len(td, mockRunner.CaptureCalls, len(tc.wantCapture))
+
+			for i, call := range mockRunner.ShowCalls {
+				show := call.Args[0].(*exec.Cmd).String()
+				show = strings.ReplaceAll(show, tmpDir, "/testdir") // replace the temp dir with a static path
+				show = strings.ReplaceAll(show, makepkgBin, "makepkg")
+				show = strings.ReplaceAll(show, pacmanBin, "pacman")
+
+				// options are in a different order on different systems and on CI root user is used
+				assert.Subset(td, strings.Split(show, " "), strings.Split(tc.wantShow[i], " "), show)
+			}
+
+			for i, call := range mockRunner.CaptureCalls {
+				capture := call.Args[0].(*exec.Cmd).String()
+				capture = strings.ReplaceAll(capture, tmpDir, "/testdir") // replace the temp dir with a static path
+				capture = strings.ReplaceAll(capture, makepkgBin, "makepkg")
+				capture = strings.ReplaceAll(capture, pacmanBin, "pacman")
+				assert.Subset(td, strings.Split(capture, " "), strings.Split(tc.wantCapture[i], " "), capture)
+			}
+		})
+	}
+}

+ 8 - 0
errors.go

@@ -2,6 +2,14 @@ package main
 
 import "github.com/leonelquinteros/gotext"
 
+type NoPkgDestsFoundError struct {
+	dir string
+}
+
+func (e *NoPkgDestsFoundError) Error() string {
+	return gotext.Get("could not find any package archives listed in %s", e.dir)
+}
+
 type SetPkgReasonError struct {
 	exp bool // explicit
 }

+ 52 - 28
install.go

@@ -27,9 +27,14 @@ import (
 	"github.com/Jguer/yay/v11/pkg/settings/parser"
 	"github.com/Jguer/yay/v11/pkg/stringset"
 	"github.com/Jguer/yay/v11/pkg/text"
+	"github.com/Jguer/yay/v11/pkg/vcs"
 )
 
-func setPkgReason(ctx context.Context, cmdArgs *parser.Arguments, pkgs []string, exp bool) error {
+func setPkgReason(ctx context.Context,
+	cmdBuilder exe.ICmdBuilder,
+	mode parser.TargetMode,
+	cmdArgs *parser.Arguments, pkgs []string, exp bool,
+) error {
 	if len(pkgs) == 0 {
 		return nil
 	}
@@ -56,20 +61,26 @@ func setPkgReason(ctx context.Context, cmdArgs *parser.Arguments, pkgs []string,
 		cmdArgs.AddTarget(pkgName)
 	}
 
-	if err := config.Runtime.CmdBuilder.Show(config.Runtime.CmdBuilder.BuildPacmanCmd(ctx,
-		cmdArgs, config.Runtime.Mode, settings.NoConfirm)); err != nil {
+	if err := cmdBuilder.Show(cmdBuilder.BuildPacmanCmd(ctx,
+		cmdArgs, mode, settings.NoConfirm)); err != nil {
 		return &SetPkgReasonError{exp: exp}
 	}
 
 	return nil
 }
 
-func asdeps(ctx context.Context, cmdArgs *parser.Arguments, pkgs []string) error {
-	return setPkgReason(ctx, cmdArgs, pkgs, false)
+func asdeps(ctx context.Context,
+	cmdBuilder exe.ICmdBuilder,
+	mode parser.TargetMode, cmdArgs *parser.Arguments, pkgs []string,
+) error {
+	return setPkgReason(ctx, cmdBuilder, mode, cmdArgs, pkgs, false)
 }
 
-func asexp(ctx context.Context, cmdArgs *parser.Arguments, pkgs []string) error {
-	return setPkgReason(ctx, cmdArgs, pkgs, true)
+func asexp(ctx context.Context,
+	cmdBuilder exe.ICmdBuilder,
+	mode parser.TargetMode, cmdArgs *parser.Arguments, pkgs []string,
+) error {
+	return setPkgReason(ctx, cmdBuilder, mode, cmdArgs, pkgs, true)
 }
 
 // Install handles package installs.
@@ -323,11 +334,11 @@ func install(ctx context.Context, cmdArgs *parser.Arguments, dbExecutor db.Execu
 			}
 		}
 
-		if errDeps := asdeps(ctx, cmdArgs, deps); errDeps != nil {
+		if errDeps := asdeps(ctx, config.Runtime.CmdBuilder, config.Runtime.Mode, cmdArgs, deps); errDeps != nil {
 			return errDeps
 		}
 
-		if errExp := asexp(ctx, cmdArgs, exp); errExp != nil {
+		if errExp := asexp(ctx, config.Runtime.CmdBuilder, config.Runtime.Mode, cmdArgs, exp); errExp != nil {
 			return errExp
 		}
 	}
@@ -487,9 +498,11 @@ nextpkg:
 	return nil
 }
 
-func parsePackageList(ctx context.Context, dir string) (pkgdests map[string]string, pkgVersion string, err error) {
-	stdout, stderr, err := config.Runtime.CmdBuilder.Capture(
-		config.Runtime.CmdBuilder.BuildMakepkgCmd(ctx, dir, "--packagelist"))
+func parsePackageList(ctx context.Context, cmdBuilder exe.ICmdBuilder,
+	dir string,
+) (pkgdests map[string]string, pkgVersion string, err error) {
+	stdout, stderr, err := cmdBuilder.Capture(
+		cmdBuilder.BuildMakepkgCmd(ctx, dir, "--packagelist"))
 	if err != nil {
 		return nil, "", fmt.Errorf("%s %s", stderr, err)
 	}
@@ -517,6 +530,10 @@ func parsePackageList(ctx context.Context, dir string) (pkgdests map[string]stri
 		pkgdests[pkgName] = line
 	}
 
+	if len(pkgdests) == 0 {
+		return nil, "", &NoPkgDestsFoundError{dir}
+	}
+
 	return pkgdests, pkgVersion, nil
 }
 
@@ -644,8 +661,9 @@ func buildInstallPkgbuilds(
 
 		if !satisfied || !config.BatchInstall {
 			text.Debugln("non batch installing archives:", pkgArchives)
-			errArchive := installPkgArchive(ctx, cmdArgs, pkgArchives)
-			errReason := setInstallReason(ctx, cmdArgs, deps, exp)
+			errArchive := installPkgArchive(ctx, config.Runtime.CmdBuilder,
+				config.Runtime.Mode, config.Runtime.VCSStore, cmdArgs, pkgArchives)
+			errReason := setInstallReason(ctx, config.Runtime.CmdBuilder, config.Runtime.Mode, cmdArgs, deps, exp)
 
 			deps = make([]string, 0)
 			exp = make([]string, 0)
@@ -678,7 +696,7 @@ func buildInstallPkgbuilds(
 			return errors.New(gotext.Get("error making: %s", base.String()))
 		}
 
-		pkgdests, pkgVersion, errList := parsePackageList(ctx, dir)
+		pkgdests, pkgVersion, errList := parsePackageList(ctx, config.Runtime.CmdBuilder, dir)
 		if errList != nil {
 			return errList
 		}
@@ -797,12 +815,12 @@ func buildInstallPkgbuilds(
 	}
 
 	text.Debugln("installing archives:", pkgArchives)
-	errArchive := installPkgArchive(ctx, cmdArgs, pkgArchives)
+	errArchive := installPkgArchive(ctx, config.Runtime.CmdBuilder, config.Runtime.Mode, config.Runtime.VCSStore, cmdArgs, pkgArchives)
 	if errArchive != nil {
 		go config.Runtime.VCSStore.RemovePackage([]string{do.Aur[len(do.Aur)-1].String()})
 	}
 
-	errReason := setInstallReason(ctx, cmdArgs, deps, exp)
+	errReason := setInstallReason(ctx, config.Runtime.CmdBuilder, config.Runtime.Mode, cmdArgs, deps, exp)
 	if errReason != nil {
 		go config.Runtime.VCSStore.RemovePackage([]string{do.Aur[len(do.Aur)-1].String()})
 	}
@@ -812,7 +830,13 @@ func buildInstallPkgbuilds(
 	return nil
 }
 
-func installPkgArchive(ctx context.Context, cmdArgs *parser.Arguments, pkgArchives []string) error {
+func installPkgArchive(ctx context.Context,
+	cmdBuilder exe.ICmdBuilder,
+	mode parser.TargetMode,
+	vcsStore vcs.Store,
+	cmdArgs *parser.Arguments,
+	pkgArchives []string,
+) error {
 	if len(pkgArchives) == 0 {
 		return nil
 	}
@@ -833,28 +857,31 @@ func installPkgArchive(ctx context.Context, cmdArgs *parser.Arguments, pkgArchiv
 
 	arguments.AddTarget(pkgArchives...)
 
-	if errShow := config.Runtime.CmdBuilder.Show(config.Runtime.CmdBuilder.BuildPacmanCmd(ctx,
-		arguments, config.Runtime.Mode, settings.NoConfirm)); errShow != nil {
+	if errShow := cmdBuilder.Show(cmdBuilder.BuildPacmanCmd(ctx,
+		arguments, mode, settings.NoConfirm)); errShow != nil {
 		return errShow
 	}
 
-	if errStore := config.Runtime.VCSStore.Save(); errStore != nil {
+	if errStore := vcsStore.Save(); errStore != nil {
 		fmt.Fprintln(os.Stderr, errStore)
 	}
 
 	return nil
 }
 
-func setInstallReason(ctx context.Context, cmdArgs *parser.Arguments, deps, exps []string) error {
+func setInstallReason(ctx context.Context,
+	cmdBuilder exe.ICmdBuilder, mode parser.TargetMode,
+	cmdArgs *parser.Arguments, deps, exps []string,
+) error {
 	if len(deps)+len(exps) == 0 {
 		return nil
 	}
 
-	if errDeps := asdeps(ctx, cmdArgs, deps); errDeps != nil {
+	if errDeps := asdeps(ctx, cmdBuilder, mode, cmdArgs, deps); errDeps != nil {
 		return errDeps
 	}
 
-	return asexp(ctx, cmdArgs, exps)
+	return asexp(ctx, cmdBuilder, mode, cmdArgs, exps)
 }
 
 func doAddTarget(dp *dep.Pool, localNamesCache, remoteNamesCache stringset.StringSet,
@@ -875,10 +902,7 @@ func doAddTarget(dp *dep.Pool, localNamesCache, remoteNamesCache stringset.Strin
 			return deps, exp, pkgArchives, nil
 		}
 
-		return deps, exp, pkgArchives, errors.New(
-			gotext.Get(
-				"the PKGDEST for %s is listed by makepkg but does not exist: %s",
-				name, pkgdest))
+		return deps, exp, pkgArchives, &FindPkgDestError{pkgDest: pkgdest, name: name}
 	}
 
 	pkgArchives = append(pkgArchives, pkgdest)

+ 4 - 0
pkg/db/mock/executor.go

@@ -16,6 +16,7 @@ type (
 
 type DBExecutor struct {
 	db.Executor
+	IsCorrectVersionInstalledFunc func(string, string) bool
 }
 
 func (t DBExecutor) AlpmArchitectures() ([]string, error) {
@@ -31,6 +32,9 @@ func (t DBExecutor) Cleanup() {
 }
 
 func (t DBExecutor) IsCorrectVersionInstalled(s, s2 string) bool {
+	if t.IsCorrectVersionInstalledFunc != nil {
+		return t.IsCorrectVersionInstalledFunc(s, s2)
+	}
 	panic("implement me")
 }
 

+ 109 - 0
pkg/settings/exe/mock.go

@@ -0,0 +1,109 @@
+package exe
+
+import (
+	"context"
+	"os/exec"
+
+	"github.com/Jguer/yay/v11/pkg/settings/parser"
+)
+
+type Call struct {
+	Res  []interface{}
+	Args []interface{}
+}
+
+type MockBuilder struct {
+	Runner               Runner
+	BuildMakepkgCmdCalls []Call
+	BuildMakepkgCmdFn    func(ctx context.Context, dir string, extraArgs ...string) *exec.Cmd
+	BuildPacmanCmdFn     func(ctx context.Context, args *parser.Arguments, mode parser.TargetMode, noConfirm bool) *exec.Cmd
+}
+
+type MockRunner struct {
+	ShowCalls    []Call
+	CaptureCalls []Call
+	ShowFn       func(cmd *exec.Cmd) error
+	CaptureFn    func(cmd *exec.Cmd) (stdout string, stderr string, err error)
+}
+
+func (m *MockBuilder) BuildMakepkgCmd(ctx context.Context, dir string, extraArgs ...string) *exec.Cmd {
+	var res *exec.Cmd
+	if m.BuildMakepkgCmdFn != nil {
+		res = m.BuildMakepkgCmdFn(ctx, dir, extraArgs...)
+	} else {
+		res = exec.CommandContext(ctx, "makepkg", extraArgs...)
+	}
+
+	m.BuildMakepkgCmdCalls = append(m.BuildMakepkgCmdCalls, Call{
+		Res: []interface{}{res},
+		Args: []interface{}{
+			ctx,
+			dir,
+			extraArgs,
+		},
+	})
+
+	return res
+}
+
+func (m *MockBuilder) AddMakepkgFlag(flag string) {
+}
+
+func (m *MockBuilder) BuildGitCmd(ctx context.Context, dir string, extraArgs ...string) *exec.Cmd {
+	return exec.CommandContext(ctx, "git", extraArgs...)
+}
+
+func (m *MockBuilder) BuildPacmanCmd(ctx context.Context, args *parser.Arguments, mode parser.TargetMode, noConfirm bool) *exec.Cmd {
+	var res *exec.Cmd
+
+	if m.BuildPacmanCmdFn != nil {
+		res = m.BuildPacmanCmdFn(ctx, args, mode, noConfirm)
+	} else {
+		res = exec.CommandContext(ctx, "pacman")
+	}
+
+	return res
+}
+
+func (m *MockBuilder) SetPacmanDBPath(path string) {
+}
+
+func (m *MockBuilder) SudoLoop() {
+}
+
+func (m *MockBuilder) Capture(cmd *exec.Cmd) (stdout, stderr string, err error) {
+	return m.Runner.Capture(cmd)
+}
+
+func (m *MockBuilder) Show(cmd *exec.Cmd) error {
+	return m.Runner.Show(cmd)
+}
+
+func (m *MockRunner) Capture(cmd *exec.Cmd) (stdout, stderr string, err error) {
+	m.CaptureCalls = append(m.CaptureCalls, Call{
+		Args: []interface{}{
+			cmd,
+		},
+	})
+
+	if m.CaptureFn != nil {
+		return m.CaptureFn(cmd)
+	}
+
+	return "", "", nil
+}
+
+func (m *MockRunner) Show(cmd *exec.Cmd) error {
+	var err error
+	if m.ShowFn != nil {
+		err = m.ShowFn(cmd)
+	}
+
+	m.ShowCalls = append(m.ShowCalls, Call{
+		Args: []interface{}{
+			cmd,
+		},
+	})
+
+	return err
+}

+ 25 - 0
pkg/vcs/mock.go

@@ -0,0 +1,25 @@
+package vcs
+
+import (
+	"context"
+	"sync"
+
+	gosrc "github.com/Morganamilo/go-srcinfo"
+)
+
+type Mock struct{}
+
+func (m *Mock) Update(ctx context.Context, pkgName string, sources []gosrc.ArchString, mux sync.Locker, wg *sync.WaitGroup) {
+	wg.Done()
+}
+
+func (m *Mock) Save() error {
+	return nil
+}
+
+func (m *Mock) RemovePackage(pkgs []string) {
+}
+
+func (m *Mock) Load() error {
+	return nil
+}

+ 9 - 0
pkg/vcs/vcs.go

@@ -17,6 +17,15 @@ import (
 	"github.com/Jguer/yay/v11/pkg/text"
 )
 
+type Store interface {
+	Update(ctx context.Context, pkgName string,
+		sources []gosrc.ArchString, mux sync.Locker, wg *sync.WaitGroup,
+	)
+	Save() error
+	RemovePackage(pkgs []string)
+	Load() error
+}
+
 // InfoStore is a collection of OriginInfoByURL by Package.
 // Containing a map of last commit SHAs of a repo.
 type InfoStore struct {

+ 4 - 4
sync.go

@@ -58,14 +58,14 @@ func syncInstall(ctx context.Context,
 
 type OperationService struct {
 	ctx        context.Context
-	config     *settings.Configuration
+	cfg        *settings.Configuration
 	dbExecutor db.Executor
 }
 
-func NewOperationService(ctx context.Context, config *settings.Configuration, dbExecutor db.Executor) *OperationService {
+func NewOperationService(ctx context.Context, cfg *settings.Configuration, dbExecutor db.Executor) *OperationService {
 	return &OperationService{
 		ctx:        ctx,
-		config:     config,
+		cfg:        cfg,
 		dbExecutor: dbExecutor,
 	}
 }
@@ -79,7 +79,7 @@ func (o *OperationService) Run(ctx context.Context,
 		return nil
 	}
 	preparer := NewPreparer(o.dbExecutor, config.Runtime.CmdBuilder, config)
-	installer := NewInstaller(o.dbExecutor)
+	installer := NewInstaller(o.dbExecutor, o.cfg.Runtime.CmdBuilder, o.cfg.Runtime.VCSStore, o.cfg.Runtime.Mode)
 
 	pkgBuildDirs, err := preparer.Run(ctx, os.Stdout, targets)
 	if err != nil {