From f7286b25aed3c9e3ced200a00b59bcc1ceef5dd2 Mon Sep 17 00:00:00 2001 From: jguer Date: Tue, 6 Sep 2022 23:25:44 +0200 Subject: [PATCH] add local graph util --- go.mod | 9 +- go.sum | 8 - main.go | 59 +------ pkg/cmd/graph/main.go | 97 +++++++++++ pkg/dep/depGraph.go | 177 ++++++++++++++++++++ pkg/metadata/metadata_aur.go | 13 +- pkg/settings/pacman.go | 64 +++++++ main_test.go => pkg/settings/pacman_test.go | 4 +- pkg/topo/dep.go | 24 +-- pkg/topo/errors.go | 8 +- 10 files changed, 369 insertions(+), 94 deletions(-) create mode 100644 pkg/cmd/graph/main.go create mode 100644 pkg/dep/depGraph.go create mode 100644 pkg/settings/pacman.go rename main_test.go => pkg/settings/pacman_test.go (93%) diff --git a/go.mod b/go.mod index 53976ad..be50fab 100644 --- a/go.mod +++ b/go.mod @@ -27,15 +27,8 @@ require ( require github.com/tidwall/gjson v1.14.3 require ( - github.com/josharian/intern v1.0.0 // indirect - github.com/pkg/profile v1.6.0 // indirect -) - -require ( - github.com/goccy/go-json v0.9.11 // indirect - github.com/itchyny/gojq v0.12.8 // indirect + github.com/itchyny/gojq v0.12.8 github.com/itchyny/timefmt-go v0.1.3 // indirect - github.com/mailru/easyjson v0.7.7 github.com/ohler55/ojg v1.14.4 github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.0 // indirect diff --git a/go.sum b/go.sum index 29037e6..2b3c544 100644 --- a/go.sum +++ b/go.sum @@ -17,8 +17,6 @@ github.com/bradleyjkemp/cupaloy v2.3.0+incompatible/go.mod h1:Au1Xw1sgaJ5iSFktEh github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/goccy/go-json v0.9.11 h1:/pAaQDLHEoCq/5FFmSKBswWmK6H0e8g4159Kc/X/nqk= -github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= @@ -26,12 +24,8 @@ github.com/itchyny/gojq v0.12.8 h1:Zxcwq8w4IeR8JJYEtoG2MWJZUv0RGY6QqJcO1cqV8+A= github.com/itchyny/gojq v0.12.8/go.mod h1:gE2kZ9fVRU0+JAksaTzjIlgnCa2akU+a1V0WXgJQN5c= github.com/itchyny/timefmt-go v0.1.3 h1:7M3LGVDsqcd0VZH2U+x393obrzZisp7C0uEe921iRkU= github.com/itchyny/timefmt-go v0.1.3/go.mod h1:0osSSCQSASBJMsIZnhAaF1C2fCBTJZXrnj37mG8/c+A= -github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= -github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/leonelquinteros/gotext v1.5.0 h1:ODY7LzLpZWWSJdAHnzhreOr6cwLXTAmc914FOauSkBM= github.com/leonelquinteros/gotext v1.5.0/go.mod h1:OCiUVHuhP9LGFBQ1oAmdtNCHJCiHiQA8lf4nAifHkr0= -github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= -github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy/FJl/rCYT0+EuS8+Z0z4= @@ -40,8 +34,6 @@ github.com/ohler55/ojg v1.14.4 h1:L2ds8AlB5t/QbqSfhRwvagJzQ7pgmdrefMIypQs0Xik= github.com/ohler55/ojg v1.14.4/go.mod h1:7Ghirupn8NC8hSSDpI0gcjorPxj+vSVIONDWfliHR1k= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/profile v1.6.0 h1:hUDfIISABYI59DyeB3OTay/HxSRwTQ8rB/H83k6r5dM= -github.com/pkg/profile v1.6.0/go.mod h1:qBsxPvzyUincmltOk6iyRVxHYg4adc0OFOv72ZdLa18= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= diff --git a/main.go b/main.go index a0a530e..6f4db69 100644 --- a/main.go +++ b/main.go @@ -2,14 +2,11 @@ package main // import "github.com/Jguer/yay" import ( "context" - "fmt" "os" "os/exec" "runtime/debug" - pacmanconf "github.com/Morganamilo/go-pacmanconf" "github.com/leonelquinteros/gotext" - "golang.org/x/term" "github.com/Jguer/yay/v11/pkg/db" "github.com/Jguer/yay/v11/pkg/db/ialpm" @@ -35,60 +32,6 @@ func initGotext() { } } -func initAlpm(cmdArgs *parser.Arguments, pacmanConfigPath string) (*pacmanconf.Config, bool, error) { - root := "/" - if value, _, exists := cmdArgs.GetArg("root", "r"); exists { - root = value - } - - pacmanConf, stderr, err := pacmanconf.PacmanConf("--config", pacmanConfigPath, "--root", root) - if err != nil { - cmdErr := err - if stderr != "" { - cmdErr = fmt.Errorf("%s\n%s", err, stderr) - } - - return nil, false, cmdErr - } - - if dbPath, _, exists := cmdArgs.GetArg("dbpath", "b"); exists { - pacmanConf.DBPath = dbPath - } - - if arch := cmdArgs.GetArgs("arch"); arch != nil { - pacmanConf.Architecture = append(pacmanConf.Architecture, arch...) - } - - if ignoreArray := cmdArgs.GetArgs("ignore"); ignoreArray != nil { - pacmanConf.IgnorePkg = append(pacmanConf.IgnorePkg, ignoreArray...) - } - - if ignoreGroupsArray := cmdArgs.GetArgs("ignoregroup"); ignoreGroupsArray != nil { - pacmanConf.IgnoreGroup = append(pacmanConf.IgnoreGroup, ignoreGroupsArray...) - } - - if cacheArray := cmdArgs.GetArgs("cachedir"); cacheArray != nil { - pacmanConf.CacheDir = cacheArray - } - - if gpgDir, _, exists := cmdArgs.GetArg("gpgdir"); exists { - pacmanConf.GPGDir = gpgDir - } - - useColor := pacmanConf.Color && term.IsTerminal(int(os.Stdout.Fd())) - - switch value, _, _ := cmdArgs.GetArg("color"); value { - case "always": - useColor = true - case "auto": - useColor = term.IsTerminal(int(os.Stdout.Fd())) - case "never": - useColor = false - } - - return pacmanConf, useColor, nil -} - func main() { var ( err error @@ -155,7 +98,7 @@ func main() { var useColor bool - config.Runtime.PacmanConf, useColor, err = initAlpm(cmdArgs, config.PacmanConf) + config.Runtime.PacmanConf, useColor, err = settings.RetrievePacmanConfig(cmdArgs, config.PacmanConf) if err != nil { if str := err.Error(); str != "" { text.Errorln(str) diff --git a/pkg/cmd/graph/main.go b/pkg/cmd/graph/main.go new file mode 100644 index 0000000..c6fe5ac --- /dev/null +++ b/pkg/cmd/graph/main.go @@ -0,0 +1,97 @@ +package main + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/Jguer/yay/v11/pkg/db/ialpm" + "github.com/Jguer/yay/v11/pkg/dep" + "github.com/Jguer/yay/v11/pkg/metadata" + "github.com/Jguer/yay/v11/pkg/settings" + "github.com/Jguer/yay/v11/pkg/settings/parser" + "github.com/Jguer/yay/v11/pkg/text" + "github.com/leonelquinteros/gotext" + "github.com/pkg/errors" +) + +func splitDep(dep string) (pkg, mod, ver string) { + split := strings.FieldsFunc(dep, func(c rune) bool { + match := c == '>' || c == '<' || c == '=' + + if match { + mod += string(c) + } + + return match + }) + + if len(split) == 0 { + return "", "", "" + } + + if len(split) == 1 { + return split[0], "", "" + } + + return split[0], mod, split[1] +} + +func handleCmd() error { + config, err := settings.NewConfig("") + if err != nil { + return err + } + + cmdArgs := parser.MakeArguments() + if err := config.ParseCommandLine(cmdArgs); err != nil { + return err + } + + pacmanConf, _, err := settings.RetrievePacmanConfig(cmdArgs, config.PacmanConf) + if err != nil { + return err + } + + dbExecutor, err := ialpm.NewExecutor(pacmanConf) + if err != nil { + return err + } + + aurCache, err := metadata.NewAURCache(filepath.Join(config.BuildDir, "aur.json")) + if err != nil { + return errors.Wrap(err, gotext.Get("failed to retrieve aur Cache")) + } + + grapher := dep.NewGrapher(dbExecutor, aurCache, true, settings.NoConfirm, os.Stdout) + + return graphPackage(grapher, cmdArgs.Targets) +} + +func main() { + if err := handleCmd(); err != nil { + text.Errorln(err) + os.Exit(1) + } +} + +func graphPackage( + grapher *dep.Grapher, + targets []string, +) error { + if len(targets) != 1 { + return errors.New(gotext.Get("only one target is allowed")) + } + + graph, err := grapher.GraphFromAURCache([]string{targets[0]}) + if err != nil { + return err + } + + fmt.Fprintln(os.Stdout, graph.String()) + fmt.Fprintln(os.Stdout, graph.TopoSortedLayers()) + fmt.Fprintln(os.Stdout, graph.TopoSorted()) + + return nil +} diff --git a/pkg/dep/depGraph.go b/pkg/dep/depGraph.go new file mode 100644 index 0000000..b97ee99 --- /dev/null +++ b/pkg/dep/depGraph.go @@ -0,0 +1,177 @@ +package dep + +import ( + "fmt" + "io" + "os" + "strconv" + + "github.com/Jguer/yay/v11/pkg/db" + "github.com/Jguer/yay/v11/pkg/metadata" + aur "github.com/Jguer/yay/v11/pkg/query" + "github.com/Jguer/yay/v11/pkg/text" + "github.com/Jguer/yay/v11/pkg/topo" + "github.com/leonelquinteros/gotext" +) + +type Grapher struct { + dbExecutor db.Executor + aurCache *metadata.AURCache + fullGraph bool // If true, the graph will include all dependencies including already installed ones or repo + noConfirm bool + w io.Writer // output writer +} + +func NewGrapher(dbExecutor db.Executor, aurCache *metadata.AURCache, fullGraph, noConfirm bool, output io.Writer) *Grapher { + return &Grapher{ + dbExecutor: dbExecutor, + aurCache: aurCache, + fullGraph: fullGraph, + noConfirm: noConfirm, + w: output, + } +} + +func (g *Grapher) GraphFromAURCache(targets []string) (*topo.Graph[string], error) { + graph := topo.New[string]() + + for _, target := range targets { + aurPkgs, _ := g.aurCache.FindPackage(target) + pkg := provideMenu(g.w, target, aurPkgs, g.noConfirm) + + depSlice := ComputeCombinedDepList(pkg, false, false) + g.addNodes(graph, pkg.Name, depSlice) + } + + return graph, nil +} + +func (g *Grapher) addNodes( + graph *topo.Graph[string], + parentPkgName string, + deps []string, +) { + for _, depString := range deps { + depName, _, _ := splitDep(depString) + + if g.dbExecutor.LocalSatisfierExists(depString) { + if g.fullGraph { + graph.SetNodeInfo(depName, &topo.NodeInfo{Color: "green"}) + if err := graph.DependOn(depName, parentPkgName); err != nil { + text.Warnln(depName, parentPkgName, err) + } + } + continue + } + + if graph.Exists(depName) { + if err := graph.DependOn(depName, parentPkgName); err != nil { + text.Warnln(depName, parentPkgName, err) + } + + continue + } + + // Check ALPM + if alpmPkg := g.dbExecutor.SyncSatisfier(depString); alpmPkg != nil { + if err := graph.DependOn(alpmPkg.Name(), parentPkgName); err != nil { + text.Warnln("repo dep warn:", depName, parentPkgName, err) + } + + graph.SetNodeInfo(alpmPkg.Name(), &topo.NodeInfo{Color: "blue"}) + + if newDeps := alpmPkg.Depends().Slice(); len(newDeps) != 0 && g.fullGraph { + newDepsSlice := make([]string, 0, len(newDeps)) + for _, newDep := range newDeps { + newDepsSlice = append(newDepsSlice, newDep.Name) + } + + g.addNodes(graph, alpmPkg.Name(), newDepsSlice) + } + + continue + } + + if aurPkgs, _ := g.aurCache.FindPackage(depName); len(aurPkgs) != 0 { // Check AUR + pkg := aurPkgs[0] + if len(aurPkgs) > 1 { + pkg := provideMenu(g.w, depName, aurPkgs, g.noConfirm) + g.aurCache.SetProvideCache(depName, []*aur.Pkg{pkg}) + } + + if err := graph.Alias(pkg.PackageBase, pkg.Name); err != nil { + text.Warnln("aur alias warn:", pkg.PackageBase, pkg.Name, err) + } + + if err := graph.DependOn(pkg.PackageBase, parentPkgName); err != nil { + text.Warnln("aur dep warn:", pkg.PackageBase, parentPkgName, err) + } + + graph.SetNodeInfo(pkg.PackageBase, &topo.NodeInfo{Color: "lightgreen"}) + + if newDeps := ComputeCombinedDepList(pkg, false, false); len(newDeps) != 0 { + g.addNodes(graph, pkg.Name, newDeps) + } + + continue + } + } +} + +func provideMenu(w io.Writer, dep string, options []*aur.Pkg, noConfirm bool) *aur.Pkg { + size := len(options) + if size == 1 { + return options[0] + } + + str := text.Bold(gotext.Get("There are %d providers available for %s:", size, dep)) + str += "\n" + + size = 1 + str += text.SprintOperationInfo(gotext.Get("Repository AUR"), "\n ") + + for _, pkg := range options { + str += fmt.Sprintf("%d) %s ", size, pkg.Name) + size++ + } + + text.OperationInfoln(str) + + for { + fmt.Fprintln(w, gotext.Get("\nEnter a number (default=1): ")) + + if noConfirm { + fmt.Fprintln(w, "1") + + return options[0] + } + + numberBuf, err := text.GetInput("", false) + if err != nil { + fmt.Fprintln(os.Stderr, err) + + break + } + + if numberBuf == "" { + return options[0] + } + + num, err := strconv.Atoi(numberBuf) + if err != nil { + text.Errorln(gotext.Get("invalid number: %s", numberBuf)) + + continue + } + + if num < 1 || num >= size { + text.Errorln(gotext.Get("invalid value: %d is not between %d and %d", num, 1, size-1)) + + continue + } + + return options[num-1] + } + + return nil +} diff --git a/pkg/metadata/metadata_aur.go b/pkg/metadata/metadata_aur.go index 95f7646..c021936 100644 --- a/pkg/metadata/metadata_aur.go +++ b/pkg/metadata/metadata_aur.go @@ -40,18 +40,23 @@ func (a *AURCache) DebugInfo() { fmt.Println("Cache Hits", a.cacheHits) } -func (a *AURCache) FindDep(depName string) ([]*aur.Pkg, error) { - if pkgs, ok := a.provideCache[depName]; ok { +func (a *AURCache) SetProvideCache(needle string, pkgs []*aur.Pkg) { + a.provideCache[needle] = pkgs +} + +// Get returns a list of packages that provide the given search term +func (a *AURCache) FindPackage(needle string) ([]*aur.Pkg, error) { + if pkgs, ok := a.provideCache[needle]; ok { a.cacheHits++ return pkgs, nil } - final, error := a.gojqGet(depName) + final, error := a.gojqGet(needle) if error != nil { return nil, error } - a.provideCache[depName] = final + a.provideCache[needle] = final return final, nil } diff --git a/pkg/settings/pacman.go b/pkg/settings/pacman.go new file mode 100644 index 0000000..b7bc909 --- /dev/null +++ b/pkg/settings/pacman.go @@ -0,0 +1,64 @@ +package settings + +import ( + "fmt" + "os" + + "github.com/Jguer/yay/v11/pkg/settings/parser" + pacmanconf "github.com/Morganamilo/go-pacmanconf" + "golang.org/x/term" +) + +func RetrievePacmanConfig(cmdArgs *parser.Arguments, pacmanConfigPath string) (*pacmanconf.Config, bool, error) { + root := "/" + if value, _, exists := cmdArgs.GetArg("root", "r"); exists { + root = value + } + + pacmanConf, stderr, err := pacmanconf.PacmanConf("--config", pacmanConfigPath, "--root", root) + if err != nil { + cmdErr := err + if stderr != "" { + cmdErr = fmt.Errorf("%s\n%s", err, stderr) + } + + return nil, false, cmdErr + } + + if dbPath, _, exists := cmdArgs.GetArg("dbpath", "b"); exists { + pacmanConf.DBPath = dbPath + } + + if arch := cmdArgs.GetArgs("arch"); arch != nil { + pacmanConf.Architecture = append(pacmanConf.Architecture, arch...) + } + + if ignoreArray := cmdArgs.GetArgs("ignore"); ignoreArray != nil { + pacmanConf.IgnorePkg = append(pacmanConf.IgnorePkg, ignoreArray...) + } + + if ignoreGroupsArray := cmdArgs.GetArgs("ignoregroup"); ignoreGroupsArray != nil { + pacmanConf.IgnoreGroup = append(pacmanConf.IgnoreGroup, ignoreGroupsArray...) + } + + if cacheArray := cmdArgs.GetArgs("cachedir"); cacheArray != nil { + pacmanConf.CacheDir = cacheArray + } + + if gpgDir, _, exists := cmdArgs.GetArg("gpgdir"); exists { + pacmanConf.GPGDir = gpgDir + } + + useColor := pacmanConf.Color && term.IsTerminal(int(os.Stdout.Fd())) + + switch value, _, _ := cmdArgs.GetArg("color"); value { + case "always": + useColor = true + case "auto": + useColor = term.IsTerminal(int(os.Stdout.Fd())) + case "never": + useColor = false + } + + return pacmanConf, useColor, nil +} diff --git a/main_test.go b/pkg/settings/pacman_test.go similarity index 93% rename from main_test.go rename to pkg/settings/pacman_test.go index 51356a3..3b393af 100644 --- a/main_test.go +++ b/pkg/settings/pacman_test.go @@ -1,4 +1,4 @@ -package main +package settings import ( "testing" @@ -43,7 +43,7 @@ func TestPacmanConf(t *testing.T) { }, } - pacmanConf, color, err := initAlpm(parser.MakeArguments(), "testdata/pacman.conf") + pacmanConf, color, err := RetrievePacmanConfig(parser.MakeArguments(), "../../testdata/pacman.conf") assert.Nil(t, err) assert.NotNil(t, pacmanConf) assert.Equal(t, color, false) diff --git a/pkg/topo/dep.go b/pkg/topo/dep.go index 4502cb0..7e8b2cd 100644 --- a/pkg/topo/dep.go +++ b/pkg/topo/dep.go @@ -49,9 +49,7 @@ func (g *Graph[T]) Len() int { func (g *Graph[T]) Exists(node T) bool { // check aliases - if aliasNode, ok := g.alias[node]; ok { - node = aliasNode - } + node = g.getAlias(node) _, ok := g.nodes[node] return ok @@ -59,7 +57,7 @@ func (g *Graph[T]) Exists(node T) bool { func (g *Graph[T]) Alias(node, alias T) error { if alias == node { - return ErrSelfReferential + return nil } // add node @@ -75,24 +73,28 @@ func (g *Graph[T]) Alias(node, alias T) error { } func (g *Graph[T]) AddNode(node T) { - // check aliases - if aliasNode, ok := g.alias[node]; ok { - node = aliasNode - } + node = g.getAlias(node) g.nodes[node] = true } -func (g *Graph[T]) SetNodeInfo(node T, nodeInfo *NodeInfo) { - // check aliases +func (g *Graph[T]) getAlias(node T) T { if aliasNode, ok := g.alias[node]; ok { - node = aliasNode + return aliasNode } + return node +} + +func (g *Graph[T]) SetNodeInfo(node T, nodeInfo *NodeInfo) { + node = g.getAlias(node) g.nodeInfo[node] = *nodeInfo } func (g *Graph[T]) DependOn(child, parent T) error { + child = g.getAlias(child) + parent = g.getAlias(parent) + if child == parent { return ErrSelfReferential } diff --git a/pkg/topo/errors.go b/pkg/topo/errors.go index f5a30b1..43e8159 100644 --- a/pkg/topo/errors.go +++ b/pkg/topo/errors.go @@ -2,6 +2,8 @@ package topo import "errors" -var ErrSelfReferential = errors.New("self-referential dependencies not allowed") -var ErrConflictingAlias = errors.New("alias already defined") -var ErrCircular = errors.New("circular dependencies not allowed") +var ( + ErrSelfReferential = errors.New("self-referential dependencies not allowed") + ErrConflictingAlias = errors.New("alias already defined") + ErrCircular = errors.New("circular dependencies not allowed") +)