add local graph util

This commit is contained in:
jguer 2022-09-06 23:25:44 +02:00
parent 650809eba1
commit f7286b25ae
No known key found for this signature in database
GPG key ID: 6D6CC9BEA8556B35
10 changed files with 369 additions and 94 deletions

9
go.mod
View file

@ -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

8
go.sum
View file

@ -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=

59
main.go
View file

@ -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)

97
pkg/cmd/graph/main.go Normal file
View file

@ -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
}

177
pkg/dep/depGraph.go Normal file
View file

@ -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
}

View file

@ -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
}

64
pkg/settings/pacman.go Normal file
View file

@ -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
}

View file

@ -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)

View file

@ -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
}

View file

@ -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")
)