From 841395c31865722c2dd42f0e01b872593450eed0 Mon Sep 17 00:00:00 2001 From: Jo Date: Sat, 25 Feb 2023 17:44:24 +0000 Subject: [PATCH] feat(local_install): check PKGBUILD and .SRCINFO presence and generate .SRCINFO if necessary (#1938) check build file presence and generate if needed --- errors.go | 8 +- local_install.go | 51 ++++++- local_install_test.go | 303 ++++++++++++++++++++++++++++++++++++++++- main.go | 2 +- pkg/dep/depCheck.go | 2 +- pkg/text/input.go | 2 +- pkg/text/print.go | 27 ++-- pkg/text/service.go | 8 +- sync.go | 3 +- testdata/jfin/PKGBUILD | 0 10 files changed, 372 insertions(+), 34 deletions(-) create mode 100644 testdata/jfin/PKGBUILD diff --git a/errors.go b/errors.go index 39ab6ada..4e517977 100644 --- a/errors.go +++ b/errors.go @@ -1,6 +1,12 @@ package main -import "github.com/leonelquinteros/gotext" +import ( + "errors" + + "github.com/leonelquinteros/gotext" +) + +var ErrPackagesNotFound = errors.New(gotext.Get("could not find all required packages")) type NoPkgDestsFoundError struct { dir string diff --git a/local_install.go b/local_install.go index a3f87a0e..1d036a13 100644 --- a/local_install.go +++ b/local_install.go @@ -4,6 +4,8 @@ package main import ( "context" + "fmt" + "os" "path/filepath" "strings" @@ -11,6 +13,7 @@ import ( "github.com/Jguer/yay/v11/pkg/dep" "github.com/Jguer/yay/v11/pkg/multierror" "github.com/Jguer/yay/v11/pkg/settings" + "github.com/Jguer/yay/v11/pkg/settings/exe" "github.com/Jguer/yay/v11/pkg/settings/parser" "github.com/Jguer/yay/v11/pkg/topo" @@ -19,7 +22,38 @@ import ( "github.com/pkg/errors" ) -var ErrInstallRepoPkgs = errors.New(gotext.Get("error installing repo packages")) +var ( + ErrInstallRepoPkgs = errors.New(gotext.Get("error installing repo packages")) + ErrNoBuildFiles = errors.New(gotext.Get("cannot find PKGBUILD and .SRCINFO in directory")) +) + +func srcinfoExists(ctx context.Context, + cmdBuilder exe.ICmdBuilder, targetDir string, +) error { + srcInfoDir := filepath.Join(targetDir, ".SRCINFO") + pkgbuildDir := filepath.Join(targetDir, "PKGBUILD") + if _, err := os.Stat(srcInfoDir); err == nil { + if _, err := os.Stat(pkgbuildDir); err == nil { + return nil + } + } + + if _, err := os.Stat(pkgbuildDir); err == nil { + // run makepkg to generate .SRCINFO + srcinfo, stderr, err := cmdBuilder.Capture(cmdBuilder.BuildMakepkgCmd(ctx, targetDir, "--printsrcinfo")) + if err != nil { + return fmt.Errorf("unable to generate .SRCINFO: %w - %s", err, stderr) + } + + if err := os.WriteFile(srcInfoDir, []byte(srcinfo), 0o600); err != nil { + return fmt.Errorf("unable to write .SRCINFO: %w", err) + } + + return nil + } + + return fmt.Errorf("%w: %s", ErrNoBuildFiles, targetDir) +} func installLocalPKGBUILD( ctx context.Context, @@ -38,17 +72,20 @@ func installLocalPKGBUILD( cmdArgs.ExistsDouble("d", "nodeps"), noCheck, cmdArgs.ExistsArg("needed"), config.Runtime.Logger.Child("grapher")) graph := topo.New[string, *dep.InstallInfo]() - for _, target := range cmdArgs.Targets { - var errG error + for _, targetDir := range cmdArgs.Targets { + if err := srcinfoExists(ctx, config.Runtime.CmdBuilder, targetDir); err != nil { + return err + } - pkgbuild, err := gosrc.ParseFile(filepath.Join(target, ".SRCINFO")) + pkgbuild, err := gosrc.ParseFile(filepath.Join(targetDir, ".SRCINFO")) if err != nil { return errors.Wrap(err, gotext.Get("failed to parse .SRCINFO")) } - graph, errG = grapher.GraphFromSrcInfo(ctx, graph, target, pkgbuild) + var errG error + graph, errG = grapher.GraphFromSrcInfo(ctx, graph, targetDir, pkgbuild) if errG != nil { - return err + return errG } } @@ -56,7 +93,7 @@ func installLocalPKGBUILD( 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))) + multiErr.Add(fmt.Errorf("%w: %s %s", ErrPackagesNotFound, name, ii.Version)) } return nil }) diff --git a/local_install_test.go b/local_install_test.go index d3392838..16b97fa6 100644 --- a/local_install_test.go +++ b/local_install_test.go @@ -6,6 +6,7 @@ import ( "io" "os" "os/exec" + "path/filepath" "strings" "sync" "testing" @@ -169,7 +170,7 @@ func TestIntegrationLocalInstall(t *testing.T) { } func TestIntegrationLocalInstallMissingDep(t *testing.T) { - wantErr := "could not find dotnet-sdk<7" + wantErr := ErrPackagesNotFound makepkgBin := t.TempDir() + "/makepkg" pacmanBin := t.TempDir() + "/pacman" gitBin := t.TempDir() + "/git" @@ -270,8 +271,7 @@ func TestIntegrationLocalInstallMissingDep(t *testing.T) { } err = handleCmd(context.Background(), config, cmdArgs, db) - require.Error(t, err) - require.EqualError(t, err, wantErr) + require.ErrorContains(t, err, wantErr.Error()) require.Len(t, mockRunner.ShowCalls, len(wantShow)) require.Len(t, mockRunner.CaptureCalls, len(wantCapture)) @@ -445,3 +445,300 @@ func TestIntegrationLocalInstallNeeded(t *testing.T) { assert.Subset(t, strings.Split(show, " "), strings.Split(wantShow[i], " "), fmt.Sprintf("%d - %s", i, show)) } } + +func TestIntegrationLocalInstallGenerateSRCINFO(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()) + + srcinfo, err := os.ReadFile("testdata/jfin/.SRCINFO") + require.NoError(t, err) + assert.True(t, strings.HasPrefix(string(srcinfo), "pkgbase = jellyfin"), string(srcinfo)) + + targetDir := t.TempDir() + f, err = os.OpenFile(filepath.Join(targetDir, "PKGBUILD"), 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 --printsrcinfo", + "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) { + for _, arg := range cmd.Args { + if arg == "--printsrcinfo" { + return string(srcinfo), "", nil + } + } + 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(targetDir) + settings.NoConfirm = true + defer func() { settings.NoConfirm = false }() + 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{ + RemoveMake: "no", + Debug: false, + Runtime: &settings.Runtime{ + Logger: text.NewLogger(io.Discard, strings.NewReader(""), true, "test"), + CmdBuilder: cmdBuilder, + VCSStore: &vcs.Mock{}, + AURCache: &mockaur.MockAUR{ + GetFn: func(ctx context.Context, query *aur.Query) ([]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 TestIntegrationLocalInstallMissingFiles(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()) + + srcinfo, err := os.ReadFile("testdata/jfin/.SRCINFO") + require.NoError(t, err) + + targetDir := t.TempDir() + + 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) { + fmt.Println(cmd.Args) + if cmd.Args[1] == "--printsrcinfo" { + return string(srcinfo), "", nil + } + 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(targetDir) + settings.NoConfirm = true + defer func() { settings.NoConfirm = false }() + 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{ + RemoveMake: "no", + Runtime: &settings.Runtime{ + Logger: text.NewLogger(io.Discard, strings.NewReader(""), true, "test"), + CmdBuilder: cmdBuilder, + VCSStore: &vcs.Mock{}, + AURCache: &mockaur.MockAUR{ + GetFn: func(ctx context.Context, query *aur.Query) ([]aur.Pkg, error) { + return []aur.Pkg{}, nil + }, + }, + }, + } + + err = handleCmd(context.Background(), config, cmdArgs, db) + require.ErrorIs(t, err, ErrNoBuildFiles) + + 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 cd1ef2e2..d72964a5 100644 --- a/main.go +++ b/main.go @@ -67,7 +67,7 @@ func main() { } if config.Debug { - text.DebugMode = true + text.GlobalLogger.Debug = true } if errS := config.RunMigrations( diff --git a/pkg/dep/depCheck.go b/pkg/dep/depCheck.go index 16ea0a73..d5dce12d 100644 --- a/pkg/dep/depCheck.go +++ b/pkg/dep/depCheck.go @@ -300,7 +300,7 @@ func (dp *Pool) CheckMissing(noDeps, noCheckDeps bool) error { return nil } - text.Errorln(gotext.Get("Could not find all required packages:")) + text.Errorln(gotext.Get("could not find all required packages:")) for dep, trees := range missing.Missing { for _, tree := range trees { diff --git a/pkg/text/input.go b/pkg/text/input.go index b93ad860..1739420d 100644 --- a/pkg/text/input.go +++ b/pkg/text/input.go @@ -29,5 +29,5 @@ func (l *Logger) GetInput(defaultValue string, noConfirm bool) (string, error) { } func GetInput(r io.Reader, defaultValue string, noConfirm bool) (string, error) { - return globalLogger.GetInput(defaultValue, noConfirm) + return GlobalLogger.GetInput(defaultValue, noConfirm) } diff --git a/pkg/text/print.go b/pkg/text/print.go index 8a263c84..f2b6d320 100644 --- a/pkg/text/print.go +++ b/pkg/text/print.go @@ -20,56 +20,55 @@ const ( var ( cachedColumnCount = -1 - DebugMode = false - globalLogger = NewLogger(os.Stdout, os.Stdin, DebugMode, "global") + GlobalLogger = NewLogger(os.Stdout, os.Stdin, false, "global") ) func Debugln(a ...interface{}) { - globalLogger.Debugln(a...) + GlobalLogger.Debugln(a...) } func OperationInfoln(a ...interface{}) { - globalLogger.OperationInfoln(a...) + GlobalLogger.OperationInfoln(a...) } func OperationInfo(a ...interface{}) { - globalLogger.OperationInfo(a...) + GlobalLogger.OperationInfo(a...) } func SprintOperationInfo(a ...interface{}) string { - return globalLogger.SprintOperationInfo(a...) + return GlobalLogger.SprintOperationInfo(a...) } func Info(a ...interface{}) { - globalLogger.Info(a...) + GlobalLogger.Info(a...) } func Infoln(a ...interface{}) { - globalLogger.Infoln(a...) + GlobalLogger.Infoln(a...) } func SprintWarn(a ...interface{}) string { - return globalLogger.SprintWarn(a...) + return GlobalLogger.SprintWarn(a...) } func Warn(a ...interface{}) { - globalLogger.Warn(a...) + GlobalLogger.Warn(a...) } func Warnln(a ...interface{}) { - globalLogger.Warnln(a...) + GlobalLogger.Warnln(a...) } func SprintError(a ...interface{}) string { - return globalLogger.SprintError(a...) + return GlobalLogger.SprintError(a...) } func Error(a ...interface{}) { - globalLogger.Error(a...) + GlobalLogger.Error(a...) } func Errorln(a ...interface{}) { - globalLogger.Errorln(a...) + GlobalLogger.Errorln(a...) } func getColumnCount() int { diff --git a/pkg/text/service.go b/pkg/text/service.go index 478e679e..1e448c39 100644 --- a/pkg/text/service.go +++ b/pkg/text/service.go @@ -7,7 +7,7 @@ import ( type Logger struct { name string - debug bool + Debug bool w io.Writer r io.Reader } @@ -16,17 +16,17 @@ func NewLogger(w io.Writer, r io.Reader, debug bool, name string) *Logger { return &Logger{ w: w, name: name, - debug: debug, + Debug: debug, r: r, } } func (l *Logger) Child(name string) *Logger { - return NewLogger(l.w, l.r, l.debug, name) + return NewLogger(l.w, l.r, l.Debug, name) } func (l *Logger) Debugln(a ...any) { - if !DebugMode { + if !l.Debug { return } diff --git a/sync.go b/sync.go index 447a49e0..93330fbd 100644 --- a/sync.go +++ b/sync.go @@ -2,7 +2,6 @@ package main import ( "context" - "errors" "fmt" "os" "strings" @@ -70,7 +69,7 @@ func syncInstall(ctx context.Context, 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))) + multiErr.Add(fmt.Errorf("%w: %s %s", ErrPackagesNotFound, s, ii.Version)) } return nil }) diff --git a/testdata/jfin/PKGBUILD b/testdata/jfin/PKGBUILD new file mode 100644 index 00000000..e69de29b