Browse Source

feat(local_install): check PKGBUILD and .SRCINFO presence and generate .SRCINFO if necessary (#1938)

check build file presence and generate if needed
Jo 2 years ago
parent
commit
841395c318
10 changed files with 372 additions and 34 deletions
  1. 7 1
      errors.go
  2. 44 7
      local_install.go
  3. 300 3
      local_install_test.go
  4. 1 1
      main.go
  5. 1 1
      pkg/dep/depCheck.go
  6. 1 1
      pkg/text/input.go
  7. 13 14
      pkg/text/print.go
  8. 4 4
      pkg/text/service.go
  9. 1 2
      sync.go
  10. 0 0
      testdata/jfin/PKGBUILD

+ 7 - 1
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

+ 44 - 7
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
 	})

+ 300 - 3
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))
+	}
+}

+ 1 - 1
main.go

@@ -67,7 +67,7 @@ func main() {
 	}
 
 	if config.Debug {
-		text.DebugMode = true
+		text.GlobalLogger.Debug = true
 	}
 
 	if errS := config.RunMigrations(

+ 1 - 1
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 {

+ 1 - 1
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)
 }

+ 13 - 14
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 {

+ 4 - 4
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
 	}
 

+ 1 - 2
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
 	})

+ 0 - 0
testdata/jfin/PKGBUILD