diff --git a/cmd.go b/cmd.go index d4a847e5..7f2af4e0 100644 --- a/cmd.go +++ b/cmd.go @@ -149,13 +149,13 @@ getpkgbuild specific options: -p --print Print pkgbuild of packages`) } -func handleCmd(ctx context.Context, cmdArgs *parser.Arguments, dbExecutor db.Executor) error { +func handleCmd(ctx context.Context, cfg *settings.Configuration, cmdArgs *parser.Arguments, dbExecutor db.Executor) error { if cmdArgs.ExistsArg("h", "help") { return handleHelp(ctx, cmdArgs) } - if config.SudoLoop && cmdArgs.NeedRoot(config.Runtime.Mode) { - config.Runtime.CmdBuilder.SudoLoop() + if cfg.SudoLoop && cmdArgs.NeedRoot(cfg.Runtime.Mode) { + cfg.Runtime.CmdBuilder.SudoLoop() } switch cmdArgs.Op { @@ -164,30 +164,30 @@ func handleCmd(ctx context.Context, cmdArgs *parser.Arguments, dbExecutor db.Exe return nil case "D", "database": - return config.Runtime.CmdBuilder.Show(config.Runtime.CmdBuilder.BuildPacmanCmd(ctx, - cmdArgs, config.Runtime.Mode, settings.NoConfirm)) + return cfg.Runtime.CmdBuilder.Show(cfg.Runtime.CmdBuilder.BuildPacmanCmd(ctx, + cmdArgs, cfg.Runtime.Mode, settings.NoConfirm)) case "F", "files": - return config.Runtime.CmdBuilder.Show(config.Runtime.CmdBuilder.BuildPacmanCmd(ctx, - cmdArgs, config.Runtime.Mode, settings.NoConfirm)) + return cfg.Runtime.CmdBuilder.Show(cfg.Runtime.CmdBuilder.BuildPacmanCmd(ctx, + cmdArgs, cfg.Runtime.Mode, settings.NoConfirm)) case "Q", "query": return handleQuery(ctx, cmdArgs, dbExecutor) case "R", "remove": - return handleRemove(ctx, cmdArgs, config.Runtime.VCSStore) + return handleRemove(ctx, cmdArgs, cfg.Runtime.VCSStore) case "S", "sync": return handleSync(ctx, cmdArgs, dbExecutor) case "T", "deptest": - return config.Runtime.CmdBuilder.Show(config.Runtime.CmdBuilder.BuildPacmanCmd(ctx, - cmdArgs, config.Runtime.Mode, settings.NoConfirm)) + return cfg.Runtime.CmdBuilder.Show(cfg.Runtime.CmdBuilder.BuildPacmanCmd(ctx, + cmdArgs, cfg.Runtime.Mode, settings.NoConfirm)) case "U", "upgrade": - return handleUpgrade(ctx, config, cmdArgs) + return handleUpgrade(ctx, cfg, cmdArgs) case "B", "build": - return handleBuild(ctx, config, dbExecutor, cmdArgs) + return handleBuild(ctx, cfg, dbExecutor, cmdArgs) case "G", "getpkgbuild": return handleGetpkgbuild(ctx, cmdArgs, dbExecutor) case "P", "show": return handlePrint(ctx, cmdArgs, dbExecutor) case "Y", "yay": - return handleYay(ctx, cmdArgs, dbExecutor, config.Runtime.QueryBuilder) + return handleYay(ctx, cmdArgs, dbExecutor, cfg.Runtime.QueryBuilder) case "W", "web": return handleWeb(ctx, cmdArgs) } @@ -379,7 +379,7 @@ func handleSync(ctx context.Context, cmdArgs *parser.Arguments, dbExecutor db.Ex return nil } -func handleRemove(ctx context.Context, cmdArgs *parser.Arguments, localCache *vcs.InfoStore) error { +func handleRemove(ctx context.Context, cmdArgs *parser.Arguments, localCache vcs.Store) error { err := config.Runtime.CmdBuilder.Show(config.Runtime.CmdBuilder.BuildPacmanCmd(ctx, cmdArgs, config.Runtime.Mode, settings.NoConfirm)) if err == nil { diff --git a/install.go b/install.go index 9aec1872..21b949d9 100644 --- a/install.go +++ b/install.go @@ -278,7 +278,7 @@ func install(ctx context.Context, cmdArgs *parser.Arguments, dbExecutor db.Execu text.Errorln(errDiffMenu) } - if errM := mergePkgbuilds(ctx, pkgbuildDirs); errM != nil { + if errM := mergePkgbuilds(ctx, config.Runtime.CmdBuilder, pkgbuildDirs); errM != nil { return errM } @@ -302,7 +302,7 @@ func install(ctx context.Context, cmdArgs *parser.Arguments, dbExecutor db.Execu } if config.PGPFetch { - if _, errCPK := pgp.CheckPgpKeys(pkgbuildDirs, srcinfos, config.GpgBin, config.GpgFlags, settings.NoConfirm); errCPK != nil { + if _, errCPK := pgp.CheckPgpKeys(ctx, pkgbuildDirs, srcinfos, config.Runtime.CmdBuilder, settings.NoConfirm); errCPK != nil { return errCPK } } @@ -587,16 +587,16 @@ func pkgbuildsToSkip(bases []dep.Base, targets stringset.StringSet) stringset.St return toSkip } -func gitMerge(ctx context.Context, dir string) error { - _, stderr, err := config.Runtime.CmdBuilder.Capture( - config.Runtime.CmdBuilder.BuildGitCmd(ctx, +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 = config.Runtime.CmdBuilder.Capture( - config.Runtime.CmdBuilder.BuildGitCmd(ctx, + _, 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)) @@ -605,9 +605,9 @@ func gitMerge(ctx context.Context, dir string) error { return nil } -func mergePkgbuilds(ctx context.Context, pkgbuildDirs map[string]string) error { +func mergePkgbuilds(ctx context.Context, cmdBuilder exe.ICmdBuilder, pkgbuildDirs map[string]string) error { for _, dir := range pkgbuildDirs { - err := gitMerge(ctx, dir) + err := gitMerge(ctx, cmdBuilder, dir) if err != nil { return err } @@ -710,7 +710,7 @@ func buildInstallPkgbuilds( for _, split := range base { pkgdest, ok := pkgdests[split.Name] if !ok { - return errors.New(gotext.Get("could not find PKGDEST for: %s", split.Name)) + return &PkgDestNotInListError{split.Name} } if _, errStat := os.Stat(pkgdest); os.IsNotExist(errStat) { @@ -894,7 +894,7 @@ func doAddTarget(dp *dep.Pool, localNamesCache, remoteNamesCache stringset.Strin return deps, exp, pkgArchives, nil } - return deps, exp, pkgArchives, errors.New(gotext.Get("could not find PKGDEST for: %s", name)) + return deps, exp, pkgArchives, &PkgDestNotInListError{name} } if _, errStat := os.Stat(pkgdest); os.IsNotExist(errStat) { diff --git a/local_install.go b/local_install.go index b24c6ada..7f1a5b77 100644 --- a/local_install.go +++ b/local_install.go @@ -10,6 +10,7 @@ import ( "github.com/Jguer/yay/v11/pkg/db" "github.com/Jguer/yay/v11/pkg/dep" + "github.com/Jguer/yay/v11/pkg/multierror" "github.com/Jguer/yay/v11/pkg/settings" "github.com/Jguer/yay/v11/pkg/settings/parser" "github.com/Jguer/yay/v11/pkg/topo" @@ -51,5 +52,16 @@ func installLocalPKGBUILD( } opService := NewOperationService(ctx, config, dbExecutor) - return opService.Run(ctx, cmdArgs, graph.TopoSortedLayerMap()) + multiErr := &multierror.MultiError{} + targets := graph.TopoSortedLayerMap(func(name string, ii *dep.InstallInfo) error { + if ii.Source == dep.Missing { + multiErr.Add(errors.New(gotext.Get("could not find %s%s", name, ii.Version))) + } + return nil + }) + + if err := multiErr.Return(); err != nil { + return err + } + return opService.Run(ctx, cmdArgs, targets) } diff --git a/local_install_test.go b/local_install_test.go new file mode 100644 index 00000000..cb3fdbe4 --- /dev/null +++ b/local_install_test.go @@ -0,0 +1,281 @@ +package main + +import ( + "context" + "fmt" + "os" + "os/exec" + "strings" + "sync" + "testing" + + aur "github.com/Jguer/aur" + "github.com/Jguer/aur/metadata" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/Jguer/yay/v11/pkg/db/mock" + mockaur "github.com/Jguer/yay/v11/pkg/dep/mock" + "github.com/Jguer/yay/v11/pkg/settings" + "github.com/Jguer/yay/v11/pkg/settings/exe" + "github.com/Jguer/yay/v11/pkg/settings/parser" + "github.com/Jguer/yay/v11/pkg/vcs" +) + +func TestIntegrationLocalInstall(t *testing.T) { + makepkgBin := t.TempDir() + "/makepkg" + pacmanBin := t.TempDir() + "/pacman" + gitBin := t.TempDir() + "/git" + tmpDir := t.TempDir() + f, err := os.OpenFile(makepkgBin, os.O_RDONLY|os.O_CREATE, 0o755) + require.NoError(t, err) + require.NoError(t, f.Close()) + + f, err = os.OpenFile(pacmanBin, os.O_RDONLY|os.O_CREATE, 0o755) + require.NoError(t, err) + require.NoError(t, f.Close()) + + f, err = os.OpenFile(gitBin, os.O_RDONLY|os.O_CREATE, 0o755) + require.NoError(t, err) + require.NoError(t, f.Close()) + + tars := []string{ + tmpDir + "/jellyfin-10.8.4-1-x86_64.pkg.tar.zst", + tmpDir + "/jellyfin-web-10.8.4-1-x86_64.pkg.tar.zst", + tmpDir + "/jellyfin-server-10.8.4-1-x86_64.pkg.tar.zst", + } + + wantShow := []string{ + "makepkg --verifysource -Ccf", + "pacman -S --config /etc/pacman.conf -- community/dotnet-sdk-6.0 community/dotnet-runtime-6.0", + "pacman -D -q --asdeps --config /etc/pacman.conf -- dotnet-runtime-6.0 dotnet-sdk-6.0", + "makepkg --nobuild -fC --ignorearch", + "makepkg -c --nobuild --noextract --ignorearch", + "makepkg --nobuild -fC --ignorearch", + "makepkg -c --nobuild --noextract --ignorearch", + "makepkg --nobuild -fC --ignorearch", + "makepkg -c --nobuild --noextract --ignorearch", + "pacman -U --config /etc/pacman.conf -- /testdir/jellyfin-server-10.8.4-1-x86_64.pkg.tar.zst /testdir/jellyfin-10.8.4-1-x86_64.pkg.tar.zst /testdir/jellyfin-web-10.8.4-1-x86_64.pkg.tar.zst", + "pacman -D -q --asexplicit --config /etc/pacman.conf -- jellyfin-server jellyfin jellyfin-web", + } + + wantCapture := []string{ + "makepkg --packagelist", + "git -C testdata/jfin git reset --hard HEAD", + "git -C testdata/jfin git merge --no-edit --ff", + "makepkg --packagelist", + "makepkg --packagelist", + } + + captureOverride := func(cmd *exec.Cmd) (stdout string, stderr string, err error) { + return strings.Join(tars, "\n"), "", nil + } + + once := sync.Once{} + + showOverride := func(cmd *exec.Cmd) error { + once.Do(func() { + for _, tar := range tars { + f, err := os.OpenFile(tar, os.O_RDONLY|os.O_CREATE, 0o666) + require.NoError(t, err) + require.NoError(t, f.Close()) + } + }) + return nil + } + + mockRunner := &exe.MockRunner{CaptureFn: captureOverride, ShowFn: showOverride} + cmdBuilder := &exe.CmdBuilder{ + MakepkgBin: makepkgBin, + SudoBin: "su", + PacmanBin: pacmanBin, + PacmanConfigPath: "/etc/pacman.conf", + GitBin: "git", + Runner: mockRunner, + SudoLoopEnabled: false, + } + + cmdArgs := parser.MakeArguments() + cmdArgs.AddArg("B") + cmdArgs.AddArg("i") + cmdArgs.AddTarget("testdata/jfin") + db := &mock.DBExecutor{ + AlpmArchitecturesFn: func() ([]string, error) { + return []string{"x86_64"}, nil + }, + LocalSatisfierExistsFn: func(s string) bool { + switch s { + case "dotnet-sdk>=6", "dotnet-sdk<7", "dotnet-runtime>=6", "dotnet-runtime<7", "jellyfin-server=10.8.4", "jellyfin-web=10.8.4": + return false + } + + return true + }, + SyncSatisfierFn: func(s string) mock.IPackage { + switch s { + case "dotnet-runtime>=6", "dotnet-runtime<7": + return &mock.Package{ + PName: "dotnet-runtime-6.0", + PBase: "dotnet-runtime-6.0", + PVersion: "6.0.100-1", + PDB: mock.NewDB("community"), + } + case "dotnet-sdk>=6", "dotnet-sdk<7": + return &mock.Package{ + PName: "dotnet-sdk-6.0", + PBase: "dotnet-sdk-6.0", + PVersion: "6.0.100-1", + PDB: mock.NewDB("community"), + } + } + + return nil + }, + } + + config := &settings.Configuration{ + Runtime: &settings.Runtime{ + CmdBuilder: cmdBuilder, + VCSStore: &vcs.Mock{}, + AURCache: &mockaur.MockAUR{ + GetFn: func(ctx context.Context, query *metadata.AURQuery) ([]*aur.Pkg, error) { + return []*aur.Pkg{}, nil + }, + }, + }, + } + + err = handleCmd(context.Background(), config, cmdArgs, db) + require.NoError(t, err) + + require.Len(t, mockRunner.ShowCalls, len(wantShow)) + require.Len(t, mockRunner.CaptureCalls, len(wantCapture)) + + for i, call := range mockRunner.ShowCalls { + show := call.Args[0].(*exec.Cmd).String() + show = strings.ReplaceAll(show, tmpDir, "/testdir") // replace the temp dir with a static path + show = strings.ReplaceAll(show, makepkgBin, "makepkg") + show = strings.ReplaceAll(show, pacmanBin, "pacman") + show = strings.ReplaceAll(show, gitBin, "pacman") + + // options are in a different order on different systems and on CI root user is used + assert.Subset(t, strings.Split(show, " "), strings.Split(wantShow[i], " "), fmt.Sprintf("%d - %s", i, show)) + } +} + +func TestIntegrationLocalInstallMissingDep(t *testing.T) { + wantErr := "could not find dotnet-sdk>=6" + makepkgBin := t.TempDir() + "/makepkg" + pacmanBin := t.TempDir() + "/pacman" + gitBin := t.TempDir() + "/git" + tmpDir := t.TempDir() + f, err := os.OpenFile(makepkgBin, os.O_RDONLY|os.O_CREATE, 0o755) + require.NoError(t, err) + require.NoError(t, f.Close()) + + f, err = os.OpenFile(pacmanBin, os.O_RDONLY|os.O_CREATE, 0o755) + require.NoError(t, err) + require.NoError(t, f.Close()) + + f, err = os.OpenFile(gitBin, os.O_RDONLY|os.O_CREATE, 0o755) + require.NoError(t, err) + require.NoError(t, f.Close()) + + tars := []string{ + tmpDir + "/jellyfin-10.8.4-1-x86_64.pkg.tar.zst", + tmpDir + "/jellyfin-web-10.8.4-1-x86_64.pkg.tar.zst", + tmpDir + "/jellyfin-server-10.8.4-1-x86_64.pkg.tar.zst", + } + + wantShow := []string{} + wantCapture := []string{} + + captureOverride := func(cmd *exec.Cmd) (stdout string, stderr string, err error) { + return strings.Join(tars, "\n"), "", nil + } + + once := sync.Once{} + + showOverride := func(cmd *exec.Cmd) error { + once.Do(func() { + for _, tar := range tars { + f, err := os.OpenFile(tar, os.O_RDONLY|os.O_CREATE, 0o666) + require.NoError(t, err) + require.NoError(t, f.Close()) + } + }) + return nil + } + + mockRunner := &exe.MockRunner{CaptureFn: captureOverride, ShowFn: showOverride} + cmdBuilder := &exe.CmdBuilder{ + MakepkgBin: makepkgBin, + SudoBin: "su", + PacmanBin: pacmanBin, + PacmanConfigPath: "/etc/pacman.conf", + GitBin: "git", + Runner: mockRunner, + SudoLoopEnabled: false, + } + + cmdArgs := parser.MakeArguments() + cmdArgs.AddArg("B") + cmdArgs.AddArg("i") + cmdArgs.AddTarget("testdata/jfin") + db := &mock.DBExecutor{ + AlpmArchitecturesFn: func() ([]string, error) { + return []string{"x86_64"}, nil + }, + LocalSatisfierExistsFn: func(s string) bool { + switch s { + case "dotnet-sdk>=6", "dotnet-sdk<7", "dotnet-runtime>=6", "dotnet-runtime<7", "jellyfin-server=10.8.4", "jellyfin-web=10.8.4": + return false + } + + return true + }, + SyncSatisfierFn: func(s string) mock.IPackage { + switch s { + case "dotnet-runtime>=6", "dotnet-runtime<7": + return &mock.Package{ + PName: "dotnet-runtime-6.0", + PBase: "dotnet-runtime-6.0", + PVersion: "6.0.100-1", + PDB: mock.NewDB("community"), + } + } + + return nil + }, + } + + config := &settings.Configuration{ + Runtime: &settings.Runtime{ + CmdBuilder: cmdBuilder, + VCSStore: &vcs.Mock{}, + AURCache: &mockaur.MockAUR{ + GetFn: func(ctx context.Context, query *metadata.AURQuery) ([]*aur.Pkg, error) { + return []*aur.Pkg{}, nil + }, + }, + }, + } + + err = handleCmd(context.Background(), config, cmdArgs, db) + require.Error(t, err) + require.EqualError(t, err, wantErr) + + require.Len(t, mockRunner.ShowCalls, len(wantShow)) + require.Len(t, mockRunner.CaptureCalls, len(wantCapture)) + + for i, call := range mockRunner.ShowCalls { + show := call.Args[0].(*exec.Cmd).String() + show = strings.ReplaceAll(show, tmpDir, "/testdir") // replace the temp dir with a static path + show = strings.ReplaceAll(show, makepkgBin, "makepkg") + show = strings.ReplaceAll(show, pacmanBin, "pacman") + show = strings.ReplaceAll(show, gitBin, "pacman") + + // options are in a different order on different systems and on CI root user is used + assert.Subset(t, strings.Split(show, " "), strings.Split(wantShow[i], " "), fmt.Sprintf("%d - %s", i, show)) + } +} diff --git a/main.go b/main.go index a1751b97..1e4ae7a4 100644 --- a/main.go +++ b/main.go @@ -141,7 +141,7 @@ func main() { dbExecutor.Cleanup() }() - if err = handleCmd(ctx, cmdArgs, db.Executor(dbExecutor)); err != nil { + if err = handleCmd(ctx, config, cmdArgs, db.Executor(dbExecutor)); err != nil { if str := err.Error(); str != "" { text.Errorln(str) } diff --git a/pkg/cmd/graph/main.go b/pkg/cmd/graph/main.go index ae0bd0f9..75580db1 100644 --- a/pkg/cmd/graph/main.go +++ b/pkg/cmd/graph/main.go @@ -74,7 +74,7 @@ func graphPackage( fmt.Fprintln(os.Stdout, graph.String()) fmt.Fprintln(os.Stdout, "\nlayers\n", graph.TopoSortedLayers()) fmt.Fprintln(os.Stdout, "\ninverted order\n", graph.TopoSorted()) - fmt.Fprintln(os.Stdout, "\nlayers map\n", graph.TopoSortedLayerMap()) + fmt.Fprintln(os.Stdout, "\nlayers map\n", graph.TopoSortedLayerMap(nil)) return nil } diff --git a/pkg/db/mock/executor.go b/pkg/db/mock/executor.go index b72ef2d0..cf3c7493 100644 --- a/pkg/db/mock/executor.go +++ b/pkg/db/mock/executor.go @@ -21,9 +21,13 @@ type DBExecutor struct { PackagesFromGroupFn func(string) []IPackage LocalSatisfierExistsFn func(string) bool SyncSatisfierFn func(string) IPackage + AlpmArchitecturesFn func() ([]string, error) } func (t *DBExecutor) AlpmArchitectures() ([]string, error) { + if t.AlpmArchitecturesFn != nil { + return t.AlpmArchitecturesFn() + } panic("implement me") } diff --git a/pkg/dep/dep_graph.go b/pkg/dep/dep_graph.go index 84175bd5..bcd402cc 100644 --- a/pkg/dep/dep_graph.go +++ b/pkg/dep/dep_graph.go @@ -300,7 +300,7 @@ func (g *Grapher) addNodes( depType Reason, ) { for _, depString := range deps { - depName, _, _ := splitDep(depString) + depName, mod, ver := splitDep(depString) if g.dbExecutor.LocalSatisfierExists(depString) { if g.fullGraph { @@ -403,7 +403,16 @@ func (g *Grapher) addNodes( } // no dep found. add as missing - graph.SetNodeInfo(depString, &topo.NodeInfo[*InstallInfo]{Color: colorMap[depType], Background: bgColorMap[Missing]}) + graph.AddNode(depName) + graph.SetNodeInfo(depName, &topo.NodeInfo[*InstallInfo]{ + Color: colorMap[depType], + Background: bgColorMap[Missing], + Value: &InstallInfo{ + Source: Missing, + Reason: depType, + Version: fmt.Sprintf("%s%s", mod, ver), + }, + }) } } diff --git a/pkg/dep/dep_graph_test.go b/pkg/dep/dep_graph_test.go index dc72d859..dab314f1 100644 --- a/pkg/dep/dep_graph_test.go +++ b/pkg/dep/dep_graph_test.go @@ -9,6 +9,7 @@ import ( "github.com/Jguer/yay/v11/pkg/db" "github.com/Jguer/yay/v11/pkg/db/mock" + mockaur "github.com/Jguer/yay/v11/pkg/dep/mock" aur "github.com/Jguer/yay/v11/pkg/query" "github.com/Jguer/aur/metadata" @@ -19,21 +20,7 @@ func ptrString(s string) *string { return &s } -type getFunc func(ctx context.Context, query *metadata.AURQuery) ([]*aur.Pkg, error) - -type MockAUR struct { - GetFn getFunc -} - -func (m *MockAUR) Get(ctx context.Context, query *metadata.AURQuery) ([]*aur.Pkg, error) { - if m.GetFn != nil { - return m.GetFn(ctx, query) - } - - panic("implement me") -} - -func getFromFile(t *testing.T, filePath string) getFunc { +func getFromFile(t *testing.T, filePath string) mockaur.GetFunc { f, err := os.Open(filePath) require.NoError(t, err) @@ -85,7 +72,7 @@ func TestGrapher_GraphFromTargets_jellyfin(t *testing.T) { }, } - mockAUR := &MockAUR{GetFn: func(ctx context.Context, query *metadata.AURQuery) ([]*aur.Pkg, error) { + mockAUR := &mockaur.MockAUR{GetFn: func(ctx context.Context, query *metadata.AURQuery) ([]*aur.Pkg, error) { if query.Needles[0] == "jellyfin" { jfinFn := getFromFile(t, "testdata/jellyfin.json") return jfinFn(ctx, query) @@ -210,7 +197,7 @@ func TestGrapher_GraphFromTargets_jellyfin(t *testing.T) { tt.fields.noDeps, tt.fields.noCheckDeps) got, err := g.GraphFromTargets(context.Background(), nil, tt.args.targets) require.NoError(t, err) - layers := got.TopoSortedLayerMap() + layers := got.TopoSortedLayerMap(nil) require.EqualValues(t, tt.want, layers, layers) }) } diff --git a/pkg/dep/mock/aur.go b/pkg/dep/mock/aur.go new file mode 100644 index 00000000..51d55d55 --- /dev/null +++ b/pkg/dep/mock/aur.go @@ -0,0 +1,22 @@ +package mock + +import ( + "context" + + "github.com/Jguer/aur" + "github.com/Jguer/aur/metadata" +) + +type GetFunc func(ctx context.Context, query *metadata.AURQuery) ([]*aur.Pkg, error) + +type MockAUR struct { + GetFn GetFunc +} + +func (m *MockAUR) Get(ctx context.Context, query *metadata.AURQuery) ([]*aur.Pkg, error) { + if m.GetFn != nil { + return m.GetFn(ctx, query) + } + + panic("implement me") +} diff --git a/pkg/pgp/keys.go b/pkg/pgp/keys.go index 440ca61f..d16ebb59 100644 --- a/pkg/pgp/keys.go +++ b/pkg/pgp/keys.go @@ -2,6 +2,7 @@ package pgp import ( "bytes" + "context" "errors" "fmt" "os" @@ -11,6 +12,7 @@ import ( gosrc "github.com/Morganamilo/go-srcinfo" "github.com/leonelquinteros/gotext" + "github.com/Jguer/yay/v11/pkg/settings/exe" "github.com/Jguer/yay/v11/pkg/text" ) @@ -41,17 +43,20 @@ func (set pgpKeySet) get(key string) bool { return exists } +type GPGCmdBuilder interface { + exe.Runner + BuildGPGCmd(ctx context.Context, extraArgs ...string) *exec.Cmd +} + // CheckPgpKeys iterates through the keys listed in the PKGBUILDs and if needed, // asks the user whether yay should try to import them. -func CheckPgpKeys(pkgbuildDirsByBase map[string]string, srcinfos map[string]*gosrc.Srcinfo, - gpgBin, gpgFlags string, noConfirm bool, +func CheckPgpKeys(ctx context.Context, 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 // the problematic ones. problematic := make(pgpKeySet) - args := append(strings.Fields(gpgFlags), "--list-keys") - // Mapping all the keys. for pkg := range pkgbuildDirsByBase { srcinfo := srcinfos[pkg] @@ -64,8 +69,7 @@ func CheckPgpKeys(pkgbuildDirsByBase map[string]string, srcinfos map[string]*gos continue } - cmd := exec.Command(gpgBin, append(args, key)...) - if err := cmd.Run(); err != nil { + if err := cmdBuilder.Show(cmdBuilder.BuildGPGCmd(ctx, "--list-keys", key)); err != nil { problematic.set(key, pkg) } } @@ -85,21 +89,17 @@ func CheckPgpKeys(pkgbuildDirsByBase map[string]string, srcinfos map[string]*gos fmt.Println(str) if text.ContinueTask(os.Stdin, gotext.Get("Import?"), true, noConfirm) { - return problematic.toSlice(), importKeys(problematic.toSlice(), gpgBin, gpgFlags) + return problematic.toSlice(), importKeys(ctx, cmdBuilder, problematic.toSlice()) } return problematic.toSlice(), nil } // importKeys tries to import the list of keys specified in its argument. -func importKeys(keys []string, gpgBin, gpgFlags string) error { - args := append(strings.Fields(gpgFlags), "--recv-keys") - cmd := exec.Command(gpgBin, append(args, keys...)...) - cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr - +func importKeys(ctx context.Context, cmdBuilder GPGCmdBuilder, keys []string) error { text.OperationInfoln(gotext.Get("Importing keys with gpg...")) - if err := cmd.Run(); err != nil { + if err := cmdBuilder.Show(cmdBuilder.BuildGPGCmd(ctx, append([]string{"--recv-keys"}, keys...)...)); err != nil { return errors.New(gotext.Get("problem importing keys")) } diff --git a/pkg/pgp/keys_test.go b/pkg/pgp/keys_test.go index b1709f25..8d4cc3ba 100644 --- a/pkg/pgp/keys_test.go +++ b/pkg/pgp/keys_test.go @@ -1,134 +1,21 @@ package pgp import ( - "bytes" "context" "fmt" - "net/http" "os" - "path" - "regexp" + "os/exec" + "sort" + "strings" "testing" - "time" gosrc "github.com/Morganamilo/go-srcinfo" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/Jguer/yay/v11/pkg/settings/exe" ) -const ( - // The default port used by the PGP key server. - gpgServerPort = 11371 -) - -func init() { - http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - regex := regexp.MustCompile(`search=0[xX]([a-fA-F0-9]+)`) - matches := regex.FindStringSubmatch(r.RequestURI) - data := "" - if matches != nil { - data = getPgpKey(matches[1]) - } - w.Header().Set("Content-Type", "application/pgp-keys") - _, err := w.Write([]byte(data)) - if err != nil { - fmt.Fprintln(os.Stderr, err) - } - }) -} - -func getPgpKey(key string) string { - var buffer bytes.Buffer - - if contents, err := os.ReadFile(path.Join("testdata", key)); err == nil { - buffer.WriteString("-----BEGIN PGP PUBLIC KEY BLOCK-----\n") - buffer.WriteString("Version: SKS 1.1.6\n") - buffer.WriteString("Comment: Hostname: yay\n\n") - buffer.Write(contents) - buffer.WriteString("\n-----END PGP PUBLIC KEY BLOCK-----\n") - } - return buffer.String() -} - -func startPgpKeyServer() *http.Server { - srv := &http.Server{Addr: fmt.Sprintf("127.0.0.1:%d", gpgServerPort), ReadHeaderTimeout: 1 * time.Second} - - go func() { - err := srv.ListenAndServe() - if err != nil { - fmt.Fprintln(os.Stderr, err) - } - }() - return srv -} - -func TestImportKeys(t *testing.T) { - keyringDir := t.TempDir() - - server := startPgpKeyServer() - defer func() { - err := server.Shutdown(context.TODO()) - if err != nil { - fmt.Fprintln(os.Stderr, err) - } - }() - - casetests := []struct { - keys []string - wantError bool - }{ - // Single key, should succeed. - // C52048C0C0748FEE227D47A2702353E0F7E48EDB: Thomas Dickey. - { - keys: []string{"C52048C0C0748FEE227D47A2702353E0F7E48EDB"}, - wantError: false, - }, - // Two keys, should succeed as well. - // 11E521D646982372EB577A1F8F0871F202119294: Tom Stellard. - // B6C8F98282B944E3B0D5C2530FC3042E345AD05D: Hans Wennborg. - { - keys: []string{ - "11E521D646982372EB577A1F8F0871F202119294", - "B6C8F98282B944E3B0D5C2530FC3042E345AD05D", - }, - wantError: false, - }, - // Single invalid key, should fail. - { - keys: []string{"THIS-SHOULD-FAIL"}, - wantError: true, - }, - // Two invalid keys, should fail. - { - keys: []string{"THIS-SHOULD-FAIL", "THIS-ONE-SHOULD-FAIL-TOO"}, - wantError: true, - }, - // Invalid + valid key. Should fail as well. - // 647F28654894E3BD457199BE38DBBDC86092693E: Greg Kroah-Hartman. - { - keys: []string{ - "THIS-SHOULD-FAIL", - "647F28654894E3BD457199BE38DBBDC86092693E", - }, - wantError: true, - }, - } - - for _, tt := range casetests { - err := importKeys(tt.keys, "gpg", fmt.Sprintf("--homedir %s --keyserver 127.0.0.1", keyringDir)) - if !tt.wantError { - if err != nil { - t.Fatalf("Got error %q, want no error", err) - } - continue - } - // Here, we want to see the error. - if err == nil { - t.Fatalf("Got no error; want error") - } - } -} - func makeSrcinfo(pkgbase string, pgpkeys ...string) *gosrc.Srcinfo { srcinfo := gosrc.Srcinfo{} srcinfo.Pkgbase = pkgbase @@ -138,22 +25,21 @@ func makeSrcinfo(pkgbase string, pgpkeys ...string) *gosrc.Srcinfo { } func TestCheckPgpKeys(t *testing.T) { - keyringDir := t.TempDir() + gpgBin := t.TempDir() + "/gpg" - server := startPgpKeyServer() - defer func() { - err := server.Shutdown(context.TODO()) - if err != nil { - fmt.Fprintln(os.Stderr, err) - } - }() + f, err := os.OpenFile(gpgBin, os.O_RDONLY|os.O_CREATE, 0o755) + require.NoError(t, err) + require.NoError(t, f.Close()) - casetests := []struct { - name string - pkgs map[string]string - srcinfos map[string]*gosrc.Srcinfo - wantError bool - expected []string + testcases := []struct { + name string + pkgs map[string]string + srcinfos map[string]*gosrc.Srcinfo + wantError bool + wantShow []string + wantCapture []string + showFn func(cmd *exec.Cmd) error + expected []string }{ // cower: single package, one valid key not yet in the keyring. // 487EACC08557AD082088DABA1EB2638FF56C0C53: Dave Reisner. @@ -162,22 +48,44 @@ func TestCheckPgpKeys(t *testing.T) { pkgs: map[string]string{"cower": ""}, srcinfos: map[string]*gosrc.Srcinfo{"cower": makeSrcinfo("cower", "487EACC08557AD082088DABA1EB2638FF56C0C53")}, wantError: false, - expected: []string{"487EACC08557AD082088DABA1EB2638FF56C0C53"}, + wantShow: []string{ + "gpg --homedir /tmp --list-keys 487EACC08557AD082088DABA1EB2638FF56C0C53", + "gpg --homedir /tmp --recv-keys 487EACC08557AD082088DABA1EB2638FF56C0C53", + }, + wantCapture: []string{}, + showFn: func(cmd *exec.Cmd) error { + s := cmd.String() + if strings.Contains(s, "--list-keys") { + return fmt.Errorf("key not found") + } + return nil + }, + expected: []string{"487EACC08557AD082088DABA1EB2638FF56C0C53"}, }, // libc++: single package, two valid keys not yet in the keyring. // 11E521D646982372EB577A1F8F0871F202119294: Tom Stellard. // B6C8F98282B944E3B0D5C2530FC3042E345AD05D: Hans Wennborg. { - name: "two valid keys not yet in the keyring", - pkgs: map[string]string{"libc++": ""}, - srcinfos: map[string]*gosrc.Srcinfo{ - "libc++": makeSrcinfo("libc++", "11E521D646982372EB577A1F8F0871F202119294", "B6C8F98282B944E3B0D5C2530FC3042E345AD05D"), - }, + name: "two valid keys not yet in the keyring", + pkgs: map[string]string{"libc++": ""}, + srcinfos: map[string]*gosrc.Srcinfo{"libc++": makeSrcinfo("libc++", "11E521D646982372EB577A1F8F0871F202119294", "B6C8F98282B944E3B0D5C2530FC3042E345AD05D")}, wantError: false, - expected: []string{"11E521D646982372EB577A1F8F0871F202119294", "B6C8F98282B944E3B0D5C2530FC3042E345AD05D"}, + wantShow: []string{ + "gpg --homedir /tmp --list-keys 11E521D646982372EB577A1F8F0871F202119294", + "gpg --homedir /tmp --list-keys B6C8F98282B944E3B0D5C2530FC3042E345AD05D", + "gpg --homedir /tmp --recv-keys 11E521D646982372EB577A1F8F0871F202119294 B6C8F98282B944E3B0D5C2530FC3042E345AD05D", + }, + wantCapture: []string{}, + showFn: func(cmd *exec.Cmd) error { + s := cmd.String() + if strings.Contains(s, "--list-keys") { + return fmt.Errorf("key not found") + } + + return nil + }, + expected: []string{"11E521D646982372EB577A1F8F0871F202119294", "B6C8F98282B944E3B0D5C2530FC3042E345AD05D"}, }, - // Two dummy packages requiring the same key. - // ABAF11C65A2970B130ABE3C479BE3E4300411886: Linus Torvalds. { name: "Two dummy packages requiring the same key", pkgs: map[string]string{"dummy-1": "", "dummy-2": ""}, @@ -186,8 +94,21 @@ func TestCheckPgpKeys(t *testing.T) { "ABAF11C65A2970B130ABE3C479BE3E4300411886"), "dummy-2": makeSrcinfo("dummy-2", "ABAF11C65A2970B130ABE3C479BE3E4300411886"), }, - wantError: false, - expected: []string{"ABAF11C65A2970B130ABE3C479BE3E4300411886"}, + wantError: false, + expected: []string{"ABAF11C65A2970B130ABE3C479BE3E4300411886"}, + wantCapture: []string{}, + wantShow: []string{ + "gpg --homedir /tmp --list-keys ABAF11C65A2970B130ABE3C479BE3E4300411886", + "gpg --homedir /tmp --recv-keys ABAF11C65A2970B130ABE3C479BE3E4300411886", + }, + showFn: func(cmd *exec.Cmd) error { + s := cmd.String() + if strings.Contains(s, "--list-keys") { + return fmt.Errorf("key not found") + } + + return nil + }, }, // dummy package: single package, two valid keys, one of them already // in the keyring. @@ -199,8 +120,23 @@ func TestCheckPgpKeys(t *testing.T) { srcinfos: map[string]*gosrc.Srcinfo{ "dummy-3": makeSrcinfo("dummy-3", "11E521D646982372EB577A1F8F0871F202119294", "C52048C0C0748FEE227D47A2702353E0F7E48EDB"), }, - wantError: false, - expected: []string{"C52048C0C0748FEE227D47A2702353E0F7E48EDB"}, + wantError: false, + expected: []string{"C52048C0C0748FEE227D47A2702353E0F7E48EDB"}, + wantCapture: []string{}, + showFn: func(cmd *exec.Cmd) error { + s := cmd.String() + if strings.Contains(s, "--list-keys") && + !strings.Contains(s, "11E521D646982372EB577A1F8F0871F202119294") { + return fmt.Errorf("key not found") + } + + return nil + }, + wantShow: []string{ + "gpg --homedir /tmp --list-keys 11E521D646982372EB577A1F8F0871F202119294", + "gpg --homedir /tmp --list-keys C52048C0C0748FEE227D47A2702353E0F7E48EDB", + "gpg --homedir /tmp --recv-keys C52048C0C0748FEE227D47A2702353E0F7E48EDB", + }, }, // Two dummy packages with existing keys. { @@ -210,32 +146,106 @@ func TestCheckPgpKeys(t *testing.T) { "dummy-4": makeSrcinfo("dummy-4", "11E521D646982372EB577A1F8F0871F202119294"), "dummy-5": makeSrcinfo("dummy-5", "C52048C0C0748FEE227D47A2702353E0F7E48EDB"), }, - wantError: false, - expected: []string{}, + wantError: false, + expected: []string{}, + wantCapture: []string{}, + showFn: func(cmd *exec.Cmd) error { + return nil + }, + wantShow: []string{ + "gpg --homedir /tmp --list-keys 11E521D646982372EB577A1F8F0871F202119294", + "gpg --homedir /tmp --list-keys C52048C0C0748FEE227D47A2702353E0F7E48EDB", + }, }, // Dummy package with invalid key, should fail. { - name: "one invalid", - pkgs: map[string]string{"dummy-7": ""}, - srcinfos: map[string]*gosrc.Srcinfo{"dummy-7": makeSrcinfo("dummy-7", "THIS-SHOULD-FAIL")}, - wantError: true, + name: "one invalid", + pkgs: map[string]string{"dummy-7": ""}, + srcinfos: map[string]*gosrc.Srcinfo{"dummy-7": makeSrcinfo("dummy-7", "THIS-SHOULD-FAIL")}, + wantError: true, + wantCapture: []string{}, + wantShow: []string{ + "gpg --homedir /tmp --list-keys THIS-SHOULD-FAIL", + "gpg --homedir /tmp --recv-keys THIS-SHOULD-FAIL", + }, + showFn: func(cmd *exec.Cmd) error { + s := cmd.String() + if strings.Contains(s, "--list-keys") { + return fmt.Errorf("key not found") + } + + if strings.Contains(s, "--recv-keys") { + return fmt.Errorf("invalid key") + } + + return nil + }, }, // Dummy package with both an invalid an another valid key, should fail. // A314827C4E4250A204CE6E13284FC34C8E4B1A25: Thomas Bächler. { - name: "one invalid, one valid", - pkgs: map[string]string{"dummy-8": ""}, - srcinfos: map[string]*gosrc.Srcinfo{"dummy-8": makeSrcinfo("dummy-8", "A314827C4E4250A204CE6E13284FC34C8E4B1A25", "THIS-SHOULD-FAIL")}, - wantError: true, - expected: []string{}, + name: "one invalid, one valid", + pkgs: map[string]string{"dummy-8": ""}, + srcinfos: map[string]*gosrc.Srcinfo{"dummy-8": makeSrcinfo("dummy-8", "A314827C4E4250A204CE6E13284FC34C8E4B1A25", "THIS-SHOULD-FAIL")}, + wantError: true, + expected: []string{}, + wantCapture: []string{}, + showFn: func(cmd *exec.Cmd) error { + s := cmd.String() + if strings.Contains(s, "--list-keys") { + return fmt.Errorf("key not found") + } + + if strings.Contains(s, "--recv-keys") { + return fmt.Errorf("invalid key") + } + + return nil + }, + wantShow: []string{ + "gpg --homedir /tmp --list-keys A314827C4E4250A204CE6E13284FC34C8E4B1A25", + "gpg --homedir /tmp --list-keys THIS-SHOULD-FAIL", + "gpg --homedir /tmp --recv-keys A314827C4E4250A204CE6E13284FC34C8E4B1A25 THIS-SHOULD-FAIL", + }, }, } - for _, tt := range casetests { + for _, tt := range testcases { tt := tt t.Run(tt.name, func(t *testing.T) { - problematic, err := CheckPgpKeys(tt.pkgs, tt.srcinfos, "gpg", - fmt.Sprintf("--homedir %s --keyserver 127.0.0.1", keyringDir), true) + mockRunner := &exe.MockRunner{ + ShowFn: tt.showFn, + CaptureFn: func(cmd *exec.Cmd) (stdout string, stderr string, err error) { + return "", "", nil + }, + } + + cmdBuilder := exe.CmdBuilder{ + GPGBin: gpgBin, + GPGFlags: []string{"--homedir /tmp"}, + Runner: mockRunner, + } + problematic, err := CheckPgpKeys(context.Background(), tt.pkgs, tt.srcinfos, &cmdBuilder, true) + + require.Len(t, mockRunner.ShowCalls, len(tt.wantShow)) + require.Len(t, mockRunner.CaptureCalls, len(tt.wantCapture)) + + sort.SliceStable(mockRunner.ShowCalls, func(i, j int) bool { + return mockRunner.ShowCalls[i].Args[0].(*exec.Cmd).String() < mockRunner.ShowCalls[j].Args[0].(*exec.Cmd).String() + }) + for i, call := range mockRunner.ShowCalls { + show := call.Args[0].(*exec.Cmd).String() + show = strings.ReplaceAll(show, gpgBin, "gpg") + + // options are in a different order on different systems and on CI root user is used + assert.Subset(t, strings.Split(show, " "), strings.Split(tt.wantShow[i], " "), show) + } + + for i, call := range mockRunner.CaptureCalls { + capture := call.Args[0].(*exec.Cmd).String() + capture = strings.ReplaceAll(capture, gpgBin, "gpg") + assert.Subset(t, strings.Split(capture, " "), strings.Split(tt.wantCapture[i], " "), capture) + } if tt.wantError { require.Error(t, err) diff --git a/pkg/query/source.go b/pkg/query/source.go index 5849d444..9cdb44b3 100644 --- a/pkg/query/source.go +++ b/pkg/query/source.go @@ -28,6 +28,10 @@ const ( Minimal ) +type AURCache interface { + Get(ctx context.Context, query *metadata.AURQuery) ([]*aur.Pkg, error) +} + type SourceQueryBuilder struct { repoQuery aurQuery @@ -40,12 +44,12 @@ type SourceQueryBuilder struct { singleLineResults bool aurClient aur.ClientInterface - aurCache *metadata.Client + aurCache AURCache } func NewSourceQueryBuilder( aurClient aur.ClientInterface, - aurCache *metadata.Client, + aurCache AURCache, sortBy string, targetMode parser.TargetMode, searchBy string, @@ -193,7 +197,7 @@ func filterAURResults(pkgS []string, results []aur.Pkg) []aur.Pkg { // queryAUR searches AUR and narrows based on subarguments. func queryAUR(ctx context.Context, - aurClient aur.ClientInterface, aurMetadata *metadata.Client, + aurClient aur.ClientInterface, aurMetadata AURCache, pkgS []string, searchBy string, newEngine bool, ) ([]aur.Pkg, error) { var ( diff --git a/pkg/settings/exe/cmd_builder.go b/pkg/settings/exe/cmd_builder.go index 8c0a0ebd..60c7b2aa 100644 --- a/pkg/settings/exe/cmd_builder.go +++ b/pkg/settings/exe/cmd_builder.go @@ -28,6 +28,7 @@ type GitCmdBuilder interface { type ICmdBuilder interface { Runner BuildGitCmd(ctx context.Context, dir string, extraArgs ...string) *exec.Cmd + BuildGPGCmd(ctx context.Context, extraArgs ...string) *exec.Cmd 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) @@ -38,6 +39,8 @@ type ICmdBuilder interface { type CmdBuilder struct { GitBin string GitFlags []string + GPGBin string + GPGFlags []string MakepkgFlags []string MakepkgConfPath string MakepkgBin string @@ -50,6 +53,21 @@ type CmdBuilder struct { Runner Runner } +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) + + if len(extraArgs) > 0 { + args = append(args, extraArgs...) + } + + cmd := exec.CommandContext(ctx, c.GPGBin, args...) + + cmd = c.deElevateCommand(ctx, cmd) + + return cmd +} + func (c *CmdBuilder) BuildGitCmd(ctx context.Context, dir string, extraArgs ...string) *exec.Cmd { args := make([]string, len(c.GitFlags), len(c.GitFlags)+len(extraArgs)) copy(args, c.GitFlags) diff --git a/pkg/settings/exe/mock.go b/pkg/settings/exe/mock.go index 5f35b27e..5841fc51 100644 --- a/pkg/settings/exe/mock.go +++ b/pkg/settings/exe/mock.go @@ -2,6 +2,7 @@ package exe import ( "context" + "fmt" "os/exec" "github.com/Jguer/yay/v11/pkg/settings/parser" @@ -10,6 +11,11 @@ import ( type Call struct { Res []interface{} Args []interface{} + Dir string +} + +func (c *Call) String() string { + return fmt.Sprintf("%+v", c.Args) } type MockBuilder struct { @@ -84,6 +90,7 @@ func (m *MockRunner) Capture(cmd *exec.Cmd) (stdout, stderr string, err error) { Args: []interface{}{ cmd, }, + Dir: cmd.Dir, }) if m.CaptureFn != nil { @@ -103,6 +110,7 @@ func (m *MockRunner) Show(cmd *exec.Cmd) error { Args: []interface{}{ cmd, }, + Dir: cmd.Dir, }) return err diff --git a/pkg/settings/runtime.go b/pkg/settings/runtime.go index e7bb3d44..eb37d469 100644 --- a/pkg/settings/runtime.go +++ b/pkg/settings/runtime.go @@ -1,6 +1,7 @@ package settings import ( + "context" "net/http" "github.com/Jguer/yay/v11/pkg/db" @@ -15,6 +16,10 @@ import ( "github.com/Morganamilo/go-pacmanconf" ) +type AURCache interface { + Get(ctx context.Context, query *metadata.AURQuery) ([]*aur.Pkg, error) +} + type Runtime struct { Mode parser.TargetMode QueryBuilder query.Builder @@ -23,11 +28,11 @@ type Runtime struct { CompletionPath string ConfigPath string PacmanConf *pacmanconf.Config - VCSStore *vcs.InfoStore + VCSStore vcs.Store CmdBuilder exe.ICmdBuilder HTTPClient *http.Client AURClient *aur.Client VoteClient *vote.Client - AURCache *metadata.Client + AURCache AURCache DBExecutor db.Executor } diff --git a/pkg/topo/dep.go b/pkg/topo/dep.go index 0b550ae7..fba097cd 100644 --- a/pkg/topo/dep.go +++ b/pkg/topo/dep.go @@ -17,6 +17,8 @@ type NodeInfo[V any] struct { Value V } +type CheckFn[T comparable, V any] func(T, V) error + type Graph[T comparable, V any] struct { alias AliasMap[T] // alias -> aliased aliases DepMap[T] // aliased -> alias @@ -234,7 +236,7 @@ func (g *Graph[T, V]) TopoSortedLayers() [][]T { } // TopoSortedLayerMap returns a slice of all of the graph nodes in topological sort order with their node info. -func (g *Graph[T, V]) TopoSortedLayerMap() []map[T]V { +func (g *Graph[T, V]) TopoSortedLayerMap(checkFn CheckFn[T, V]) []map[T]V { layers := []map[T]V{} // Copy the graph @@ -249,6 +251,11 @@ func (g *Graph[T, V]) TopoSortedLayerMap() []map[T]V { layers = append(layers, leaves) for leafNode := range leaves { + if checkFn != nil { + if err := checkFn(leafNode, leaves[leafNode]); err != nil { + return nil + } + } shrinkingGraph.remove(leafNode) } } diff --git a/preparer.go b/preparer.go index afbc345d..b0dad0f4 100644 --- a/preparer.go +++ b/preparer.go @@ -26,31 +26,31 @@ type PreparerHookFunc func(ctx context.Context, config *settings.Configuration, type Preparer struct { dbExecutor db.Executor cmdBuilder exe.ICmdBuilder - config *settings.Configuration + cfg *settings.Configuration postDownloadHooks []PreparerHookFunc postMergeHooks []PreparerHookFunc makeDeps []string } -func NewPreparer(dbExecutor db.Executor, cmdBuilder exe.ICmdBuilder, config *settings.Configuration) *Preparer { +func NewPreparer(dbExecutor db.Executor, cmdBuilder exe.ICmdBuilder, cfg *settings.Configuration) *Preparer { preper := &Preparer{ dbExecutor: dbExecutor, cmdBuilder: cmdBuilder, - config: config, + cfg: cfg, postDownloadHooks: []PreparerHookFunc{}, postMergeHooks: []PreparerHookFunc{}, } - if config.CleanMenu { + if cfg.CleanMenu { preper.postDownloadHooks = append(preper.postDownloadHooks, menus.CleanFn) } - if config.DiffMenu { + if cfg.DiffMenu { preper.postMergeHooks = append(preper.postMergeHooks, menus.DiffFn) } - if config.EditMenu { + if cfg.EditMenu { preper.postMergeHooks = append(preper.postMergeHooks, menus.EditFn) } @@ -58,14 +58,14 @@ func NewPreparer(dbExecutor db.Executor, cmdBuilder exe.ICmdBuilder, config *set } func (preper *Preparer) ShouldCleanAURDirs(pkgBuildDirs map[string]string) PostInstallHookFunc { - if !preper.config.CleanAfter || len(pkgBuildDirs) == 0 { + if !preper.cfg.CleanAfter || len(pkgBuildDirs) == 0 { return nil } text.Debugln("added post install hook to clean up AUR dirs", pkgBuildDirs) return func(ctx context.Context) error { - cleanAfter(ctx, preper.config.Runtime.CmdBuilder, pkgBuildDirs) + cleanAfter(ctx, preper.cfg.Runtime.CmdBuilder, pkgBuildDirs) return nil } } @@ -75,7 +75,7 @@ func (preper *Preparer) ShouldCleanMakeDeps() PostInstallHookFunc { return nil } - switch preper.config.RemoveMake { + switch preper.cfg.RemoveMake { case "yes": break case "no": @@ -89,7 +89,7 @@ func (preper *Preparer) ShouldCleanMakeDeps() PostInstallHookFunc { text.Debugln("added post install hook to clean up AUR makedeps", preper.makeDeps) return func(ctx context.Context) error { - return removeMake(ctx, preper.config.Runtime.CmdBuilder, preper.makeDeps) + return removeMake(ctx, preper.cfg.Runtime.CmdBuilder, preper.makeDeps) } } @@ -152,7 +152,7 @@ func (preper *Preparer) PrepareWorkspace(ctx context.Context, targets []map[stri for _, info := range layer { if info.Source == dep.AUR { pkgBase := *info.AURBase - pkgBuildDir := filepath.Join(preper.config.BuildDir, pkgBase) + pkgBuildDir := filepath.Join(preper.cfg.BuildDir, pkgBase) if preper.needToCloneAURBase(info, pkgBuildDir) { aurBasesToClone.Add(pkgBase) } @@ -166,27 +166,27 @@ func (preper *Preparer) PrepareWorkspace(ctx context.Context, targets []map[stri if _, errA := download.AURPKGBUILDRepos(ctx, preper.cmdBuilder, aurBasesToClone.ToSlice(), - config.AURURL, config.BuildDir, false); errA != nil { + preper.cfg.AURURL, preper.cfg.BuildDir, false); errA != nil { return nil, errA } - if errP := downloadPKGBUILDSourceFanout(ctx, config.Runtime.CmdBuilder, - pkgBuildDirsByBase, false, config.MaxConcurrentDownloads); errP != nil { + if errP := downloadPKGBUILDSourceFanout(ctx, preper.cmdBuilder, + pkgBuildDirsByBase, false, preper.cfg.MaxConcurrentDownloads); errP != nil { text.Errorln(errP) } for _, hookFn := range preper.postDownloadHooks { - if err := hookFn(ctx, preper.config, os.Stdout, pkgBuildDirsByBase); err != nil { + if err := hookFn(ctx, preper.cfg, os.Stdout, pkgBuildDirsByBase); err != nil { return nil, err } } - if err := mergePkgbuilds(ctx, pkgBuildDirsByBase); err != nil { + if err := mergePkgbuilds(ctx, preper.cmdBuilder, pkgBuildDirsByBase); err != nil { return nil, err } for _, hookFn := range preper.postMergeHooks { - if err := hookFn(ctx, preper.config, os.Stdout, pkgBuildDirsByBase); err != nil { + if err := hookFn(ctx, preper.cfg, os.Stdout, pkgBuildDirsByBase); err != nil { return nil, err } } @@ -195,7 +195,7 @@ func (preper *Preparer) PrepareWorkspace(ctx context.Context, targets []map[stri } func (preper *Preparer) needToCloneAURBase(installInfo *dep.InstallInfo, pkgbuildDir string) bool { - if preper.config.ReDownload == "all" { + if preper.cfg.ReDownload == "all" { return true } diff --git a/srcinfo.go b/srcinfo.go index 9a37e774..f6a761e4 100644 --- a/srcinfo.go +++ b/srcinfo.go @@ -1,18 +1,23 @@ package main import ( + "context" + "github.com/Jguer/yay/v11/pkg/db" "github.com/Jguer/yay/v11/pkg/pgp" "github.com/Jguer/yay/v11/pkg/settings" + "github.com/Jguer/yay/v11/pkg/settings/exe" gosrc "github.com/Morganamilo/go-srcinfo" ) type srcinfoOperator struct { dbExecutor db.Executor + cfg *settings.Configuration + cmdBuilder exe.ICmdBuilder } -func (s *srcinfoOperator) Run(pkgbuildDirs map[string]string) (map[string]*gosrc.Srcinfo, error) { +func (s *srcinfoOperator) Run(ctx context.Context, pkgbuildDirs map[string]string) (map[string]*gosrc.Srcinfo, error) { srcinfos, err := parseSrcinfoFiles(pkgbuildDirs, true) if err != nil { return nil, err @@ -22,8 +27,8 @@ func (s *srcinfoOperator) Run(pkgbuildDirs map[string]string) (map[string]*gosrc return nil, err } - if config.PGPFetch { - if _, errCPK := pgp.CheckPgpKeys(pkgbuildDirs, srcinfos, config.GpgBin, config.GpgFlags, settings.NoConfirm); errCPK != nil { + if s.cfg.PGPFetch { + if _, errCPK := pgp.CheckPgpKeys(ctx, pkgbuildDirs, srcinfos, s.cmdBuilder, settings.NoConfirm); errCPK != nil { return nil, errCPK } } diff --git a/sync.go b/sync.go index 1c79067d..ac5a9ef4 100644 --- a/sync.go +++ b/sync.go @@ -2,6 +2,7 @@ package main import ( "context" + "errors" "fmt" "os" "strings" @@ -59,7 +60,18 @@ func syncInstall(ctx context.Context, } opService := NewOperationService(ctx, config, dbExecutor) - return opService.Run(ctx, cmdArgs, graph.TopoSortedLayerMap()) + multiErr := &multierror.MultiError{} + targets := graph.TopoSortedLayerMap(func(s string, ii *dep.InstallInfo) error { + if ii.Source == dep.Missing { + multiErr.Add(errors.New(gotext.Get("could not find %s%s", s, ii.Version))) + } + return nil + }) + + if err := multiErr.Return(); err != nil { + return err + } + return opService.Run(ctx, cmdArgs, targets) } type OperationService struct { @@ -84,7 +96,7 @@ func (o *OperationService) Run(ctx context.Context, fmt.Fprintln(os.Stdout, "", gotext.Get("there is nothing to do")) return nil } - preparer := NewPreparer(o.dbExecutor, config.Runtime.CmdBuilder, config) + preparer := NewPreparer(o.dbExecutor, o.cfg.Runtime.CmdBuilder, o.cfg) installer := NewInstaller(o.dbExecutor, o.cfg.Runtime.CmdBuilder, o.cfg.Runtime.VCSStore, o.cfg.Runtime.Mode) pkgBuildDirs, err := preparer.Run(ctx, os.Stdout, targets) @@ -101,15 +113,19 @@ func (o *OperationService) Run(ctx context.Context, installer.AddPostInstallHook(cleanAURDirsFunc) } - srcinfoOp := srcinfoOperator{dbExecutor: o.dbExecutor} - srcinfos, err := srcinfoOp.Run(pkgBuildDirs) + srcinfoOp := srcinfoOperator{ + dbExecutor: o.dbExecutor, + cfg: o.cfg, + cmdBuilder: installer.exeCmd, + } + srcinfos, err := srcinfoOp.Run(ctx, pkgBuildDirs) if err != nil { return err } go func() { - _ = completion.Update(ctx, config.Runtime.HTTPClient, o.dbExecutor, - config.AURURL, config.Runtime.CompletionPath, config.CompletionInterval, false) + _ = completion.Update(ctx, o.cfg.Runtime.HTTPClient, o.dbExecutor, + o.cfg.AURURL, o.cfg.Runtime.CompletionPath, o.cfg.CompletionInterval, false) }() err = installer.Install(ctx, cmdArgs, targets, pkgBuildDirs, srcinfos) diff --git a/testdata/jfin/.SRCINFO b/testdata/jfin/.SRCINFO new file mode 100644 index 00000000..abedaafa --- /dev/null +++ b/testdata/jfin/.SRCINFO @@ -0,0 +1,43 @@ +pkgbase = jellyfin + pkgdesc = The Free Software Media System + pkgver = 10.8.4 + pkgrel = 1 + url = https://github.com/jellyfin/jellyfin + arch = i686 + arch = x86_64 + arch = armv6h + license = GPL2 + makedepends = dotnet-sdk>=6 + makedepends = dotnet-sdk<7 + makedepends = nodejs + makedepends = npm + makedepends = git + source = jellyfin-10.8.4.tar.gz::https://github.com/jellyfin/jellyfin/archive/v10.8.4.tar.gz + source = jellyfin-web-10.8.4.tar.gz::https://github.com/jellyfin/jellyfin-web/archive/v10.8.4.tar.gz + source = jellyfin.conf + source = jellyfin.service + source = jellyfin.sysusers + source = jellyfin.tmpfiles + sha512sums = cf472f36a759a7eb3724dac79d3bd2d6c9c58fc375293ad6eb8b5ce1ea1a8f6dd296cc36113b80b1c705a99eafb2bd9ffd9381fd52fa19aa12018d50656c9bde + sha512sums = 21983940689475de7f9d37a1016fb2dd740986ac27ffa2e0eac0bc9c84d68ac557fdc8afb64ca70b867af2d1e438293b98d5c155da402d3e985ab831042ba176 + sha512sums = 2aa97a1a7a8a447171b59be3e93183e09cbbc32c816843cc47c6777b9aec48bd9c1d9d354f166e0b000ad8d2e94e6e4b0559aa52e5c159abbc103ed2c5afa3f0 + sha512sums = 99d02080b1b92e731250f39ddd13ceca7129d69d0c05e0939620cbc3f499a9574668c63fa889704a4905560888131e980d7ab1fbcc5837b04d33ce26daa9d42b + sha512sums = 6fc2638e6ec4b1ee0240e17815c91107b694e5fde72c1bc7956c83067bbeacb632de899b86837e47a0ec04288131b15c20746373b45e0669c8976069a55d627a + sha512sums = 45a62b62d97b9a83289d4dfde684163b1bcf340c1921fb958e5a701812c61b392901841940c67e5fa5148783277d5b4dc65ba01d3a22e8f855ea62154ad9be33 + +pkgname = jellyfin + depends = jellyfin-web=10.8.4 + depends = jellyfin-server=10.8.4 + +pkgname = jellyfin-web + pkgdesc = Jellyfin web client + +pkgname = jellyfin-server + pkgdesc = Jellyfin server component + depends = dotnet-runtime>=6 + depends = dotnet-runtime<7 + depends = aspnet-runtime>=6 + depends = aspnet-runtime<7 + depends = ffmpeg + depends = sqlite + backup = etc/conf.d/jellyfin diff --git a/upgrade.go b/upgrade.go index cf7c7b11..8021cc0f 100644 --- a/upgrade.go +++ b/upgrade.go @@ -37,7 +37,7 @@ func filterUpdateList(list []db.Upgrade, filter upgrade.Filter) []db.Upgrade { } // upList returns lists of packages to upgrade from each source. -func upList(ctx context.Context, aurCache *metadata.Client, +func upList(ctx context.Context, aurCache settings.AURCache, warnings *query.AURWarnings, dbExecutor db.Executor, enableDowngrade bool, filter upgrade.Filter, ) (aurUp, repoUp upgrade.UpSlice, err error) { @@ -262,7 +262,7 @@ func sysupgradeTargets(ctx context.Context, dbExecutor db.Executor, // Targets for sys upgrade. func sysupgradeTargetsV2(ctx context.Context, - aurCache *metadata.Client, + aurCache settings.AURCache, dbExecutor db.Executor, graph *topo.Graph[string, *dep.InstallInfo], enableDowngrade bool,