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

check build file presence and generate if needed
This commit is contained in:
Jo
2023-02-25 17:44:24 +00:00
committed by GitHub
parent 4e0a5c8520
commit 841395c318
10 changed files with 372 additions and 34 deletions

View File

@@ -1,6 +1,12 @@
package main 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 { type NoPkgDestsFoundError struct {
dir string dir string

View File

@@ -4,6 +4,8 @@ package main
import ( import (
"context" "context"
"fmt"
"os"
"path/filepath" "path/filepath"
"strings" "strings"
@@ -11,6 +13,7 @@ import (
"github.com/Jguer/yay/v11/pkg/dep" "github.com/Jguer/yay/v11/pkg/dep"
"github.com/Jguer/yay/v11/pkg/multierror" "github.com/Jguer/yay/v11/pkg/multierror"
"github.com/Jguer/yay/v11/pkg/settings" "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/settings/parser"
"github.com/Jguer/yay/v11/pkg/topo" "github.com/Jguer/yay/v11/pkg/topo"
@@ -19,7 +22,38 @@ import (
"github.com/pkg/errors" "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( func installLocalPKGBUILD(
ctx context.Context, ctx context.Context,
@@ -38,17 +72,20 @@ func installLocalPKGBUILD(
cmdArgs.ExistsDouble("d", "nodeps"), noCheck, cmdArgs.ExistsArg("needed"), cmdArgs.ExistsDouble("d", "nodeps"), noCheck, cmdArgs.ExistsArg("needed"),
config.Runtime.Logger.Child("grapher")) config.Runtime.Logger.Child("grapher"))
graph := topo.New[string, *dep.InstallInfo]() graph := topo.New[string, *dep.InstallInfo]()
for _, target := range cmdArgs.Targets { for _, targetDir := range cmdArgs.Targets {
var errG error 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 { if err != nil {
return errors.Wrap(err, gotext.Get("failed to parse .SRCINFO")) 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 { if errG != nil {
return err return errG
} }
} }
@@ -56,7 +93,7 @@ func installLocalPKGBUILD(
multiErr := &multierror.MultiError{} multiErr := &multierror.MultiError{}
targets := graph.TopoSortedLayerMap(func(name string, ii *dep.InstallInfo) error { targets := graph.TopoSortedLayerMap(func(name string, ii *dep.InstallInfo) error {
if ii.Source == dep.Missing { 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 return nil
}) })

View File

@@ -6,6 +6,7 @@ import (
"io" "io"
"os" "os"
"os/exec" "os/exec"
"path/filepath"
"strings" "strings"
"sync" "sync"
"testing" "testing"
@@ -169,7 +170,7 @@ func TestIntegrationLocalInstall(t *testing.T) {
} }
func TestIntegrationLocalInstallMissingDep(t *testing.T) { func TestIntegrationLocalInstallMissingDep(t *testing.T) {
wantErr := "could not find dotnet-sdk<7" wantErr := ErrPackagesNotFound
makepkgBin := t.TempDir() + "/makepkg" makepkgBin := t.TempDir() + "/makepkg"
pacmanBin := t.TempDir() + "/pacman" pacmanBin := t.TempDir() + "/pacman"
gitBin := t.TempDir() + "/git" gitBin := t.TempDir() + "/git"
@@ -270,8 +271,7 @@ func TestIntegrationLocalInstallMissingDep(t *testing.T) {
} }
err = handleCmd(context.Background(), config, cmdArgs, db) err = handleCmd(context.Background(), config, cmdArgs, db)
require.Error(t, err) require.ErrorContains(t, err, wantErr.Error())
require.EqualError(t, err, wantErr)
require.Len(t, mockRunner.ShowCalls, len(wantShow)) require.Len(t, mockRunner.ShowCalls, len(wantShow))
require.Len(t, mockRunner.CaptureCalls, len(wantCapture)) 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)) 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))
}
}

View File

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

View File

@@ -300,7 +300,7 @@ func (dp *Pool) CheckMissing(noDeps, noCheckDeps bool) error {
return nil 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 dep, trees := range missing.Missing {
for _, tree := range trees { for _, tree := range trees {

View File

@@ -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) { func GetInput(r io.Reader, defaultValue string, noConfirm bool) (string, error) {
return globalLogger.GetInput(defaultValue, noConfirm) return GlobalLogger.GetInput(defaultValue, noConfirm)
} }

View File

@@ -20,56 +20,55 @@ const (
var ( var (
cachedColumnCount = -1 cachedColumnCount = -1
DebugMode = false GlobalLogger = NewLogger(os.Stdout, os.Stdin, false, "global")
globalLogger = NewLogger(os.Stdout, os.Stdin, DebugMode, "global")
) )
func Debugln(a ...interface{}) { func Debugln(a ...interface{}) {
globalLogger.Debugln(a...) GlobalLogger.Debugln(a...)
} }
func OperationInfoln(a ...interface{}) { func OperationInfoln(a ...interface{}) {
globalLogger.OperationInfoln(a...) GlobalLogger.OperationInfoln(a...)
} }
func OperationInfo(a ...interface{}) { func OperationInfo(a ...interface{}) {
globalLogger.OperationInfo(a...) GlobalLogger.OperationInfo(a...)
} }
func SprintOperationInfo(a ...interface{}) string { func SprintOperationInfo(a ...interface{}) string {
return globalLogger.SprintOperationInfo(a...) return GlobalLogger.SprintOperationInfo(a...)
} }
func Info(a ...interface{}) { func Info(a ...interface{}) {
globalLogger.Info(a...) GlobalLogger.Info(a...)
} }
func Infoln(a ...interface{}) { func Infoln(a ...interface{}) {
globalLogger.Infoln(a...) GlobalLogger.Infoln(a...)
} }
func SprintWarn(a ...interface{}) string { func SprintWarn(a ...interface{}) string {
return globalLogger.SprintWarn(a...) return GlobalLogger.SprintWarn(a...)
} }
func Warn(a ...interface{}) { func Warn(a ...interface{}) {
globalLogger.Warn(a...) GlobalLogger.Warn(a...)
} }
func Warnln(a ...interface{}) { func Warnln(a ...interface{}) {
globalLogger.Warnln(a...) GlobalLogger.Warnln(a...)
} }
func SprintError(a ...interface{}) string { func SprintError(a ...interface{}) string {
return globalLogger.SprintError(a...) return GlobalLogger.SprintError(a...)
} }
func Error(a ...interface{}) { func Error(a ...interface{}) {
globalLogger.Error(a...) GlobalLogger.Error(a...)
} }
func Errorln(a ...interface{}) { func Errorln(a ...interface{}) {
globalLogger.Errorln(a...) GlobalLogger.Errorln(a...)
} }
func getColumnCount() int { func getColumnCount() int {

View File

@@ -7,7 +7,7 @@ import (
type Logger struct { type Logger struct {
name string name string
debug bool Debug bool
w io.Writer w io.Writer
r io.Reader r io.Reader
} }
@@ -16,17 +16,17 @@ func NewLogger(w io.Writer, r io.Reader, debug bool, name string) *Logger {
return &Logger{ return &Logger{
w: w, w: w,
name: name, name: name,
debug: debug, Debug: debug,
r: r, r: r,
} }
} }
func (l *Logger) Child(name string) *Logger { 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) { func (l *Logger) Debugln(a ...any) {
if !DebugMode { if !l.Debug {
return return
} }

View File

@@ -2,7 +2,6 @@ package main
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"os" "os"
"strings" "strings"
@@ -70,7 +69,7 @@ func syncInstall(ctx context.Context,
multiErr := &multierror.MultiError{} multiErr := &multierror.MultiError{}
targets := graph.TopoSortedLayerMap(func(s string, ii *dep.InstallInfo) error { targets := graph.TopoSortedLayerMap(func(s string, ii *dep.InstallInfo) error {
if ii.Source == dep.Missing { 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 return nil
}) })

0
testdata/jfin/PKGBUILD vendored Normal file
View File