diff --git a/aur_install.go b/aur_install.go index 37e65c3f..063b4d1e 100644 --- a/aur_install.go +++ b/aur_install.go @@ -349,7 +349,7 @@ func (installer *Installer) installSyncPackages(ctx context.Context, cmdArgs *pa arguments.AddTarget(repoTargets...) errShow := installer.exeCmd.Show(installer.exeCmd.BuildPacmanCmd(ctx, - arguments, config.Runtime.Mode, settings.NoConfirm)) + arguments, installer.targetMode, settings.NoConfirm)) if errD := asdeps(ctx, installer.exeCmd, installer.targetMode, cmdArgs, syncDeps.ToSlice()); errD != nil { return errD diff --git a/aur_install_test.go b/aur_install_test.go index 538c3033..e850ab66 100644 --- a/aur_install_test.go +++ b/aur_install_test.go @@ -2,6 +2,8 @@ package main import ( "context" + "errors" + "fmt" "os" "os/exec" "strings" @@ -146,7 +148,6 @@ func TestInstaller_InstallNeeded(t *testing.T) { Version: "91.0.0-1", SrcinfoPath: ptrString(tmpDir + "/.SRCINFO"), AURBase: ptrString("yay"), - SyncDBName: nil, }, }, } @@ -177,3 +178,583 @@ func TestInstaller_InstallNeeded(t *testing.T) { }) } } + +func TestInstaller_InstallMixedSourcesAndLayers(t *testing.T) { + t.Parallel() + + makepkgBin := t.TempDir() + "/makepkg" + pacmanBin := t.TempDir() + "/pacman" + f, err := os.OpenFile(makepkgBin, os.O_RDONLY|os.O_CREATE, 0o755) + require.NoError(t, err) + require.NoError(t, f.Close()) + + f, err = os.OpenFile(pacmanBin, os.O_RDONLY|os.O_CREATE, 0o755) + require.NoError(t, err) + require.NoError(t, f.Close()) + + type testCase struct { + desc string + targets []map[string]*dep.InstallInfo + wantShow []string + wantCapture []string + } + + tmpDir := t.TempDir() + tmpDirJfin := t.TempDir() + + testCases := []testCase{ + { + desc: "same layer -- different sources", + wantShow: []string{ + "pacman -S --config /etc/pacman.conf -- core/linux", + "pacman -D -q --asdeps --config /etc/pacman.conf -- linux", + "makepkg --nobuild -fC --ignorearch", + "makepkg -cf --noconfirm --noextract --noprepare --holdver --ignorearch", + "pacman -U --config /etc/pacman.conf -- /testdir/yay-91.0.0-1-x86_64.pkg.tar.zst", + "pacman -D -q --asexplicit --config /etc/pacman.conf -- yay", + }, + wantCapture: []string{"makepkg --packagelist"}, + targets: []map[string]*dep.InstallInfo{ + { + "yay": { + Source: dep.AUR, + Reason: dep.Explicit, + Version: "91.0.0-1", + SrcinfoPath: ptrString(tmpDir + "/.SRCINFO"), + AURBase: ptrString("yay"), + }, + "linux": { + Source: dep.Sync, + Reason: dep.Dep, + Version: "17.0.0-1", + SyncDBName: ptrString("core"), + }, + }, + }, + }, + { + desc: "different layer -- different sources", + wantShow: []string{ + "pacman -S --config /etc/pacman.conf -- core/linux", + "pacman -D -q --asdeps --config /etc/pacman.conf -- linux", + "makepkg --nobuild -fC --ignorearch", + "makepkg -cf --noconfirm --noextract --noprepare --holdver --ignorearch", + "pacman -U --config /etc/pacman.conf -- /testdir/yay-91.0.0-1-x86_64.pkg.tar.zst", + "pacman -D -q --asexplicit --config /etc/pacman.conf -- yay", + }, + wantCapture: []string{"makepkg --packagelist"}, + targets: []map[string]*dep.InstallInfo{ + { + "yay": { + Source: dep.AUR, + Reason: dep.Explicit, + Version: "91.0.0-1", + SrcinfoPath: ptrString(tmpDir + "/.SRCINFO"), + AURBase: ptrString("yay"), + }, + }, { + "linux": { + Source: dep.Sync, + Reason: dep.Dep, + Version: "17.0.0-1", + SyncDBName: ptrString("core"), + }, + }, + }, + }, + { + desc: "same layer -- sync", + wantShow: []string{ + "pacman -S --config /etc/pacman.conf -- extra/linux-zen core/linux", + "pacman -D -q --asexplicit --config /etc/pacman.conf -- linux-zen linux", + }, + wantCapture: []string{}, + targets: []map[string]*dep.InstallInfo{ + { + "linux-zen": { + Source: dep.Sync, + Reason: dep.Explicit, + Version: "18.0.0-1", + SyncDBName: ptrString("extra"), + }, + "linux": { + Source: dep.Sync, + Reason: dep.Explicit, + Version: "17.0.0-1", + SyncDBName: ptrString("core"), + }, + }, + }, + }, + { + desc: "same layer -- aur", + wantShow: []string{ + "makepkg --nobuild -fC --ignorearch", + "makepkg -cf --noconfirm --noextract --noprepare --holdver --ignorearch", + "makepkg --nobuild -fC --ignorearch", + "makepkg -cf --noconfirm --noextract --noprepare --holdver --ignorearch", + "pacman -U --config /etc/pacman.conf -- pacman -U --config /etc/pacman.conf -- /testdir/yay-91.0.0-1-x86_64.pkg.tar.zst", + "pacman -D -q --asexplicit --config /etc/pacman.conf -- yay", + }, + wantCapture: []string{"makepkg --packagelist", "makepkg --packagelist"}, + targets: []map[string]*dep.InstallInfo{ + { + "yay": { + Source: dep.AUR, + Reason: dep.Explicit, + Version: "91.0.0-1", + SrcinfoPath: ptrString(tmpDir + "/.SRCINFO"), + AURBase: ptrString("yay"), + }, + "jellyfin-server": { + Source: dep.AUR, + Reason: dep.Explicit, + Version: "10.8.8-1", + SrcinfoPath: ptrString(tmpDirJfin + "/.SRCINFO"), + AURBase: ptrString("jellyfin"), + }, + }, + }, + }, + { + desc: "different layer -- aur", + wantShow: []string{ + "makepkg --nobuild -fC --ignorearch", + "makepkg -cf --noconfirm --noextract --noprepare --holdver --ignorearch", + "pacman -U --config /etc/pacman.conf -- pacman -U --config /etc/pacman.conf -- /testdir/jellyfin-server-10.8.8-1-x86_64.pkg.tar.zst", + "pacman -D -q --asdeps --config /etc/pacman.conf -- jellyfin-server", + "makepkg --nobuild -fC --ignorearch", + "makepkg -cf --noconfirm --noextract --noprepare --holdver --ignorearch", + "pacman -U --config /etc/pacman.conf -- pacman -U --config /etc/pacman.conf -- /testdir/yay-91.0.0-1-x86_64.pkg.tar.zst", + "pacman -D -q --asexplicit --config /etc/pacman.conf -- yay", + }, + wantCapture: []string{"makepkg --packagelist", "makepkg --packagelist"}, + targets: []map[string]*dep.InstallInfo{ + { + "yay": { + Source: dep.AUR, + Reason: dep.Explicit, + Version: "91.0.0-1", + SrcinfoPath: ptrString(tmpDir + "/.SRCINFO"), + AURBase: ptrString("yay"), + }, + }, { + "jellyfin-server": { + Source: dep.AUR, + Reason: dep.MakeDep, + Version: "10.8.8-1", + SrcinfoPath: ptrString(tmpDirJfin + "/.SRCINFO"), + AURBase: ptrString("jellyfin"), + }, + }, + }, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.desc, func(td *testing.T) { + pkgTar := tmpDir + "/yay-91.0.0-1-x86_64.pkg.tar.zst" + jfinPkgTar := tmpDirJfin + "/jellyfin-server-10.8.8-1-x86_64.pkg.tar.zst" + + captureOverride := func(cmd *exec.Cmd) (stdout string, stderr string, err error) { + if cmd.Dir == tmpDirJfin { + return jfinPkgTar, "", nil + } + + if cmd.Dir == tmpDir { + return pkgTar, "", nil + } + + return "", "", fmt.Errorf("unexpected command: %s - %s", cmd.String(), cmd.Dir) + } + + showOverride := func(cmd *exec.Cmd) error { + if strings.Contains(cmd.String(), "makepkg -cf --noconfirm") && cmd.Dir == tmpDir { + f, err := os.OpenFile(pkgTar, os.O_RDONLY|os.O_CREATE, 0o666) + require.NoError(td, err) + require.NoError(td, f.Close()) + } + + if strings.Contains(cmd.String(), "makepkg -cf --noconfirm") && cmd.Dir == tmpDirJfin { + f, err := os.OpenFile(jfinPkgTar, os.O_RDONLY|os.O_CREATE, 0o666) + require.NoError(td, err) + require.NoError(td, f.Close()) + } + + return nil + } + defer os.Remove(pkgTar) + defer os.Remove(jfinPkgTar) + + isCorrectInstalledOverride := func(string, string) bool { + return false + } + + mockDB := &mock.DBExecutor{IsCorrectVersionInstalledFunc: isCorrectInstalledOverride} + mockRunner := &exe.MockRunner{CaptureFn: captureOverride, ShowFn: showOverride} + cmdBuilder := &exe.CmdBuilder{ + MakepkgBin: makepkgBin, + SudoBin: "su", + PacmanBin: pacmanBin, + PacmanConfigPath: "/etc/pacman.conf", + Runner: mockRunner, + SudoLoopEnabled: false, + } + + cmdBuilder.Runner = mockRunner + + installer := NewInstaller(mockDB, cmdBuilder, &vcs.Mock{}, parser.ModeAny) + + cmdArgs := parser.MakeArguments() + cmdArgs.AddTarget("yay") + + pkgBuildDirs := map[string]string{ + "yay": tmpDir, + "jellyfin": tmpDirJfin, + } + + srcInfos := map[string]*gosrc.Srcinfo{"yay": {}, "jellyfin": {}} + + errI := installer.Install(context.Background(), cmdArgs, tc.targets, pkgBuildDirs, srcInfos) + require.NoError(td, errI) + + require.Len(td, mockRunner.ShowCalls, len(tc.wantShow)) + require.Len(td, mockRunner.CaptureCalls, len(tc.wantCapture)) + + for i, call := range mockRunner.ShowCalls { + show := call.Args[0].(*exec.Cmd).String() + show = strings.ReplaceAll(show, tmpDir, "/testdir") // replace the temp dir with a static path + show = strings.ReplaceAll(show, tmpDirJfin, "/testdir") // replace the temp dir with a static path + show = strings.ReplaceAll(show, makepkgBin, "makepkg") + show = strings.ReplaceAll(show, pacmanBin, "pacman") + + // options are in a different order on different systems and on CI root user is used + assert.Subset(td, strings.Split(show, " "), strings.Split(tc.wantShow[i], " "), show) + } + + for i, call := range mockRunner.CaptureCalls { + capture := call.Args[0].(*exec.Cmd).String() + capture = strings.ReplaceAll(capture, tmpDir, "/testdir") // replace the temp dir with a static path + capture = strings.ReplaceAll(capture, tmpDirJfin, "/testdir") + capture = strings.ReplaceAll(capture, makepkgBin, "makepkg") + capture = strings.ReplaceAll(capture, pacmanBin, "pacman") + assert.Subset(td, strings.Split(capture, " "), strings.Split(tc.wantCapture[i], " "), capture) + } + }) + } +} + +func TestInstaller_RunPostHooks(t *testing.T) { + mockDB := &mock.DBExecutor{} + mockRunner := &exe.MockRunner{} + cmdBuilder := &exe.CmdBuilder{ + MakepkgBin: "makepkg", + SudoBin: "su", + PacmanBin: "pacman", + PacmanConfigPath: "/etc/pacman.conf", + Runner: mockRunner, + SudoLoopEnabled: false, + } + + cmdBuilder.Runner = mockRunner + + installer := NewInstaller(mockDB, cmdBuilder, &vcs.Mock{}, parser.ModeAny) + + called := false + hook := func(ctx context.Context) error { + called = true + return nil + } + + installer.AddPostInstallHook(hook) + installer.RunPostInstallHooks(context.Background()) + + assert.True(t, called) +} + +func TestInstaller_CompileFailed(t *testing.T) { + t.Parallel() + + makepkgBin := t.TempDir() + "/makepkg" + pacmanBin := t.TempDir() + "/pacman" + f, err := os.OpenFile(makepkgBin, os.O_RDONLY|os.O_CREATE, 0o755) + require.NoError(t, err) + require.NoError(t, f.Close()) + + f, err = os.OpenFile(pacmanBin, os.O_RDONLY|os.O_CREATE, 0o755) + require.NoError(t, err) + require.NoError(t, f.Close()) + + type testCase struct { + desc string + targets []map[string]*dep.InstallInfo + lastLayer bool + } + + tmpDir := t.TempDir() + + testCases := []testCase{ + { + desc: "last layer", + lastLayer: true, + targets: []map[string]*dep.InstallInfo{ + { + "yay": { + Source: dep.AUR, + Reason: dep.Explicit, + Version: "91.0.0-1", + SrcinfoPath: ptrString(tmpDir + "/.SRCINFO"), + AURBase: ptrString("yay"), + }, + }, + }, + }, + { + desc: "not last layer", + lastLayer: false, + targets: []map[string]*dep.InstallInfo{ + {"bob": {}}, + { + "yay": { + Source: dep.AUR, + Reason: dep.Explicit, + Version: "91.0.0-1", + SrcinfoPath: ptrString(tmpDir + "/.SRCINFO"), + AURBase: ptrString("yay"), + }, + }, + }, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.desc, func(td *testing.T) { + pkgTar := tmpDir + "/yay-91.0.0-1-x86_64.pkg.tar.zst" + + captureOverride := func(cmd *exec.Cmd) (stdout string, stderr string, err error) { + return pkgTar, "", nil + } + + showOverride := func(cmd *exec.Cmd) error { + if strings.Contains(cmd.String(), "makepkg -cf --noconfirm") && cmd.Dir == tmpDir { + return errors.New("makepkg failed") + } + return nil + } + + isCorrectInstalledOverride := func(string, string) bool { + return false + } + + mockDB := &mock.DBExecutor{IsCorrectVersionInstalledFunc: isCorrectInstalledOverride} + mockRunner := &exe.MockRunner{CaptureFn: captureOverride, ShowFn: showOverride} + cmdBuilder := &exe.CmdBuilder{ + MakepkgBin: makepkgBin, + SudoBin: "su", + PacmanBin: pacmanBin, + Runner: mockRunner, + SudoLoopEnabled: false, + } + + cmdBuilder.Runner = mockRunner + + installer := NewInstaller(mockDB, cmdBuilder, &vcs.Mock{}, parser.ModeAny) + + cmdArgs := parser.MakeArguments() + cmdArgs.AddArg("needed") + cmdArgs.AddTarget("yay") + + pkgBuildDirs := map[string]string{ + "yay": tmpDir, + } + + srcInfos := map[string]*gosrc.Srcinfo{"yay": {}} + errI := installer.Install(context.Background(), cmdArgs, tc.targets, pkgBuildDirs, srcInfos) + if tc.lastLayer { + require.NoError(td, errI) // last layer error + } else { + require.Error(td, errI) + } + err := installer.CompileFailedAndIgnored() + if tc.lastLayer { + require.Error(td, err) + assert.ErrorContains(td, err, "yay") + } else { + require.NoError(td, err) + } + }) + } +} + +func TestInstaller_InstallSplitPackage(t *testing.T) { + t.Parallel() + + makepkgBin := t.TempDir() + "/makepkg" + pacmanBin := t.TempDir() + "/pacman" + f, err := os.OpenFile(makepkgBin, os.O_RDONLY|os.O_CREATE, 0o755) + require.NoError(t, err) + require.NoError(t, f.Close()) + + f, err = os.OpenFile(pacmanBin, os.O_RDONLY|os.O_CREATE, 0o755) + require.NoError(t, err) + require.NoError(t, f.Close()) + + type testCase struct { + desc string + wantShow []string + wantCapture []string + targets []map[string]*dep.InstallInfo + } + + tmpDir := t.TempDir() + + testCases := []testCase{ + { + desc: "jellyfin", + targets: []map[string]*dep.InstallInfo{ + {"jellyfin": { + Source: dep.AUR, + Reason: dep.Explicit, + Version: "10.8.4-1", + SrcinfoPath: ptrString(tmpDir + "/.SRCINFO"), + AURBase: ptrString("jellyfin"), + }}, + { + "jellyfin-server": { + Source: dep.AUR, + Reason: dep.Dep, + Version: "10.8.4-1", + SrcinfoPath: ptrString(tmpDir + "/.SRCINFO"), + AURBase: ptrString("jellyfin"), + }, + "jellyfin-web": { + Source: dep.AUR, + Reason: dep.Dep, + Version: "10.8.4-1", + SrcinfoPath: ptrString(tmpDir + "/.SRCINFO"), + AURBase: ptrString("jellyfin"), + }, + }, + { + "dotnet-runtime-6.0": { + Source: dep.Sync, + Reason: dep.Dep, + Version: "6.0.12.sdk112-1", + SyncDBName: ptrString("community"), + }, + "aspnet-runtime": { + Source: dep.Sync, + Reason: dep.Dep, + Version: "6.0.12.sdk112-1", + SyncDBName: ptrString("community"), + }, + "dotnet-sdk-6.0": { + Source: dep.Sync, + Reason: dep.MakeDep, + Version: "6.0.12.sdk112-1", + SyncDBName: ptrString("community"), + }, + }, + }, + wantShow: []string{ + "pacman -S --config /etc/pacman.conf -- community/dotnet-runtime-6.0 community/aspnet-runtime community/dotnet-sdk-6.0", + "pacman -D -q --asdeps --config /etc/pacman.conf -- dotnet-runtime-6.0 aspnet-runtime dotnet-sdk-6.0", + "makepkg --nobuild -fC --ignorearch", + "makepkg -cf --noconfirm --noextract --noprepare --holdver --ignorearch", + "makepkg --nobuild -fC --ignorearch", + "makepkg -c --nobuild --noextract --ignorearch", + "pacman -U --config /etc/pacman.conf -- /testdir/jellyfin-web-10.8.4-1-x86_64.pkg.tar.zst /testdir/jellyfin-server-10.8.4-1-x86_64.pkg.tar.zst", + "pacman -D -q --asdeps --config /etc/pacman.conf -- jellyfin-server jellyfin-web", + "makepkg --nobuild -fC --ignorearch", + "makepkg -c --nobuild --noextract --ignorearch", + "pacman -U --config /etc/pacman.conf -- /testdir/jellyfin-10.8.4-1-x86_64.pkg.tar.zst", + "pacman -D -q --asexplicit --config /etc/pacman.conf -- jellyfin", + }, + wantCapture: []string{"makepkg --packagelist", "makepkg --packagelist", "makepkg --packagelist"}, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.desc, func(td *testing.T) { + pkgTars := []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", + } + + captureOverride := func(cmd *exec.Cmd) (stdout string, stderr string, err error) { + return strings.Join(pkgTars, "\n"), "", nil + } + + i := 0 + showOverride := func(cmd *exec.Cmd) error { + i++ + if i == 4 { + for _, pkgTar := range pkgTars { + f, err := os.OpenFile(pkgTar, os.O_RDONLY|os.O_CREATE, 0o666) + require.NoError(td, err) + require.NoError(td, f.Close()) + } + } + return nil + } + + isCorrectInstalledOverride := func(string, string) bool { + return false + } + + mockDB := &mock.DBExecutor{IsCorrectVersionInstalledFunc: isCorrectInstalledOverride} + mockRunner := &exe.MockRunner{CaptureFn: captureOverride, ShowFn: showOverride} + cmdBuilder := &exe.CmdBuilder{ + MakepkgBin: makepkgBin, + SudoBin: "su", + PacmanBin: pacmanBin, + PacmanConfigPath: "/etc/pacman.conf", + Runner: mockRunner, + SudoLoopEnabled: false, + } + + cmdBuilder.Runner = mockRunner + + installer := NewInstaller(mockDB, cmdBuilder, &vcs.Mock{}, parser.ModeAny) + + cmdArgs := parser.MakeArguments() + cmdArgs.AddTarget("jellyfin") + + pkgBuildDirs := map[string]string{ + "jellyfin": tmpDir, + } + + srcInfos := map[string]*gosrc.Srcinfo{"jellyfin": {}} + + errI := installer.Install(context.Background(), cmdArgs, tc.targets, pkgBuildDirs, srcInfos) + require.NoError(td, errI) + + require.Len(td, mockRunner.ShowCalls, len(tc.wantShow)) + require.Len(td, mockRunner.CaptureCalls, len(tc.wantCapture)) + + for i, call := range mockRunner.ShowCalls { + show := call.Args[0].(*exec.Cmd).String() + show = strings.ReplaceAll(show, tmpDir, "/testdir") // replace the temp dir with a static path + show = strings.ReplaceAll(show, makepkgBin, "makepkg") + show = strings.ReplaceAll(show, pacmanBin, "pacman") + + // options are in a different order on different systems and on CI root user is used + assert.Subset(td, strings.Split(show, " "), + strings.Split(tc.wantShow[i], " "), + fmt.Sprintf("got at %d: %s \n", i, show)) + } + + for i, call := range mockRunner.CaptureCalls { + capture := call.Args[0].(*exec.Cmd).String() + capture = strings.ReplaceAll(capture, tmpDir, "/testdir") // replace the temp dir with a static path + capture = strings.ReplaceAll(capture, makepkgBin, "makepkg") + capture = strings.ReplaceAll(capture, pacmanBin, "pacman") + assert.Subset(td, strings.Split(capture, " "), strings.Split(tc.wantCapture[i], " "), capture) + } + }) + } +} diff --git a/pkg/cmd/graph/main.go b/pkg/cmd/graph/main.go index 57f8f44b..77ed1290 100644 --- a/pkg/cmd/graph/main.go +++ b/pkg/cmd/graph/main.go @@ -72,8 +72,9 @@ func graphPackage( } fmt.Fprintln(os.Stdout, graph.String()) - fmt.Fprintln(os.Stdout, graph.TopoSortedLayers()) - fmt.Fprintln(os.Stdout, graph.TopoSorted()) + 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()) return nil }