Преглед на файлове

refactor(yay): move cfg inside of runtime (#2259)

* rework relationship between runtime and cfg

* separate runtime from cfg

* simplify instantiation logic

* move installer to appropriate package

* move operator to sync package

* add tests for srcinfo service

* consolidate srcinfo service in sync

* add logger to srcinfo

* add logger to preparer

* remove unused text functions

* remove remaining text.* from srcinfo

* remove global logger parts

* remove global org method exports

* remove global logger

* move text->input

* add rule to prevent fmt.Print

* update golangci go version

* remove outdated FAQs

* remove outdated FAQs
Jo преди 1 година
родител
ревизия
8916cd174b
променени са 74 файла, в които са добавени 1435 реда и са изтрити 1327 реда
  1. 6 1
      .golangci.yml
  2. 1 27
      README.md
  3. 26 54
      clean.go
  4. 3 4
      clean_test.go
  5. 93 92
      cmd.go
  6. 12 11
      cmd_test.go
  7. 0 53
      errors.go
  8. 11 11
      get.go
  9. 10 11
      local_install.go
  10. 82 76
      local_install_test.go
  11. 19 49
      main.go
  12. 11 9
      pkg/cmd/graph/main.go
  13. 0 4
      pkg/dep/topo/dep.go
  14. 2 2
      pkg/download/aur.go
  15. 1 1
      pkg/download/aur_test.go
  16. 9 9
      pkg/download/unified.go
  17. 12 7
      pkg/download/unified_test.go
  18. 9 8
      pkg/menus/clean_menu.go
  19. 10 10
      pkg/menus/diff_menu.go
  20. 9 8
      pkg/menus/edit_menu.go
  21. 9 8
      pkg/menus/menu.go
  22. 9 9
      pkg/news/news.go
  23. 9 8
      pkg/news/news_test.go
  24. 0 3
      pkg/query/aur_warnings.go
  25. 3 3
      pkg/query/filter.go
  26. 1 1
      pkg/query/query_builder.go
  27. 2 2
      pkg/settings/pacman.go
  28. 2 2
      pkg/settings/pacman_test.go
  29. 27 11
      pkg/settings/runtime.go
  30. 15 15
      pkg/settings/runtime_test.go
  31. 0 5
      pkg/settings/args.go
  32. 50 79
      pkg/settings/config.go
  33. 3 3
      pkg/settings/config_test.go
  34. 22 6
      pkg/settings/exe/cmd_builder.go
  35. 4 0
      pkg/settings/exe/exec.go
  36. 4 0
      pkg/settings/exe/mock.go
  37. 2 2
      pkg/settings/migrations.go
  38. 6 11
      pkg/settings/migrations_test.go
  39. 62 0
      pkg/sync/build/errors.go
  40. 13 13
      aur_install.go
  41. 13 12
      aur_install_test.go
  42. 56 122
      install.go
  43. 10 13
      pkg/pgp/keys.go
  44. 7 1
      pkg/pgp/keys_test.go
  45. 0 0
      pkg/sync/srcinfo/pgp/testdata/11E521D646982372EB577A1F8F0871F202119294
  46. 0 0
      pkg/sync/srcinfo/pgp/testdata/487EACC08557AD082088DABA1EB2638FF56C0C53
  47. 0 0
      pkg/sync/srcinfo/pgp/testdata/647F28654894E3BD457199BE38DBBDC86092693E
  48. 0 0
      pkg/sync/srcinfo/pgp/testdata/A314827C4E4250A204CE6E13284FC34C8E4B1A25
  49. 0 0
      pkg/sync/srcinfo/pgp/testdata/ABAF11C65A2970B130ABE3C479BE3E4300411886
  50. 0 0
      pkg/sync/srcinfo/pgp/testdata/B6C8F98282B944E3B0D5C2530FC3042E345AD05D
  51. 0 0
      pkg/sync/srcinfo/pgp/testdata/C52048C0C0748FEE227D47A2702353E0F7E48EDB
  52. 13 12
      pkg/srcinfo/service.go
  53. 132 0
      pkg/sync/srcinfo/service_test.go
  54. 138 0
      pkg/sync/sync.go
  55. 1 1
      aur_source.go
  56. 1 1
      aur_source_test.go
  57. 62 0
      pkg/sync/workdir/clean.go
  58. 39 0
      pkg/sync/workdir/merge.go
  59. 29 23
      preparer.go
  60. 9 2
      preparer_test.go
  61. 51 5
      pkg/text/input.go
  62. 0 139
      pkg/text/print.go
  63. 6 0
      pkg/text/service.go
  64. 0 51
      pkg/text/text.go
  65. 7 3
      pkg/text/text_test.go
  66. 8 6
      pkg/vcs/vcs_test.go
  67. 129 55
      print.go
  68. 13 13
      print_test.go
  69. 12 11
      query.go
  70. 17 16
      query_test.go
  71. 26 125
      sync.go
  72. 72 73
      sync_test.go
  73. 11 12
      vcs.go
  74. 4 3
      vote.go

+ 6 - 1
.golangci.yml

@@ -47,6 +47,7 @@ linters:
   # inverted configuration with `enable-all` and `disable` is not scalable during updates of golangci-lint
   disable-all: true
   enable:
+    - forbidigo
     - bodyclose
     - dogsled
     - dupl
@@ -80,8 +81,12 @@ linters:
     - whitespace
 
 run:
-  go: "1.18"
+  go: "1.20"
   timeout: "10m"
+  forbidigo:
+    forbid:
+      - p: ^fmt\.Print.*$
+        msg: Do not commit print statements.
 
 issues:
   exclude-rules:

+ 1 - 27
README.md

@@ -111,17 +111,6 @@ pacman -S --needed git base-devel yay
   Make sure you have the `Color` option in your `/etc/pacman.conf`
   (see issue [#123](https://github.com/Jguer/yay/issues/123)).
 
-- **Yay is not prompting to skip packages during system upgrade.**
-
-  The default behavior was changed after
-  [v8.918](https://github.com/Jguer/yay/releases/tag/v8.918)
-  (see [3bdb534](https://github.com/Jguer/yay/commit/3bdb5343218d99d40f8a449b887348611f6bdbfc)
-  and issue [#554](https://github.com/Jguer/yay/issues/554)).
-  To restore the package-skip behavior use `--combinedupgrade` (make
-  it permanent by appending `--save`). Note: skipping packages will leave your
-  system in a
-  [partially-upgraded state](https://wiki.archlinux.org/index.php/System_maintenance#Partial_upgrades_are_unsupported).
-
 - **Sometimes diffs are printed to the terminal, and other times they are paged via less. How do I fix this?**
 
   Yay uses `git diff` to display diffs, which by default tells less not to
@@ -137,7 +126,7 @@ pacman -S --needed git base-devel yay
   `yay -{OPERATION} --aur`
   `yay -{OPERATION} --repo`
 
-- **An `Out Of Date AUR Packages` message is displayed. Why doesn't Yay update them?**
+- **A `Flagged Out Of Date AUR Packages` message is displayed. Why doesn't Yay update them?**
 
   This message does not mean that updated AUR packages are available. It means
   the packages have been flagged out of date on the AUR, but
@@ -159,21 +148,6 @@ pacman -S --needed git base-devel yay
 
   Check [CONTRIBUTING.md](./CONTRIBUTING.md) for more information.
 
-- **What settings do you use?**
-
-  ```sh
-  yay -Y --devel --combinedupgrade --batchinstall --save
-  ```
-
-  Pacman conf options:
-
-  ```conf
-  UseSyslog
-  Color
-  CheckSpace
-  VerbosePkgLists
-  ```
-
 ## Support
 
 All support related to Yay should be requested via GitHub issues. Since Yay is not

+ 26 - 54
clean.go

@@ -2,7 +2,6 @@ package main
 
 import (
 	"context"
-	"fmt"
 	"os"
 	"path/filepath"
 
@@ -11,10 +10,10 @@ import (
 	"github.com/leonelquinteros/gotext"
 
 	"github.com/Jguer/yay/v12/pkg/db"
+	"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/text"
 )
 
 // CleanDependencies removes all dangling dependencies in system.
@@ -49,13 +48,13 @@ func cleanRemove(ctx context.Context, cfg *settings.Configuration,
 			arguments, cfg.Mode, settings.NoConfirm))
 }
 
-func syncClean(ctx context.Context, cfg *settings.Configuration, cmdArgs *parser.Arguments, dbExecutor db.Executor) error {
+func syncClean(ctx context.Context, run *runtime.Runtime, cmdArgs *parser.Arguments, dbExecutor db.Executor) error {
 	keepInstalled := false
 	keepCurrent := false
 
 	_, removeAll, _ := cmdArgs.GetArg("c", "clean")
 
-	for _, v := range cfg.Runtime.PacmanConf.CleanMethod {
+	for _, v := range run.PacmanConf.CleanMethod {
 		if v == "KeepInstalled" {
 			keepInstalled = true
 		} else if v == "KeepCurrent" {
@@ -63,14 +62,14 @@ func syncClean(ctx context.Context, cfg *settings.Configuration, cmdArgs *parser
 		}
 	}
 
-	if cfg.Mode.AtLeastRepo() {
-		if err := cfg.Runtime.CmdBuilder.Show(cfg.Runtime.CmdBuilder.BuildPacmanCmd(ctx,
-			cmdArgs, cfg.Mode, settings.NoConfirm)); err != nil {
+	if run.Cfg.Mode.AtLeastRepo() {
+		if err := run.CmdBuilder.Show(run.CmdBuilder.BuildPacmanCmd(ctx,
+			cmdArgs, run.Cfg.Mode, settings.NoConfirm)); err != nil {
 			return err
 		}
 	}
 
-	if !cfg.Mode.AtLeastAUR() {
+	if !run.Cfg.Mode.AtLeastAUR() {
 		return nil
 	}
 
@@ -81,10 +80,10 @@ func syncClean(ctx context.Context, cfg *settings.Configuration, cmdArgs *parser
 		question = gotext.Get("Do you want to remove all other AUR packages from cache?")
 	}
 
-	fmt.Println(gotext.Get("\nBuild directory:"), cfg.BuildDir)
+	run.Logger.Println(gotext.Get("\nBuild directory:"), run.Cfg.BuildDir)
 
-	if text.ContinueTask(os.Stdin, question, true, settings.NoConfirm) {
-		if err := cleanAUR(ctx, cfg, keepInstalled, keepCurrent, removeAll, dbExecutor); err != nil {
+	if run.Logger.ContinueTask(question, true, settings.NoConfirm) {
+		if err := cleanAUR(ctx, run, keepInstalled, keepCurrent, removeAll, dbExecutor); err != nil {
 			return err
 		}
 	}
@@ -93,24 +92,24 @@ func syncClean(ctx context.Context, cfg *settings.Configuration, cmdArgs *parser
 		return nil
 	}
 
-	if text.ContinueTask(os.Stdin, gotext.Get("Do you want to remove ALL untracked AUR files?"), true, settings.NoConfirm) {
-		return cleanUntracked(ctx, cfg)
+	if run.Logger.ContinueTask(gotext.Get("Do you want to remove ALL untracked AUR files?"), true, settings.NoConfirm) {
+		return cleanUntracked(ctx, run)
 	}
 
 	return nil
 }
 
-func cleanAUR(ctx context.Context, cfg *settings.Configuration,
+func cleanAUR(ctx context.Context, run *runtime.Runtime,
 	keepInstalled, keepCurrent, removeAll bool, dbExecutor db.Executor,
 ) error {
-	cfg.Runtime.Logger.Println(gotext.Get("removing AUR packages from cache..."))
+	run.Logger.Println(gotext.Get("removing AUR packages from cache..."))
 
 	installedBases := mapset.NewThreadUnsafeSet[string]()
 	inAURBases := mapset.NewThreadUnsafeSet[string]()
 
 	remotePackages := dbExecutor.InstalledRemotePackages()
 
-	files, err := os.ReadDir(cfg.BuildDir)
+	files, err := os.ReadDir(run.Cfg.BuildDir)
 	if err != nil {
 		return err
 	}
@@ -130,7 +129,7 @@ func cleanAUR(ctx context.Context, cfg *settings.Configuration,
 	// Querying the AUR is slow and needs internet so don't do it if we
 	// don't need to.
 	if keepCurrent {
-		info, errInfo := cfg.Runtime.AURClient.Get(ctx, &aur.Query{
+		info, errInfo := run.AURClient.Get(ctx, &aur.Query{
 			Needles: cachedPackages,
 		})
 		if errInfo != nil {
@@ -165,20 +164,20 @@ func cleanAUR(ctx context.Context, cfg *settings.Configuration,
 			}
 		}
 
-		dir := filepath.Join(cfg.BuildDir, file.Name())
-		cfg.Runtime.Logger.Debugln("removing", dir)
+		dir := filepath.Join(run.Cfg.BuildDir, file.Name())
+		run.Logger.Debugln("removing", dir)
 		if err = os.RemoveAll(dir); err != nil {
-			cfg.Runtime.Logger.Warnln(gotext.Get("Unable to remove %s: %s", dir, err))
+			run.Logger.Warnln(gotext.Get("Unable to remove %s: %s", dir, err))
 		}
 	}
 
 	return nil
 }
 
-func cleanUntracked(ctx context.Context, cfg *settings.Configuration) error {
-	cfg.Runtime.Logger.Println(gotext.Get("removing untracked AUR files from cache..."))
+func cleanUntracked(ctx context.Context, run *runtime.Runtime) error {
+	run.Logger.Println(gotext.Get("removing untracked AUR files from cache..."))
 
-	files, err := os.ReadDir(cfg.BuildDir)
+	files, err := os.ReadDir(run.Cfg.BuildDir)
 	if err != nil {
 		return err
 	}
@@ -188,12 +187,11 @@ func cleanUntracked(ctx context.Context, cfg *settings.Configuration) error {
 			continue
 		}
 
-		dir := filepath.Join(cfg.BuildDir, file.Name())
-		cfg.Runtime.Logger.Debugln("cleaning", dir)
+		dir := filepath.Join(run.Cfg.BuildDir, file.Name())
+		run.Logger.Debugln("cleaning", dir)
 		if isGitRepository(dir) {
-			if err := cfg.Runtime.CmdBuilder.Show(cfg.Runtime.CmdBuilder.BuildGitCmd(ctx, dir, "clean", "-fx")); err != nil {
-				cfg.Runtime.Logger.Warnln(gotext.Get("Unable to clean:"), dir)
-
+			if err := run.CmdBuilder.Show(run.CmdBuilder.BuildGitCmd(ctx, dir, "clean", "-fx")); err != nil {
+				run.Logger.Warnln(gotext.Get("Unable to clean:"), dir)
 				return err
 			}
 		}
@@ -206,29 +204,3 @@ func isGitRepository(dir string) bool {
 	_, err := os.Stat(filepath.Join(dir, ".git"))
 	return !os.IsNotExist(err)
 }
-
-func cleanAfter(ctx context.Context, config *settings.Configuration,
-	cmdBuilder exe.ICmdBuilder, pkgbuildDirs map[string]string,
-) {
-	fmt.Println(gotext.Get("removing untracked AUR files from cache..."))
-
-	i := 0
-	for _, dir := range pkgbuildDirs {
-		text.OperationInfoln(gotext.Get("Cleaning (%d/%d): %s", i+1, len(pkgbuildDirs), text.Cyan(dir)))
-
-		_, stderr, err := cmdBuilder.Capture(
-			cmdBuilder.BuildGitCmd(
-				ctx, dir, "reset", "--hard", "HEAD"))
-		if err != nil {
-			text.Errorln(gotext.Get("error resetting %s: %s", dir, stderr))
-		}
-
-		if err := config.Runtime.CmdBuilder.Show(
-			config.Runtime.CmdBuilder.BuildGitCmd(
-				ctx, dir, "clean", "-fx", "--exclude", "*.pkg.*")); err != nil {
-			fmt.Fprintln(os.Stderr, err)
-		}
-
-		i++
-	}
-}

+ 3 - 4
clean_test.go

@@ -15,6 +15,7 @@ import (
 	"github.com/stretchr/testify/require"
 
 	"github.com/Jguer/yay/v12/pkg/db/mock"
+	"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"
@@ -90,15 +91,13 @@ func TestCleanHanging(t *testing.T) {
 				Runner:           mockRunner,
 				SudoLoopEnabled:  false,
 			}
-			cfg := &settings.Configuration{
-				Runtime: &settings.Runtime{CmdBuilder: cmdBuilder},
-			}
 
+			run := &runtime.Runtime{CmdBuilder: cmdBuilder, Cfg: &settings.Configuration{}}
 			cmdArgs := parser.MakeArguments()
 			cmdArgs.AddArg(tc.args...)
 
 			err := handleCmd(context.Background(),
-				cfg, cmdArgs, dbExc,
+				run, cmdArgs, dbExc,
 			)
 
 			require.NoError(t, err)

+ 93 - 92
cmd.go

@@ -17,6 +17,7 @@ import (
 	"github.com/Jguer/yay/v12/pkg/intrange"
 	"github.com/Jguer/yay/v12/pkg/news"
 	"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"
@@ -25,8 +26,8 @@ import (
 	"github.com/Jguer/yay/v12/pkg/vcs"
 )
 
-func usage() {
-	fmt.Println(`Usage:
+func usage(logger *text.Logger) {
+	logger.Println(`Usage:
     yay
     yay <operation> [...]
     yay <package(s)>
@@ -146,50 +147,49 @@ getpkgbuild specific options:
     -p --print            Print pkgbuild of packages`)
 }
 
-func handleCmd(ctx context.Context, cfg *settings.Configuration,
+func handleCmd(ctx context.Context, run *runtime.Runtime,
 	cmdArgs *parser.Arguments, dbExecutor db.Executor,
 ) error {
 	if cmdArgs.ExistsArg("h", "help") {
-		return handleHelp(ctx, cfg, cmdArgs)
+		return handleHelp(ctx, run, cmdArgs)
 	}
 
-	if cfg.SudoLoop && cmdArgs.NeedRoot(cfg.Mode) {
-		cfg.Runtime.CmdBuilder.SudoLoop()
+	if run.Cfg.SudoLoop && cmdArgs.NeedRoot(run.Cfg.Mode) {
+		run.CmdBuilder.SudoLoop()
 	}
 
 	switch cmdArgs.Op {
 	case "V", "version":
-		handleVersion()
-
+		handleVersion(run.Logger)
 		return nil
 	case "D", "database":
-		return cfg.Runtime.CmdBuilder.Show(cfg.Runtime.CmdBuilder.BuildPacmanCmd(ctx,
-			cmdArgs, cfg.Mode, settings.NoConfirm))
+		return run.CmdBuilder.Show(run.CmdBuilder.BuildPacmanCmd(ctx,
+			cmdArgs, run.Cfg.Mode, settings.NoConfirm))
 	case "F", "files":
-		return cfg.Runtime.CmdBuilder.Show(cfg.Runtime.CmdBuilder.BuildPacmanCmd(ctx,
-			cmdArgs, cfg.Mode, settings.NoConfirm))
+		return run.CmdBuilder.Show(run.CmdBuilder.BuildPacmanCmd(ctx,
+			cmdArgs, run.Cfg.Mode, settings.NoConfirm))
 	case "Q", "query":
-		return handleQuery(ctx, cfg, cmdArgs, dbExecutor)
+		return handleQuery(ctx, run, cmdArgs, dbExecutor)
 	case "R", "remove":
-		return handleRemove(ctx, cfg, cmdArgs, cfg.Runtime.VCSStore)
+		return handleRemove(ctx, run, cmdArgs, run.VCSStore)
 	case "S", "sync":
-		return handleSync(ctx, cfg, cmdArgs, dbExecutor)
+		return handleSync(ctx, run, cmdArgs, dbExecutor)
 	case "T", "deptest":
-		return cfg.Runtime.CmdBuilder.Show(cfg.Runtime.CmdBuilder.BuildPacmanCmd(ctx,
-			cmdArgs, cfg.Mode, settings.NoConfirm))
+		return run.CmdBuilder.Show(run.CmdBuilder.BuildPacmanCmd(ctx,
+			cmdArgs, run.Cfg.Mode, settings.NoConfirm))
 	case "U", "upgrade":
-		return handleUpgrade(ctx, cfg, cmdArgs)
+		return handleUpgrade(ctx, run, cmdArgs)
 	case "B", "build":
-		return handleBuild(ctx, cfg, dbExecutor, cmdArgs)
+		return handleBuild(ctx, run, dbExecutor, cmdArgs)
 	case "G", "getpkgbuild":
-		return handleGetpkgbuild(ctx, cfg, cmdArgs, dbExecutor)
+		return handleGetpkgbuild(ctx, run, cmdArgs, dbExecutor)
 	case "P", "show":
-		return handlePrint(ctx, cfg, cmdArgs, dbExecutor)
+		return handlePrint(ctx, run, cmdArgs, dbExecutor)
 	case "Y", "yay":
-		return handleYay(ctx, cfg, cmdArgs, cfg.Runtime.CmdBuilder,
-			dbExecutor, cfg.Runtime.QueryBuilder)
+		return handleYay(ctx, run, cmdArgs, run.CmdBuilder,
+			dbExecutor, run.QueryBuilder)
 	case "W", "web":
-		return handleWeb(ctx, cfg, cmdArgs)
+		return handleWeb(ctx, run, cmdArgs)
 	}
 
 	return errors.New(gotext.Get("unhandled operation"))
@@ -219,19 +219,19 @@ func getFilter(cmdArgs *parser.Arguments) (upgrade.Filter, error) {
 	}, nil
 }
 
-func handleQuery(ctx context.Context, cfg *settings.Configuration, cmdArgs *parser.Arguments, dbExecutor db.Executor) error {
+func handleQuery(ctx context.Context, run *runtime.Runtime, cmdArgs *parser.Arguments, dbExecutor db.Executor) error {
 	if cmdArgs.ExistsArg("u", "upgrades") {
 		filter, err := getFilter(cmdArgs)
 		if err != nil {
 			return err
 		}
 
-		return printUpdateList(ctx, cfg, cmdArgs, dbExecutor,
+		return printUpdateList(ctx, run, cmdArgs, dbExecutor,
 			cmdArgs.ExistsDouble("u", "sysupgrade"), filter)
 	}
 
-	if err := cfg.Runtime.CmdBuilder.Show(cfg.Runtime.CmdBuilder.BuildPacmanCmd(ctx,
-		cmdArgs, cfg.Mode, settings.NoConfirm)); err != nil {
+	if err := run.CmdBuilder.Show(run.CmdBuilder.BuildPacmanCmd(ctx,
+		cmdArgs, run.Cfg.Mode, settings.NoConfirm)); err != nil {
 		if str := err.Error(); strings.Contains(str, "exit status") {
 			// yay -Qdt should not output anything in case of error
 			return fmt.Errorf("")
@@ -243,138 +243,139 @@ func handleQuery(ctx context.Context, cfg *settings.Configuration, cmdArgs *pars
 	return nil
 }
 
-func handleHelp(ctx context.Context, cfg *settings.Configuration, cmdArgs *parser.Arguments) error {
-	usage()
+func handleHelp(ctx context.Context, run *runtime.Runtime, cmdArgs *parser.Arguments) error {
+	usage(run.Logger)
 	switch cmdArgs.Op {
 	case "Y", "yay", "G", "getpkgbuild", "P", "show", "W", "web", "B", "build":
 		return nil
 	}
 
-	cfg.Runtime.Logger.Println("\npacman operation specific options:")
-	return cfg.Runtime.CmdBuilder.Show(cfg.Runtime.CmdBuilder.BuildPacmanCmd(ctx,
-		cmdArgs, cfg.Mode, settings.NoConfirm))
+	run.Logger.Println("\npacman operation specific options:")
+	return run.CmdBuilder.Show(run.CmdBuilder.BuildPacmanCmd(ctx,
+		cmdArgs, run.Cfg.Mode, settings.NoConfirm))
 }
 
-func handleVersion() {
-	fmt.Printf("yay v%s - libalpm v%s\n", yayVersion, alpm.Version())
+func handleVersion(logger *text.Logger) {
+	logger.Printf("yay v%s - libalpm v%s\n", yayVersion, alpm.Version())
 }
 
-func handlePrint(ctx context.Context, cfg *settings.Configuration, cmdArgs *parser.Arguments, dbExecutor db.Executor) error {
+func handlePrint(ctx context.Context, run *runtime.Runtime, cmdArgs *parser.Arguments, dbExecutor db.Executor) error {
 	switch {
 	case cmdArgs.ExistsArg("d", "defaultconfig"):
 		tmpConfig := settings.DefaultConfig(yayVersion)
-		fmt.Printf("%v", tmpConfig)
+		run.Logger.Printf("%v", tmpConfig)
 
 		return nil
 	case cmdArgs.ExistsArg("g", "currentconfig"):
-		fmt.Printf("%v", cfg)
+		run.Logger.Printf("%v", run.Cfg)
 
 		return nil
 	case cmdArgs.ExistsArg("w", "news"):
 		double := cmdArgs.ExistsDouble("w", "news")
 		quiet := cmdArgs.ExistsArg("q", "quiet")
 
-		return news.PrintNewsFeed(ctx, cfg.Runtime.HTTPClient, dbExecutor.LastBuildTime(), cfg.BottomUp, double, quiet)
+		return news.PrintNewsFeed(ctx, run.HTTPClient, run.Logger,
+			dbExecutor.LastBuildTime(), run.Cfg.BottomUp, double, quiet)
 	case cmdArgs.ExistsArg("c", "complete"):
-		return completion.Show(ctx, cfg.Runtime.HTTPClient, dbExecutor,
-			cfg.AURURL, cfg.CompletionPath, cfg.CompletionInterval, cmdArgs.ExistsDouble("c", "complete"))
+		return completion.Show(ctx, run.HTTPClient, dbExecutor,
+			run.Cfg.AURURL, run.Cfg.CompletionPath, run.Cfg.CompletionInterval, cmdArgs.ExistsDouble("c", "complete"))
 	case cmdArgs.ExistsArg("s", "stats"):
-		return localStatistics(ctx, cfg, dbExecutor)
+		return localStatistics(ctx, run, dbExecutor)
 	}
 
 	return nil
 }
 
-func handleYay(ctx context.Context, cfg *settings.Configuration,
+func handleYay(ctx context.Context, run *runtime.Runtime,
 	cmdArgs *parser.Arguments, cmdBuilder exe.ICmdBuilder,
 	dbExecutor db.Executor, queryBuilder query.Builder,
 ) error {
 	switch {
 	case cmdArgs.ExistsArg("gendb"):
-		return createDevelDB(ctx, cfg, dbExecutor)
+		return createDevelDB(ctx, run, dbExecutor)
 	case cmdArgs.ExistsDouble("c"):
-		return cleanDependencies(ctx, cfg, cmdBuilder, cmdArgs, dbExecutor, true)
+		return cleanDependencies(ctx, run.Cfg, cmdBuilder, cmdArgs, dbExecutor, true)
 	case cmdArgs.ExistsArg("c", "clean"):
-		return cleanDependencies(ctx, cfg, cmdBuilder, cmdArgs, dbExecutor, false)
+		return cleanDependencies(ctx, run.Cfg, cmdBuilder, cmdArgs, dbExecutor, false)
 	case len(cmdArgs.Targets) > 0:
-		return displayNumberMenu(ctx, cfg, cmdArgs.Targets, dbExecutor, queryBuilder, cmdArgs)
+		return displayNumberMenu(ctx, run, cmdArgs.Targets, dbExecutor, queryBuilder, cmdArgs)
 	}
 
 	return nil
 }
 
-func handleWeb(ctx context.Context, cfg *settings.Configuration, cmdArgs *parser.Arguments) error {
+func handleWeb(ctx context.Context, run *runtime.Runtime, cmdArgs *parser.Arguments) error {
 	switch {
 	case cmdArgs.ExistsArg("v", "vote"):
-		return handlePackageVote(ctx, cmdArgs.Targets, cfg.Runtime.AURClient,
-			cfg.Runtime.VoteClient, true)
+		return handlePackageVote(ctx, cmdArgs.Targets, run.AURClient, run.Logger,
+			run.VoteClient, true)
 	case cmdArgs.ExistsArg("u", "unvote"):
-		return handlePackageVote(ctx, cmdArgs.Targets, cfg.Runtime.AURClient,
-			cfg.Runtime.VoteClient, false)
+		return handlePackageVote(ctx, cmdArgs.Targets, run.AURClient, run.Logger,
+			run.VoteClient, false)
 	}
 
 	return nil
 }
 
-func handleGetpkgbuild(ctx context.Context, cfg *settings.Configuration, cmdArgs *parser.Arguments, dbExecutor download.DBSearcher) error {
+func handleGetpkgbuild(ctx context.Context, run *runtime.Runtime, cmdArgs *parser.Arguments, dbExecutor download.DBSearcher) error {
 	if cmdArgs.ExistsArg("p", "print") {
-		return printPkgbuilds(dbExecutor, cfg.Runtime.AURClient,
-			cfg.Runtime.HTTPClient, cmdArgs.Targets, cfg.Mode, cfg.AURURL)
+		return printPkgbuilds(dbExecutor, run.AURClient,
+			run.HTTPClient, run.Logger, cmdArgs.Targets, run.Cfg.Mode, run.Cfg.AURURL)
 	}
 
-	return getPkgbuilds(ctx, dbExecutor, cfg.Runtime.AURClient, cfg,
+	return getPkgbuilds(ctx, dbExecutor, run.AURClient, run,
 		cmdArgs.Targets, cmdArgs.ExistsArg("f", "force"))
 }
 
 func handleUpgrade(ctx context.Context,
-	config *settings.Configuration, cmdArgs *parser.Arguments,
+	run *runtime.Runtime, cmdArgs *parser.Arguments,
 ) error {
-	return config.Runtime.CmdBuilder.Show(config.Runtime.CmdBuilder.BuildPacmanCmd(ctx,
-		cmdArgs, config.Mode, settings.NoConfirm))
+	return run.CmdBuilder.Show(run.CmdBuilder.BuildPacmanCmd(ctx,
+		cmdArgs, run.Cfg.Mode, settings.NoConfirm))
 }
 
 // -B* options
 func handleBuild(ctx context.Context,
-	config *settings.Configuration, dbExecutor db.Executor, cmdArgs *parser.Arguments,
+	run *runtime.Runtime, dbExecutor db.Executor, cmdArgs *parser.Arguments,
 ) error {
 	if cmdArgs.ExistsArg("i", "install") {
-		return installLocalPKGBUILD(ctx, config, cmdArgs, dbExecutor)
+		return installLocalPKGBUILD(ctx, run, cmdArgs, dbExecutor)
 	}
 
 	return nil
 }
 
-func handleSync(ctx context.Context, cfg *settings.Configuration, cmdArgs *parser.Arguments, dbExecutor db.Executor) error {
+func handleSync(ctx context.Context, run *runtime.Runtime, cmdArgs *parser.Arguments, dbExecutor db.Executor) error {
 	targets := cmdArgs.Targets
 
 	switch {
 	case cmdArgs.ExistsArg("s", "search"):
-		return syncSearch(ctx, targets, dbExecutor, cfg.Runtime.QueryBuilder, !cmdArgs.ExistsArg("q", "quiet"))
+		return syncSearch(ctx, targets, dbExecutor, run.QueryBuilder, !cmdArgs.ExistsArg("q", "quiet"))
 	case cmdArgs.ExistsArg("p", "print", "print-format"):
-		return cfg.Runtime.CmdBuilder.Show(cfg.Runtime.CmdBuilder.BuildPacmanCmd(ctx,
-			cmdArgs, cfg.Mode, settings.NoConfirm))
+		return run.CmdBuilder.Show(run.CmdBuilder.BuildPacmanCmd(ctx,
+			cmdArgs, run.Cfg.Mode, settings.NoConfirm))
 	case cmdArgs.ExistsArg("c", "clean"):
-		return syncClean(ctx, cfg, cmdArgs, dbExecutor)
+		return syncClean(ctx, run, cmdArgs, dbExecutor)
 	case cmdArgs.ExistsArg("l", "list"):
-		return syncList(ctx, cfg, cfg.Runtime.HTTPClient, cmdArgs, dbExecutor)
+		return syncList(ctx, run, run.HTTPClient, cmdArgs, dbExecutor)
 	case cmdArgs.ExistsArg("g", "groups"):
-		return cfg.Runtime.CmdBuilder.Show(cfg.Runtime.CmdBuilder.BuildPacmanCmd(ctx,
-			cmdArgs, cfg.Mode, settings.NoConfirm))
+		return run.CmdBuilder.Show(run.CmdBuilder.BuildPacmanCmd(ctx,
+			cmdArgs, run.Cfg.Mode, settings.NoConfirm))
 	case cmdArgs.ExistsArg("i", "info"):
-		return syncInfo(ctx, cfg, cmdArgs, targets, dbExecutor)
+		return syncInfo(ctx, run, cmdArgs, targets, dbExecutor)
 	case cmdArgs.ExistsArg("u", "sysupgrade") || len(cmdArgs.Targets) > 0:
-		return syncInstall(ctx, cfg, cmdArgs, dbExecutor)
+		return syncInstall(ctx, run, cmdArgs, dbExecutor)
 	case cmdArgs.ExistsArg("y", "refresh"):
-		return cfg.Runtime.CmdBuilder.Show(cfg.Runtime.CmdBuilder.BuildPacmanCmd(ctx,
-			cmdArgs, cfg.Mode, settings.NoConfirm))
+		return run.CmdBuilder.Show(run.CmdBuilder.BuildPacmanCmd(ctx,
+			cmdArgs, run.Cfg.Mode, settings.NoConfirm))
 	}
 
 	return nil
 }
 
-func handleRemove(ctx context.Context, cfg *settings.Configuration, cmdArgs *parser.Arguments, localCache vcs.Store) error {
-	err := cfg.Runtime.CmdBuilder.Show(cfg.Runtime.CmdBuilder.BuildPacmanCmd(ctx,
-		cmdArgs, cfg.Mode, settings.NoConfirm))
+func handleRemove(ctx context.Context, run *runtime.Runtime, cmdArgs *parser.Arguments, localCache vcs.Store) error {
+	err := run.CmdBuilder.Show(run.CmdBuilder.BuildPacmanCmd(ctx,
+		cmdArgs, run.Cfg.Mode, settings.NoConfirm))
 	if err == nil {
 		localCache.RemovePackages(cmdArgs.Targets)
 	}
@@ -383,7 +384,7 @@ func handleRemove(ctx context.Context, cfg *settings.Configuration, cmdArgs *par
 }
 
 // NumberMenu presents a CLI for selecting packages to install.
-func displayNumberMenu(ctx context.Context, cfg *settings.Configuration, pkgS []string, dbExecutor db.Executor,
+func displayNumberMenu(ctx context.Context, run *runtime.Runtime, pkgS []string, dbExecutor db.Executor,
 	queryBuilder query.Builder, cmdArgs *parser.Arguments,
 ) error {
 	queryBuilder.Execute(ctx, dbExecutor, pkgS)
@@ -397,9 +398,9 @@ func displayNumberMenu(ctx context.Context, cfg *settings.Configuration, pkgS []
 		return nil
 	}
 
-	cfg.Runtime.Logger.Infoln(gotext.Get("Packages to install (eg: 1 2 3, 1-3 or ^4)"))
+	run.Logger.Infoln(gotext.Get("Packages to install (eg: 1 2 3, 1-3 or ^4)"))
 
-	numberBuf, err := cfg.Runtime.Logger.GetInput("", false)
+	numberBuf, err := run.Logger.GetInput("", false)
 	if err != nil {
 		return err
 	}
@@ -415,27 +416,27 @@ func displayNumberMenu(ctx context.Context, cfg *settings.Configuration, pkgS []
 	cmdArgs.Targets = targets
 
 	if len(cmdArgs.Targets) == 0 {
-		fmt.Println(gotext.Get(" there is nothing to do"))
+		run.Logger.Println(gotext.Get(" there is nothing to do"))
 		return nil
 	}
 
-	return syncInstall(ctx, cfg, cmdArgs, dbExecutor)
+	return syncInstall(ctx, run, cmdArgs, dbExecutor)
 }
 
-func syncList(ctx context.Context, cfg *settings.Configuration,
+func syncList(ctx context.Context, run *runtime.Runtime,
 	httpClient *http.Client, cmdArgs *parser.Arguments, dbExecutor db.Executor,
 ) error {
 	aur := false
 
 	for i := len(cmdArgs.Targets) - 1; i >= 0; i-- {
-		if cmdArgs.Targets[i] == "aur" && cfg.Mode.AtLeastAUR() {
+		if cmdArgs.Targets[i] == "aur" && run.Cfg.Mode.AtLeastAUR() {
 			cmdArgs.Targets = append(cmdArgs.Targets[:i], cmdArgs.Targets[i+1:]...)
 			aur = true
 		}
 	}
 
-	if cfg.Mode.AtLeastAUR() && (len(cmdArgs.Targets) == 0 || aur) {
-		req, err := http.NewRequestWithContext(ctx, http.MethodGet, cfg.AURURL+"/packages.gz", http.NoBody)
+	if run.Cfg.Mode.AtLeastAUR() && (len(cmdArgs.Targets) == 0 || aur) {
+		req, err := http.NewRequestWithContext(ctx, http.MethodGet, run.Cfg.AURURL+"/packages.gz", http.NoBody)
 		if err != nil {
 			return err
 		}
@@ -453,22 +454,22 @@ func syncList(ctx context.Context, cfg *settings.Configuration,
 		for scanner.Scan() {
 			name := scanner.Text()
 			if cmdArgs.ExistsArg("q", "quiet") {
-				fmt.Println(name)
+				run.Logger.Println(name)
 			} else {
-				fmt.Printf("%s %s %s", text.Magenta("aur"), text.Bold(name), text.Bold(text.Green(gotext.Get("unknown-version"))))
+				run.Logger.Printf("%s %s %s", text.Magenta("aur"), text.Bold(name), text.Bold(text.Green(gotext.Get("unknown-version"))))
 
 				if dbExecutor.LocalPackage(name) != nil {
-					fmt.Print(text.Bold(text.Blue(gotext.Get(" [Installed]"))))
+					run.Logger.Print(text.Bold(text.Blue(gotext.Get(" [Installed]"))))
 				}
 
-				fmt.Println()
+				run.Logger.Println()
 			}
 		}
 	}
 
-	if cfg.Mode.AtLeastRepo() && (len(cmdArgs.Targets) != 0 || !aur) {
-		return cfg.Runtime.CmdBuilder.Show(cfg.Runtime.CmdBuilder.BuildPacmanCmd(ctx,
-			cmdArgs, cfg.Mode, settings.NoConfirm))
+	if run.Cfg.Mode.AtLeastRepo() && (len(cmdArgs.Targets) != 0 || !aur) {
+		return run.CmdBuilder.Show(run.CmdBuilder.BuildPacmanCmd(ctx,
+			cmdArgs, run.Cfg.Mode, settings.NoConfirm))
 	}
 
 	return nil

+ 12 - 11
cmd_test.go

@@ -19,6 +19,7 @@ import (
 	"github.com/Jguer/yay/v12/pkg/db/mock"
 	mockaur "github.com/Jguer/yay/v12/pkg/dep/mock"
 	"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"
@@ -103,19 +104,19 @@ func TestYogurtMenuAURDB(t *testing.T) {
 		},
 	}
 	logger := text.NewLogger(io.Discard, os.Stderr, strings.NewReader("1\n"), true, "test")
-	cfg := &settings.Configuration{
-		RemoveMake: "no",
-		Runtime: &settings.Runtime{
-			Logger:     logger,
-			CmdBuilder: cmdBuilder,
-			VCSStore:   &vcs.Mock{},
-			QueryBuilder: query.NewSourceQueryBuilder(aurCache, logger, "votes", parser.ModeAny, "name",
-				true, false, true),
-			AURClient: aurCache,
+
+	run := &runtime.Runtime{
+		Cfg: &settings.Configuration{
+			RemoveMake: "no",
 		},
+		Logger:     logger,
+		CmdBuilder: cmdBuilder,
+		VCSStore:   &vcs.Mock{},
+		QueryBuilder: query.NewSourceQueryBuilder(aurCache, logger, "votes", parser.ModeAny, "name",
+			true, false, true),
+		AURClient: aurCache,
 	}
-
-	err = handleCmd(context.Background(), cfg, cmdArgs, db)
+	err = handleCmd(context.Background(), run, cmdArgs, db)
 	require.NoError(t, err)
 
 	wantCapture := []string{}

+ 0 - 53
errors.go

@@ -7,56 +7,3 @@ import (
 )
 
 var ErrPackagesNotFound = errors.New(gotext.Get("could not find all required packages"))
-
-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
-}
-
-func (e *SetPkgReasonError) Error() string {
-	reason := gotext.Get("explicit")
-	if !e.exp {
-		reason = gotext.Get("dependency")
-	}
-
-	return gotext.Get("error updating package install reason to %s", reason)
-}
-
-type FindPkgDestError struct {
-	name, pkgDest string
-}
-
-func (e *FindPkgDestError) Error() string {
-	return gotext.Get(
-		"the PKGDEST for %s is listed by makepkg but does not exist: %s",
-		e.name, e.pkgDest)
-}
-
-type PkgDestNotInListError struct {
-	name string
-}
-
-func (e *PkgDestNotInListError) Error() string {
-	return gotext.Get("could not find PKGDEST for: %s", e.name)
-}
-
-type FailedIgnoredPkgError struct {
-	pkgErrors map[string]error
-}
-
-func (e *FailedIgnoredPkgError) Error() string {
-	msg := gotext.Get("Failed to install the following packages. Manual intervention is required:")
-
-	for pkg, err := range e.pkgErrors {
-		msg += "\n" + pkg + " - " + err.Error()
-	}
-
-	return msg
-}

+ 11 - 11
get.go

@@ -11,24 +11,24 @@ import (
 	"github.com/leonelquinteros/gotext"
 
 	"github.com/Jguer/yay/v12/pkg/download"
-	"github.com/Jguer/yay/v12/pkg/settings"
+	"github.com/Jguer/yay/v12/pkg/runtime"
 	"github.com/Jguer/yay/v12/pkg/settings/parser"
 	"github.com/Jguer/yay/v12/pkg/text"
 )
 
 // yay -Gp.
-func printPkgbuilds(dbExecutor download.DBSearcher, aurClient aur.QueryClient, httpClient *http.Client, targets []string,
+func printPkgbuilds(dbExecutor download.DBSearcher, aurClient aur.QueryClient,
+	httpClient *http.Client, logger *text.Logger, targets []string,
 	mode parser.TargetMode, aurURL string,
 ) error {
-	pkgbuilds, err := download.PKGBUILDs(dbExecutor, aurClient, httpClient, targets, aurURL, mode)
+	pkgbuilds, err := download.PKGBUILDs(dbExecutor, aurClient, httpClient, logger, targets, aurURL, mode)
 	if err != nil {
-		text.Errorln(err)
+		logger.Errorln(err)
 	}
 
 	if len(pkgbuilds) != 0 {
 		for target, pkgbuild := range pkgbuilds {
-			fmt.Printf("\n\n# %s\n\n", target)
-			fmt.Print(string(pkgbuild))
+			logger.Printf("\n\n# %s\n\n%s", target, string(pkgbuild))
 		}
 	}
 
@@ -41,7 +41,7 @@ func printPkgbuilds(dbExecutor download.DBSearcher, aurClient aur.QueryClient, h
 			}
 		}
 
-		text.Warnln(gotext.Get("Unable to find the following packages:"), " ", strings.Join(missing, ", "))
+		logger.Warnln(gotext.Get("Unable to find the following packages:"), " ", strings.Join(missing, ", "))
 
 		return fmt.Errorf("")
 	}
@@ -51,7 +51,7 @@ func printPkgbuilds(dbExecutor download.DBSearcher, aurClient aur.QueryClient, h
 
 // yay -G.
 func getPkgbuilds(ctx context.Context, dbExecutor download.DBSearcher, aurClient aur.QueryClient,
-	config *settings.Configuration, targets []string, force bool,
+	run *runtime.Runtime, targets []string, force bool,
 ) error {
 	wd, err := os.Getwd()
 	if err != nil {
@@ -59,9 +59,9 @@ func getPkgbuilds(ctx context.Context, dbExecutor download.DBSearcher, aurClient
 	}
 
 	cloned, errD := download.PKGBUILDRepos(ctx, dbExecutor, aurClient,
-		config.Runtime.CmdBuilder, targets, config.Mode, config.AURURL, wd, force)
+		run.CmdBuilder, run.Logger, targets, run.Cfg.Mode, run.Cfg.AURURL, wd, force)
 	if errD != nil {
-		text.Errorln(errD)
+		run.Logger.Errorln(errD)
 	}
 
 	if len(targets) != len(cloned) {
@@ -73,7 +73,7 @@ func getPkgbuilds(ctx context.Context, dbExecutor download.DBSearcher, aurClient
 			}
 		}
 
-		text.Warnln(gotext.Get("Unable to find the following packages:"), " ", strings.Join(missing, ", "))
+		run.Logger.Warnln(gotext.Get("Unable to find the following packages:"), " ", strings.Join(missing, ", "))
 
 		err = fmt.Errorf("")
 	}

+ 10 - 11
local_install.go

@@ -12,19 +12,18 @@ import (
 	"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/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"
 
 	gosrc "github.com/Morganamilo/go-srcinfo"
 	"github.com/leonelquinteros/gotext"
 	"github.com/pkg/errors"
 )
 
-var (
-	ErrInstallRepoPkgs = errors.New(gotext.Get("error installing repo packages"))
-	ErrNoBuildFiles    = errors.New(gotext.Get("cannot find PKGBUILD and .SRCINFO in directory"))
-)
+var ErrNoBuildFiles = errors.New(gotext.Get("cannot find PKGBUILD and .SRCINFO in directory"))
 
 func srcinfoExists(ctx context.Context,
 	cmdBuilder exe.ICmdBuilder, targetDir string,
@@ -56,12 +55,12 @@ func srcinfoExists(ctx context.Context,
 
 func installLocalPKGBUILD(
 	ctx context.Context,
-	config *settings.Configuration,
+	run *runtime.Runtime,
 	cmdArgs *parser.Arguments,
 	dbExecutor db.Executor,
 ) error {
-	aurCache := config.Runtime.AURClient
-	noCheck := strings.Contains(config.MFlags, "--nocheck")
+	aurCache := run.AURClient
+	noCheck := strings.Contains(run.Cfg.MFlags, "--nocheck")
 
 	if len(cmdArgs.Targets) < 1 {
 		return errors.New(gotext.Get("no target directories specified"))
@@ -69,7 +68,7 @@ func installLocalPKGBUILD(
 
 	srcInfos := map[string]*gosrc.Srcinfo{}
 	for _, targetDir := range cmdArgs.Targets {
-		if err := srcinfoExists(ctx, config.Runtime.CmdBuilder, targetDir); err != nil {
+		if err := srcinfoExists(ctx, run.CmdBuilder, targetDir); err != nil {
 			return err
 		}
 
@@ -83,13 +82,13 @@ func installLocalPKGBUILD(
 
 	grapher := dep.NewGrapher(dbExecutor, aurCache, false, settings.NoConfirm,
 		cmdArgs.ExistsDouble("d", "nodeps"), noCheck, cmdArgs.ExistsArg("needed"),
-		config.Runtime.Logger.Child("grapher"))
+		run.Logger.Child("grapher"))
 	graph, err := grapher.GraphFromSrcInfos(ctx, nil, srcInfos)
 	if err != nil {
 		return err
 	}
 
-	opService := NewOperationService(ctx, config, dbExecutor)
+	opService := sync.NewOperationService(ctx, dbExecutor, run)
 	multiErr := &multierror.MultiError{}
 	targets := graph.TopoSortedLayerMap(func(name string, ii *dep.InstallInfo) error {
 		if ii.Source == dep.Missing {
@@ -101,5 +100,5 @@ func installLocalPKGBUILD(
 	if err := multiErr.Return(); err != nil {
 		return err
 	}
-	return opService.Run(ctx, cmdArgs, targets, []string{})
+	return opService.Run(ctx, run, cmdArgs, targets, []string{})
 }

+ 82 - 76
local_install_test.go

@@ -6,6 +6,7 @@ package main
 import (
 	"context"
 	"fmt"
+	"io"
 	"os"
 	"os/exec"
 	"path/filepath"
@@ -19,12 +20,18 @@ import (
 
 	"github.com/Jguer/yay/v12/pkg/db/mock"
 	mockaur "github.com/Jguer/yay/v12/pkg/dep/mock"
+	"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/text"
 	"github.com/Jguer/yay/v12/pkg/vcs"
 )
 
+func newTestLogger() *text.Logger {
+	return text.NewLogger(io.Discard, io.Discard, strings.NewReader(""), true, "test")
+}
+
 func TestIntegrationLocalInstall(t *testing.T) {
 	makepkgBin := t.TempDir() + "/makepkg"
 	pacmanBin := t.TempDir() + "/pacman"
@@ -142,21 +149,21 @@ func TestIntegrationLocalInstall(t *testing.T) {
 		InstalledRemotePackageNamesFn: func() []string { return []string{} },
 	}
 
-	config := &settings.Configuration{
-		RemoveMake: "no",
-		Runtime: &settings.Runtime{
-			Logger:     NewTestLogger(),
-			CmdBuilder: cmdBuilder,
-			VCSStore:   &vcs.Mock{},
-			AURClient: &mockaur.MockAUR{
-				GetFn: func(ctx context.Context, query *aur.Query) ([]aur.Pkg, error) {
-					return []aur.Pkg{}, nil
-				},
+	run := &runtime.Runtime{
+		Cfg: &settings.Configuration{
+			RemoveMake: "no",
+		},
+		Logger:     newTestLogger(),
+		CmdBuilder: cmdBuilder,
+		VCSStore:   &vcs.Mock{},
+		AURClient: &mockaur.MockAUR{
+			GetFn: func(ctx context.Context, query *aur.Query) ([]aur.Pkg, error) {
+				return []aur.Pkg{}, nil
 			},
 		},
 	}
 
-	err = handleCmd(context.Background(), config, cmdArgs, db)
+	err = handleCmd(context.Background(), run, cmdArgs, db)
 	require.NoError(t, err)
 
 	require.Len(t, mockRunner.ShowCalls, len(wantShow))
@@ -263,20 +270,19 @@ func TestIntegrationLocalInstallMissingDep(t *testing.T) {
 		LocalPackageFn: func(string) mock.IPackage { return nil },
 	}
 
-	config := &settings.Configuration{
-		Runtime: &settings.Runtime{
-			Logger:     NewTestLogger(),
-			CmdBuilder: cmdBuilder,
-			VCSStore:   &vcs.Mock{},
-			AURClient: &mockaur.MockAUR{
-				GetFn: func(ctx context.Context, query *aur.Query) ([]aur.Pkg, error) {
-					return []aur.Pkg{}, nil
-				},
+	run := &runtime.Runtime{
+		Cfg:        &settings.Configuration{},
+		Logger:     newTestLogger(),
+		CmdBuilder: cmdBuilder,
+		VCSStore:   &vcs.Mock{},
+		AURClient: &mockaur.MockAUR{
+			GetFn: func(ctx context.Context, query *aur.Query) ([]aur.Pkg, error) {
+				return []aur.Pkg{}, nil
 			},
 		},
 	}
 
-	err = handleCmd(context.Background(), config, cmdArgs, db)
+	err = handleCmd(context.Background(), run, cmdArgs, db)
 	require.ErrorContains(t, err, wantErr.Error())
 
 	require.Len(t, mockRunner.ShowCalls, len(wantShow))
@@ -421,21 +427,21 @@ func TestIntegrationLocalInstallNeeded(t *testing.T) {
 		InstalledRemotePackageNamesFn: func() []string { return []string{} },
 	}
 
-	config := &settings.Configuration{
-		RemoveMake: "no",
-		Runtime: &settings.Runtime{
-			Logger:     NewTestLogger(),
-			CmdBuilder: cmdBuilder,
-			VCSStore:   &vcs.Mock{},
-			AURClient: &mockaur.MockAUR{
-				GetFn: func(ctx context.Context, query *aur.Query) ([]aur.Pkg, error) {
-					return []aur.Pkg{}, nil
-				},
+	run := &runtime.Runtime{
+		Cfg: &settings.Configuration{
+			RemoveMake: "no",
+		},
+		Logger:     newTestLogger(),
+		CmdBuilder: cmdBuilder,
+		VCSStore:   &vcs.Mock{},
+		AURClient: &mockaur.MockAUR{
+			GetFn: func(ctx context.Context, query *aur.Query) ([]aur.Pkg, error) {
+				return []aur.Pkg{}, nil
 			},
 		},
 	}
 
-	err = handleCmd(context.Background(), config, cmdArgs, db)
+	err = handleCmd(context.Background(), run, cmdArgs, db)
 	require.NoError(t, err)
 
 	require.Len(t, mockRunner.ShowCalls, len(wantShow), "show calls: %v", mockRunner.ShowCalls)
@@ -585,22 +591,22 @@ func TestIntegrationLocalInstallGenerateSRCINFO(t *testing.T) {
 		InstalledRemotePackageNamesFn: func() []string { return []string{} },
 	}
 
-	config := &settings.Configuration{
-		RemoveMake: "no",
-		Debug:      false,
-		Runtime: &settings.Runtime{
-			Logger:     NewTestLogger(),
-			CmdBuilder: cmdBuilder,
-			VCSStore:   &vcs.Mock{},
-			AURClient: &mockaur.MockAUR{
-				GetFn: func(ctx context.Context, query *aur.Query) ([]aur.Pkg, error) {
-					return []aur.Pkg{}, nil
-				},
+	run := &runtime.Runtime{
+		Cfg: &settings.Configuration{
+			RemoveMake: "no",
+			Debug:      false,
+		},
+		Logger:     newTestLogger(),
+		CmdBuilder: cmdBuilder,
+		VCSStore:   &vcs.Mock{},
+		AURClient: &mockaur.MockAUR{
+			GetFn: func(ctx context.Context, query *aur.Query) ([]aur.Pkg, error) {
+				return []aur.Pkg{}, nil
 			},
 		},
 	}
 
-	err = handleCmd(context.Background(), config, cmdArgs, db)
+	err = handleCmd(context.Background(), run, cmdArgs, db)
 	require.NoError(t, err)
 
 	require.Len(t, mockRunner.ShowCalls, len(wantShow))
@@ -651,7 +657,6 @@ func TestIntegrationLocalInstallMissingFiles(t *testing.T) {
 	wantCapture := []string{}
 
 	captureOverride := func(cmd *exec.Cmd) (stdout string, stderr string, err error) {
-		fmt.Println(cmd.Args)
 		if cmd.Args[1] == "--printsrcinfo" {
 			return string(srcinfo), "", nil
 		}
@@ -722,16 +727,17 @@ func TestIntegrationLocalInstallMissingFiles(t *testing.T) {
 		},
 	}
 
-	config := &settings.Configuration{
-		RemoveMake: "no",
-		Runtime: &settings.Runtime{
-			Logger:     NewTestLogger(),
-			CmdBuilder: cmdBuilder,
-			VCSStore:   &vcs.Mock{},
-			AURClient: &mockaur.MockAUR{
-				GetFn: func(ctx context.Context, query *aur.Query) ([]aur.Pkg, error) {
-					return []aur.Pkg{}, nil
-				},
+	config := &runtime.Runtime{
+		Cfg: &settings.Configuration{
+			RemoveMake: "no",
+			Debug:      false,
+		},
+		Logger:     newTestLogger(),
+		CmdBuilder: cmdBuilder,
+		VCSStore:   &vcs.Mock{},
+		AURClient: &mockaur.MockAUR{
+			GetFn: func(ctx context.Context, query *aur.Query) ([]aur.Pkg, error) {
+				return []aur.Pkg{}, nil
 			},
 		},
 	}
@@ -848,16 +854,16 @@ func TestIntegrationLocalInstallWithDepsProvides(t *testing.T) {
 		InstalledRemotePackageNamesFn: func() []string { return []string{} },
 	}
 
-	config := &settings.Configuration{
-		RemoveMake: "no",
-		Runtime: &settings.Runtime{
-			Logger:     NewTestLogger(),
-			CmdBuilder: cmdBuilder,
-			VCSStore:   &vcs.Mock{},
-			AURClient: &mockaur.MockAUR{
-				GetFn: func(ctx context.Context, query *aur.Query) ([]aur.Pkg, error) {
-					return []aur.Pkg{}, nil
-				},
+	config := &runtime.Runtime{
+		Cfg: &settings.Configuration{
+			RemoveMake: "no",
+		},
+		Logger:     newTestLogger(),
+		CmdBuilder: cmdBuilder,
+		VCSStore:   &vcs.Mock{},
+		AURClient: &mockaur.MockAUR{
+			GetFn: func(ctx context.Context, query *aur.Query) ([]aur.Pkg, error) {
+				return []aur.Pkg{}, nil
 			},
 		},
 	}
@@ -988,21 +994,21 @@ func TestIntegrationLocalInstallTwoSrcInfosWithDeps(t *testing.T) {
 		InstalledRemotePackageNamesFn: func() []string { return []string{} },
 	}
 
-	config := &settings.Configuration{
-		RemoveMake: "no",
-		Runtime: &settings.Runtime{
-			Logger:     NewTestLogger(),
-			CmdBuilder: cmdBuilder,
-			VCSStore:   &vcs.Mock{},
-			AURClient: &mockaur.MockAUR{
-				GetFn: func(ctx context.Context, query *aur.Query) ([]aur.Pkg, error) {
-					return []aur.Pkg{}, nil
-				},
+	run := &runtime.Runtime{
+		Cfg: &settings.Configuration{
+			RemoveMake: "no",
+		},
+		Logger:     newTestLogger(),
+		CmdBuilder: cmdBuilder,
+		VCSStore:   &vcs.Mock{},
+		AURClient: &mockaur.MockAUR{
+			GetFn: func(ctx context.Context, query *aur.Query) ([]aur.Pkg, error) {
+				return []aur.Pkg{}, nil
 			},
 		},
 	}
 
-	err = handleCmd(context.Background(), config, cmdArgs, db)
+	err = handleCmd(context.Background(), run, cmdArgs, db)
 	require.NoError(t, err)
 
 	require.Len(t, mockRunner.ShowCalls, len(wantShow))

+ 19 - 49
main.go

@@ -9,9 +9,8 @@ import (
 
 	"github.com/leonelquinteros/gotext"
 
-	"github.com/Jguer/yay/v12/pkg/db"
 	"github.com/Jguer/yay/v12/pkg/db/ialpm"
-	"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"
@@ -39,6 +38,7 @@ func initGotext() {
 }
 
 func main() {
+	fallbackLog := text.NewLogger(os.Stdout, os.Stderr, os.Stdin, false, "fallback")
 	var (
 		err error
 		ctx = context.Background()
@@ -47,7 +47,7 @@ func main() {
 
 	defer func() {
 		if rec := recover(); rec != nil {
-			text.Errorln(rec)
+			fallbackLog.Errorln(rec)
 			debug.PrintStack()
 		}
 
@@ -57,15 +57,15 @@ func main() {
 	initGotext()
 
 	if os.Geteuid() == 0 {
-		text.Warnln(gotext.Get("Avoid running yay as root/sudo."))
+		fallbackLog.Warnln(gotext.Get("Avoid running yay as root/sudo."))
 	}
 
 	configPath := settings.GetConfigPath()
 	// Parse config
-	cfg, err := settings.NewConfig(configPath, yayVersion)
+	cfg, err := settings.NewConfig(fallbackLog, configPath, yayVersion)
 	if err != nil {
 		if str := err.Error(); str != "" {
-			text.Errorln(str)
+			fallbackLog.Errorln(str)
 		}
 
 		ret = 1
@@ -73,13 +73,9 @@ func main() {
 		return
 	}
 
-	if cfg.Debug {
-		text.GlobalLogger.Debug = true
-	}
-
-	if errS := cfg.RunMigrations(
+	if errS := cfg.RunMigrations(fallbackLog,
 		settings.DefaultMigrations(), configPath, yayVersion); errS != nil {
-		text.Errorln(errS)
+		fallbackLog.Errorln(errS)
 	}
 
 	cmdArgs := parser.MakeArguments()
@@ -87,7 +83,7 @@ func main() {
 	// Parse command line
 	if err = cfg.ParseCommandLine(cmdArgs); err != nil {
 		if str := err.Error(); str != "" {
-			text.Errorln(str)
+			fallbackLog.Errorln(str)
 		}
 
 		ret = 1
@@ -97,15 +93,15 @@ func main() {
 
 	if cfg.SaveConfig {
 		if errS := cfg.Save(configPath, yayVersion); errS != nil {
-			text.Errorln(errS)
+			fallbackLog.Errorln(errS)
 		}
 	}
 
-	// Build runtime
-	runtime, err := settings.BuildRuntime(cfg, cmdArgs, yayVersion)
+	// Build run
+	run, err := runtime.NewRuntime(cfg, cmdArgs, yayVersion)
 	if err != nil {
 		if str := err.Error(); str != "" {
-			text.Errorln(str)
+			fallbackLog.Errorln(str)
 		}
 
 		ret = 1
@@ -113,35 +109,10 @@ func main() {
 		return
 	}
 
-	cfg.Runtime = runtime
-
-	cfg.Runtime.QueryBuilder = query.NewSourceQueryBuilder(
-		cfg.Runtime.AURClient,
-		cfg.Runtime.Logger.Child("mixed.querybuilder"), cfg.SortBy,
-		cfg.Mode, cfg.SearchBy,
-		cfg.BottomUp, cfg.SingleLineResults, cfg.SeparateSources)
-
-	var useColor bool
-
-	cfg.Runtime.PacmanConf, useColor, err = settings.RetrievePacmanConfig(cmdArgs, cfg.PacmanConf)
+	dbExecutor, err := ialpm.NewExecutor(run.PacmanConf, run.Logger.Child("db"))
 	if err != nil {
 		if str := err.Error(); str != "" {
-			text.Errorln(str)
-		}
-
-		ret = 1
-
-		return
-	}
-
-	cfg.Runtime.CmdBuilder.SetPacmanDBPath(cfg.Runtime.PacmanConf.DBPath)
-
-	text.UseColor = useColor
-
-	dbExecutor, err := ialpm.NewExecutor(cfg.Runtime.PacmanConf, runtime.Logger.Child("db"))
-	if err != nil {
-		if str := err.Error(); str != "" {
-			text.Errorln(str)
+			fallbackLog.Errorln(str)
 		}
 
 		ret = 1
@@ -151,19 +122,18 @@ func main() {
 
 	defer func() {
 		if rec := recover(); rec != nil {
-			text.Errorln(rec)
-			debug.PrintStack()
+			fallbackLog.Errorln(rec, string(debug.Stack()))
 		}
 
 		dbExecutor.Cleanup()
 	}()
 
-	if err = handleCmd(ctx, cfg, cmdArgs, db.Executor(dbExecutor)); err != nil {
+	if err = handleCmd(ctx, run, cmdArgs, dbExecutor); err != nil {
 		if str := err.Error(); str != "" {
-			text.Errorln(str)
+			fallbackLog.Errorln(str)
 			if cmdArgs.ExistsArg("c") && cmdArgs.ExistsArg("y") && cmdArgs.Op == "S" {
 				// Remove after 2023-10-01
-				text.Errorln("Did you mean 'yay -Yc'?")
+				fallbackLog.Errorln("Did you mean 'yay -Yc'?")
 			}
 		}
 

+ 11 - 9
pkg/cmd/graph/main.go

@@ -8,6 +8,7 @@ import (
 
 	"github.com/Jguer/yay/v12/pkg/db/ialpm"
 	"github.com/Jguer/yay/v12/pkg/dep"
+	"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"
@@ -17,44 +18,45 @@ import (
 	"github.com/pkg/errors"
 )
 
-func handleCmd() error {
-	config, err := settings.NewConfig(settings.GetConfigPath(), "")
+func handleCmd(logger *text.Logger) error {
+	cfg, err := settings.NewConfig(logger, settings.GetConfigPath(), "")
 	if err != nil {
 		return err
 	}
 
 	cmdArgs := parser.MakeArguments()
-	if errP := config.ParseCommandLine(cmdArgs); errP != nil {
+	if errP := cfg.ParseCommandLine(cmdArgs); errP != nil {
 		return errP
 	}
 
-	pacmanConf, _, err := settings.RetrievePacmanConfig(cmdArgs, config.PacmanConf)
+	run, err := runtime.NewRuntime(cfg, cmdArgs, "1.0.0")
 	if err != nil {
 		return err
 	}
 
-	dbExecutor, err := ialpm.NewExecutor(pacmanConf, text.GlobalLogger)
+	dbExecutor, err := ialpm.NewExecutor(run.PacmanConf, logger)
 	if err != nil {
 		return err
 	}
 
 	aurCache, err := metadata.New(
 		metadata.WithCacheFilePath(
-			filepath.Join(config.BuildDir, "aur.json")))
+			filepath.Join(cfg.BuildDir, "aur.json")))
 	if err != nil {
 		return errors.Wrap(err, gotext.Get("failed to retrieve aur Cache"))
 	}
 
 	grapher := dep.NewGrapher(dbExecutor, aurCache, true, settings.NoConfirm,
 		cmdArgs.ExistsDouble("d", "nodeps"), false, false,
-		config.Runtime.Logger.Child("grapher"))
+		run.Logger.Child("grapher"))
 
 	return graphPackage(context.Background(), grapher, cmdArgs.Targets)
 }
 
 func main() {
-	if err := handleCmd(); err != nil {
-		text.Errorln(err)
+	fallbackLog := text.NewLogger(os.Stdout, os.Stderr, os.Stdin, false, "fallback")
+	if err := handleCmd(fallbackLog); err != nil {
+		fallbackLog.Errorln(err)
 		os.Exit(1)
 	}
 }

+ 0 - 4
pkg/dep/topo/dep.go

@@ -5,8 +5,6 @@ import (
 	"strings"
 
 	"github.com/Jguer/go-alpm/v2"
-
-	"github.com/Jguer/yay/v12/pkg/text"
 )
 
 type (
@@ -244,7 +242,6 @@ func (g *Graph[T, V]) Prune(node T) []T {
 	// Remove edges from things that depend on `node`.
 	for dependent := range g.dependents[node] {
 		last := g.dependencies.removeFromDepmap(dependent, node)
-		text.Debugln("pruning dependent", dependent, last)
 		if last {
 			pruned = append(pruned, g.Prune(dependent)...)
 		}
@@ -255,7 +252,6 @@ func (g *Graph[T, V]) Prune(node T) []T {
 	// Remove all edges from node to the things it depends on.
 	for dependency := range g.dependencies[node] {
 		last := g.dependents.removeFromDepmap(dependency, node)
-		text.Debugln("pruning dependency", dependency, last)
 		if last {
 			pruned = append(pruned, g.Prune(dependency)...)
 		}

+ 2 - 2
pkg/download/aur.go

@@ -48,7 +48,7 @@ func AURPKGBUILDRepo(ctx context.Context, cmdBuilder exe.GitCmdBuilder, aurURL,
 
 func AURPKGBUILDRepos(
 	ctx context.Context,
-	cmdBuilder exe.GitCmdBuilder,
+	cmdBuilder exe.GitCmdBuilder, logger *text.Logger,
 	targets []string, aurURL, dest string, force bool,
 ) (map[string]bool, error) {
 	cloned := make(map[string]bool, len(targets))
@@ -80,7 +80,7 @@ func AURPKGBUILDRepos(
 				mux.Unlock()
 			}
 
-			text.OperationInfoln(
+			logger.OperationInfoln(
 				gotext.Get("(%d/%d) Downloaded PKGBUILD: %s",
 					progress, len(targets), text.Cyan(target)))
 

+ 1 - 1
pkg/download/aur_test.go

@@ -158,7 +158,7 @@ func TestAURPKGBUILDRepos(t *testing.T) {
 			GitFlags: []string{},
 		},
 	}
-	cloned, err := AURPKGBUILDRepos(context.Background(), cmdBuilder, targets, "https://aur.archlinux.org", dir, false)
+	cloned, err := AURPKGBUILDRepos(context.Background(), cmdBuilder, newTestLogger(), targets, "https://aur.archlinux.org", dir, false)
 
 	assert.NoError(t, err)
 	assert.EqualValues(t, map[string]bool{"yay": true, "yay-bin": false, "yay-git": true}, cloned)

+ 9 - 9
pkg/download/unified.go

@@ -81,8 +81,8 @@ func getURLName(pkg db.IPackage) string {
 	return name
 }
 
-func PKGBUILDs(dbExecutor DBSearcher, aurClient aur.QueryClient, httpClient *http.Client, targets []string,
-	aurURL string, mode parser.TargetMode,
+func PKGBUILDs(dbExecutor DBSearcher, aurClient aur.QueryClient, httpClient *http.Client,
+	logger *text.Logger, targets []string, aurURL string, mode parser.TargetMode,
 ) (map[string][]byte, error) {
 	pkgbuilds := make(map[string][]byte, len(targets))
 
@@ -96,7 +96,7 @@ func PKGBUILDs(dbExecutor DBSearcher, aurClient aur.QueryClient, httpClient *htt
 
 	for _, target := range targets {
 		// Probably replaceable by something in query.
-		dbName, name, isAUR, toSkip := getPackageUsableName(dbExecutor, aurClient, target, mode)
+		dbName, name, isAUR, toSkip := getPackageUsableName(dbExecutor, aurClient, logger, target, mode)
 		if toSkip {
 			continue
 		}
@@ -136,7 +136,7 @@ func PKGBUILDs(dbExecutor DBSearcher, aurClient aur.QueryClient, httpClient *htt
 }
 
 func PKGBUILDRepos(ctx context.Context, dbExecutor DBSearcher, aurClient aur.QueryClient,
-	cmdBuilder exe.GitCmdBuilder,
+	cmdBuilder exe.GitCmdBuilder, logger *text.Logger,
 	targets []string, mode parser.TargetMode, aurURL, dest string, force bool,
 ) (map[string]bool, error) {
 	cloned := make(map[string]bool, len(targets))
@@ -151,7 +151,7 @@ func PKGBUILDRepos(ctx context.Context, dbExecutor DBSearcher, aurClient aur.Que
 
 	for _, target := range targets {
 		// Probably replaceable by something in query.
-		dbName, name, isAUR, toSkip := getPackageUsableName(dbExecutor, aurClient, target, mode)
+		dbName, name, isAUR, toSkip := getPackageUsableName(dbExecutor, aurClient, logger, target, mode)
 		if toSkip {
 			continue
 		}
@@ -184,11 +184,11 @@ func PKGBUILDRepos(ctx context.Context, dbExecutor DBSearcher, aurClient aur.Que
 			}
 
 			if aur {
-				text.OperationInfoln(
+				logger.OperationInfoln(
 					gotext.Get("(%d/%d) Downloaded PKGBUILD: %s",
 						progress, len(targets), text.Cyan(pkgName)))
 			} else {
-				text.OperationInfoln(
+				logger.OperationInfoln(
 					gotext.Get("(%d/%d) Downloaded PKGBUILD from ABS: %s",
 						progress, len(targets), text.Cyan(pkgName)))
 			}
@@ -206,7 +206,7 @@ func PKGBUILDRepos(ctx context.Context, dbExecutor DBSearcher, aurClient aur.Que
 
 // TODO: replace with dep.ResolveTargets.
 func getPackageUsableName(dbExecutor DBSearcher, aurClient aur.QueryClient,
-	target string, mode parser.TargetMode,
+	logger *text.Logger, target string, mode parser.TargetMode,
 ) (dbname, pkgname string, isAUR, toSkip bool) {
 	dbName, name := text.SplitDBFromName(target)
 	if dbName != "aur" && mode.AtLeastRepo() {
@@ -239,7 +239,7 @@ func getPackageUsableName(dbExecutor DBSearcher, aurClient aur.QueryClient,
 		Needles:  []string{name},
 	})
 	if err != nil {
-		text.Warnln(err)
+		logger.Warnln(err)
 		return dbName, name, true, true
 	}
 

+ 12 - 7
pkg/download/unified_test.go

@@ -5,6 +5,7 @@ package download
 
 import (
 	"context"
+	"io"
 	"net/http"
 	"os"
 	"path/filepath"
@@ -22,6 +23,10 @@ import (
 	"github.com/Jguer/yay/v12/pkg/text"
 )
 
+func newTestLogger() *text.Logger {
+	return text.NewLogger(io.Discard, io.Discard, strings.NewReader(""), true, "test")
+}
+
 // GIVEN 2 aur packages and 1 in repo
 // GIVEN package in repo is already present
 // WHEN defining package db as a target
@@ -56,7 +61,7 @@ func TestPKGBUILDReposDefinedDBPull(t *testing.T) {
 		absPackagesDB: map[string]string{"yay": "core"},
 	}
 	cloned, err := PKGBUILDRepos(context.Background(), searcher, mockClient,
-		cmdBuilder,
+		cmdBuilder, newTestLogger(),
 		targets, parser.ModeAny, "https://aur.archlinux.org", dir, false)
 
 	assert.NoError(t, err)
@@ -90,7 +95,7 @@ func TestPKGBUILDReposDefinedDBClone(t *testing.T) {
 		absPackagesDB: map[string]string{"yay": "core"},
 	}
 	cloned, err := PKGBUILDRepos(context.Background(), searcher, mockClient,
-		cmdBuilder,
+		cmdBuilder, newTestLogger(),
 		targets, parser.ModeAny, "https://aur.archlinux.org", dir, false)
 
 	assert.NoError(t, err)
@@ -124,7 +129,7 @@ func TestPKGBUILDReposClone(t *testing.T) {
 		absPackagesDB: map[string]string{"yay": "core"},
 	}
 	cloned, err := PKGBUILDRepos(context.Background(), searcher, mockClient,
-		cmdBuilder,
+		cmdBuilder, newTestLogger(),
 		targets, parser.ModeAny, "https://aur.archlinux.org", dir, false)
 
 	assert.NoError(t, err)
@@ -158,7 +163,7 @@ func TestPKGBUILDReposNotFound(t *testing.T) {
 		absPackagesDB: map[string]string{"yay": "core"},
 	}
 	cloned, err := PKGBUILDRepos(context.Background(), searcher, mockClient,
-		cmdBuilder,
+		cmdBuilder, newTestLogger(),
 		targets, parser.ModeAny, "https://aur.archlinux.org", dir, false)
 
 	assert.NoError(t, err)
@@ -192,7 +197,7 @@ func TestPKGBUILDReposRepoMode(t *testing.T) {
 		absPackagesDB: map[string]string{"yay": "core"},
 	}
 	cloned, err := PKGBUILDRepos(context.Background(), searcher, mockClient,
-		cmdBuilder,
+		cmdBuilder, newTestLogger(),
 		targets, parser.ModeRepo, "https://aur.archlinux.org", dir, false)
 
 	assert.NoError(t, err)
@@ -230,7 +235,7 @@ func TestPKGBUILDFull(t *testing.T) {
 		absPackagesDB: map[string]string{"yay": "core"},
 	}
 
-	fetched, err := PKGBUILDs(searcher, mockClient, &http.Client{},
+	fetched, err := PKGBUILDs(searcher, mockClient, &http.Client{}, newTestLogger(),
 		targets, "https://aur.archlinux.org", parser.ModeAny)
 
 	assert.NoError(t, err)
@@ -268,7 +273,7 @@ func TestPKGBUILDReposMissingAUR(t *testing.T) {
 		absPackagesDB: map[string]string{"yay": "core"},
 	}
 	cloned, err := PKGBUILDRepos(context.Background(), searcher, mockClient,
-		cmdBuilder,
+		cmdBuilder, newTestLogger(),
 		targets, parser.ModeAny, "https://aur.archlinux.org", dir, false)
 
 	assert.NoError(t, err)

+ 9 - 8
pkg/menus/clean_menu.go

@@ -9,6 +9,7 @@ import (
 	mapset "github.com/deckarep/golang-set/v2"
 	"github.com/leonelquinteros/gotext"
 
+	"github.com/Jguer/yay/v12/pkg/runtime"
 	"github.com/Jguer/yay/v12/pkg/settings"
 	"github.com/Jguer/yay/v12/pkg/text"
 )
@@ -23,7 +24,7 @@ func anyExistInCache(pkgbuildDirs map[string]string) bool {
 	return false
 }
 
-func CleanFn(ctx context.Context, config *settings.Configuration, w io.Writer,
+func CleanFn(ctx context.Context, run *runtime.Runtime, w io.Writer,
 	pkgbuildDirsByBase map[string]string, installed mapset.Set[string],
 ) error {
 	if len(pkgbuildDirsByBase) == 0 {
@@ -49,25 +50,25 @@ func CleanFn(ctx context.Context, config *settings.Configuration, w io.Writer,
 		bases = append(bases, pkg)
 	}
 
-	toClean, errClean := selectionMenu(w, pkgbuildDirsByBase, bases, installed,
+	toClean, errClean := selectionMenu(run.Logger, pkgbuildDirsByBase, bases, installed,
 		gotext.Get("Packages to cleanBuild?"),
-		settings.NoConfirm, config.AnswerClean, skipFunc)
+		settings.NoConfirm, run.Cfg.AnswerClean, skipFunc)
 	if errClean != nil {
 		return errClean
 	}
 
 	for i, base := range toClean {
 		dir := pkgbuildDirsByBase[base]
-		text.OperationInfoln(gotext.Get("Deleting (%d/%d): %s", i+1, len(toClean), text.Cyan(dir)))
+		run.Logger.OperationInfoln(gotext.Get("Deleting (%d/%d): %s", i+1, len(toClean), text.Cyan(dir)))
 
-		if err := config.Runtime.CmdBuilder.Show(config.Runtime.CmdBuilder.BuildGitCmd(ctx, dir, "reset", "--hard", "origin/HEAD")); err != nil {
-			text.Warnln(gotext.Get("Unable to clean:"), dir)
+		if err := run.CmdBuilder.Show(run.CmdBuilder.BuildGitCmd(ctx, dir, "reset", "--hard", "origin/HEAD")); err != nil {
+			run.Logger.Warnln(gotext.Get("Unable to clean:"), dir)
 
 			return err
 		}
 
-		if err := config.Runtime.CmdBuilder.Show(config.Runtime.CmdBuilder.BuildGitCmd(ctx, dir, "clean", "-fdx")); err != nil {
-			text.Warnln(gotext.Get("Unable to clean:"), dir)
+		if err := run.CmdBuilder.Show(run.CmdBuilder.BuildGitCmd(ctx, dir, "clean", "-fdx")); err != nil {
+			run.Logger.Warnln(gotext.Get("Unable to clean:"), dir)
 
 			return err
 		}

+ 10 - 10
pkg/menus/diff_menu.go

@@ -5,13 +5,13 @@ import (
 	"context"
 	"fmt"
 	"io"
-	"os"
 	"strings"
 
 	mapset "github.com/deckarep/golang-set/v2"
 	"github.com/leonelquinteros/gotext"
 
 	"github.com/Jguer/yay/v12/pkg/multierror"
+	"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/text"
@@ -22,7 +22,7 @@ const (
 	gitDiffRefName = "AUR_SEEN"
 )
 
-func showPkgbuildDiffs(ctx context.Context, cmdBuilder exe.ICmdBuilder,
+func showPkgbuildDiffs(ctx context.Context, cmdBuilder exe.ICmdBuilder, logger *text.Logger,
 	pkgbuildDirs map[string]string, bases []string,
 ) error {
 	var errMulti multierror.MultiError
@@ -46,7 +46,7 @@ func showPkgbuildDiffs(ctx context.Context, cmdBuilder exe.ICmdBuilder,
 			}
 
 			if !hasDiff {
-				text.Warnln(gotext.Get("%s: No changes -- skipping", text.Cyan(pkg)))
+				logger.Warnln(gotext.Get("%s: No changes -- skipping", text.Cyan(pkg)))
 
 				continue
 			}
@@ -145,7 +145,7 @@ func updatePkgbuildSeenRef(ctx context.Context, cmdBuilder exe.ICmdBuilder, pkgb
 	return errMulti.Return()
 }
 
-func DiffFn(ctx context.Context, config *settings.Configuration, w io.Writer,
+func DiffFn(ctx context.Context, run *runtime.Runtime, w io.Writer,
 	pkgbuildDirsByBase map[string]string, installed mapset.Set[string],
 ) error {
 	if len(pkgbuildDirsByBase) == 0 {
@@ -157,23 +157,23 @@ func DiffFn(ctx context.Context, config *settings.Configuration, w io.Writer,
 		bases = append(bases, base)
 	}
 
-	toDiff, errMenu := selectionMenu(w, pkgbuildDirsByBase, bases, installed, gotext.Get("Diffs to show?"),
-		settings.NoConfirm, config.AnswerDiff, nil)
+	toDiff, errMenu := selectionMenu(run.Logger, pkgbuildDirsByBase, bases, installed, gotext.Get("Diffs to show?"),
+		settings.NoConfirm, run.Cfg.AnswerDiff, nil)
 	if errMenu != nil || len(toDiff) == 0 {
 		return errMenu
 	}
 
-	if errD := showPkgbuildDiffs(ctx, config.Runtime.CmdBuilder, pkgbuildDirsByBase, toDiff); errD != nil {
+	if errD := showPkgbuildDiffs(ctx, run.CmdBuilder, run.Logger, pkgbuildDirsByBase, toDiff); errD != nil {
 		return errD
 	}
 
-	fmt.Println()
+	run.Logger.Println()
 
-	if !text.ContinueTask(os.Stdin, gotext.Get("Proceed with install?"), true, false) {
+	if !run.Logger.ContinueTask(gotext.Get("Proceed with install?"), true, false) {
 		return settings.ErrUserAbort{}
 	}
 
-	if errUpd := updatePkgbuildSeenRef(ctx, config.Runtime.CmdBuilder, pkgbuildDirsByBase, toDiff); errUpd != nil {
+	if errUpd := updatePkgbuildSeenRef(ctx, run.CmdBuilder, pkgbuildDirsByBase, toDiff); errUpd != nil {
 		return errUpd
 	}
 

+ 9 - 8
pkg/menus/edit_menu.go

@@ -14,6 +14,7 @@ import (
 	mapset "github.com/deckarep/golang-set/v2"
 	"github.com/leonelquinteros/gotext"
 
+	"github.com/Jguer/yay/v12/pkg/runtime"
 	"github.com/Jguer/yay/v12/pkg/settings"
 	"github.com/Jguer/yay/v12/pkg/text"
 )
@@ -59,7 +60,7 @@ func editor(log *text.Logger, editorConfig, editorFlags string, noConfirm bool)
 		for {
 			log.Infoln(gotext.Get("Edit PKGBUILD with?"))
 
-			editorInput, err := text.GetInput(os.Stdin, "", noConfirm)
+			editorInput, err := log.GetInput("", noConfirm)
 			if err != nil {
 				log.Errorln(err)
 				continue
@@ -113,7 +114,7 @@ func editPkgbuilds(log *text.Logger, pkgbuildDirs map[string]string, bases []str
 	return nil
 }
 
-func EditFn(ctx context.Context, cfg *settings.Configuration, w io.Writer,
+func EditFn(ctx context.Context, run *runtime.Runtime, w io.Writer,
 	pkgbuildDirsByBase map[string]string, installed mapset.Set[string],
 ) error {
 	if len(pkgbuildDirsByBase) == 0 {
@@ -125,21 +126,21 @@ func EditFn(ctx context.Context, cfg *settings.Configuration, w io.Writer,
 		bases = append(bases, pkg)
 	}
 
-	toEdit, errMenu := selectionMenu(w, pkgbuildDirsByBase, bases, installed,
-		gotext.Get("PKGBUILDs to edit?"), settings.NoConfirm, cfg.AnswerEdit, nil)
+	toEdit, errMenu := selectionMenu(run.Logger, pkgbuildDirsByBase, bases, installed,
+		gotext.Get("PKGBUILDs to edit?"), settings.NoConfirm, run.Cfg.AnswerEdit, nil)
 	if errMenu != nil || len(toEdit) == 0 {
 		return errMenu
 	}
 
 	// TOFIX: remove or use srcinfo data
-	if errEdit := editPkgbuilds(cfg.Runtime.Logger, pkgbuildDirsByBase,
-		toEdit, cfg.Editor, cfg.EditorFlags, nil, settings.NoConfirm); errEdit != nil {
+	if errEdit := editPkgbuilds(run.Logger, pkgbuildDirsByBase,
+		toEdit, run.Cfg.Editor, run.Cfg.EditorFlags, nil, settings.NoConfirm); errEdit != nil {
 		return errEdit
 	}
 
-	cfg.Runtime.Logger.Println()
+	run.Logger.Println()
 
-	if !text.ContinueTask(os.Stdin, gotext.Get("Proceed with install?"), true, false) {
+	if !run.Logger.ContinueTask(gotext.Get("Proceed with install?"), true, false) {
 		return settings.ErrUserAbort{}
 	}
 

+ 9 - 8
pkg/menus/menu.go

@@ -2,7 +2,6 @@ package menus
 
 import (
 	"fmt"
-	"io"
 	"os"
 
 	"github.com/leonelquinteros/gotext"
@@ -14,7 +13,9 @@ import (
 	mapset "github.com/deckarep/golang-set/v2"
 )
 
-func pkgbuildNumberMenu(w io.Writer, pkgbuildDirs map[string]string, bases []string, installed mapset.Set[string]) {
+func pkgbuildNumberMenu(logger *text.Logger, pkgbuildDirs map[string]string,
+	bases []string, installed mapset.Set[string],
+) {
 	toPrint := ""
 
 	for n, pkgBase := range bases {
@@ -34,20 +35,20 @@ func pkgbuildNumberMenu(w io.Writer, pkgbuildDirs map[string]string, bases []str
 		toPrint += "\n"
 	}
 
-	fmt.Fprint(w, toPrint)
+	logger.Print(toPrint)
 }
 
-func selectionMenu(w io.Writer, pkgbuildDirs map[string]string, bases []string, installed mapset.Set[string],
+func selectionMenu(logger *text.Logger, pkgbuildDirs map[string]string, bases []string, installed mapset.Set[string],
 	message string, noConfirm bool, defaultAnswer string, skipFunc func(string) bool,
 ) ([]string, error) {
 	selected := make([]string, 0)
 
-	pkgbuildNumberMenu(w, pkgbuildDirs, bases, installed)
+	pkgbuildNumberMenu(logger, pkgbuildDirs, bases, installed)
 
-	text.Infoln(message)
-	text.Infoln(gotext.Get("%s [A]ll [Ab]ort [I]nstalled [No]tInstalled or (1 2 3, 1-3, ^4)", text.Cyan(gotext.Get("[N]one"))))
+	logger.Infoln(message)
+	logger.Infoln(gotext.Get("%s [A]ll [Ab]ort [I]nstalled [No]tInstalled or (1 2 3, 1-3, ^4)", text.Cyan(gotext.Get("[N]one"))))
 
-	selectInput, err := text.GetInput(os.Stdin, defaultAnswer, noConfirm)
+	selectInput, err := logger.GetInput(defaultAnswer, noConfirm)
 	if err != nil {
 		return nil, err
 	}

+ 9 - 9
pkg/news/news.go

@@ -4,11 +4,9 @@ import (
 	"bytes"
 	"context"
 	"encoding/xml"
-	"fmt"
 	"html"
 	"io"
 	"net/http"
-	"os"
 	"strings"
 	"time"
 
@@ -23,13 +21,13 @@ type item struct {
 	Creator     string `xml:"dc:creator"`
 }
 
-func (item *item) print(buildTime time.Time, all, quiet bool) {
+func (item *item) printNews(logger *text.Logger, buildTime time.Time, all, quiet bool) {
 	var fd string
 
 	date, err := time.Parse(time.RFC1123Z, item.PubDate)
 
 	if err != nil {
-		fmt.Fprintln(os.Stderr, err)
+		logger.Errorln(err)
 	} else {
 		fd = text.FormatTime(int(date.Unix()))
 		if !all && !buildTime.IsZero() {
@@ -39,11 +37,11 @@ func (item *item) print(buildTime time.Time, all, quiet bool) {
 		}
 	}
 
-	fmt.Println(text.Bold(text.Magenta(fd)), text.Bold(strings.TrimSpace(item.Title)))
+	logger.Println(text.Bold(text.Magenta(fd)), text.Bold(strings.TrimSpace(item.Title)))
 
 	if !quiet {
 		desc := strings.TrimSpace(parseNews(item.Description))
-		fmt.Println(desc)
+		logger.Println(desc)
 	}
 }
 
@@ -60,7 +58,9 @@ type rss struct {
 	Channel channel `xml:"channel"`
 }
 
-func PrintNewsFeed(ctx context.Context, client *http.Client, cutOffDate time.Time, bottomUp, all, quiet bool) error {
+func PrintNewsFeed(ctx context.Context, client *http.Client, logger *text.Logger,
+	cutOffDate time.Time, bottomUp, all, quiet bool,
+) error {
 	req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://archlinux.org/feeds/news", http.NoBody)
 	if err != nil {
 		return err
@@ -87,11 +87,11 @@ func PrintNewsFeed(ctx context.Context, client *http.Client, cutOffDate time.Tim
 
 	if bottomUp {
 		for i := len(rssGot.Channel.Items) - 1; i >= 0; i-- {
-			rssGot.Channel.Items[i].print(cutOffDate, all, quiet)
+			rssGot.Channel.Items[i].printNews(logger, cutOffDate, all, quiet)
 		}
 	} else {
 		for i := 0; i < len(rssGot.Channel.Items); i++ {
-			rssGot.Channel.Items[i].print(cutOffDate, all, quiet)
+			rssGot.Channel.Items[i].printNews(logger, cutOffDate, all, quiet)
 		}
 	}
 

+ 9 - 8
pkg/news/news_test.go

@@ -8,12 +8,15 @@ import (
 	"io"
 	"net/http"
 	"os"
+	"strings"
 	"testing"
 	"time"
 
 	"github.com/bradleyjkemp/cupaloy"
 	"github.com/stretchr/testify/assert"
 	"gopkg.in/h2non/gock.v1"
+
+	"github.com/Jguer/yay/v12/pkg/text"
 )
 
 const lastNews = `
@@ -135,17 +138,16 @@ func TestPrintNewsFeed(t *testing.T) {
 
 			defer gock.Off()
 
-			rescueStdout := os.Stdout
 			r, w, _ := os.Pipe()
-			os.Stdout = w
+			logger := text.NewLogger(w, w, strings.NewReader(""), false, "logger")
 
-			err := PrintNewsFeed(context.Background(), &http.Client{}, tt.args.cutOffDate, tt.args.bottomUp, tt.args.all, tt.args.quiet)
+			err := PrintNewsFeed(context.Background(), &http.Client{}, logger,
+				tt.args.cutOffDate, tt.args.bottomUp, tt.args.all, tt.args.quiet)
 			assert.NoError(t, err)
 
 			w.Close()
 			out, _ := io.ReadAll(r)
 			cupaloy.SnapshotT(t, out)
-			os.Stdout = rescueStdout
 		})
 	}
 }
@@ -164,15 +166,14 @@ func TestPrintNewsFeedSameDay(t *testing.T) {
 
 	defer gock.Off()
 
-	rescueStdout := os.Stdout
 	r, w, _ := os.Pipe()
-	os.Stdout = w
+	logger := text.NewLogger(w, w, strings.NewReader(""), false, "logger")
 
-	err := PrintNewsFeed(context.Background(), &http.Client{}, lastNewsTime, true, false, false)
+	err := PrintNewsFeed(context.Background(), &http.Client{}, logger,
+		lastNewsTime, true, false, false)
 	assert.NoError(t, err)
 
 	w.Close()
 	out, _ := io.ReadAll(r)
 	cupaloy.SnapshotT(t, out)
-	os.Stdout = rescueStdout
 }

+ 0 - 3
pkg/query/aur_warnings.go

@@ -22,9 +22,6 @@ type AURWarnings struct {
 }
 
 func NewWarnings(logger *text.Logger) *AURWarnings {
-	if logger == nil {
-		logger = text.GlobalLogger
-	}
 	return &AURWarnings{log: logger}
 }
 

+ 3 - 3
pkg/query/filter.go

@@ -7,19 +7,19 @@ import (
 	"github.com/Jguer/yay/v12/pkg/text"
 )
 
-func RemoveInvalidTargets(targets []string, mode parser.TargetMode) []string {
+func RemoveInvalidTargets(logger *text.Logger, targets []string, mode parser.TargetMode) []string {
 	filteredTargets := make([]string, 0)
 
 	for _, target := range targets {
 		dbName, _ := text.SplitDBFromName(target)
 
 		if dbName == "aur" && !mode.AtLeastAUR() {
-			text.Warnln(gotext.Get("%s: can't use target with option --repo -- skipping", text.Cyan(target)))
+			logger.Warnln(gotext.Get("%s: can't use target with option --repo -- skipping", text.Cyan(target)))
 			continue
 		}
 
 		if dbName != "aur" && dbName != "" && !mode.AtLeastRepo() {
-			text.Warnln(gotext.Get("%s: can't use target with option --aur -- skipping", text.Cyan(target)))
+			logger.Warnln(gotext.Get("%s: can't use target with option --aur -- skipping", text.Cyan(target)))
 			continue
 		}
 

+ 1 - 1
pkg/query/query_builder.go

@@ -130,7 +130,7 @@ func (a *abstractResults) Less(i, j int) bool {
 func (s *SourceQueryBuilder) Execute(ctx context.Context, dbExecutor db.Executor, pkgS []string) {
 	var aurErr error
 
-	pkgS = RemoveInvalidTargets(pkgS, s.targetMode)
+	pkgS = RemoveInvalidTargets(s.logger, pkgS, s.targetMode)
 
 	metric := &metrics.Hamming{
 		CaseSensitive: false,

+ 2 - 2
pkg/settings/pacman.go

@@ -1,4 +1,4 @@
-package settings
+package runtime
 
 import (
 	"fmt"
@@ -10,7 +10,7 @@ import (
 	"golang.org/x/term"
 )
 
-func RetrievePacmanConfig(cmdArgs *parser.Arguments, pacmanConfigPath string) (*pacmanconf.Config, bool, error) {
+func retrievePacmanConfig(cmdArgs *parser.Arguments, pacmanConfigPath string) (*pacmanconf.Config, bool, error) {
 	root := "/"
 	if value, _, exists := cmdArgs.GetArg("root", "r"); exists {
 		root = value

+ 2 - 2
pkg/settings/pacman_test.go

@@ -1,7 +1,7 @@
 //go:build !integration
 // +build !integration
 
-package settings
+package runtime
 
 import (
 	"testing"
@@ -46,7 +46,7 @@ func TestPacmanConf(t *testing.T) {
 		},
 	}
 
-	pacmanConf, color, err := RetrievePacmanConfig(parser.MakeArguments(), "../../testdata/pacman.conf")
+	pacmanConf, color, err := retrievePacmanConfig(parser.MakeArguments(), "../../testdata/pacman.conf")
 	assert.Nil(t, err)
 	assert.NotNil(t, pacmanConf)
 	assert.Equal(t, color, false)

+ 27 - 11
pkg/settings/runtime.go

@@ -1,4 +1,4 @@
-package settings
+package runtime
 
 import (
 	"context"
@@ -9,8 +9,8 @@ import (
 
 	"github.com/leonelquinteros/gotext"
 
-	"github.com/Jguer/yay/v12/pkg/db"
 	"github.com/Jguer/yay/v12/pkg/query"
+	"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/text"
@@ -24,6 +24,7 @@ import (
 )
 
 type Runtime struct {
+	Cfg          *settings.Configuration
 	QueryBuilder query.Builder
 	PacmanConf   *pacmanconf.Config
 	VCSStore     vcs.Store
@@ -31,13 +32,12 @@ type Runtime struct {
 	HTTPClient   *http.Client
 	VoteClient   *vote.Client
 	AURClient    aur.QueryClient
-	DBExecutor   db.Executor
 	Logger       *text.Logger
 }
 
-func BuildRuntime(cfg *Configuration, cmdArgs *parser.Arguments, version string) (*Runtime, error) {
+func NewRuntime(cfg *settings.Configuration, cmdArgs *parser.Arguments, version string) (*Runtime, error) {
 	logger := text.NewLogger(os.Stdout, os.Stderr, os.Stdin, cfg.Debug, "runtime")
-	cmdBuilder := cfg.CmdBuilder(nil)
+	runner := exe.NewOSRunner(logger.Child("runner"))
 
 	httpClient := &http.Client{
 		CheckRedirect: func(req *http.Request, via []*http.Request) error {
@@ -86,6 +86,16 @@ func BuildRuntime(cfg *Configuration, cmdArgs *parser.Arguments, version string)
 		aurCache = aurClient
 	}
 
+	pacmanConf, useColor, err := retrievePacmanConfig(cmdArgs, cfg.PacmanConf)
+	if err != nil {
+		return nil, err
+	}
+
+	// FIXME: get rid of global
+	text.UseColor = useColor
+
+	cmdBuilder := exe.NewCmdBuilder(cfg, runner, logger.Child("cmdbuilder"), pacmanConf.DBPath)
+
 	vcsStore := vcs.NewInfoStore(
 		cfg.VCSFilePath, cmdBuilder,
 		logger.Child("vcs"))
@@ -94,17 +104,23 @@ func BuildRuntime(cfg *Configuration, cmdArgs *parser.Arguments, version string)
 		return nil, err
 	}
 
-	runtime := &Runtime{
-		QueryBuilder: nil,
-		PacmanConf:   nil,
+	queryBuilder := query.NewSourceQueryBuilder(
+		aurClient,
+		logger.Child("mixed.querybuilder"), cfg.SortBy,
+		cfg.Mode, cfg.SearchBy,
+		cfg.BottomUp, cfg.SingleLineResults, cfg.SeparateSources)
+
+	run := &Runtime{
+		Cfg:          cfg,
+		QueryBuilder: queryBuilder,
+		PacmanConf:   pacmanConf,
 		VCSStore:     vcsStore,
 		CmdBuilder:   cmdBuilder,
 		HTTPClient:   &http.Client{},
 		VoteClient:   voteClient,
 		AURClient:    aurCache,
-		DBExecutor:   nil,
-		Logger:       text.NewLogger(os.Stdout, os.Stderr, os.Stdin, cfg.Debug, "runtime"),
+		Logger:       logger,
 	}
 
-	return runtime, nil
+	return run, nil
 }

+ 15 - 15
pkg/settings/runtime_test.go

@@ -1,16 +1,17 @@
 //go:build !integration
 // +build !integration
 
-package settings_test
+package runtime_test
 
 import (
 	"testing"
 
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 
+	"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"
 )
 
 func TestBuildRuntime(t *testing.T) {
@@ -23,24 +24,23 @@ func TestBuildRuntime(t *testing.T) {
 		AURRPCURL:   "https://aur.archlinux.org/rpc",
 		BuildDir:    "/tmp",
 		VCSFilePath: "",
-		Runtime:     &settings.Runtime{Logger: text.NewLogger(nil, nil, nil, false, "")},
+		PacmanConf:  "../../testdata/pacman.conf",
 	}
 	cmdArgs := parser.MakeArguments()
 	version := "1.0.0"
 
 	// Call the function being tested
-	runtime, err := settings.BuildRuntime(cfg, cmdArgs, version)
+	run, err := runtime.NewRuntime(cfg, cmdArgs, version)
+	require.NoError(t, err)
 
 	// Assert the function's output
-	assert.NotNil(t, runtime)
-	assert.Nil(t, err)
-	assert.Nil(t, runtime.QueryBuilder)
-	assert.Nil(t, runtime.PacmanConf)
-	assert.NotNil(t, runtime.VCSStore)
-	assert.NotNil(t, runtime.CmdBuilder)
-	assert.NotNil(t, runtime.HTTPClient)
-	assert.NotNil(t, runtime.VoteClient)
-	assert.NotNil(t, runtime.AURClient)
-	assert.Nil(t, runtime.DBExecutor)
-	assert.NotNil(t, runtime.Logger)
+	assert.NotNil(t, run)
+	assert.NotNil(t, run.QueryBuilder)
+	assert.NotNil(t, run.PacmanConf)
+	assert.NotNil(t, run.VCSStore)
+	assert.NotNil(t, run.CmdBuilder)
+	assert.NotNil(t, run.HTTPClient)
+	assert.NotNil(t, run.VoteClient)
+	assert.NotNil(t, run.AURClient)
+	assert.NotNil(t, run.Logger)
 }

+ 0 - 5
pkg/settings/args.go

@@ -5,7 +5,6 @@ import (
 	"strings"
 
 	"github.com/Jguer/yay/v12/pkg/settings/parser"
-	"github.com/Jguer/yay/v12/pkg/text"
 )
 
 func (c *Configuration) ParseCommandLine(a *parser.Arguments) error {
@@ -15,9 +14,6 @@ func (c *Configuration) ParseCommandLine(a *parser.Arguments) error {
 
 	c.extractYayOptions(a)
 
-	// Reload CmdBuilder
-	c.Runtime.CmdBuilder = c.CmdBuilder(nil)
-
 	return nil
 }
 
@@ -59,7 +55,6 @@ func (c *Configuration) handleOption(option, value string) bool {
 		c.CleanAfter = false
 	case "debug":
 		c.Debug = true
-		text.GlobalLogger.Debug = true
 		return false
 	case "devel":
 		c.Devel = true

+ 50 - 79
pkg/settings/config.go

@@ -9,7 +9,6 @@ import (
 	"path/filepath"
 	"strings"
 
-	"github.com/Jguer/yay/v12/pkg/settings/exe"
 	"github.com/Jguer/yay/v12/pkg/settings/parser"
 	"github.com/Jguer/yay/v12/pkg/text"
 
@@ -24,53 +23,52 @@ var NoConfirm = false
 
 // Configuration stores yay's config.
 type Configuration struct {
-	Runtime                *Runtime `json:"-"`
-	AURURL                 string   `json:"aururl"`
-	AURRPCURL              string   `json:"aurrpcurl"`
-	BuildDir               string   `json:"buildDir"`
-	Editor                 string   `json:"editor"`
-	EditorFlags            string   `json:"editorflags"`
-	MakepkgBin             string   `json:"makepkgbin"`
-	MakepkgConf            string   `json:"makepkgconf"`
-	PacmanBin              string   `json:"pacmanbin"`
-	PacmanConf             string   `json:"pacmanconf"`
-	ReDownload             string   `json:"redownload"`
-	AnswerClean            string   `json:"answerclean"`
-	AnswerDiff             string   `json:"answerdiff"`
-	AnswerEdit             string   `json:"answeredit"`
-	AnswerUpgrade          string   `json:"answerupgrade"`
-	GitBin                 string   `json:"gitbin"`
-	GpgBin                 string   `json:"gpgbin"`
-	GpgFlags               string   `json:"gpgflags"`
-	MFlags                 string   `json:"mflags"`
-	SortBy                 string   `json:"sortby"`
-	SearchBy               string   `json:"searchby"`
-	GitFlags               string   `json:"gitflags"`
-	RemoveMake             string   `json:"removemake"`
-	SudoBin                string   `json:"sudobin"`
-	SudoFlags              string   `json:"sudoflags"`
-	Version                string   `json:"version"`
-	RequestSplitN          int      `json:"requestsplitn"`
-	CompletionInterval     int      `json:"completionrefreshtime"`
-	MaxConcurrentDownloads int      `json:"maxconcurrentdownloads"`
-	BottomUp               bool     `json:"bottomup"`
-	SudoLoop               bool     `json:"sudoloop"`
-	TimeUpdate             bool     `json:"timeupdate"`
-	Devel                  bool     `json:"devel"`
-	CleanAfter             bool     `json:"cleanAfter"`
-	Provides               bool     `json:"provides"`
-	PGPFetch               bool     `json:"pgpfetch"`
-	CleanMenu              bool     `json:"cleanmenu"`
-	DiffMenu               bool     `json:"diffmenu"`
-	EditMenu               bool     `json:"editmenu"`
-	CombinedUpgrade        bool     `json:"combinedupgrade"`
-	UseAsk                 bool     `json:"useask"`
-	BatchInstall           bool     `json:"batchinstall"`
-	SingleLineResults      bool     `json:"singlelineresults"`
-	SeparateSources        bool     `json:"separatesources"`
-	Debug                  bool     `json:"debug"`
-	UseRPC                 bool     `json:"rpc"`
-	DoubleConfirm          bool     `json:"doubleconfirm"` // confirm install before and after build
+	AURURL                 string `json:"aururl"`
+	AURRPCURL              string `json:"aurrpcurl"`
+	BuildDir               string `json:"buildDir"`
+	Editor                 string `json:"editor"`
+	EditorFlags            string `json:"editorflags"`
+	MakepkgBin             string `json:"makepkgbin"`
+	MakepkgConf            string `json:"makepkgconf"`
+	PacmanBin              string `json:"pacmanbin"`
+	PacmanConf             string `json:"pacmanconf"`
+	ReDownload             string `json:"redownload"`
+	AnswerClean            string `json:"answerclean"`
+	AnswerDiff             string `json:"answerdiff"`
+	AnswerEdit             string `json:"answeredit"`
+	AnswerUpgrade          string `json:"answerupgrade"`
+	GitBin                 string `json:"gitbin"`
+	GpgBin                 string `json:"gpgbin"`
+	GpgFlags               string `json:"gpgflags"`
+	MFlags                 string `json:"mflags"`
+	SortBy                 string `json:"sortby"`
+	SearchBy               string `json:"searchby"`
+	GitFlags               string `json:"gitflags"`
+	RemoveMake             string `json:"removemake"`
+	SudoBin                string `json:"sudobin"`
+	SudoFlags              string `json:"sudoflags"`
+	Version                string `json:"version"`
+	RequestSplitN          int    `json:"requestsplitn"`
+	CompletionInterval     int    `json:"completionrefreshtime"`
+	MaxConcurrentDownloads int    `json:"maxconcurrentdownloads"`
+	BottomUp               bool   `json:"bottomup"`
+	SudoLoop               bool   `json:"sudoloop"`
+	TimeUpdate             bool   `json:"timeupdate"`
+	Devel                  bool   `json:"devel"`
+	CleanAfter             bool   `json:"cleanAfter"`
+	Provides               bool   `json:"provides"`
+	PGPFetch               bool   `json:"pgpfetch"`
+	CleanMenu              bool   `json:"cleanmenu"`
+	DiffMenu               bool   `json:"diffmenu"`
+	EditMenu               bool   `json:"editmenu"`
+	CombinedUpgrade        bool   `json:"combinedupgrade"`
+	UseAsk                 bool   `json:"useask"`
+	BatchInstall           bool   `json:"batchinstall"`
+	SingleLineResults      bool   `json:"singlelineresults"`
+	SeparateSources        bool   `json:"separatesources"`
+	Debug                  bool   `json:"debug"`
+	UseRPC                 bool   `json:"rpc"`
+	DoubleConfirm          bool   `json:"doubleconfirm"` // confirm install before and after build
 
 	CompletionPath string `json:"-"`
 	VCSFilePath    string `json:"-"`
@@ -237,19 +235,16 @@ func DefaultConfig(version string) *Configuration {
 		Debug:                  false,
 		UseRPC:                 true,
 		DoubleConfirm:          true,
-		Runtime: &Runtime{
-			Logger: text.GlobalLogger,
-		},
-		Mode: parser.ModeAny,
+		Mode:                   parser.ModeAny,
 	}
 }
 
-func NewConfig(configPath, version string) (*Configuration, error) {
+func NewConfig(logger *text.Logger, configPath, version string) (*Configuration, error) {
 	newConfig := DefaultConfig(version)
 
 	cacheHome, errCache := getCacheHome()
-	if errCache != nil {
-		text.Errorln(errCache)
+	if errCache != nil && logger != nil {
+		logger.Errorln(errCache)
 	}
 
 	newConfig.BuildDir = cacheHome
@@ -295,27 +290,3 @@ func (c *Configuration) load(configPath string) {
 		}
 	}
 }
-
-func (c *Configuration) CmdBuilder(runner exe.Runner) exe.ICmdBuilder {
-	if runner == nil {
-		runner = &exe.OSRunner{Log: c.Runtime.Logger.Child("runner")}
-	}
-
-	return &exe.CmdBuilder{
-		GitBin:           c.GitBin,
-		GitFlags:         strings.Fields(c.GitFlags),
-		GPGBin:           c.GpgBin,
-		GPGFlags:         strings.Fields(c.GpgFlags),
-		MakepkgFlags:     strings.Fields(c.MFlags),
-		MakepkgConfPath:  c.MakepkgConf,
-		MakepkgBin:       c.MakepkgBin,
-		SudoBin:          c.SudoBin,
-		SudoFlags:        strings.Fields(c.SudoFlags),
-		SudoLoopEnabled:  c.SudoLoop,
-		PacmanBin:        c.PacmanBin,
-		PacmanConfigPath: c.PacmanConf,
-		PacmanDBPath:     "",
-		Runner:           runner,
-		Log:              c.Runtime.Logger.Child("cmd_builder"),
-	}
-}

+ 3 - 3
pkg/settings/config_test.go

@@ -36,7 +36,7 @@ func TestNewConfig(t *testing.T) {
 	_, err = f.WriteString(string(configJSON))
 	assert.NoError(t, err)
 
-	newConfig, err := NewConfig(GetConfigPath(), "v1.0.0")
+	newConfig, err := NewConfig(nil, GetConfigPath(), "v1.0.0")
 	assert.NoError(t, err)
 
 	assert.Equal(t, filepath.Join(cacheDir, "test-build-dir"), newConfig.BuildDir)
@@ -69,7 +69,7 @@ func TestNewConfigAURDEST(t *testing.T) {
 	_, err = f.WriteString(string(configJSON))
 	assert.NoError(t, err)
 
-	newConfig, err := NewConfig(GetConfigPath(), "v1.0.0")
+	newConfig, err := NewConfig(nil, GetConfigPath(), "v1.0.0")
 	assert.NoError(t, err)
 
 	assert.Equal(t, filepath.Join(cacheDir, "test-build-dir"), newConfig.BuildDir)
@@ -102,7 +102,7 @@ func TestNewConfigAURDESTTildeExpansion(t *testing.T) {
 	_, err = f.WriteString(string(configJSON))
 	assert.NoError(t, err)
 
-	newConfig, err := NewConfig(GetConfigPath(), "v1.0.0")
+	newConfig, err := NewConfig(nil, GetConfigPath(), "v1.0.0")
 	assert.NoError(t, err)
 
 	assert.Equal(t, filepath.Join(homeDir, "test-build-dir"), newConfig.BuildDir)

+ 22 - 6
pkg/settings/exe/cmd_builder.go

@@ -15,6 +15,7 @@ import (
 	mapset "github.com/deckarep/golang-set/v2"
 	"github.com/leonelquinteros/gotext"
 
+	"github.com/Jguer/yay/v12/pkg/settings"
 	"github.com/Jguer/yay/v12/pkg/settings/parser"
 	"github.com/Jguer/yay/v12/pkg/text"
 )
@@ -38,7 +39,6 @@ type ICmdBuilder interface {
 	BuildMakepkgCmd(ctx context.Context, dir string, extraArgs ...string) *exec.Cmd
 	BuildPacmanCmd(ctx context.Context, args *parser.Arguments, mode parser.TargetMode, noConfirm bool) *exec.Cmd
 	AddMakepkgFlag(string)
-	SetPacmanDBPath(string)
 	SudoLoop()
 }
 
@@ -60,6 +60,26 @@ type CmdBuilder struct {
 	Log              *text.Logger
 }
 
+func NewCmdBuilder(cfg *settings.Configuration, runner Runner, logger *text.Logger, dbPath string) *CmdBuilder {
+	return &CmdBuilder{
+		GitBin:           cfg.GitBin,
+		GitFlags:         strings.Fields(cfg.GitFlags),
+		GPGBin:           cfg.GpgBin,
+		GPGFlags:         strings.Fields(cfg.GpgFlags),
+		MakepkgFlags:     strings.Fields(cfg.MFlags),
+		MakepkgConfPath:  cfg.MakepkgConf,
+		MakepkgBin:       cfg.MakepkgBin,
+		SudoBin:          cfg.SudoBin,
+		SudoFlags:        strings.Fields(cfg.SudoFlags),
+		SudoLoopEnabled:  cfg.SudoLoop,
+		PacmanBin:        cfg.PacmanBin,
+		PacmanConfigPath: cfg.PacmanConf,
+		PacmanDBPath:     dbPath,
+		Runner:           runner,
+		Log:              logger,
+	}
+}
+
 func (c *CmdBuilder) BuildGPGCmd(ctx context.Context, extraArgs ...string) *exec.Cmd {
 	args := make([]string, len(c.GPGFlags), len(c.GPGFlags)+len(extraArgs))
 	copy(args, c.GPGFlags)
@@ -135,10 +155,6 @@ func (c *CmdBuilder) BuildMakepkgCmd(ctx context.Context, dir string, extraArgs
 	return cmd
 }
 
-func (c *CmdBuilder) SetPacmanDBPath(dbPath string) {
-	c.PacmanDBPath = dbPath
-}
-
 // deElevateCommand, `systemd-run` code based on pikaur.
 func (c *CmdBuilder) deElevateCommand(ctx context.Context, cmd *exec.Cmd) *exec.Cmd {
 	if os.Geteuid() != 0 {
@@ -242,7 +258,7 @@ func (c *CmdBuilder) waitLock(dbPath string) {
 		time.Sleep(3 * time.Second)
 
 		if _, err := os.Stat(lockDBPath); err != nil {
-			fmt.Println()
+			c.Log.Println()
 
 			return
 		}

+ 4 - 0
pkg/settings/exe/exec.go

@@ -19,6 +19,10 @@ type OSRunner struct {
 	Log *text.Logger
 }
 
+func NewOSRunner(log *text.Logger) *OSRunner {
+	return &OSRunner{log}
+}
+
 func (r *OSRunner) Show(cmd *exec.Cmd) error {
 	cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
 	cmd.SysProcAttr = &syscall.SysProcAttr{

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

@@ -36,6 +36,10 @@ type MockRunner struct {
 	CaptureFn      func(cmd *exec.Cmd) (stdout string, stderr string, err error)
 }
 
+func (m *MockBuilder) BuildGPGCmd(ctx context.Context, extraArgs ...string) *exec.Cmd {
+	return exec.CommandContext(ctx, "gpg", extraArgs...)
+}
+
 func (m *MockBuilder) BuildMakepkgCmd(ctx context.Context, dir string, extraArgs ...string) *exec.Cmd {
 	var res *exec.Cmd
 	if m.BuildMakepkgCmdFn != nil {

+ 2 - 2
pkg/settings/migrations.go

@@ -45,7 +45,7 @@ func DefaultMigrations() []configMigration {
 	}
 }
 
-func (c *Configuration) RunMigrations(migrations []configMigration,
+func (c *Configuration) RunMigrations(logger *text.Logger, migrations []configMigration,
 	configPath, newVersion string,
 ) error {
 	saveConfig := false
@@ -53,7 +53,7 @@ func (c *Configuration) RunMigrations(migrations []configMigration,
 	for _, migration := range migrations {
 		if db.VerCmp(migration.TargetVersion(), c.Version) > 0 {
 			if migration.Do(c) {
-				text.Infoln("Config migration executed (",
+				logger.Infoln("Config migration executed (",
 					migration.TargetVersion(), "):", migration)
 
 				saveConfig = true

+ 6 - 11
pkg/settings/migrations_test.go

@@ -16,6 +16,10 @@ import (
 	"github.com/Jguer/yay/v12/pkg/text"
 )
 
+func newTestLogger() *text.Logger {
+	return text.NewLogger(io.Discard, io.Discard, strings.NewReader(""), true, "test")
+}
+
 func TestMigrationNothingToDo(t *testing.T) {
 	t.Parallel()
 	// Create temporary file for config
@@ -28,13 +32,10 @@ func TestMigrationNothingToDo(t *testing.T) {
 	config := Configuration{
 		Version: "99.0.0",
 		// Create runtime with runtimeVersion
-		Runtime: &Runtime{
-			Logger: text.NewLogger(io.Discard, io.Discard, strings.NewReader(""), false, "test"),
-		},
 	}
 
 	// Run Migration
-	err = config.RunMigrations(DefaultMigrations(), testFilePath, "20.0.0")
+	err = config.RunMigrations(newTestLogger(), DefaultMigrations(), testFilePath, "20.0.0")
 	require.NoError(t, err)
 
 	// Check file contents if wantSave otherwise check file empty
@@ -53,9 +54,6 @@ func TestProvidesMigrationDo(t *testing.T) {
 	migration := &configProviderMigration{}
 	config := Configuration{
 		Provides: true,
-		Runtime: &Runtime{
-			Logger: text.NewLogger(io.Discard, io.Discard, strings.NewReader(""), false, "test"),
-		},
 	}
 
 	assert.True(t, migration.Do(&config))
@@ -135,13 +133,10 @@ func TestProvidesMigration(t *testing.T) {
 				Version:  tc.testConfig.Version,
 				Provides: tc.testConfig.Provides,
 				// Create runtime with runtimeVersion
-				Runtime: &Runtime{
-					Logger: text.NewLogger(io.Discard, io.Discard, strings.NewReader(""), false, "test"),
-				},
 			}
 
 			// Run Migration
-			err = tcConfig.RunMigrations(
+			err = tcConfig.RunMigrations(newTestLogger(),
 				[]configMigration{&configProviderMigration{}},
 				testFilePath, tc.newVersion)
 

+ 62 - 0
pkg/sync/build/errors.go

@@ -0,0 +1,62 @@
+package build
+
+import (
+	"errors"
+
+	"github.com/leonelquinteros/gotext"
+)
+
+var ErrInstallRepoPkgs = errors.New(gotext.Get("error installing repo packages"))
+
+type FailedIgnoredPkgError struct {
+	pkgErrors map[string]error
+}
+
+func (e *FailedIgnoredPkgError) Error() string {
+	msg := gotext.Get("Failed to install the following packages. Manual intervention is required:")
+
+	for pkg, err := range e.pkgErrors {
+		msg += "\n" + pkg + " - " + err.Error()
+	}
+
+	return msg
+}
+
+type PkgDestNotInListError struct {
+	name string
+}
+
+func (e *PkgDestNotInListError) Error() string {
+	return gotext.Get("could not find PKGDEST for: %s", e.name)
+}
+
+type FindPkgDestError struct {
+	name, pkgDest string
+}
+
+func (e *FindPkgDestError) Error() string {
+	return gotext.Get(
+		"the PKGDEST for %s is listed by makepkg but does not exist: %s",
+		e.name, e.pkgDest)
+}
+
+type SetPkgReasonError struct {
+	exp bool // explicit
+}
+
+func (e *SetPkgReasonError) Error() string {
+	reason := gotext.Get("explicit")
+	if !e.exp {
+		reason = gotext.Get("dependency")
+	}
+
+	return gotext.Get("error updating package install reason to %s", reason)
+}
+
+type NoPkgDestsFoundError struct {
+	dir string
+}
+
+func (e *NoPkgDestsFoundError) Error() string {
+	return gotext.Get("could not find any package archives listed in %s", e.dir)
+}

+ 13 - 13
aur_install.go

@@ -1,4 +1,4 @@
-package main
+package build
 
 import (
 	"context"
@@ -54,12 +54,12 @@ func NewInstaller(dbExecutor db.Executor,
 	}
 }
 
-func (installer *Installer) CompileFailedAndIgnored() error {
+func (installer *Installer) CompileFailedAndIgnored() (map[string]error, error) {
 	if len(installer.failedAndIgnored) == 0 {
-		return nil
+		return installer.failedAndIgnored, nil
 	}
 
-	return &FailedIgnoredPkgError{
+	return installer.failedAndIgnored, &FailedIgnoredPkgError{
 		pkgErrors: installer.failedAndIgnored,
 	}
 }
@@ -234,12 +234,12 @@ func (installer *Installer) installAURPackages(ctx context.Context,
 			}
 
 			installer.failedAndIgnored[name] = errMake
-			text.Errorln(gotext.Get("error making: %s", base), "-", errMake)
+			installer.log.Errorln(gotext.Get("error making: %s", base), "-", errMake)
 			continue
 		}
 
 		if len(pkgdests) == 0 {
-			text.Warnln(gotext.Get("nothing to install for %s", text.Cyan(base)))
+			installer.log.Warnln(gotext.Get("nothing to install for %s", text.Cyan(base)))
 			continue
 		}
 
@@ -298,10 +298,10 @@ func (installer *Installer) buildPkg(ctx context.Context,
 	case needed && installer.pkgsAreAlreadyInstalled(pkgdests, pkgVersion) || installer.downloadOnly:
 		args = []string{"-c", "--nobuild", "--noextract", "--ignorearch"}
 		pkgdests = map[string]string{}
-		text.Warnln(gotext.Get("%s is up to date -- skipping", text.Cyan(base+"-"+pkgVersion)))
+		installer.log.Warnln(gotext.Get("%s is up to date -- skipping", text.Cyan(base+"-"+pkgVersion)))
 	case installer.skipAlreadyBuiltPkg(isTarget, pkgdests):
 		args = []string{"-c", "--nobuild", "--noextract", "--ignorearch"}
-		text.Warnln(gotext.Get("%s already made -- skipping build", text.Cyan(base+"-"+pkgVersion)))
+		installer.log.Warnln(gotext.Get("%s already made -- skipping build", text.Cyan(base+"-"+pkgVersion)))
 	default:
 		args = []string{"-cf", "--noconfirm", "--noextract", "--noprepare", "--holdver"}
 		if installIncompatible {
@@ -333,10 +333,10 @@ func (installer *Installer) pkgsAreAlreadyInstalled(pkgdests map[string]string,
 	return true
 }
 
-func pkgsAreBuilt(pkgdests map[string]string) bool {
+func pkgsAreBuilt(logger *text.Logger, pkgdests map[string]string) bool {
 	for _, pkgdest := range pkgdests {
 		if _, err := os.Stat(pkgdest); err != nil {
-			text.Debugln("pkgIsBuilt:", pkgdest, "does not exist")
+			logger.Debugln("pkgIsBuilt:", pkgdest, "does not exist")
 			return false
 		}
 	}
@@ -347,14 +347,14 @@ func pkgsAreBuilt(pkgdests map[string]string) bool {
 func (installer *Installer) skipAlreadyBuiltPkg(isTarget bool, pkgdests map[string]string) bool {
 	switch installer.rebuildMode {
 	case parser.RebuildModeNo:
-		return pkgsAreBuilt(pkgdests)
+		return pkgsAreBuilt(installer.log, pkgdests)
 	case parser.RebuildModeYes:
-		return !isTarget && pkgsAreBuilt(pkgdests)
+		return !isTarget && pkgsAreBuilt(installer.log, pkgdests)
 	// case parser.RebuildModeTree: // TODO
 	// case parser.RebuildModeAll: // TODO
 	default:
 		// same as RebuildModeNo
-		return pkgsAreBuilt(pkgdests)
+		return pkgsAreBuilt(installer.log, pkgdests)
 	}
 }
 

+ 13 - 12
aur_install_test.go

@@ -1,4 +1,4 @@
-package main
+package build
 
 import (
 	"context"
@@ -21,7 +21,7 @@ import (
 	"github.com/Jguer/yay/v12/pkg/vcs"
 )
 
-func NewTestLogger() *text.Logger {
+func newTestLogger() *text.Logger {
 	return text.NewLogger(io.Discard, io.Discard, strings.NewReader(""), true, "test")
 }
 
@@ -134,7 +134,7 @@ func TestInstaller_InstallNeeded(t *testing.T) {
 			cmdBuilder.Runner = mockRunner
 
 			installer := NewInstaller(mockDB, cmdBuilder, &vcs.Mock{}, parser.ModeAny,
-				parser.RebuildModeNo, false, NewTestLogger())
+				parser.RebuildModeNo, false, newTestLogger())
 
 			cmdArgs := parser.MakeArguments()
 			cmdArgs.AddArg("needed")
@@ -408,7 +408,7 @@ func TestInstaller_InstallMixedSourcesAndLayers(t *testing.T) {
 
 			cmdBuilder.Runner = mockRunner
 
-			installer := NewInstaller(mockDB, cmdBuilder, &vcs.Mock{}, parser.ModeAny, parser.RebuildModeNo, false, NewTestLogger())
+			installer := NewInstaller(mockDB, cmdBuilder, &vcs.Mock{}, parser.ModeAny, parser.RebuildModeNo, false, newTestLogger())
 
 			cmdArgs := parser.MakeArguments()
 			cmdArgs.AddTarget("yay")
@@ -462,7 +462,7 @@ func TestInstaller_RunPostHooks(t *testing.T) {
 	cmdBuilder.Runner = mockRunner
 
 	installer := NewInstaller(mockDB, cmdBuilder, &vcs.Mock{}, parser.ModeAny,
-		parser.RebuildModeNo, false, NewTestLogger())
+		parser.RebuildModeNo, false, newTestLogger())
 
 	called := false
 	hook := func(ctx context.Context) error {
@@ -593,7 +593,7 @@ func TestInstaller_CompileFailed(t *testing.T) {
 			cmdBuilder.Runner = mockRunner
 
 			installer := NewInstaller(mockDB, cmdBuilder, &vcs.Mock{}, parser.ModeAny,
-				parser.RebuildModeNo, false, NewTestLogger())
+				parser.RebuildModeNo, false, newTestLogger())
 
 			cmdArgs := parser.MakeArguments()
 			cmdArgs.AddArg("needed")
@@ -609,10 +609,11 @@ func TestInstaller_CompileFailed(t *testing.T) {
 			} else {
 				require.NoError(td, errI)
 			}
-			err := installer.CompileFailedAndIgnored()
+			failed, err := installer.CompileFailedAndIgnored()
 			if tc.wantErrCompile {
 				require.Error(td, err)
 				assert.ErrorContains(td, err, "yay")
+				assert.Len(t, failed, len(tc.targets))
 			} else {
 				require.NoError(td, err)
 			}
@@ -752,7 +753,7 @@ func TestInstaller_InstallSplitPackage(t *testing.T) {
 			cmdBuilder.Runner = mockRunner
 
 			installer := NewInstaller(mockDB, cmdBuilder, &vcs.Mock{}, parser.ModeAny,
-				parser.RebuildModeNo, false, NewTestLogger())
+				parser.RebuildModeNo, false, newTestLogger())
 
 			cmdArgs := parser.MakeArguments()
 			cmdArgs.AddTarget("jellyfin")
@@ -891,7 +892,7 @@ func TestInstaller_InstallDownloadOnly(t *testing.T) {
 			cmdBuilder.Runner = mockRunner
 
 			installer := NewInstaller(mockDB, cmdBuilder, &vcs.Mock{}, parser.ModeAny,
-				parser.RebuildModeNo, true, NewTestLogger())
+				parser.RebuildModeNo, true, newTestLogger())
 
 			cmdArgs := parser.MakeArguments()
 			cmdArgs.AddTarget("yay")
@@ -995,7 +996,7 @@ func TestInstaller_InstallGroup(t *testing.T) {
 			cmdBuilder.Runner = mockRunner
 
 			installer := NewInstaller(mockDB, cmdBuilder, &vcs.Mock{}, parser.ModeAny,
-				parser.RebuildModeNo, true, NewTestLogger())
+				parser.RebuildModeNo, true, newTestLogger())
 
 			cmdArgs := parser.MakeArguments()
 			cmdArgs.AddTarget("kubernetes-tools")
@@ -1213,7 +1214,7 @@ func TestInstaller_InstallRebuild(t *testing.T) {
 			cmdBuilder.Runner = mockRunner
 
 			installer := NewInstaller(mockDB, cmdBuilder, &vcs.Mock{}, parser.ModeAny,
-				tc.rebuildOption, false, NewTestLogger())
+				tc.rebuildOption, false, newTestLogger())
 
 			cmdArgs := parser.MakeArguments()
 			cmdArgs.AddTarget("yay")
@@ -1298,7 +1299,7 @@ func TestInstaller_InstallUpgrade(t *testing.T) {
 			}
 
 			installer := NewInstaller(mockDB, cmdBuilder, &vcs.Mock{}, tc.targetMode,
-				parser.RebuildModeNo, false, NewTestLogger())
+				parser.RebuildModeNo, false, newTestLogger())
 
 			cmdArgs := parser.MakeArguments()
 			cmdArgs.AddArg("u", "upgrades") // Make sure both args are removed

+ 56 - 122
install.go

@@ -1,4 +1,4 @@
-package main
+package build
 
 import (
 	"context"
@@ -16,6 +16,61 @@ import (
 	"github.com/Jguer/yay/v12/pkg/vcs"
 )
 
+func installPkgArchive(ctx context.Context,
+	cmdBuilder exe.ICmdBuilder,
+	mode parser.TargetMode,
+	vcsStore vcs.Store,
+	cmdArgs *parser.Arguments,
+	pkgArchives []string,
+	noConfirm bool,
+) error {
+	if len(pkgArchives) == 0 {
+		return nil
+	}
+
+	arguments := cmdArgs.Copy()
+	arguments.ClearTargets()
+	arguments.Op = "U"
+	arguments.DelArg("confirm")
+	arguments.DelArg("noconfirm")
+	arguments.DelArg("c", "clean")
+	arguments.DelArg("i", "install")
+	arguments.DelArg("q", "quiet")
+	arguments.DelArg("y", "refresh")
+	arguments.DelArg("u", "sysupgrade")
+	arguments.DelArg("w", "downloadonly")
+	arguments.DelArg("asdeps", "asdep")
+	arguments.DelArg("asexplicit", "asexp")
+
+	arguments.AddTarget(pkgArchives...)
+
+	if errShow := cmdBuilder.Show(cmdBuilder.BuildPacmanCmd(ctx,
+		arguments, mode, noConfirm)); errShow != nil {
+		return errShow
+	}
+
+	if errStore := vcsStore.Save(); errStore != nil {
+		fmt.Fprintln(os.Stderr, errStore)
+	}
+
+	return nil
+}
+
+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, cmdBuilder, mode, cmdArgs, deps); errDeps != nil {
+		return errDeps
+	}
+
+	return asexp(ctx, cmdBuilder, mode, cmdArgs, exps)
+}
+
 func setPkgReason(ctx context.Context,
 	cmdBuilder exe.ICmdBuilder,
 	mode parser.TargetMode,
@@ -69,43 +124,6 @@ func asexp(ctx context.Context,
 	return setPkgReason(ctx, cmdBuilder, mode, cmdArgs, pkgs, true)
 }
 
-func removeMake(ctx context.Context, config *settings.Configuration,
-	cmdBuilder exe.ICmdBuilder, makeDeps []string, cmdArgs *parser.Arguments,
-) error {
-	removeArguments := cmdArgs.CopyGlobal()
-
-	err := removeArguments.AddArg("R", "s", "u")
-	if err != nil {
-		return err
-	}
-
-	for _, pkg := range makeDeps {
-		removeArguments.AddTarget(pkg)
-	}
-
-	oldValue := settings.NoConfirm
-	settings.NoConfirm = true
-	err = cmdBuilder.Show(cmdBuilder.BuildPacmanCmd(ctx,
-		removeArguments, config.Mode, settings.NoConfirm))
-	settings.NoConfirm = oldValue
-
-	return err
-}
-
-func earlyRefresh(ctx context.Context, cfg *settings.Configuration, cmdBuilder exe.ICmdBuilder, cmdArgs *parser.Arguments) error {
-	arguments := cmdArgs.Copy()
-	if cfg.CombinedUpgrade {
-		arguments.DelArg("u", "sysupgrade")
-	}
-	arguments.DelArg("s", "search")
-	arguments.DelArg("i", "info")
-	arguments.DelArg("l", "list")
-	arguments.ClearTargets()
-
-	return cmdBuilder.Show(cmdBuilder.BuildPacmanCmd(ctx,
-		arguments, cfg.Mode, settings.NoConfirm))
-}
-
 func parsePackageList(ctx context.Context, cmdBuilder exe.ICmdBuilder,
 	dir string,
 ) (pkgdests map[string]string, pkgVersion string, err error) {
@@ -144,87 +162,3 @@ func parsePackageList(ctx context.Context, cmdBuilder exe.ICmdBuilder,
 
 	return pkgdests, pkgVersion, nil
 }
-
-func gitMerge(ctx context.Context, cmdBuilder exe.ICmdBuilder, dir string) error {
-	_, stderr, err := cmdBuilder.Capture(
-		cmdBuilder.BuildGitCmd(ctx,
-			dir, "reset", "--hard", "HEAD"))
-	if err != nil {
-		return errors.New(gotext.Get("error resetting %s: %s", dir, stderr))
-	}
-
-	_, stderr, err = cmdBuilder.Capture(
-		cmdBuilder.BuildGitCmd(ctx,
-			dir, "merge", "--no-edit", "--ff"))
-	if err != nil {
-		return errors.New(gotext.Get("error merging %s: %s", dir, stderr))
-	}
-
-	return nil
-}
-
-func mergePkgbuilds(ctx context.Context, cmdBuilder exe.ICmdBuilder, pkgbuildDirs map[string]string) error {
-	for _, dir := range pkgbuildDirs {
-		err := gitMerge(ctx, cmdBuilder, dir)
-		if err != nil {
-			return err
-		}
-	}
-
-	return nil
-}
-
-func installPkgArchive(ctx context.Context,
-	cmdBuilder exe.ICmdBuilder,
-	mode parser.TargetMode,
-	vcsStore vcs.Store,
-	cmdArgs *parser.Arguments,
-	pkgArchives []string,
-	noConfirm bool,
-) error {
-	if len(pkgArchives) == 0 {
-		return nil
-	}
-
-	arguments := cmdArgs.Copy()
-	arguments.ClearTargets()
-	arguments.Op = "U"
-	arguments.DelArg("confirm")
-	arguments.DelArg("noconfirm")
-	arguments.DelArg("c", "clean")
-	arguments.DelArg("i", "install")
-	arguments.DelArg("q", "quiet")
-	arguments.DelArg("y", "refresh")
-	arguments.DelArg("u", "sysupgrade")
-	arguments.DelArg("w", "downloadonly")
-	arguments.DelArg("asdeps", "asdep")
-	arguments.DelArg("asexplicit", "asexp")
-
-	arguments.AddTarget(pkgArchives...)
-
-	if errShow := cmdBuilder.Show(cmdBuilder.BuildPacmanCmd(ctx,
-		arguments, mode, noConfirm)); errShow != nil {
-		return errShow
-	}
-
-	if errStore := vcsStore.Save(); errStore != nil {
-		fmt.Fprintln(os.Stderr, errStore)
-	}
-
-	return nil
-}
-
-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, cmdBuilder, mode, cmdArgs, deps); errDeps != nil {
-		return errDeps
-	}
-
-	return asexp(ctx, cmdBuilder, mode, cmdArgs, exps)
-}

+ 10 - 13
pkg/pgp/keys.go

@@ -4,8 +4,6 @@ import (
 	"bytes"
 	"context"
 	"errors"
-	"fmt"
-	"os"
 	"os/exec"
 	"strings"
 
@@ -50,7 +48,7 @@ type GPGCmdBuilder interface {
 
 // CheckPgpKeys iterates through the keys listed in the PKGBUILDs and if needed,
 // asks the user whether yay should try to import them.
-func CheckPgpKeys(ctx context.Context, pkgbuildDirsByBase map[string]string, srcinfos map[string]*gosrc.Srcinfo,
+func CheckPgpKeys(ctx context.Context, logger *text.Logger, pkgbuildDirsByBase map[string]string, srcinfos map[string]*gosrc.Srcinfo,
 	cmdBuilder GPGCmdBuilder, noConfirm bool,
 ) ([]string, error) {
 	// Let's check the keys individually, and then we can offer to import
@@ -80,24 +78,23 @@ func CheckPgpKeys(ctx context.Context, pkgbuildDirsByBase map[string]string, src
 		return []string{}, nil
 	}
 
-	str, err := formatKeysToImport(problematic)
+	str, err := formatKeysToImport(logger, problematic)
 	if err != nil {
 		return nil, err
 	}
 
-	fmt.Println()
-	fmt.Println(str)
+	logger.Println("\n", str)
 
-	if text.ContinueTask(os.Stdin, gotext.Get("Import?"), true, noConfirm) {
-		return problematic.toSlice(), importKeys(ctx, cmdBuilder, problematic.toSlice())
+	if logger.ContinueTask(gotext.Get("Import?"), true, noConfirm) {
+		return problematic.toSlice(), importKeys(ctx, logger, cmdBuilder, problematic.toSlice())
 	}
 
 	return problematic.toSlice(), nil
 }
 
 // importKeys tries to import the list of keys specified in its argument.
-func importKeys(ctx context.Context, cmdBuilder GPGCmdBuilder, keys []string) error {
-	text.OperationInfoln(gotext.Get("Importing keys with gpg..."))
+func importKeys(ctx context.Context, logger *text.Logger, cmdBuilder GPGCmdBuilder, keys []string) error {
+	logger.OperationInfoln(gotext.Get("Importing keys with gpg..."))
 
 	if err := cmdBuilder.Show(cmdBuilder.BuildGPGCmd(ctx, append([]string{"--recv-keys"}, keys...)...)); err != nil {
 		return errors.New(gotext.Get("problem importing keys"))
@@ -108,14 +105,14 @@ func importKeys(ctx context.Context, cmdBuilder GPGCmdBuilder, keys []string) er
 
 // formatKeysToImport receives a set of keys and returns a string containing the
 // question asking the user wants to import the problematic keys.
-func formatKeysToImport(keys pgpKeySet) (string, error) {
+func formatKeysToImport(logger *text.Logger, keys pgpKeySet) (string, error) {
 	if len(keys) == 0 {
 		return "", errors.New(gotext.Get("no keys to import"))
 	}
 
 	var buffer bytes.Buffer
 
-	buffer.WriteString(text.SprintOperationInfo(gotext.Get("PGP keys need importing:")))
+	buffer.WriteString(logger.SprintOperationInfo(gotext.Get("PGP keys need importing:")))
 
 	for key, bases := range keys {
 		pkglist := ""
@@ -124,7 +121,7 @@ func formatKeysToImport(keys pgpKeySet) (string, error) {
 		}
 
 		pkglist = strings.TrimRight(pkglist, " ")
-		buffer.WriteString("\n" + text.SprintWarn(gotext.Get("%s, required by: %s", text.Cyan(key), text.Cyan(pkglist))))
+		buffer.WriteString("\n" + logger.SprintWarn(gotext.Get("%s, required by: %s", text.Cyan(key), text.Cyan(pkglist))))
 	}
 
 	return buffer.String(), nil

+ 7 - 1
pkg/pgp/keys_test.go

@@ -6,6 +6,7 @@ package pgp
 import (
 	"context"
 	"fmt"
+	"io"
 	"os"
 	"os/exec"
 	"sort"
@@ -17,8 +18,13 @@ import (
 	"github.com/stretchr/testify/require"
 
 	"github.com/Jguer/yay/v12/pkg/settings/exe"
+	"github.com/Jguer/yay/v12/pkg/text"
 )
 
+func newTestLogger() *text.Logger {
+	return text.NewLogger(io.Discard, io.Discard, strings.NewReader(""), true, "test")
+}
+
 func makeSrcinfo(pkgbase string, pgpkeys ...string) *gosrc.Srcinfo {
 	srcinfo := gosrc.Srcinfo{}
 	srcinfo.Pkgbase = pkgbase
@@ -228,7 +234,7 @@ func TestCheckPgpKeys(t *testing.T) {
 				GPGFlags: []string{"--homedir /tmp"},
 				Runner:   mockRunner,
 			}
-			problematic, err := CheckPgpKeys(context.Background(), tt.pkgs, tt.srcinfos, &cmdBuilder, true)
+			problematic, err := CheckPgpKeys(context.Background(), newTestLogger(), tt.pkgs, tt.srcinfos, &cmdBuilder, true)
 
 			require.Len(t, mockRunner.ShowCalls, len(tt.wantShow))
 			require.Len(t, mockRunner.CaptureCalls, len(tt.wantCapture))

pkg/pgp/testdata/11E521D646982372EB577A1F8F0871F202119294 → pkg/sync/srcinfo/pgp/testdata/11E521D646982372EB577A1F8F0871F202119294


pkg/pgp/testdata/487EACC08557AD082088DABA1EB2638FF56C0C53 → pkg/sync/srcinfo/pgp/testdata/487EACC08557AD082088DABA1EB2638FF56C0C53


pkg/pgp/testdata/647F28654894E3BD457199BE38DBBDC86092693E → pkg/sync/srcinfo/pgp/testdata/647F28654894E3BD457199BE38DBBDC86092693E


pkg/pgp/testdata/A314827C4E4250A204CE6E13284FC34C8E4B1A25 → pkg/sync/srcinfo/pgp/testdata/A314827C4E4250A204CE6E13284FC34C8E4B1A25


pkg/pgp/testdata/ABAF11C65A2970B130ABE3C479BE3E4300411886 → pkg/sync/srcinfo/pgp/testdata/ABAF11C65A2970B130ABE3C479BE3E4300411886


pkg/pgp/testdata/B6C8F98282B944E3B0D5C2530FC3042E345AD05D → pkg/sync/srcinfo/pgp/testdata/B6C8F98282B944E3B0D5C2530FC3042E345AD05D


pkg/pgp/testdata/C52048C0C0748FEE227D47A2702353E0F7E48EDB → pkg/sync/srcinfo/pgp/testdata/C52048C0C0748FEE227D47A2702353E0F7E48EDB


+ 13 - 12
pkg/srcinfo/service.go

@@ -11,28 +11,28 @@ import (
 
 	"github.com/Jguer/yay/v12/pkg/db"
 	"github.com/Jguer/yay/v12/pkg/dep"
-	"github.com/Jguer/yay/v12/pkg/pgp"
 	"github.com/Jguer/yay/v12/pkg/settings"
 	"github.com/Jguer/yay/v12/pkg/settings/exe"
+	"github.com/Jguer/yay/v12/pkg/sync/srcinfo/pgp"
 	"github.com/Jguer/yay/v12/pkg/text"
 	"github.com/Jguer/yay/v12/pkg/vcs"
 )
 
-// TODO: add tests
 type Service struct {
 	dbExecutor db.Executor
 	cfg        *settings.Configuration
-	cmdBuilder exe.ICmdBuilder
+	cmdBuilder pgp.GPGCmdBuilder
 	vcsStore   vcs.Store
+	log        *text.Logger
 
 	pkgBuildDirs map[string]string
 	srcInfos     map[string]*gosrc.Srcinfo
 }
 
-func NewService(dbExecutor db.Executor, cfg *settings.Configuration,
+func NewService(dbExecutor db.Executor, cfg *settings.Configuration, logger *text.Logger,
 	cmdBuilder exe.ICmdBuilder, vcsStore vcs.Store, pkgBuildDirs map[string]string,
 ) (*Service, error) {
-	srcinfos, err := ParseSrcinfoFilesByBase(pkgBuildDirs, true)
+	srcinfos, err := ParseSrcinfoFilesByBase(logger, pkgBuildDirs, true)
 	if err != nil {
 		panic(err)
 	}
@@ -43,6 +43,7 @@ func NewService(dbExecutor db.Executor, cfg *settings.Configuration,
 		vcsStore:     vcsStore,
 		pkgBuildDirs: pkgBuildDirs,
 		srcInfos:     srcinfos,
+		log:          logger,
 	}, nil
 }
 
@@ -68,7 +69,7 @@ nextpkg:
 }
 
 func (s *Service) CheckPGPKeys(ctx context.Context) error {
-	_, errCPK := pgp.CheckPgpKeys(ctx, s.pkgBuildDirs, s.srcInfos, s.cmdBuilder, settings.NoConfirm)
+	_, errCPK := pgp.CheckPgpKeys(ctx, s.log.Child("pgp"), s.pkgBuildDirs, s.srcInfos, s.cmdBuilder, settings.NoConfirm)
 	return errCPK
 }
 
@@ -83,15 +84,15 @@ func (s *Service) UpdateVCSStore(ctx context.Context, targets []map[string]*dep.
 		for i := range srcinfo.Packages {
 			for j := range targets {
 				if _, ok := targets[j][srcinfo.Packages[i].Pkgname]; !ok {
-					text.Debugln("skipping VCS update for", srcinfo.Packages[i].Pkgname, "not in targets")
+					s.log.Debugln("skipping VCS update for", srcinfo.Packages[i].Pkgname, "not in targets")
 					continue
 				}
 				if _, ok := ignore[srcinfo.Packages[i].Pkgname]; ok {
-					text.Debugln("skipping VCS update for", srcinfo.Packages[i].Pkgname, "due to install error")
+					s.log.Debugln("skipping VCS update for", srcinfo.Packages[i].Pkgname, "due to install error")
 					continue
 				}
 
-				text.Debugln("checking VCS entry for", srcinfo.Packages[i].Pkgname, fmt.Sprintf("source: %v", srcinfo.Source))
+				s.log.Debugln("checking VCS entry for", srcinfo.Packages[i].Pkgname, fmt.Sprintf("source: %v", srcinfo.Source))
 				s.vcsStore.Update(ctx, srcinfo.Packages[i].Pkgname, srcinfo.Source)
 			}
 		}
@@ -100,17 +101,17 @@ func (s *Service) UpdateVCSStore(ctx context.Context, targets []map[string]*dep.
 	return nil
 }
 
-func ParseSrcinfoFilesByBase(pkgBuildDirs map[string]string, errIsFatal bool) (map[string]*gosrc.Srcinfo, error) {
+func ParseSrcinfoFilesByBase(logger *text.Logger, pkgBuildDirs map[string]string, errIsFatal bool) (map[string]*gosrc.Srcinfo, error) {
 	srcinfos := make(map[string]*gosrc.Srcinfo)
 
 	k := 0
 	for base, dir := range pkgBuildDirs {
-		text.OperationInfoln(gotext.Get("(%d/%d) Parsing SRCINFO: %s", k+1, len(pkgBuildDirs), text.Cyan(base)))
+		logger.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, err))
+				logger.Warnln(gotext.Get("failed to parse %s -- skipping: %s", base, err))
 				continue
 			}
 

+ 132 - 0
pkg/sync/srcinfo/service_test.go

@@ -0,0 +1,132 @@
+package srcinfo
+
+import (
+	"context"
+	"io"
+	"strings"
+	"testing"
+
+	gosrc "github.com/Morganamilo/go-srcinfo"
+
+	"github.com/stretchr/testify/assert"
+
+	"github.com/Jguer/yay/v12/pkg/db/mock"
+	"github.com/Jguer/yay/v12/pkg/dep"
+	"github.com/Jguer/yay/v12/pkg/settings"
+	"github.com/Jguer/yay/v12/pkg/settings/exe"
+	"github.com/Jguer/yay/v12/pkg/text"
+	"github.com/Jguer/yay/v12/pkg/vcs"
+)
+
+func newTestLogger() *text.Logger {
+	return text.NewLogger(io.Discard, io.Discard, strings.NewReader(""), true, "test")
+}
+
+func TestNewService(t *testing.T) {
+	dbExecutor := &mock.DBExecutor{}
+	cfg := &settings.Configuration{}
+	cmdBuilder := &exe.MockBuilder{}
+	vcsStore := &vcs.Mock{}
+	pkgBuildDirs := map[string]string{
+		"jellyfin": "../../../testdata/jfin",
+		"cephbin":  "../../../testdata/cephbin",
+	}
+
+	srv, err := NewService(dbExecutor, cfg, newTestLogger(), cmdBuilder, vcsStore, pkgBuildDirs)
+	assert.NoError(t, err)
+	assert.NotNil(t, srv)
+	assert.Equal(t, dbExecutor, srv.dbExecutor)
+	assert.Equal(t, cfg, srv.cfg)
+	assert.Equal(t, cmdBuilder, srv.cmdBuilder)
+	assert.Equal(t, vcsStore, srv.vcsStore)
+	assert.Equal(t, pkgBuildDirs, srv.pkgBuildDirs)
+	assert.NotNil(t, srv.srcInfos)
+}
+
+func TestService_IncompatiblePkgs(t *testing.T) {
+	srv := &Service{
+		dbExecutor: &mock.DBExecutor{AlpmArchitecturesFn: func() ([]string, error) {
+			return []string{"x86_64"}, nil
+		}},
+		srcInfos: map[string]*gosrc.Srcinfo{
+			"pkg1": {
+				Package: gosrc.Package{
+					Arch: []string{"x86_64", "any"},
+				},
+			},
+			"pkg2": {
+				Package: gosrc.Package{
+					Arch: []string{"any"},
+				},
+			},
+			"pkg3": {
+				Package: gosrc.Package{
+					Arch: []string{"armv7h"},
+				},
+			},
+			"pkg4": {
+				Package: gosrc.Package{
+					Arch: []string{"i683", "x86_64"},
+				},
+			},
+		},
+	}
+
+	incompatible, err := srv.IncompatiblePkgs(context.Background())
+	assert.NoError(t, err)
+	assert.ElementsMatch(t, []string{"pkg3"}, incompatible)
+}
+
+func TestService_CheckPGPKeys(t *testing.T) {
+	srv := &Service{
+		log: newTestLogger(),
+		pkgBuildDirs: map[string]string{
+			"pkg1": "/path/to/pkg1",
+			"pkg2": "/path/to/pkg2",
+		},
+		srcInfos: map[string]*gosrc.Srcinfo{
+			"pkg1": {
+				Packages: []gosrc.Package{
+					{Pkgname: "pkg1"},
+				},
+			},
+			"pkg2": {
+				Packages: []gosrc.Package{
+					{Pkgname: "pkg2"},
+				},
+			},
+		},
+	}
+
+	err := srv.CheckPGPKeys(context.Background())
+	assert.NoError(t, err)
+}
+
+func TestService_UpdateVCSStore(t *testing.T) {
+	srv := &Service{
+		srcInfos: map[string]*gosrc.Srcinfo{
+			"pkg1": {
+				Packages: []gosrc.Package{
+					{Pkgname: "pkg1"},
+				},
+			},
+			"pkg2": {
+				Packages: []gosrc.Package{
+					{Pkgname: "pkg2"},
+				},
+			},
+		},
+		vcsStore: &vcs.Mock{},
+	}
+
+	targets := []map[string]*dep.InstallInfo{
+		{
+			"pkg1": {},
+			"pkg2": {},
+		},
+	}
+	ignore := map[string]error{}
+
+	err := srv.UpdateVCSStore(context.Background(), targets, ignore)
+	assert.NoError(t, err)
+}

+ 138 - 0
pkg/sync/sync.go

@@ -0,0 +1,138 @@
+package sync
+
+import (
+	"context"
+
+	"github.com/Jguer/yay/v12/pkg/completion"
+	"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/runtime"
+	"github.com/Jguer/yay/v12/pkg/settings"
+	"github.com/Jguer/yay/v12/pkg/settings/parser"
+	"github.com/Jguer/yay/v12/pkg/sync/build"
+	"github.com/Jguer/yay/v12/pkg/sync/srcinfo"
+	"github.com/Jguer/yay/v12/pkg/sync/workdir"
+	"github.com/Jguer/yay/v12/pkg/text"
+
+	"github.com/leonelquinteros/gotext"
+)
+
+type OperationService struct {
+	ctx        context.Context
+	cfg        *settings.Configuration
+	dbExecutor db.Executor
+	logger     *text.Logger
+}
+
+func NewOperationService(ctx context.Context,
+	dbExecutor db.Executor,
+	run *runtime.Runtime,
+) *OperationService {
+	return &OperationService{
+		ctx:        ctx,
+		cfg:        run.Cfg,
+		dbExecutor: dbExecutor,
+		logger:     run.Logger.Child("operation"),
+	}
+}
+
+func (o *OperationService) Run(ctx context.Context, run *runtime.Runtime,
+	cmdArgs *parser.Arguments,
+	targets []map[string]*dep.InstallInfo, excluded []string,
+) error {
+	if len(targets) == 0 {
+		o.logger.Println("", gotext.Get("there is nothing to do"))
+		return nil
+	}
+	preparer := workdir.NewPreparer(o.dbExecutor, run.CmdBuilder, o.cfg, o.logger.Child("workdir"))
+	installer := build.NewInstaller(o.dbExecutor, run.CmdBuilder,
+		run.VCSStore, o.cfg.Mode, o.cfg.ReBuild,
+		cmdArgs.ExistsArg("w", "downloadonly"), run.Logger.Child("installer"))
+
+	pkgBuildDirs, errInstall := preparer.Run(ctx, run, targets)
+	if errInstall != nil {
+		return errInstall
+	}
+
+	if cleanFunc := preparer.ShouldCleanMakeDeps(run, cmdArgs); cleanFunc != nil {
+		installer.AddPostInstallHook(cleanFunc)
+	}
+
+	if cleanAURDirsFunc := preparer.ShouldCleanAURDirs(run, pkgBuildDirs); cleanAURDirsFunc != nil {
+		installer.AddPostInstallHook(cleanAURDirsFunc)
+	}
+
+	go func() {
+		errComp := completion.Update(ctx, run.HTTPClient, o.dbExecutor,
+			o.cfg.AURURL, o.cfg.CompletionPath, o.cfg.CompletionInterval, false)
+		if errComp != nil {
+			o.logger.Warnln(errComp)
+		}
+	}()
+
+	srcInfo, errInstall := srcinfo.NewService(o.dbExecutor, o.cfg,
+		o.logger.Child("srcinfo"), run.CmdBuilder, run.VCSStore, pkgBuildDirs)
+	if errInstall != nil {
+		return errInstall
+	}
+
+	incompatible, errInstall := srcInfo.IncompatiblePkgs(ctx)
+	if errInstall != nil {
+		return errInstall
+	}
+
+	if errIncompatible := confirmIncompatible(o.logger, incompatible); errIncompatible != nil {
+		return errIncompatible
+	}
+
+	if errPGP := srcInfo.CheckPGPKeys(ctx); errPGP != nil {
+		return errPGP
+	}
+
+	if errInstall := installer.Install(ctx, cmdArgs, targets, pkgBuildDirs,
+		excluded, o.manualConfirmRequired(cmdArgs)); errInstall != nil {
+		return errInstall
+	}
+
+	var multiErr multierror.MultiError
+
+	failedAndIgnored, err := installer.CompileFailedAndIgnored()
+	if err != nil {
+		multiErr.Add(err)
+	}
+
+	if !cmdArgs.ExistsArg("w", "downloadonly") {
+		if err := srcInfo.UpdateVCSStore(ctx, targets, failedAndIgnored); err != nil {
+			o.logger.Warnln(err)
+		}
+	}
+
+	if err := installer.RunPostInstallHooks(ctx); err != nil {
+		multiErr.Add(err)
+	}
+
+	return multiErr.Return()
+}
+
+func (o *OperationService) manualConfirmRequired(cmdArgs *parser.Arguments) bool {
+	return (!cmdArgs.ExistsArg("u", "sysupgrade") && cmdArgs.Op != "Y") || o.cfg.DoubleConfirm
+}
+
+func confirmIncompatible(logger *text.Logger, incompatible []string) error {
+	if len(incompatible) > 0 {
+		logger.Warnln(gotext.Get("The following packages are not compatible with your architecture:"))
+
+		for _, pkg := range incompatible {
+			logger.Print("  " + text.Cyan(pkg))
+		}
+
+		logger.Println()
+
+		if !logger.ContinueTask(gotext.Get("Try to build them anyway?"), true, settings.NoConfirm) {
+			return &settings.ErrUserAbort{}
+		}
+	}
+
+	return nil
+}

+ 1 - 1
aur_source.go

@@ -1,4 +1,4 @@
-package main
+package workdir
 
 import (
 	"context"

+ 1 - 1
aur_source_test.go

@@ -1,7 +1,7 @@
 //go:build !integration
 // +build !integration
 
-package main
+package workdir
 
 import (
 	"context"

+ 62 - 0
pkg/sync/workdir/clean.go

@@ -0,0 +1,62 @@
+package workdir
+
+import (
+	"context"
+
+	"github.com/leonelquinteros/gotext"
+
+	"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/text"
+)
+
+func removeMake(ctx context.Context, config *settings.Configuration,
+	cmdBuilder exe.ICmdBuilder, makeDeps []string, cmdArgs *parser.Arguments,
+) error {
+	removeArguments := cmdArgs.CopyGlobal()
+
+	err := removeArguments.AddArg("R", "s", "u")
+	if err != nil {
+		return err
+	}
+
+	for _, pkg := range makeDeps {
+		removeArguments.AddTarget(pkg)
+	}
+
+	oldValue := settings.NoConfirm
+	settings.NoConfirm = true
+	err = cmdBuilder.Show(cmdBuilder.BuildPacmanCmd(ctx,
+		removeArguments, config.Mode, settings.NoConfirm))
+	settings.NoConfirm = oldValue
+
+	return err
+}
+
+func cleanAfter(ctx context.Context, run *runtime.Runtime,
+	cmdBuilder exe.ICmdBuilder, pkgbuildDirs map[string]string,
+) {
+	run.Logger.Println(gotext.Get("removing untracked AUR files from cache..."))
+
+	i := 0
+	for _, dir := range pkgbuildDirs {
+		run.Logger.OperationInfoln(gotext.Get("Cleaning (%d/%d): %s", i+1, len(pkgbuildDirs), text.Cyan(dir)))
+
+		_, stderr, err := cmdBuilder.Capture(
+			cmdBuilder.BuildGitCmd(
+				ctx, dir, "reset", "--hard", "HEAD"))
+		if err != nil {
+			run.Logger.Errorln(gotext.Get("error resetting %s: %s", dir, stderr))
+		}
+
+		if err := run.CmdBuilder.Show(
+			run.CmdBuilder.BuildGitCmd(
+				ctx, dir, "clean", "-fx", "--exclude", "*.pkg.*")); err != nil {
+			run.Logger.Errorln(err)
+		}
+
+		i++
+	}
+}

+ 39 - 0
pkg/sync/workdir/merge.go

@@ -0,0 +1,39 @@
+package workdir
+
+import (
+	"context"
+	"errors"
+
+	"github.com/leonelquinteros/gotext"
+
+	"github.com/Jguer/yay/v12/pkg/settings/exe"
+)
+
+func gitMerge(ctx context.Context, cmdBuilder exe.ICmdBuilder, dir string) error {
+	_, stderr, err := cmdBuilder.Capture(
+		cmdBuilder.BuildGitCmd(ctx,
+			dir, "reset", "--hard", "HEAD"))
+	if err != nil {
+		return errors.New(gotext.Get("error resetting %s: %s", dir, stderr))
+	}
+
+	_, stderr, err = cmdBuilder.Capture(
+		cmdBuilder.BuildGitCmd(ctx,
+			dir, "merge", "--no-edit", "--ff"))
+	if err != nil {
+		return errors.New(gotext.Get("error merging %s: %s", dir, stderr))
+	}
+
+	return nil
+}
+
+func mergePkgbuilds(ctx context.Context, cmdBuilder exe.ICmdBuilder, pkgbuildDirs map[string]string) error {
+	for _, dir := range pkgbuildDirs {
+		err := gitMerge(ctx, cmdBuilder, dir)
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}

+ 29 - 23
preparer.go

@@ -1,4 +1,4 @@
-package main
+package workdir
 
 import (
 	"context"
@@ -12,9 +12,11 @@ import (
 	"github.com/Jguer/yay/v12/pkg/dep"
 	"github.com/Jguer/yay/v12/pkg/download"
 	"github.com/Jguer/yay/v12/pkg/menus"
+	"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/build"
 	"github.com/Jguer/yay/v12/pkg/text"
 
 	gosrc "github.com/Morganamilo/go-srcinfo"
@@ -29,7 +31,7 @@ const (
 	PreDownloadSourcesHook HookType = "pre-download-sources"
 )
 
-type HookFn func(ctx context.Context, config *settings.Configuration, w io.Writer,
+type HookFn func(ctx context.Context, run *runtime.Runtime, w io.Writer,
 	pkgbuildDirsByBase map[string]string, installed mapset.Set[string],
 ) error
 
@@ -45,12 +47,13 @@ type Preparer struct {
 	cfg             *settings.Configuration
 	hooks           []Hook
 	downloadSources bool
+	log             *text.Logger
 
 	makeDeps []string
 }
 
 func NewPreparerWithoutHooks(dbExecutor db.Executor, cmdBuilder exe.ICmdBuilder,
-	cfg *settings.Configuration, downloadSources bool,
+	cfg *settings.Configuration, logger *text.Logger, downloadSources bool,
 ) *Preparer {
 	return &Preparer{
 		dbExecutor:      dbExecutor,
@@ -58,13 +61,14 @@ func NewPreparerWithoutHooks(dbExecutor db.Executor, cmdBuilder exe.ICmdBuilder,
 		cfg:             cfg,
 		hooks:           []Hook{},
 		downloadSources: downloadSources,
+		log:             logger,
 	}
 }
 
 func NewPreparer(dbExecutor db.Executor, cmdBuilder exe.ICmdBuilder,
-	cfg *settings.Configuration,
+	cfg *settings.Configuration, logger *text.Logger,
 ) *Preparer {
-	preper := NewPreparerWithoutHooks(dbExecutor, cmdBuilder, cfg, true)
+	preper := NewPreparerWithoutHooks(dbExecutor, cmdBuilder, cfg, logger, true)
 
 	if cfg.CleanMenu {
 		preper.hooks = append(preper.hooks, Hook{
@@ -93,20 +97,20 @@ func NewPreparer(dbExecutor db.Executor, cmdBuilder exe.ICmdBuilder,
 	return preper
 }
 
-func (preper *Preparer) ShouldCleanAURDirs(pkgBuildDirs map[string]string) PostInstallHookFunc {
+func (preper *Preparer) ShouldCleanAURDirs(run *runtime.Runtime, pkgBuildDirs map[string]string) build.PostInstallHookFunc {
 	if !preper.cfg.CleanAfter || len(pkgBuildDirs) == 0 {
 		return nil
 	}
 
-	text.Debugln("added post install hook to clean up AUR dirs", pkgBuildDirs)
+	preper.log.Debugln("added post install hook to clean up AUR dirs", pkgBuildDirs)
 
 	return func(ctx context.Context) error {
-		cleanAfter(ctx, preper.cfg, preper.cfg.Runtime.CmdBuilder, pkgBuildDirs)
+		cleanAfter(ctx, run, run.CmdBuilder, pkgBuildDirs)
 		return nil
 	}
 }
 
-func (preper *Preparer) ShouldCleanMakeDeps(cmdArgs *parser.Arguments) PostInstallHookFunc {
+func (preper *Preparer) ShouldCleanMakeDeps(run *runtime.Runtime, cmdArgs *parser.Arguments) build.PostInstallHookFunc {
 	if len(preper.makeDeps) == 0 {
 		return nil
 	}
@@ -118,25 +122,25 @@ func (preper *Preparer) ShouldCleanMakeDeps(cmdArgs *parser.Arguments) PostInsta
 		return nil
 	default:
 		isYesDefault := preper.cfg.RemoveMake == "askyes"
-		if !text.ContinueTask(os.Stdin, gotext.Get("Remove make dependencies after install?"),
+		if !preper.log.ContinueTask(gotext.Get("Remove make dependencies after install?"),
 			isYesDefault, settings.NoConfirm) {
 			return nil
 		}
 	}
 
-	text.Debugln("added post install hook to clean up AUR makedeps", preper.makeDeps)
+	preper.log.Debugln("added post install hook to clean up AUR makedeps", preper.makeDeps)
 
 	return func(ctx context.Context) error {
-		return removeMake(ctx, preper.cfg, preper.cfg.Runtime.CmdBuilder, preper.makeDeps, cmdArgs)
+		return removeMake(ctx, preper.cfg, run.CmdBuilder, preper.makeDeps, cmdArgs)
 	}
 }
 
-func (preper *Preparer) Run(ctx context.Context,
-	w io.Writer, targets []map[string]*dep.InstallInfo,
+func (preper *Preparer) Run(ctx context.Context, run *runtime.Runtime,
+	targets []map[string]*dep.InstallInfo,
 ) (pkgbuildDirsByBase map[string]string, err error) {
-	preper.Present(w, targets)
+	preper.Present(targets)
 
-	pkgBuildDirs, err := preper.PrepareWorkspace(ctx, targets)
+	pkgBuildDirs, err := preper.PrepareWorkspace(ctx, run, targets)
 	if err != nil {
 		return nil, err
 	}
@@ -144,7 +148,7 @@ func (preper *Preparer) Run(ctx context.Context,
 	return pkgBuildDirs, nil
 }
 
-func (preper *Preparer) Present(w io.Writer, targets []map[string]*dep.InstallInfo) {
+func (preper *Preparer) Present(targets []map[string]*dep.InstallInfo) {
 	pkgsBySourceAndReason := map[string]map[string][]string{}
 
 	for _, layer := range targets {
@@ -173,7 +177,7 @@ func (preper *Preparer) Present(w io.Writer, targets []map[string]*dep.InstallIn
 
 	for source, pkgsByReason := range pkgsBySourceAndReason {
 		for reason, pkgs := range pkgsByReason {
-			fmt.Fprintf(w, text.Bold("%s %s (%d):")+" %s\n",
+			preper.log.Printf(text.Bold("%s %s (%d):")+" %s\n",
 				source,
 				reason,
 				len(pkgs),
@@ -182,7 +186,9 @@ func (preper *Preparer) Present(w io.Writer, targets []map[string]*dep.InstallIn
 	}
 }
 
-func (preper *Preparer) PrepareWorkspace(ctx context.Context, targets []map[string]*dep.InstallInfo) (map[string]string, error) {
+func (preper *Preparer) PrepareWorkspace(ctx context.Context,
+	run *runtime.Runtime, targets []map[string]*dep.InstallInfo,
+) (map[string]string, error) {
 	aurBasesToClone := mapset.NewThreadUnsafeSet[string]()
 	pkgBuildDirsByBase := make(map[string]string, len(targets))
 
@@ -203,7 +209,7 @@ func (preper *Preparer) PrepareWorkspace(ctx context.Context, targets []map[stri
 	}
 
 	if _, errA := download.AURPKGBUILDRepos(ctx,
-		preper.cmdBuilder, aurBasesToClone.ToSlice(),
+		preper.cmdBuilder, preper.log.Child("download"), aurBasesToClone.ToSlice(),
 		preper.cfg.AURURL, preper.cfg.BuildDir, false); errA != nil {
 		return nil, errA
 	}
@@ -220,7 +226,7 @@ func (preper *Preparer) PrepareWorkspace(ctx context.Context, targets []map[stri
 	remoteNamesCache := mapset.NewThreadUnsafeSet(remoteNames...)
 	for _, hookFn := range preper.hooks {
 		if hookFn.Type == PreDownloadSourcesHook {
-			if err := hookFn.Hookfn(ctx, preper.cfg, os.Stdout, pkgBuildDirsByBase, remoteNamesCache); err != nil {
+			if err := hookFn.Hookfn(ctx, run, os.Stdout, pkgBuildDirsByBase, remoteNamesCache); err != nil {
 				return nil, err
 			}
 		}
@@ -228,7 +234,7 @@ func (preper *Preparer) PrepareWorkspace(ctx context.Context, targets []map[stri
 
 	if errP := downloadPKGBUILDSourceFanout(ctx, preper.cmdBuilder,
 		pkgBuildDirsByBase, false, preper.cfg.MaxConcurrentDownloads); errP != nil {
-		text.Errorln(errP)
+		preper.log.Errorln(errP)
 	}
 
 	return pkgBuildDirsByBase, nil
@@ -242,7 +248,7 @@ 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(
+			preper.log.OperationInfoln(
 				gotext.Get("PKGBUILD up to date, skipping download: %s",
 					text.Cyan(*installInfo.AURBase)))
 			return false

+ 9 - 2
preparer_test.go

@@ -1,16 +1,23 @@
 //go:build !integration
 // +build !integration
 
-package main
+package workdir
 
 import (
+	"io"
+	"strings"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
 
 	"github.com/Jguer/yay/v12/pkg/settings"
+	"github.com/Jguer/yay/v12/pkg/text"
 )
 
+func newTestLogger() *text.Logger {
+	return text.NewLogger(io.Discard, io.Discard, strings.NewReader(""), true, "test")
+}
+
 // Test order of pre-download-sources hooks
 func TestPreDownloadSourcesHooks(t *testing.T) {
 	testCases := []struct {
@@ -49,7 +56,7 @@ func TestPreDownloadSourcesHooks(t *testing.T) {
 
 	for _, tc := range testCases {
 		t.Run(tc.name, func(t *testing.T) {
-			preper := NewPreparer(nil, nil, tc.cfg)
+			preper := NewPreparer(nil, nil, tc.cfg, newTestLogger())
 
 			assert.Len(t, preper.hooks, len(tc.wantHook))
 

+ 51 - 5
pkg/text/input.go

@@ -3,14 +3,18 @@ package text
 import (
 	"bufio"
 	"fmt"
-	"io"
+	"strings"
+	"unicode"
+	"unicode/utf8"
+
+	"github.com/leonelquinteros/gotext"
 )
 
 func (l *Logger) GetInput(defaultValue string, noConfirm bool) (string, error) {
-	Info()
+	l.Info()
 
 	if defaultValue != "" || noConfirm {
-		fmt.Println(defaultValue)
+		l.Println(defaultValue)
 		return defaultValue, nil
 	}
 
@@ -28,6 +32,48 @@ func (l *Logger) GetInput(defaultValue string, noConfirm bool) (string, error) {
 	return string(buf), nil
 }
 
-func GetInput(r io.Reader, defaultValue string, noConfirm bool) (string, error) {
-	return GlobalLogger.GetInput(defaultValue, noConfirm)
+// ContinueTask prompts if user wants to continue task.
+// If NoConfirm is set the action will continue without user input.
+func (l *Logger) ContinueTask(s string, preset, noConfirm bool) bool {
+	if noConfirm {
+		return preset
+	}
+
+	var (
+		response string
+		postFix  string
+		n        string
+		y        string
+		yes      = gotext.Get("yes")
+		no       = gotext.Get("no")
+	)
+
+	// Only use localized "y" and "n" if they are latin characters.
+	if nRune, _ := utf8.DecodeRuneInString(no); unicode.Is(unicode.Latin, nRune) {
+		n = string(nRune)
+	} else {
+		n = nDefault
+	}
+
+	if yRune, _ := utf8.DecodeRuneInString(yes); unicode.Is(unicode.Latin, yRune) {
+		y = string(yRune)
+	} else {
+		y = yDefault
+	}
+
+	if preset { // If default behavior is true, use y as default.
+		postFix = fmt.Sprintf(" [%s/%s] ", strings.ToUpper(y), n)
+	} else { // If default behavior is anything else, use n as default.
+		postFix = fmt.Sprintf(" [%s/%s] ", y, strings.ToUpper(n))
+	}
+
+	l.OperationInfo(Bold(s), Bold(postFix))
+
+	if _, err := fmt.Fscanln(l.r, &response); err != nil {
+		return preset
+	}
+
+	return strings.EqualFold(response, yes) ||
+		strings.EqualFold(response, y) ||
+		(!strings.EqualFold(yDefault, n) && strings.EqualFold(response, yDefault))
 }

+ 0 - 139
pkg/text/print.go

@@ -1,139 +0,0 @@
-package text
-
-import (
-	"fmt"
-	"os"
-	"strconv"
-	"strings"
-	"syscall"
-	"unicode"
-
-	"github.com/leonelquinteros/gotext"
-	"golang.org/x/sys/unix"
-)
-
-const (
-	arrow      = "==>"
-	smallArrow = " ->"
-	opSymbol   = "::"
-)
-
-var (
-	cachedColumnCount = -1
-	GlobalLogger      = NewLogger(os.Stdout, os.Stderr, os.Stdin, false, "global")
-)
-
-func Debugln(a ...interface{}) {
-	GlobalLogger.Debugln(a...)
-}
-
-func OperationInfoln(a ...interface{}) {
-	GlobalLogger.OperationInfoln(a...)
-}
-
-func OperationInfo(a ...interface{}) {
-	GlobalLogger.OperationInfo(a...)
-}
-
-func SprintOperationInfo(a ...interface{}) string {
-	return GlobalLogger.SprintOperationInfo(a...)
-}
-
-func Info(a ...interface{}) {
-	GlobalLogger.Info(a...)
-}
-
-func Infoln(a ...interface{}) {
-	GlobalLogger.Infoln(a...)
-}
-
-func SprintWarn(a ...interface{}) string {
-	return GlobalLogger.SprintWarn(a...)
-}
-
-func Warn(a ...interface{}) {
-	GlobalLogger.Warn(a...)
-}
-
-func Warnln(a ...interface{}) {
-	GlobalLogger.Warnln(a...)
-}
-
-func SprintError(a ...interface{}) string {
-	return GlobalLogger.SprintError(a...)
-}
-
-func Error(a ...interface{}) {
-	GlobalLogger.Error(a...)
-}
-
-func Errorln(a ...interface{}) {
-	GlobalLogger.Errorln(a...)
-}
-
-func getColumnCount() int {
-	if cachedColumnCount > 0 {
-		return cachedColumnCount
-	}
-
-	if count, err := strconv.Atoi(os.Getenv("COLUMNS")); err == nil {
-		cachedColumnCount = count
-		return cachedColumnCount
-	}
-
-	if ws, err := unix.IoctlGetWinsize(syscall.Stdout, unix.TIOCGWINSZ); err == nil {
-		cachedColumnCount = int(ws.Col)
-		return cachedColumnCount
-	}
-
-	return 80
-}
-
-func PrintInfoValue(key string, values ...string) {
-	const (
-		keyLength  = 32
-		delimCount = 2
-	)
-
-	specialWordsCount := 0
-
-	for _, runeValue := range key {
-		// CJK handling: the character 'ー' is Katakana
-		// but if use unicode.Katakana, it will return false
-		if unicode.IsOneOf([]*unicode.RangeTable{
-			unicode.Han,
-			unicode.Hiragana,
-			unicode.Katakana,
-			unicode.Hangul,
-		}, runeValue) || runeValue == 'ー' {
-			specialWordsCount++
-		}
-	}
-
-	keyTextCount := specialWordsCount - keyLength + delimCount
-	str := fmt.Sprintf(Bold("%-*s: "), keyTextCount, key)
-
-	if len(values) == 0 || (len(values) == 1 && values[0] == "") {
-		fmt.Fprintf(os.Stdout, "%s%s\n", str, gotext.Get("None"))
-		return
-	}
-
-	maxCols := getColumnCount()
-	cols := keyLength + len(values[0])
-	str += values[0]
-
-	for _, value := range values[1:] {
-		if maxCols > keyLength && cols+len(value)+delimCount >= maxCols {
-			cols = keyLength
-			str += "\n" + strings.Repeat(" ", keyLength)
-		} else if cols != keyLength {
-			str += strings.Repeat(" ", delimCount)
-			cols += delimCount
-		}
-
-		str += value
-		cols += len(value)
-	}
-
-	fmt.Println(str)
-}

+ 6 - 0
pkg/text/service.go

@@ -5,6 +5,12 @@ import (
 	"io"
 )
 
+const (
+	arrow      = "==>"
+	smallArrow = " ->"
+	opSymbol   = "::"
+)
+
 type Logger struct {
 	name   string
 	Debug  bool

+ 0 - 51
pkg/text/text.go

@@ -1,13 +1,8 @@
 package text
 
 import (
-	"fmt"
-	"io"
 	"strings"
 	"unicode"
-	"unicode/utf8"
-
-	"github.com/leonelquinteros/gotext"
 )
 
 const (
@@ -52,49 +47,3 @@ func LessRunes(iRunes, jRunes []rune) bool {
 
 	return len(iRunes) < len(jRunes)
 }
-
-// ContinueTask prompts if user wants to continue task.
-// If NoConfirm is set the action will continue without user input.
-func ContinueTask(input io.Reader, s string, preset, noConfirm bool) bool {
-	if noConfirm {
-		return preset
-	}
-
-	var (
-		response string
-		postFix  string
-		n        string
-		y        string
-		yes      = gotext.Get("yes")
-		no       = gotext.Get("no")
-	)
-
-	// Only use localized "y" and "n" if they are latin characters.
-	if nRune, _ := utf8.DecodeRuneInString(no); unicode.Is(unicode.Latin, nRune) {
-		n = string(nRune)
-	} else {
-		n = nDefault
-	}
-
-	if yRune, _ := utf8.DecodeRuneInString(yes); unicode.Is(unicode.Latin, yRune) {
-		y = string(yRune)
-	} else {
-		y = yDefault
-	}
-
-	if preset { // If default behavior is true, use y as default.
-		postFix = fmt.Sprintf(" [%s/%s] ", strings.ToUpper(y), n)
-	} else { // If default behavior is anything else, use n as default.
-		postFix = fmt.Sprintf(" [%s/%s] ", y, strings.ToUpper(n))
-	}
-
-	OperationInfo(Bold(s), Bold(postFix))
-
-	if _, err := fmt.Fscanln(input, &response); err != nil {
-		return preset
-	}
-
-	return strings.EqualFold(response, yes) ||
-		strings.EqualFold(response, y) ||
-		(!strings.EqualFold(yDefault, n) && strings.EqualFold(response, yDefault))
-}

+ 7 - 3
pkg/text/text_test.go

@@ -4,6 +4,7 @@
 package text
 
 import (
+	"io"
 	"os"
 	"path"
 	"strings"
@@ -74,7 +75,8 @@ func TestContinueTask(t *testing.T) {
 		t.Run(tt.name, func(t *testing.T) {
 			// create io.Reader with value of input
 			in := strings.NewReader(tt.args.input)
-			got := ContinueTask(in, tt.args.s, tt.args.preset, tt.args.noConfirm)
+			logger := NewLogger(io.Discard, io.Discard, in, false, "test")
+			got := logger.ContinueTask(tt.args.s, tt.args.preset, tt.args.noConfirm)
 			require.Equal(t, tt.want, got)
 		})
 	}
@@ -120,7 +122,8 @@ msgstr "да"
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
 			in := strings.NewReader(tt.args.input)
-			got := ContinueTask(in, tt.args.s, tt.args.preset, tt.args.noConfirm)
+			logger := NewLogger(io.Discard, io.Discard, in, false, "test")
+			got := logger.ContinueTask(tt.args.s, tt.args.preset, tt.args.noConfirm)
 			require.Equal(t, tt.want, got)
 		})
 	}
@@ -168,7 +171,8 @@ msgstr "ja"
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
 			in := strings.NewReader(tt.args.input)
-			got := ContinueTask(in, tt.args.s, tt.args.preset, tt.args.noConfirm)
+			logger := NewLogger(io.Discard, io.Discard, in, false, "test")
+			got := logger.ContinueTask(tt.args.s, tt.args.preset, tt.args.noConfirm)
 			require.Equal(t, tt.want, got)
 		})
 	}

+ 8 - 6
pkg/vcs/vcs_test.go

@@ -7,7 +7,6 @@ import (
 	"context"
 	"encoding/json"
 	"errors"
-	"fmt"
 	"io"
 	"os"
 	"os/exec"
@@ -24,6 +23,10 @@ import (
 	"github.com/Jguer/yay/v12/pkg/text"
 )
 
+func newTestLogger() *text.Logger {
+	return text.NewLogger(io.Discard, io.Discard, strings.NewReader(""), true, "test")
+}
+
 func TestParsing(t *testing.T) {
 	t.Parallel()
 	type source struct {
@@ -232,7 +235,7 @@ func TestInfoStoreToUpgrade(t *testing.T) {
 		t.Run(tt.name, func(t *testing.T) {
 			t.Parallel()
 			v := &InfoStore{
-				logger:     text.GlobalLogger,
+				logger:     newTestLogger(),
 				CmdBuilder: tt.fields.CmdBuilder,
 				OriginsByPackage: map[string]OriginInfoByURL{
 					"yay": tt.args.infos,
@@ -365,7 +368,7 @@ func TestInfoStore_NeedsUpdate(t *testing.T) {
 		t.Run(tt.name, func(t *testing.T) {
 			t.Parallel()
 			v := &InfoStore{
-				logger:     text.GlobalLogger,
+				logger:     newTestLogger(),
 				CmdBuilder: tt.fields.CmdBuilder,
 			}
 			got := v.needsUpdate(context.Background(), tt.args.infos)
@@ -415,7 +418,7 @@ func TestInfoStore_Update(t *testing.T) {
 			t.Parallel()
 			v := &InfoStore{
 				OriginsByPackage: tt.fields.OriginsByPackage,
-				logger:           text.GlobalLogger,
+				logger:           newTestLogger(),
 				FilePath:         filePath,
 				CmdBuilder:       tt.fields.CmdBuilder,
 			}
@@ -429,7 +432,6 @@ func TestInfoStore_Update(t *testing.T) {
 			cupaloy.SnapshotT(t, marshalledinfo)
 
 			v.Load()
-			fmt.Println(v.OriginsByPackage)
 			assert.Len(t, tt.fields.OriginsByPackage, 1)
 
 			marshalledinfo, err = json.MarshalIndent(tt.fields.OriginsByPackage, "", "\t")
@@ -479,7 +481,7 @@ func TestInfoStore_Remove(t *testing.T) {
 			t.Parallel()
 			v := &InfoStore{
 				OriginsByPackage: tt.fields.OriginsByPackage,
-				logger:           text.GlobalLogger,
+				logger:           newTestLogger(),
 				FilePath:         filePath,
 			}
 			v.RemovePackages(tt.args.pkgs)

+ 129 - 55
print.go

@@ -6,14 +6,19 @@ import (
 	"io"
 	"os"
 	"strconv"
+	"strings"
+	"syscall"
+	"unicode"
 
 	aur "github.com/Jguer/aur"
 	mapset "github.com/deckarep/golang-set/v2"
 	"github.com/leonelquinteros/gotext"
+	"golang.org/x/sys/unix"
 
 	"github.com/Jguer/yay/v12/pkg/db"
 	"github.com/Jguer/yay/v12/pkg/dep"
 	"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"
@@ -21,47 +26,47 @@ import (
 )
 
 // printInfo prints package info like pacman -Si.
-func printInfo(config *settings.Configuration, a *aur.Pkg, extendedInfo bool) {
-	text.PrintInfoValue(gotext.Get("Repository"), "aur")
-	text.PrintInfoValue(gotext.Get("Name"), a.Name)
-	text.PrintInfoValue(gotext.Get("Version"), a.Version)
-	text.PrintInfoValue(gotext.Get("Description"), a.Description)
-	text.PrintInfoValue(gotext.Get("URL"), a.URL)
-	text.PrintInfoValue(gotext.Get("Licenses"), a.License...)
-	text.PrintInfoValue(gotext.Get("Groups"), a.Groups...)
-	text.PrintInfoValue(gotext.Get("Provides"), a.Provides...)
-	text.PrintInfoValue(gotext.Get("Depends On"), a.Depends...)
-	text.PrintInfoValue(gotext.Get("Optional Deps"), a.OptDepends...)
-	text.PrintInfoValue(gotext.Get("Make Deps"), a.MakeDepends...)
-	text.PrintInfoValue(gotext.Get("Check Deps"), a.CheckDepends...)
-	text.PrintInfoValue(gotext.Get("Conflicts With"), a.Conflicts...)
-	text.PrintInfoValue(gotext.Get("Replaces"), a.Replaces...)
-	text.PrintInfoValue(gotext.Get("AUR URL"), config.AURURL+"/packages/"+a.Name)
-	text.PrintInfoValue(gotext.Get("First Submitted"), text.FormatTimeQuery(a.FirstSubmitted))
-	text.PrintInfoValue(gotext.Get("Keywords"), a.Keywords...)
-	text.PrintInfoValue(gotext.Get("Last Modified"), text.FormatTimeQuery(a.LastModified))
-	text.PrintInfoValue(gotext.Get("Maintainer"), a.Maintainer)
-	text.PrintInfoValue(gotext.Get("Popularity"), fmt.Sprintf("%f", a.Popularity))
-	text.PrintInfoValue(gotext.Get("Votes"), fmt.Sprintf("%d", a.NumVotes))
+func printInfo(logger *text.Logger, config *settings.Configuration, a *aur.Pkg, extendedInfo bool) {
+	printInfoValue(logger, gotext.Get("Repository"), "aur")
+	printInfoValue(logger, gotext.Get("Name"), a.Name)
+	printInfoValue(logger, gotext.Get("Version"), a.Version)
+	printInfoValue(logger, gotext.Get("Description"), a.Description)
+	printInfoValue(logger, gotext.Get("URL"), a.URL)
+	printInfoValue(logger, gotext.Get("Licenses"), a.License...)
+	printInfoValue(logger, gotext.Get("Groups"), a.Groups...)
+	printInfoValue(logger, gotext.Get("Provides"), a.Provides...)
+	printInfoValue(logger, gotext.Get("Depends On"), a.Depends...)
+	printInfoValue(logger, gotext.Get("Optional Deps"), a.OptDepends...)
+	printInfoValue(logger, gotext.Get("Make Deps"), a.MakeDepends...)
+	printInfoValue(logger, gotext.Get("Check Deps"), a.CheckDepends...)
+	printInfoValue(logger, gotext.Get("Conflicts With"), a.Conflicts...)
+	printInfoValue(logger, gotext.Get("Replaces"), a.Replaces...)
+	printInfoValue(logger, gotext.Get("AUR URL"), config.AURURL+"/packages/"+a.Name)
+	printInfoValue(logger, gotext.Get("First Submitted"), text.FormatTimeQuery(a.FirstSubmitted))
+	printInfoValue(logger, gotext.Get("Keywords"), a.Keywords...)
+	printInfoValue(logger, gotext.Get("Last Modified"), text.FormatTimeQuery(a.LastModified))
+	printInfoValue(logger, gotext.Get("Maintainer"), a.Maintainer)
+	printInfoValue(logger, gotext.Get("Popularity"), fmt.Sprintf("%f", a.Popularity))
+	printInfoValue(logger, gotext.Get("Votes"), fmt.Sprintf("%d", a.NumVotes))
 
 	if a.OutOfDate != 0 {
-		text.PrintInfoValue(gotext.Get("Out-of-date"), text.FormatTimeQuery(a.OutOfDate))
+		printInfoValue(logger, gotext.Get("Out-of-date"), text.FormatTimeQuery(a.OutOfDate))
 	} else {
-		text.PrintInfoValue(gotext.Get("Out-of-date"), "No")
+		printInfoValue(logger, gotext.Get("Out-of-date"), "No")
 	}
 
 	if extendedInfo {
-		text.PrintInfoValue("ID", fmt.Sprintf("%d", a.ID))
-		text.PrintInfoValue(gotext.Get("Package Base ID"), fmt.Sprintf("%d", a.PackageBaseID))
-		text.PrintInfoValue(gotext.Get("Package Base"), a.PackageBase)
-		text.PrintInfoValue(gotext.Get("Snapshot URL"), config.AURURL+a.URLPath)
+		printInfoValue(logger, "ID", fmt.Sprintf("%d", a.ID))
+		printInfoValue(logger, gotext.Get("Package Base ID"), fmt.Sprintf("%d", a.PackageBaseID))
+		printInfoValue(logger, gotext.Get("Package Base"), a.PackageBase)
+		printInfoValue(logger, gotext.Get("Snapshot URL"), config.AURURL+a.URLPath)
 	}
 
-	fmt.Println()
+	logger.Println()
 }
 
 // BiggestPackages prints the name of the ten biggest packages in the system.
-func biggestPackages(dbExecutor db.Executor) {
+func biggestPackages(logger *text.Logger, dbExecutor db.Executor) {
 	pkgS := dbExecutor.BiggestPackages()
 
 	if len(pkgS) < 10 {
@@ -69,34 +74,34 @@ func biggestPackages(dbExecutor db.Executor) {
 	}
 
 	for i := 0; i < 10; i++ {
-		fmt.Printf("%s: %s\n", text.Bold(pkgS[i].Name()), text.Cyan(text.Human(pkgS[i].ISize())))
+		logger.Printf("%s: %s\n", text.Bold(pkgS[i].Name()), text.Cyan(text.Human(pkgS[i].ISize())))
 	}
 }
 
 // localStatistics prints installed packages statistics.
-func localStatistics(ctx context.Context, cfg *settings.Configuration, dbExecutor db.Executor) error {
-	info := statistics(cfg, dbExecutor)
+func localStatistics(ctx context.Context, run *runtime.Runtime, dbExecutor db.Executor) error {
+	info := statistics(run, dbExecutor)
 
 	remoteNames := dbExecutor.InstalledRemotePackageNames()
 	remote := dbExecutor.InstalledRemotePackages()
-	text.Infoln(gotext.Get("Yay version v%s", yayVersion))
-	fmt.Println(text.Bold(text.Cyan("===========================================")))
-	text.Infoln(gotext.Get("Total installed packages: %s", text.Cyan(strconv.Itoa(info.Totaln))))
-	text.Infoln(gotext.Get("Foreign installed packages: %s", text.Cyan(strconv.Itoa(len(remoteNames)))))
-	text.Infoln(gotext.Get("Explicitly installed packages: %s", text.Cyan(strconv.Itoa(info.Expln))))
-	text.Infoln(gotext.Get("Total Size occupied by packages: %s", text.Cyan(text.Human(info.TotalSize))))
+	run.Logger.Infoln(gotext.Get("Yay version v%s", yayVersion))
+	run.Logger.Println(text.Bold(text.Cyan("===========================================")))
+	run.Logger.Infoln(gotext.Get("Total installed packages: %s", text.Cyan(strconv.Itoa(info.Totaln))))
+	run.Logger.Infoln(gotext.Get("Foreign installed packages: %s", text.Cyan(strconv.Itoa(len(remoteNames)))))
+	run.Logger.Infoln(gotext.Get("Explicitly installed packages: %s", text.Cyan(strconv.Itoa(info.Expln))))
+	run.Logger.Infoln(gotext.Get("Total Size occupied by packages: %s", text.Cyan(text.Human(info.TotalSize))))
 
 	for path, size := range info.pacmanCaches {
-		text.Infoln(gotext.Get("Size of pacman cache %s: %s", path, text.Cyan(text.Human(size))))
+		run.Logger.Infoln(gotext.Get("Size of pacman cache %s: %s", path, text.Cyan(text.Human(size))))
 	}
 
-	text.Infoln(gotext.Get("Size of yay cache %s: %s", cfg.BuildDir, text.Cyan(text.Human(info.yayCache))))
-	fmt.Println(text.Bold(text.Cyan("===========================================")))
-	text.Infoln(gotext.Get("Ten biggest packages:"))
-	biggestPackages(dbExecutor)
-	fmt.Println(text.Bold(text.Cyan("===========================================")))
+	run.Logger.Infoln(gotext.Get("Size of yay cache %s: %s", run.Cfg.BuildDir, text.Cyan(text.Human(info.yayCache))))
+	run.Logger.Println(text.Bold(text.Cyan("===========================================")))
+	run.Logger.Infoln(gotext.Get("Ten biggest packages:"))
+	biggestPackages(run.Logger, dbExecutor)
+	run.Logger.Println(text.Bold(text.Cyan("===========================================")))
 
-	aurData, err := cfg.Runtime.AURClient.Get(ctx, &aur.Query{
+	aurData, err := run.AURClient.Get(ctx, &aur.Query{
 		Needles: remoteNames,
 		By:      aur.Name,
 	})
@@ -104,7 +109,7 @@ func localStatistics(ctx context.Context, cfg *settings.Configuration, dbExecuto
 		return err
 	}
 
-	warnings := query.NewWarnings(cfg.Runtime.Logger.Child("print"))
+	warnings := query.NewWarnings(run.Logger.Child("warnings"))
 	for i := range aurData {
 		warnings.AddToWarnings(remote, &aurData[i])
 	}
@@ -114,13 +119,13 @@ func localStatistics(ctx context.Context, cfg *settings.Configuration, dbExecuto
 	return nil
 }
 
-func printUpdateList(ctx context.Context, cfg *settings.Configuration, cmdArgs *parser.Arguments,
+func printUpdateList(ctx context.Context, run *runtime.Runtime, cmdArgs *parser.Arguments,
 	dbExecutor db.Executor, enableDowngrade bool, filter upgrade.Filter,
 ) error {
 	quietMode := cmdArgs.ExistsArg("q", "quiet")
 
 	// TODO: handle quiet mode in a better way
-	logger := text.NewLogger(io.Discard, os.Stderr, os.Stdin, cfg.Debug, "update-list")
+	logger := text.NewLogger(io.Discard, os.Stderr, os.Stdin, run.Cfg.Debug, "update-list")
 	dbExecutor.SetLogger(logger.Child("db"))
 	oldNoConfirm := settings.NoConfirm
 	settings.NoConfirm = true
@@ -128,12 +133,12 @@ func printUpdateList(ctx context.Context, cfg *settings.Configuration, cmdArgs *
 	defer func() { settings.NoConfirm = oldNoConfirm }()
 
 	targets := mapset.NewThreadUnsafeSet(cmdArgs.Targets...)
-	grapher := dep.NewGrapher(dbExecutor, cfg.Runtime.AURClient, false, true,
+	grapher := dep.NewGrapher(dbExecutor, run.AURClient, false, true,
 		false, false, cmdArgs.ExistsArg("needed"), logger.Child("grapher"))
 
 	upService := upgrade.NewUpgradeService(
-		grapher, cfg.Runtime.AURClient, dbExecutor, cfg.Runtime.VCSStore,
-		cfg, true, logger.Child("upgrade"))
+		grapher, run.AURClient, dbExecutor, run.VCSStore,
+		run.Cfg, true, logger.Child("upgrade"))
 
 	graph, errSysUp := upService.GraphUpgrades(ctx, nil,
 		enableDowngrade, filter)
@@ -163,9 +168,9 @@ func printUpdateList(ctx context.Context, cfg *settings.Configuration, cmdArgs *
 			}
 
 			if quietMode {
-				fmt.Printf("%s\n", pkgName)
+				run.Logger.Printf("%s\n", pkgName)
 			} else {
-				fmt.Printf("%s %s -> %s\n", text.Bold(pkgName), text.Bold(text.Green(ii.LocalVersion)),
+				run.Logger.Printf("%s %s -> %s\n", text.Bold(pkgName), text.Bold(text.Green(ii.LocalVersion)),
 					text.Bold(text.Green(ii.Version)))
 			}
 
@@ -179,7 +184,7 @@ func printUpdateList(ctx context.Context, cfg *settings.Configuration, cmdArgs *
 	missing := false
 	targets.Each(func(pkgName string) bool {
 		if dbExecutor.LocalPackage(pkgName) == nil {
-			cfg.Runtime.Logger.Errorln(gotext.Get("package '%s' was not found", pkgName))
+			run.Logger.Errorln(gotext.Get("package '%s' was not found", pkgName))
 			missing = true
 		}
 		return false
@@ -191,3 +196,72 @@ func printUpdateList(ctx context.Context, cfg *settings.Configuration, cmdArgs *
 
 	return nil
 }
+
+func printInfoValue(logger *text.Logger, key string, values ...string) {
+	const (
+		keyLength  = 32
+		delimCount = 2
+	)
+
+	specialWordsCount := 0
+
+	for _, runeValue := range key {
+		// CJK handling: the character 'ー' is Katakana
+		// but if use unicode.Katakana, it will return false
+		if unicode.IsOneOf([]*unicode.RangeTable{
+			unicode.Han,
+			unicode.Hiragana,
+			unicode.Katakana,
+			unicode.Hangul,
+		}, runeValue) || runeValue == 'ー' {
+			specialWordsCount++
+		}
+	}
+
+	keyTextCount := specialWordsCount - keyLength + delimCount
+	str := fmt.Sprintf(text.Bold("%-*s: "), keyTextCount, key)
+
+	if len(values) == 0 || (len(values) == 1 && values[0] == "") {
+		logger.Printf("%s%s\n", str, gotext.Get("None"))
+		return
+	}
+
+	maxCols := getColumnCount()
+	cols := keyLength + len(values[0])
+	str += values[0]
+
+	for _, value := range values[1:] {
+		if maxCols > keyLength && cols+len(value)+delimCount >= maxCols {
+			cols = keyLength
+			str += "\n" + strings.Repeat(" ", keyLength)
+		} else if cols != keyLength {
+			str += strings.Repeat(" ", delimCount)
+			cols += delimCount
+		}
+
+		str += value
+		cols += len(value)
+	}
+
+	logger.Println(str)
+}
+
+var cachedColumnCount = -1
+
+func getColumnCount() int {
+	if cachedColumnCount > 0 {
+		return cachedColumnCount
+	}
+
+	if count, err := strconv.Atoi(os.Getenv("COLUMNS")); err == nil {
+		cachedColumnCount = count
+		return cachedColumnCount
+	}
+
+	if ws, err := unix.IoctlGetWinsize(syscall.Stdout, unix.TIOCGWINSZ); err == nil {
+		cachedColumnCount = int(ws.Col)
+		return cachedColumnCount
+	}
+
+	return 80
+}

+ 13 - 13
print_test.go

@@ -20,6 +20,7 @@ import (
 	"github.com/Jguer/yay/v12/pkg/db"
 	"github.com/Jguer/yay/v12/pkg/db/mock"
 	mockaur "github.com/Jguer/yay/v12/pkg/dep/mock"
+	"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"
@@ -271,29 +272,28 @@ func TestPrintUpdateList(t *testing.T) {
 				SudoLoopEnabled:  false,
 			}
 
-			cfg := &settings.Configuration{
-				RemoveMake: "no",
-				Runtime: &settings.Runtime{
-					Logger:     NewTestLogger(),
-					CmdBuilder: cmdBuilder,
-					VCSStore:   &vcs.Mock{},
-					AURClient:  tc.mockData.aurCache,
+			r, w, _ := os.Pipe()
+
+			logger := text.NewLogger(w, io.Discard, strings.NewReader(""), true, "test")
+
+			run := &runtime.Runtime{
+				Cfg: &settings.Configuration{
+					RemoveMake: "no",
 				},
+				Logger:     logger,
+				CmdBuilder: cmdBuilder,
+				VCSStore:   &vcs.Mock{},
+				AURClient:  tc.mockData.aurCache,
 			}
 
 			cmdArgs := parser.MakeArguments()
 			cmdArgs.AddArg(tc.args...)
 			cmdArgs.AddTarget(tc.targets...)
 
-			rescueStdout := os.Stdout
-			r, w, _ := os.Pipe()
-			os.Stdout = w
-
-			err = handleCmd(context.Background(), cfg, cmdArgs, tc.mockData.db)
+			err = handleCmd(context.Background(), run, cmdArgs, tc.mockData.db)
 
 			w.Close()
 			out, _ := io.ReadAll(r)
-			os.Stdout = rescueStdout
 
 			if tc.wantErr {
 				require.Error(t, err)

+ 12 - 11
query.go

@@ -12,6 +12,7 @@ import (
 
 	"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"
@@ -32,7 +33,7 @@ func syncSearch(ctx context.Context, pkgS []string,
 }
 
 // SyncInfo serves as a pacman -Si for repo packages and AUR packages.
-func syncInfo(ctx context.Context, cfg *settings.Configuration,
+func syncInfo(ctx context.Context, run *runtime.Runtime,
 	cmdArgs *parser.Arguments, pkgS []string, dbExecutor db.Executor,
 ) error {
 	var (
@@ -41,8 +42,8 @@ func syncInfo(ctx context.Context, cfg *settings.Configuration,
 		missing = false
 	)
 
-	pkgS = query.RemoveInvalidTargets(pkgS, cfg.Mode)
-	aurS, repoS := packageSlices(pkgS, cfg, dbExecutor)
+	pkgS = query.RemoveInvalidTargets(run.Logger, pkgS, run.Cfg.Mode)
+	aurS, repoS := packageSlices(pkgS, run.Cfg, dbExecutor)
 
 	if len(aurS) != 0 {
 		noDB := make([]string, 0, len(aurS))
@@ -52,14 +53,14 @@ func syncInfo(ctx context.Context, cfg *settings.Configuration,
 			noDB = append(noDB, name)
 		}
 
-		info, err = cfg.Runtime.AURClient.Get(ctx, &aur.Query{
+		info, err = run.AURClient.Get(ctx, &aur.Query{
 			Needles: noDB,
 			By:      aur.Name,
 		})
 		if err != nil {
 			missing = true
 
-			cfg.Runtime.Logger.Errorln(err)
+			run.Logger.Errorln(err)
 		}
 	}
 
@@ -68,8 +69,8 @@ func syncInfo(ctx context.Context, cfg *settings.Configuration,
 		arguments.ClearTargets()
 		arguments.AddTarget(repoS...)
 
-		err = cfg.Runtime.CmdBuilder.Show(cfg.Runtime.CmdBuilder.BuildPacmanCmd(ctx,
-			arguments, cfg.Mode, settings.NoConfirm))
+		err = run.CmdBuilder.Show(run.CmdBuilder.BuildPacmanCmd(ctx,
+			arguments, run.Cfg.Mode, settings.NoConfirm))
 		if err != nil {
 			return err
 		}
@@ -81,7 +82,7 @@ func syncInfo(ctx context.Context, cfg *settings.Configuration,
 
 	if len(info) != 0 {
 		for i := range info {
-			printInfo(cfg, &info[i], cmdArgs.ExistsDouble("i"))
+			printInfo(run.Logger, run.Cfg, &info[i], cmdArgs.ExistsDouble("i"))
 		}
 	}
 
@@ -220,7 +221,7 @@ func getFolderSize(path string) (size int64) {
 }
 
 // Statistics returns statistics about packages installed in system.
-func statistics(cfg *settings.Configuration, dbExecutor db.Executor) (res struct {
+func statistics(run *runtime.Runtime, dbExecutor db.Executor) (res struct {
 	Totaln       int
 	Expln        int
 	TotalSize    int64
@@ -238,11 +239,11 @@ func statistics(cfg *settings.Configuration, dbExecutor db.Executor) (res struct
 	}
 
 	res.pacmanCaches = make(map[string]int64)
-	for _, path := range cfg.Runtime.PacmanConf.CacheDir {
+	for _, path := range run.PacmanConf.CacheDir {
 		res.pacmanCaches[path] = getFolderSize(path)
 	}
 
-	res.yayCache = getFolderSize(cfg.BuildDir)
+	res.yayCache = getFolderSize(run.Cfg.BuildDir)
 
 	return
 }

+ 17 - 16
query_test.go

@@ -16,6 +16,7 @@ import (
 	"github.com/Jguer/yay/v12/pkg/db/mock"
 	mockaur "github.com/Jguer/yay/v12/pkg/dep/mock"
 	"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"
@@ -125,12 +126,12 @@ func TestSyncInfo(t *testing.T) {
 				Runner:           mockRunner,
 				SudoLoopEnabled:  false,
 			}
-			cfg := &settings.Configuration{
-				Runtime: &settings.Runtime{
-					CmdBuilder: cmdBuilder,
-					AURClient:  mockAUR,
-					Logger:     NewTestLogger(),
-				},
+
+			run := &runtime.Runtime{
+				CmdBuilder: cmdBuilder,
+				AURClient:  mockAUR,
+				Logger:     newTestLogger(),
+				Cfg:        &settings.Configuration{},
 			}
 
 			cmdArgs := parser.MakeArguments()
@@ -138,7 +139,7 @@ func TestSyncInfo(t *testing.T) {
 			cmdArgs.AddTarget(tc.targets...)
 
 			err := handleCmd(context.Background(),
-				cfg, cmdArgs, dbExc,
+				run, cmdArgs, dbExc,
 			)
 
 			if tc.wantErr {
@@ -266,14 +267,14 @@ func TestSyncSearchAURDB(t *testing.T) {
 				Runner:           mockRunner,
 				SudoLoopEnabled:  false,
 			}
-			cfg := &settings.Configuration{
-				Runtime: &settings.Runtime{
-					CmdBuilder: cmdBuilder,
-					AURClient:  mockAUR,
-					QueryBuilder: query.NewSourceQueryBuilder(mockAUR, NewTestLogger(), "votes", parser.ModeAny, "name",
-						tc.bottomUp, tc.singleLine, tc.mixed),
-					Logger: NewTestLogger(),
-				},
+
+			run := &runtime.Runtime{
+				CmdBuilder: cmdBuilder,
+				AURClient:  mockAUR,
+				QueryBuilder: query.NewSourceQueryBuilder(mockAUR, newTestLogger(), "votes", parser.ModeAny, "name",
+					tc.bottomUp, tc.singleLine, tc.mixed),
+				Logger: newTestLogger(),
+				Cfg:    &settings.Configuration{},
 			}
 
 			cmdArgs := parser.MakeArguments()
@@ -281,7 +282,7 @@ func TestSyncSearchAURDB(t *testing.T) {
 			cmdArgs.AddTarget(tc.targets...)
 
 			err := handleCmd(context.Background(),
-				cfg, cmdArgs, dbExc,
+				run, cmdArgs, dbExc,
 			)
 
 			if tc.wantErr {

+ 26 - 125
sync.go

@@ -3,37 +3,36 @@ package main
 import (
 	"context"
 	"fmt"
-	"os"
 	"strings"
 
-	"github.com/Jguer/yay/v12/pkg/completion"
+	"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/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/srcinfo"
-	"github.com/Jguer/yay/v12/pkg/text"
+	"github.com/Jguer/yay/v12/pkg/sync"
 	"github.com/Jguer/yay/v12/pkg/upgrade"
-
-	"github.com/leonelquinteros/gotext"
 )
 
 func syncInstall(ctx context.Context,
-	cfg *settings.Configuration,
+	run *runtime.Runtime,
 	cmdArgs *parser.Arguments,
 	dbExecutor db.Executor,
 ) error {
-	aurCache := cfg.Runtime.AURClient
+	aurCache := run.AURClient
 	refreshArg := cmdArgs.ExistsArg("y", "refresh")
 	noDeps := cmdArgs.ExistsArg("d", "nodeps")
-	noCheck := strings.Contains(cfg.MFlags, "--nocheck")
+	noCheck := strings.Contains(run.Cfg.MFlags, "--nocheck")
 	if noDeps {
-		cfg.Runtime.CmdBuilder.AddMakepkgFlag("-d")
+		run.CmdBuilder.AddMakepkgFlag("-d")
 	}
 
-	if refreshArg && cfg.Mode.AtLeastRepo() {
-		if errR := earlyRefresh(ctx, cfg, cfg.Runtime.CmdBuilder, cmdArgs); errR != nil {
+	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)
 		}
 
@@ -45,7 +44,7 @@ func syncInstall(ctx context.Context,
 	}
 
 	grapher := dep.NewGrapher(dbExecutor, aurCache, false, settings.NoConfirm,
-		noDeps, noCheck, cmdArgs.ExistsArg("needed"), cfg.Runtime.Logger.Child("grapher"))
+		noDeps, noCheck, cmdArgs.ExistsArg("needed"), run.Logger.Child("grapher"))
 
 	graph, err := grapher.GraphFromTargets(ctx, nil, cmdArgs.Targets)
 	if err != nil {
@@ -57,8 +56,8 @@ func syncInstall(ctx context.Context,
 		var errSysUp error
 
 		upService := upgrade.NewUpgradeService(
-			grapher, aurCache, dbExecutor, cfg.Runtime.VCSStore,
-			cfg, settings.NoConfirm, cfg.Runtime.Logger.Child("upgrade"))
+			grapher, aurCache, dbExecutor, run.VCSStore,
+			run.Cfg, settings.NoConfirm, run.Logger.Child("upgrade"))
 
 		graph, errSysUp = upService.GraphUpgrades(ctx,
 			graph, cmdArgs.ExistsDouble("u", "sysupgrade"),
@@ -75,7 +74,7 @@ func syncInstall(ctx context.Context,
 		}
 	}
 
-	opService := NewOperationService(ctx, cfg, dbExecutor)
+	opService := sync.NewOperationService(ctx, dbExecutor, run)
 	multiErr := &multierror.MultiError{}
 	targets := graph.TopoSortedLayerMap(func(s string, ii *dep.InstallInfo) error {
 		if ii.Source == dep.Missing {
@@ -88,117 +87,19 @@ func syncInstall(ctx context.Context,
 		return err
 	}
 
-	return opService.Run(ctx, cmdArgs, targets, excluded)
-}
-
-type OperationService struct {
-	ctx        context.Context
-	cfg        *settings.Configuration
-	dbExecutor db.Executor
-}
-
-func NewOperationService(ctx context.Context, cfg *settings.Configuration, dbExecutor db.Executor) *OperationService {
-	return &OperationService{
-		ctx:        ctx,
-		cfg:        cfg,
-		dbExecutor: dbExecutor,
-	}
-}
-
-func (o *OperationService) Run(ctx context.Context,
-	cmdArgs *parser.Arguments,
-	targets []map[string]*dep.InstallInfo, excluded []string,
-) error {
-	if len(targets) == 0 {
-		fmt.Fprintln(os.Stdout, "", gotext.Get("there is nothing to do"))
-		return nil
-	}
-	preparer := NewPreparer(o.dbExecutor, o.cfg.Runtime.CmdBuilder, o.cfg)
-	installer := NewInstaller(o.dbExecutor, o.cfg.Runtime.CmdBuilder,
-		o.cfg.Runtime.VCSStore, o.cfg.Mode, o.cfg.ReBuild,
-		cmdArgs.ExistsArg("w", "downloadonly"), o.cfg.Runtime.Logger.Child("installer"))
-
-	pkgBuildDirs, errInstall := preparer.Run(ctx, os.Stdout, targets)
-	if errInstall != nil {
-		return errInstall
-	}
-
-	if cleanFunc := preparer.ShouldCleanMakeDeps(cmdArgs); cleanFunc != nil {
-		installer.AddPostInstallHook(cleanFunc)
-	}
-
-	if cleanAURDirsFunc := preparer.ShouldCleanAURDirs(pkgBuildDirs); cleanAURDirsFunc != nil {
-		installer.AddPostInstallHook(cleanAURDirsFunc)
-	}
-
-	go func() {
-		errComp := completion.Update(ctx, o.cfg.Runtime.HTTPClient, o.dbExecutor,
-			o.cfg.AURURL, o.cfg.CompletionPath, o.cfg.CompletionInterval, false)
-		if errComp != nil {
-			text.Warnln(errComp)
-		}
-	}()
-
-	srcInfo, errInstall := srcinfo.NewService(o.dbExecutor, o.cfg, o.cfg.Runtime.CmdBuilder, o.cfg.Runtime.VCSStore, pkgBuildDirs)
-	if errInstall != nil {
-		return errInstall
-	}
-
-	incompatible, errInstall := srcInfo.IncompatiblePkgs(ctx)
-	if errInstall != nil {
-		return errInstall
-	}
-
-	if errIncompatible := confirmIncompatible(incompatible); errIncompatible != nil {
-		return errIncompatible
-	}
-
-	if errPGP := srcInfo.CheckPGPKeys(ctx); errPGP != nil {
-		return errPGP
-	}
-
-	if errInstall := installer.Install(ctx, cmdArgs, targets, pkgBuildDirs,
-		excluded, o.manualConfirmRequired(cmdArgs)); errInstall != nil {
-		return errInstall
-	}
-
-	var multiErr multierror.MultiError
-
-	if err := installer.CompileFailedAndIgnored(); err != nil {
-		multiErr.Add(err)
-	}
-
-	if !cmdArgs.ExistsArg("w", "downloadonly") {
-		if err := srcInfo.UpdateVCSStore(ctx, targets, installer.failedAndIgnored); err != nil {
-			text.Warnln(err)
-		}
-	}
-
-	if err := installer.RunPostInstallHooks(ctx); err != nil {
-		multiErr.Add(err)
-	}
-
-	return multiErr.Return()
-}
-
-func (o *OperationService) manualConfirmRequired(cmdArgs *parser.Arguments) bool {
-	return (!cmdArgs.ExistsArg("u", "sysupgrade") && cmdArgs.Op != "Y") || o.cfg.DoubleConfirm
+	return opService.Run(ctx, run, cmdArgs, targets, excluded)
 }
 
-func confirmIncompatible(incompatible []string) error {
-	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(pkg))
-		}
-
-		fmt.Println()
-
-		if !text.ContinueTask(os.Stdin, gotext.Get("Try to build them anyway?"), true, settings.NoConfirm) {
-			return &settings.ErrUserAbort{}
-		}
+func earlyRefresh(ctx context.Context, cfg *settings.Configuration, cmdBuilder exe.ICmdBuilder, cmdArgs *parser.Arguments) error {
+	arguments := cmdArgs.Copy()
+	if cfg.CombinedUpgrade {
+		arguments.DelArg("u", "sysupgrade")
 	}
+	arguments.DelArg("s", "search")
+	arguments.DelArg("i", "info")
+	arguments.DelArg("l", "list")
+	arguments.ClearTargets()
 
-	return nil
+	return cmdBuilder.Show(cmdBuilder.BuildPacmanCmd(ctx,
+		arguments, cfg.Mode, settings.NoConfirm))
 }

+ 72 - 73
sync_test.go

@@ -23,6 +23,7 @@ import (
 	"github.com/Jguer/yay/v12/pkg/db"
 	"github.com/Jguer/yay/v12/pkg/db/mock"
 	mockaur "github.com/Jguer/yay/v12/pkg/dep/mock"
+	"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"
@@ -106,21 +107,20 @@ func TestSyncUpgrade(t *testing.T) {
 		},
 	}
 
-	cfg := &settings.Configuration{
-		RemoveMake: "no",
-		Runtime: &settings.Runtime{
-			Logger:     text.NewLogger(io.Discard, os.Stderr, strings.NewReader("\n"), true, "test"),
-			CmdBuilder: cmdBuilder,
-			VCSStore:   &vcs.Mock{},
-			AURClient: &mockaur.MockAUR{
-				GetFn: func(ctx context.Context, query *aur.Query) ([]aur.Pkg, error) {
-					return []aur.Pkg{}, nil
-				},
+	run := &runtime.Runtime{
+		Cfg: &settings.Configuration{
+			RemoveMake: "no",
+		},
+		Logger:     text.NewLogger(io.Discard, os.Stderr, strings.NewReader("\n"), true, "test"),
+		CmdBuilder: cmdBuilder,
+		VCSStore:   &vcs.Mock{},
+		AURClient: &mockaur.MockAUR{
+			GetFn: func(ctx context.Context, query *aur.Query) ([]aur.Pkg, error) {
+				return []aur.Pkg{}, nil
 			},
 		},
 	}
-
-	err = handleCmd(context.Background(), cfg, cmdArgs, db)
+	err = handleCmd(context.Background(), run, cmdArgs, db)
 	require.NoError(t, err)
 
 	wantCapture := []string{}
@@ -219,21 +219,20 @@ func TestSyncUpgrade_IgnoreAll(t *testing.T) {
 		},
 	}
 
-	cfg := &settings.Configuration{
-		RemoveMake: "no",
-		Runtime: &settings.Runtime{
-			Logger:     text.NewLogger(io.Discard, os.Stderr, strings.NewReader("1\n"), true, "test"),
-			CmdBuilder: cmdBuilder,
-			VCSStore:   &vcs.Mock{},
-			AURClient: &mockaur.MockAUR{
-				GetFn: func(ctx context.Context, query *aur.Query) ([]aur.Pkg, error) {
-					return []aur.Pkg{}, nil
-				},
+	run := &runtime.Runtime{
+		Cfg: &settings.Configuration{
+			RemoveMake: "no",
+		},
+		Logger:     text.NewLogger(io.Discard, os.Stderr, strings.NewReader("1\n"), true, "test"),
+		CmdBuilder: cmdBuilder,
+		VCSStore:   &vcs.Mock{},
+		AURClient: &mockaur.MockAUR{
+			GetFn: func(ctx context.Context, query *aur.Query) ([]aur.Pkg, error) {
+				return []aur.Pkg{}, nil
 			},
 		},
 	}
-
-	err = handleCmd(context.Background(), cfg, cmdArgs, db)
+	err = handleCmd(context.Background(), run, cmdArgs, db)
 	require.NoError(t, err)
 
 	wantCapture := []string{}
@@ -349,21 +348,21 @@ func TestSyncUpgrade_IgnoreOne(t *testing.T) {
 		},
 	}
 
-	cfg := &settings.Configuration{
-		RemoveMake: "no",
-		Runtime: &settings.Runtime{
-			Logger:     text.NewLogger(io.Discard, os.Stderr, strings.NewReader("1\n"), true, "test"),
-			CmdBuilder: cmdBuilder,
-			VCSStore:   &vcs.Mock{},
-			AURClient: &mockaur.MockAUR{
-				GetFn: func(ctx context.Context, query *aur.Query) ([]aur.Pkg, error) {
-					return []aur.Pkg{}, nil
-				},
+	run := &runtime.Runtime{
+		Cfg: &settings.Configuration{
+			RemoveMake: "no",
+		},
+		Logger:     text.NewLogger(io.Discard, os.Stderr, strings.NewReader("1\n"), true, "test"),
+		CmdBuilder: cmdBuilder,
+		VCSStore:   &vcs.Mock{},
+		AURClient: &mockaur.MockAUR{
+			GetFn: func(ctx context.Context, query *aur.Query) ([]aur.Pkg, error) {
+				return []aur.Pkg{}, nil
 			},
 		},
 	}
 
-	err = handleCmd(context.Background(), cfg, cmdArgs, db)
+	err = handleCmd(context.Background(), run, cmdArgs, db)
 	require.NoError(t, err)
 
 	wantCapture := []string{}
@@ -512,37 +511,37 @@ pkgname = python-vosk
 		},
 	}
 
-	cfg := &settings.Configuration{
-		DoubleConfirm: true,
-		RemoveMake:    "no",
-		BuildDir:      tmpDir,
-		Runtime: &settings.Runtime{
-			Logger:     text.NewLogger(io.Discard, os.Stderr, strings.NewReader("\n\n\n\n"), true, "test"),
-			CmdBuilder: cmdBuilder,
-			VCSStore:   &vcs.Mock{},
-			AURClient: &mockaur.MockAUR{
-				GetFn: func(ctx context.Context, query *aur.Query) ([]aur.Pkg, error) {
-					return []aur.Pkg{
-						{
-							Name:        "vosk-api",
-							PackageBase: "vosk-api",
-							Version:     "0.3.45-1",
-						},
-						{
-							Name:        "python-vosk",
-							PackageBase: "vosk-api",
-							Version:     "0.3.45-1",
-							Depends: []string{
-								"vosk-api=0.3.45",
-							},
+	run := &runtime.Runtime{
+		Cfg: &settings.Configuration{
+			DoubleConfirm: true,
+			RemoveMake:    "no",
+			BuildDir:      tmpDir,
+		},
+		Logger:     text.NewLogger(io.Discard, os.Stderr, strings.NewReader("\n\n\n\n"), true, "test"),
+		CmdBuilder: cmdBuilder,
+		VCSStore:   &vcs.Mock{},
+		AURClient: &mockaur.MockAUR{
+			GetFn: func(ctx context.Context, query *aur.Query) ([]aur.Pkg, error) {
+				return []aur.Pkg{
+					{
+						Name:        "vosk-api",
+						PackageBase: "vosk-api",
+						Version:     "0.3.45-1",
+					},
+					{
+						Name:        "python-vosk",
+						PackageBase: "vosk-api",
+						Version:     "0.3.45-1",
+						Depends: []string{
+							"vosk-api=0.3.45",
 						},
-					}, nil
-				},
+					},
+				}, nil
 			},
 		},
 	}
 
-	err = handleCmd(context.Background(), cfg, cmdArgs, db)
+	err = handleCmd(context.Background(), run, cmdArgs, db)
 	require.NoError(t, err)
 
 	wantCapture := []string{
@@ -697,22 +696,22 @@ func TestSyncUpgrade_NoCombinedUpgrade(t *testing.T) {
 				},
 			}
 
-			cfg := &settings.Configuration{
-				RemoveMake:      "no",
-				CombinedUpgrade: false,
-				Runtime: &settings.Runtime{
-					Logger:     text.NewLogger(io.Discard, os.Stderr, strings.NewReader("1\n"), true, "test"),
-					CmdBuilder: cmdBuilder,
-					VCSStore:   &vcs.Mock{},
-					AURClient: &mockaur.MockAUR{
-						GetFn: func(ctx context.Context, query *aur.Query) ([]aur.Pkg, error) {
-							return []aur.Pkg{}, nil
-						},
+			run := &runtime.Runtime{
+				Cfg: &settings.Configuration{
+					RemoveMake:      "no",
+					CombinedUpgrade: false,
+				},
+				Logger:     text.NewLogger(io.Discard, os.Stderr, strings.NewReader("1\n"), true, "test"),
+				CmdBuilder: cmdBuilder,
+				VCSStore:   &vcs.Mock{},
+				AURClient: &mockaur.MockAUR{
+					GetFn: func(ctx context.Context, query *aur.Query) ([]aur.Pkg, error) {
+						return []aur.Pkg{}, nil
 					},
 				},
 			}
 
-			err = handleCmd(context.Background(), cfg, cmdArgs, db)
+			err = handleCmd(context.Background(), run, cmdArgs, db)
 			require.NoError(t, err)
 
 			require.Len(t, mockRunner.ShowCalls, len(tc.want))

+ 11 - 12
vcs.go

@@ -2,7 +2,6 @@ package main
 
 import (
 	"context"
-	"os"
 	"sync"
 
 	"github.com/Jguer/aur"
@@ -10,9 +9,9 @@ import (
 
 	"github.com/Jguer/yay/v12/pkg/db"
 	"github.com/Jguer/yay/v12/pkg/dep"
-	"github.com/Jguer/yay/v12/pkg/settings"
-	"github.com/Jguer/yay/v12/pkg/srcinfo"
-	"github.com/Jguer/yay/v12/pkg/text"
+	"github.com/Jguer/yay/v12/pkg/runtime"
+	"github.com/Jguer/yay/v12/pkg/sync/srcinfo"
+	"github.com/Jguer/yay/v12/pkg/sync/workdir"
 )
 
 func infoToInstallInfo(info []aur.Pkg) []map[string]*dep.InstallInfo {
@@ -31,11 +30,11 @@ func infoToInstallInfo(info []aur.Pkg) []map[string]*dep.InstallInfo {
 }
 
 // createDevelDB forces yay to create a DB of the existing development packages.
-func createDevelDB(ctx context.Context, cfg *settings.Configuration, dbExecutor db.Executor) error {
+func createDevelDB(ctx context.Context, run *runtime.Runtime, dbExecutor db.Executor) error {
 	remoteNames := dbExecutor.InstalledRemotePackageNames()
 
-	cfg.Runtime.QueryBuilder.Execute(ctx, dbExecutor, remoteNames)
-	info, err := cfg.Runtime.AURClient.Get(ctx, &aur.Query{
+	run.QueryBuilder.Execute(ctx, dbExecutor, remoteNames)
+	info, err := run.AURClient.Get(ctx, &aur.Query{
 		Needles:  remoteNames,
 		By:       aur.Name,
 		Contains: false,
@@ -44,15 +43,15 @@ func createDevelDB(ctx context.Context, cfg *settings.Configuration, dbExecutor
 		return err
 	}
 
-	preper := NewPreparerWithoutHooks(dbExecutor, cfg.Runtime.CmdBuilder, cfg, false)
+	preper := workdir.NewPreparerWithoutHooks(dbExecutor, run.CmdBuilder, run.Cfg, run.Logger.Child("workdir"), false)
 
 	mapInfo := infoToInstallInfo(info)
-	pkgBuildDirsByBase, err := preper.Run(ctx, os.Stdout, mapInfo)
+	pkgBuildDirsByBase, err := preper.Run(ctx, run, mapInfo)
 	if err != nil {
 		return err
 	}
 
-	srcinfos, err := srcinfo.ParseSrcinfoFilesByBase(pkgBuildDirsByBase, false)
+	srcinfos, err := srcinfo.ParseSrcinfoFilesByBase(run.Logger.Child("srcinfo"), pkgBuildDirsByBase, false)
 	if err != nil {
 		return err
 	}
@@ -63,14 +62,14 @@ func createDevelDB(ctx context.Context, cfg *settings.Configuration, dbExecutor
 			wg.Add(1)
 
 			go func(i string, iP int) {
-				cfg.Runtime.VCSStore.Update(ctx, srcinfos[i].Packages[iP].Pkgname, srcinfos[i].Source)
+				run.VCSStore.Update(ctx, srcinfos[i].Packages[iP].Pkgname, srcinfos[i].Source)
 				wg.Done()
 			}(i, iP)
 		}
 	}
 
 	wg.Wait()
-	text.OperationInfoln(gotext.Get("GenDB finished. No packages were installed"))
+	run.Logger.OperationInfoln(gotext.Get("GenDB finished. No packages were installed"))
 
 	return err
 }

+ 4 - 3
vote.go

@@ -3,11 +3,12 @@ package main
 import (
 	"context"
 	"errors"
-	"fmt"
 
 	"github.com/Jguer/aur"
 	"github.com/Jguer/votar/pkg/vote"
 	"github.com/leonelquinteros/gotext"
+
+	"github.com/Jguer/yay/v12/pkg/text"
 )
 
 type ErrAURVote struct {
@@ -20,7 +21,7 @@ func (e *ErrAURVote) Error() string {
 }
 
 func handlePackageVote(ctx context.Context,
-	targets []string, aurClient aur.QueryClient,
+	targets []string, aurClient aur.QueryClient, logger *text.Logger,
 	voteClient *vote.Client, upvote bool,
 ) error {
 	infos, err := aurClient.Get(ctx, &aur.Query{
@@ -32,7 +33,7 @@ func handlePackageVote(ctx context.Context,
 	}
 
 	if len(infos) == 0 {
-		fmt.Println(gotext.Get(" there is nothing to do"))
+		logger.Println(gotext.Get(" there is nothing to do"))
 		return nil
 	}