From 43ed11e91cc60c503d8e71adb098b1957e07a2a5 Mon Sep 17 00:00:00 2001 From: Akash Kumar Date: Sat, 10 Feb 2024 20:04:16 +0530 Subject: [PATCH 1/3] feat: add graph subcommand to print dependency graph Signed-off-by: Akash Kumar --- go.mod | 1 + go.sum | 2 + kpm.go | 1 + pkg/client/client.go | 126 +++++++++++++++++++++++++++++++++++++++---- pkg/cmd/cmd_graph.go | 68 +++++++++++++++++++++++ 5 files changed, 188 insertions(+), 10 deletions(-) create mode 100644 pkg/cmd/cmd_graph.go diff --git a/go.mod b/go.mod index eef68bf1..4ba9e5de 100644 --- a/go.mod +++ b/go.mod @@ -35,6 +35,7 @@ require ( github.com/docker/go-connections v0.4.0 // indirect github.com/docker/go-metrics v0.0.1 // indirect github.com/docker/go-units v0.5.0 // indirect + github.com/dominikbraun/graph v0.23.0 // indirect github.com/emirpasic/gods v1.18.1 // indirect github.com/fatih/color v1.10.0 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect diff --git a/go.sum b/go.sum index e87dfd21..baf30a44 100644 --- a/go.sum +++ b/go.sum @@ -113,6 +113,8 @@ github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHz github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1 h1:ZClxb8laGDf5arXfYcAtECDFgAgHklGI8CxgjHnXKJ4= +github.com/dominikbraun/graph v0.23.0 h1:TdZB4pPqCLFxYhdyMFb1TBdFxp8XLcJfTTBQucVPgCo= +github.com/dominikbraun/graph v0.23.0/go.mod h1:yOjYyogZLY1LSG9E33JWZJiq5k83Qy2C6POAuiViluc= github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= diff --git a/kpm.go b/kpm.go index 65ae09d2..fbafb996 100644 --- a/kpm.go +++ b/kpm.go @@ -25,6 +25,7 @@ func main() { app.UsageText = "kpm [arguments]..." app.Commands = []*cli.Command{ cmd.NewInitCmd(kpmcli), + cmd.NewGraphCmd(kpmcli), cmd.NewAddCmd(kpmcli), cmd.NewPkgCmd(kpmcli), cmd.NewMetadataCmd(kpmcli), diff --git a/pkg/client/client.go b/pkg/client/client.go index 62833d0c..b1195f2e 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -9,6 +9,7 @@ import ( "reflect" "strings" + "github.com/dominikbraun/graph" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "github.com/otiai10/copy" "kcl-lang.io/kcl-go/pkg/kcl" @@ -567,7 +568,7 @@ func (c *KpmClient) AddDepToPkg(kclPkg *pkg.KclPkg, d *pkg.Dependency) error { } // download all the dependencies. - changedDeps, err := c.downloadDeps(kclPkg.ModFile.Dependencies, kclPkg.Dependencies) + changedDeps, _, err := c.downloadDeps(kclPkg.ModFile.Dependencies, kclPkg.Dependencies) if err != nil { return err @@ -1068,6 +1069,59 @@ func (c *KpmClient) ParseOciOptionFromString(oci string, tag string) (*opt.OciOp return ociOpt, nil } +// PrintDependencyGraph will print the dependency graph of kcl package dependencies +func (c *KpmClient) PrintDependencyGraph(kclPkg *pkg.KclPkg) error { + // create the main graph with a single root vertex. + root := fmt.Sprint(kclPkg.GetPkgName()) + mainGraph := graph.New(graph.StringHash, graph.Directed()) + err := mainGraph.AddVertex(root) + if err != nil { + return err + } + + // get the dependency graphs and merge them into the main graph at root vertex. + _, depGraphs, err := c.downloadDeps(kclPkg.Dependencies, kclPkg.ModFile.Dependencies) + if err != nil { + return err + } + + for _, g := range depGraphs { + mainGraph, err = graph.Union(mainGraph, g) + if err != nil { + return err + } + src, err := FindSource(g) + if err != nil { + return err + } + err = mainGraph.AddEdge(root, src) + if err != nil { + return err + } + } + + adjMap, err := mainGraph.AdjacencyMap() + if err != nil { + return err + } + + // print the dependency graph to stdout. + err = graph.BFS(mainGraph, root, func(source string) bool { + for target := range adjMap[source] { + reporter.ReportMsgTo( + fmt.Sprint(source, target), + c.logWriter, + ) + } + return false + }) + if err != nil { + return err + } + + return nil +} + // dependencyExists will check whether the dependency exists in the local filesystem. func (c *KpmClient) dependencyExists(dep *pkg.Dependency, lockDeps *pkg.Dependencies) *pkg.Dependency { @@ -1092,15 +1146,24 @@ func (c *KpmClient) dependencyExists(dep *pkg.Dependency, lockDeps *pkg.Dependen } // downloadDeps will download all the dependencies of the current kcl package. -func (c *KpmClient) downloadDeps(deps pkg.Dependencies, lockDeps pkg.Dependencies) (*pkg.Dependencies, error) { +func (c *KpmClient) downloadDeps(deps pkg.Dependencies, lockDeps pkg.Dependencies) (*pkg.Dependencies, []graph.Graph[string, string], error) { newDeps := pkg.Dependencies{ Deps: make(map[string]pkg.Dependency), } + depGraphs := make([]graph.Graph[string, string], len(deps.Deps)) + i := 0 + // Traverse all dependencies in kcl.mod for _, d := range deps.Deps { + depGraphs[i] = graph.New(graph.StringHash, graph.Directed()) + err := depGraphs[i].AddVertex(fmt.Sprintf("%s@%s", d.Name, d.Version)) + if err != nil { + return nil, nil, err + } + i++ if len(d.Name) == 0 { - return nil, errors.InvalidDependency + return nil, nil, errors.InvalidDependency } existDep := c.dependencyExists(&d, &lockDeps) @@ -1112,7 +1175,7 @@ func (c *KpmClient) downloadDeps(deps pkg.Dependencies, lockDeps pkg.Dependencie expectedSum := lockDeps.Deps[d.Name].Sum // Clean the cache if len(c.homePath) == 0 || len(d.FullName) == 0 { - return nil, errors.InternalBug + return nil, nil, errors.InternalBug } dir := filepath.Join(c.homePath, d.FullName) os.RemoveAll(dir) @@ -1121,7 +1184,7 @@ func (c *KpmClient) downloadDeps(deps pkg.Dependencies, lockDeps pkg.Dependencie lockedDep, err := c.Download(&d, dir) if err != nil { - return nil, err + return nil, nil, err } if !lockedDep.IsFromLocal() { @@ -1129,7 +1192,7 @@ func (c *KpmClient) downloadDeps(deps pkg.Dependencies, lockDeps pkg.Dependencie lockedDep.Sum != expectedSum && existDep != nil && existDep.FullName == d.FullName { - return nil, reporter.NewErrorEvent( + return nil, nil, reporter.NewErrorEvent( reporter.CheckSumMismatch, errors.CheckSumMismatchError, fmt.Sprintf("checksum for '%s' changed in lock file", lockedDep.Name), @@ -1142,6 +1205,7 @@ func (c *KpmClient) downloadDeps(deps pkg.Dependencies, lockDeps pkg.Dependencie lockDeps.Deps[d.Name] = *lockedDep } + i = 0 // Recursively download the dependencies of the new dependencies. for _, d := range newDeps.Deps { // Load kcl.mod file of the new downloaded dependencies. @@ -1154,13 +1218,34 @@ func (c *KpmClient) downloadDeps(deps pkg.Dependencies, lockDeps pkg.Dependencie if os.IsNotExist(err) { continue } - return nil, err + return nil, nil, err } // Download the dependencies. - nested, err := c.downloadDeps(deppkg.ModFile.Dependencies, lockDeps) + nested, nestedDepGraphs, err := c.downloadDeps(deppkg.ModFile.Dependencies, lockDeps) if err != nil { - return nil, err + return nil, nil, err + } + + // merge the depGraph with the nestedDepGraphs. + src, err := FindSource(depGraphs[i]) + if err != nil { + return nil, nil, err + } + + for _, g := range nestedDepGraphs { + depGraphs[i], err = graph.Union(g, depGraphs[i]) + if err != nil { + return nil, nil, err + } + srcOfNestedg, err := FindSource(g) + if err != nil { + return nil, nil, err + } + err = depGraphs[i].AddEdge(src, srcOfNestedg) + if err != nil { + return nil, nil, err + } } // Update kcl.mod. @@ -1169,9 +1254,10 @@ func (c *KpmClient) downloadDeps(deps pkg.Dependencies, lockDeps pkg.Dependencie newDeps.Deps[d.Name] = d } } + i++ } - return &newDeps, nil + return &newDeps, depGraphs, nil } // pullTarFromOci will pull a kcl package tar file from oci registry. @@ -1244,3 +1330,23 @@ func check(dep pkg.Dependency, newDepPath string) bool { return dep.Sum == sum } + +func FindSource[K comparable, T any](g graph.Graph[K, T]) (K, error) { + var src K + if !g.Traits().IsDirected { + return src, fmt.Errorf("cannot find source of a non-DAG graph ") + } + + predecessorMap, err := g.PredecessorMap() + if err != nil { + return src, fmt.Errorf("failed to get predecessor map: %w", err) + } + + for vertex, predecessors := range predecessorMap { + if len(predecessors) == 0 { + src = vertex + break + } + } + return src, nil +} diff --git a/pkg/cmd/cmd_graph.go b/pkg/cmd/cmd_graph.go new file mode 100644 index 00000000..7a421580 --- /dev/null +++ b/pkg/cmd/cmd_graph.go @@ -0,0 +1,68 @@ +// Copyright 2024 The KCL Authors. All rights reserved. + +package cmd + +import ( + "os" + + "github.com/urfave/cli/v2" + "kcl-lang.io/kpm/pkg/client" + "kcl-lang.io/kpm/pkg/env" + pkg "kcl-lang.io/kpm/pkg/package" + "kcl-lang.io/kpm/pkg/reporter" +) + +// NewGraphCmd new a Command for `kpm graph`. +func NewGraphCmd(kpmcli *client.KpmClient) *cli.Command { + return &cli.Command{ + Hidden: false, + Name: "graph", + Usage: "prints the module dependency graph", + Action: func(c *cli.Context) error { + return KpmGraph(c, kpmcli) + }, + } +} + +func KpmGraph(c *cli.Context, kpmcli *client.KpmClient) error { + // acquire the lock of the package cache. + err := kpmcli.AcquirePackageCacheLock() + if err != nil { + return err + } + + defer func() { + // release the lock of the package cache after the function returns. + releaseErr := kpmcli.ReleasePackageCacheLock() + if releaseErr != nil && err == nil { + err = releaseErr + } + }() + + pwd, err := os.Getwd() + + if err != nil { + return reporter.NewErrorEvent(reporter.Bug, err, "internal bugs, please contact us to fix it.") + } + + globalPkgPath, err := env.GetAbsPkgPath() + if err != nil { + return err + } + + kclPkg, err := pkg.LoadKclPkg(pwd) + if err != nil { + return err + } + + err = kclPkg.ValidateKpmHome(globalPkgPath) + if err != (*reporter.KpmEvent)(nil) { + return err + } + + err = kpmcli.PrintDependencyGraph(kclPkg) + if err != nil { + return err + } + return nil +} From 58049394939246478e0f2e8ed2a9e51bd14feb23 Mon Sep 17 00:00:00 2001 From: Akash Kumar Date: Sun, 11 Feb 2024 18:22:11 +0530 Subject: [PATCH 2/3] feat: remove array of graphs Signed-off-by: Akash Kumar --- pkg/client/client.go | 93 +++++++++---------- .../help_msg/test_suite.stdout | 1 + 2 files changed, 43 insertions(+), 51 deletions(-) diff --git a/pkg/client/client.go b/pkg/client/client.go index b1195f2e..86ed0a99 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -1071,42 +1071,38 @@ func (c *KpmClient) ParseOciOptionFromString(oci string, tag string) (*opt.OciOp // PrintDependencyGraph will print the dependency graph of kcl package dependencies func (c *KpmClient) PrintDependencyGraph(kclPkg *pkg.KclPkg) error { - // create the main graph with a single root vertex. - root := fmt.Sprint(kclPkg.GetPkgName()) - mainGraph := graph.New(graph.StringHash, graph.Directed()) - err := mainGraph.AddVertex(root) + _, depGraph, err := c.downloadDeps(kclPkg.Dependencies, kclPkg.ModFile.Dependencies) if err != nil { return err } - // get the dependency graphs and merge them into the main graph at root vertex. - _, depGraphs, err := c.downloadDeps(kclPkg.Dependencies, kclPkg.ModFile.Dependencies) + // add the root vertex(package name) to the dependency graph. + root := fmt.Sprint(kclPkg.GetPkgName()) + err = depGraph.AddVertex(root) if err != nil { return err } - for _, g := range depGraphs { - mainGraph, err = graph.Union(mainGraph, g) - if err != nil { - return err - } - src, err := FindSource(g) - if err != nil { - return err - } - err = mainGraph.AddEdge(root, src) + sources, err := FindSource(depGraph) + if err != nil { + return err + } + + // make an edge between the root vertex and all the sources of the dependency graph. + for _, source := range sources { + err = depGraph.AddEdge(source, root) if err != nil { return err } } - adjMap, err := mainGraph.AdjacencyMap() + adjMap, err := depGraph.AdjacencyMap() if err != nil { return err } // print the dependency graph to stdout. - err = graph.BFS(mainGraph, root, func(source string) bool { + err = graph.BFS(depGraph, root, func(source string) bool { for target := range adjMap[source] { reporter.ReportMsgTo( fmt.Sprint(source, target), @@ -1146,22 +1142,13 @@ func (c *KpmClient) dependencyExists(dep *pkg.Dependency, lockDeps *pkg.Dependen } // downloadDeps will download all the dependencies of the current kcl package. -func (c *KpmClient) downloadDeps(deps pkg.Dependencies, lockDeps pkg.Dependencies) (*pkg.Dependencies, []graph.Graph[string, string], error) { +func (c *KpmClient) downloadDeps(deps pkg.Dependencies, lockDeps pkg.Dependencies) (*pkg.Dependencies, graph.Graph[string, string], error) { newDeps := pkg.Dependencies{ Deps: make(map[string]pkg.Dependency), } - depGraphs := make([]graph.Graph[string, string], len(deps.Deps)) - i := 0 - // Traverse all dependencies in kcl.mod for _, d := range deps.Deps { - depGraphs[i] = graph.New(graph.StringHash, graph.Directed()) - err := depGraphs[i].AddVertex(fmt.Sprintf("%s@%s", d.Name, d.Version)) - if err != nil { - return nil, nil, err - } - i++ if len(d.Name) == 0 { return nil, nil, errors.InvalidDependency } @@ -1205,7 +1192,15 @@ func (c *KpmClient) downloadDeps(deps pkg.Dependencies, lockDeps pkg.Dependencie lockDeps.Deps[d.Name] = *lockedDep } - i = 0 + depGraph := graph.New(graph.StringHash, graph.Directed()) + + for _, d := range newDeps.Deps { + err := depGraph.AddVertex(fmt.Sprintf("%s@%s", d.Name, d.Version)) + if err != nil { + return nil, nil, err + } + } + // Recursively download the dependencies of the new dependencies. for _, d := range newDeps.Deps { // Load kcl.mod file of the new downloaded dependencies. @@ -1222,27 +1217,25 @@ func (c *KpmClient) downloadDeps(deps pkg.Dependencies, lockDeps pkg.Dependencie } // Download the dependencies. - nested, nestedDepGraphs, err := c.downloadDeps(deppkg.ModFile.Dependencies, lockDeps) + nested, nestedDepGraph, err := c.downloadDeps(deppkg.ModFile.Dependencies, lockDeps) if err != nil { return nil, nil, err } - // merge the depGraph with the nestedDepGraphs. - src, err := FindSource(depGraphs[i]) + source := fmt.Sprintf("%s@%s", d.Name, d.Version) + sourcesOfNestedDepGraph, err := FindSource(nestedDepGraph) if err != nil { return nil, nil, err } - for _, g := range nestedDepGraphs { - depGraphs[i], err = graph.Union(g, depGraphs[i]) - if err != nil { - return nil, nil, err - } - srcOfNestedg, err := FindSource(g) - if err != nil { - return nil, nil, err - } - err = depGraphs[i].AddEdge(src, srcOfNestedg) + depGraph, err = graph.Union(depGraph, nestedDepGraph) + if err != nil { + return nil, nil, err + } + + // make an edge between the source of all nested dep graph and main dep graph + for _, sourceOfNestedDepGraph := range sourcesOfNestedDepGraph { + err = depGraph.AddEdge(source, sourceOfNestedDepGraph) if err != nil { return nil, nil, err } @@ -1254,10 +1247,9 @@ func (c *KpmClient) downloadDeps(deps pkg.Dependencies, lockDeps pkg.Dependencie newDeps.Deps[d.Name] = d } } - i++ } - return &newDeps, depGraphs, nil + return &newDeps, depGraph, nil } // pullTarFromOci will pull a kcl package tar file from oci registry. @@ -1331,22 +1323,21 @@ func check(dep pkg.Dependency, newDepPath string) bool { return dep.Sum == sum } -func FindSource[K comparable, T any](g graph.Graph[K, T]) (K, error) { - var src K +func FindSource[K comparable, T any](g graph.Graph[K, T]) ([]K, error) { if !g.Traits().IsDirected { - return src, fmt.Errorf("cannot find source of a non-DAG graph ") + return nil, fmt.Errorf("cannot find source of a non-DAG graph ") } predecessorMap, err := g.PredecessorMap() if err != nil { - return src, fmt.Errorf("failed to get predecessor map: %w", err) + return nil, fmt.Errorf("failed to get predecessor map: %w", err) } + var sources []K for vertex, predecessors := range predecessorMap { if len(predecessors) == 0 { - src = vertex - break + sources = append(sources, vertex) } } - return src, nil + return sources, nil } diff --git a/test/e2e/test_suites/kpm/exec_outside_pkg/help_msg/test_suite.stdout b/test/e2e/test_suites/kpm/exec_outside_pkg/help_msg/test_suite.stdout index f58a3cb7..ff5794fa 100644 --- a/test/e2e/test_suites/kpm/exec_outside_pkg/help_msg/test_suite.stdout +++ b/test/e2e/test_suites/kpm/exec_outside_pkg/help_msg/test_suite.stdout @@ -1,5 +1,6 @@ COMMANDS: init initialize new module in current directory + graph prints the module dependency graph add add new dependency pkg package a kcl package into tar metadata output the resolved dependencies of a package From 6fcfb5c0b21f92a79c744bab39d78fd08b646691 Mon Sep 17 00:00:00 2001 From: Akash Kumar Date: Sun, 11 Feb 2024 22:54:32 +0530 Subject: [PATCH 3/3] Add unit test and address review comments Signed-off-by: Akash Kumar --- pkg/client/client.go | 85 +++++-------------- pkg/client/client_test.go | 40 +++++++++ .../test_data/test_dependency_graph/kcl.mod | 9 ++ pkg/cmd/cmd_graph.go | 24 +++++- pkg/graph/graph.go | 83 ++++++++++++++++++ 5 files changed, 178 insertions(+), 63 deletions(-) create mode 100644 pkg/client/test_data/test_dependency_graph/kcl.mod create mode 100644 pkg/graph/graph.go diff --git a/pkg/client/client.go b/pkg/client/client.go index 86ed0a99..2644385b 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -17,6 +17,7 @@ import ( "kcl-lang.io/kpm/pkg/env" "kcl-lang.io/kpm/pkg/errors" "kcl-lang.io/kpm/pkg/git" + pkgGraph "kcl-lang.io/kpm/pkg/graph" "kcl-lang.io/kpm/pkg/oci" "kcl-lang.io/kpm/pkg/opt" pkg "kcl-lang.io/kpm/pkg/package" @@ -1069,53 +1070,34 @@ func (c *KpmClient) ParseOciOptionFromString(oci string, tag string) (*opt.OciOp return ociOpt, nil } -// PrintDependencyGraph will print the dependency graph of kcl package dependencies -func (c *KpmClient) PrintDependencyGraph(kclPkg *pkg.KclPkg) error { - _, depGraph, err := c.downloadDeps(kclPkg.Dependencies, kclPkg.ModFile.Dependencies) +// GetDependencyGraph will get the dependency graph of kcl package dependencies +func (c *KpmClient) GetDependencyGraph(kclPkg *pkg.KclPkg) (graph.Graph[string, string], error) { + _, depGraph, err := c.downloadDeps(kclPkg.ModFile.Dependencies, kclPkg.Dependencies) if err != nil { - return err + return nil, err } - // add the root vertex(package name) to the dependency graph. - root := fmt.Sprint(kclPkg.GetPkgName()) - err = depGraph.AddVertex(root) + sources, err := pkgGraph.FindSources(depGraph) if err != nil { - return err + return nil, err } - sources, err := FindSource(depGraph) + // add the root vertex(package name) to the dependency graph. + root := fmt.Sprintf("%s@%s", kclPkg.GetPkgName(), kclPkg.GetPkgVersion()) + err = depGraph.AddVertex(root) if err != nil { - return err + return nil, err } // make an edge between the root vertex and all the sources of the dependency graph. for _, source := range sources { - err = depGraph.AddEdge(source, root) + err = depGraph.AddEdge(root, source) if err != nil { - return err - } - } - - adjMap, err := depGraph.AdjacencyMap() - if err != nil { - return err - } - - // print the dependency graph to stdout. - err = graph.BFS(depGraph, root, func(source string) bool { - for target := range adjMap[source] { - reporter.ReportMsgTo( - fmt.Sprint(source, target), - c.logWriter, - ) + return nil, err } - return false - }) - if err != nil { - return err } - return nil + return depGraph, nil } // dependencyExists will check whether the dependency exists in the local filesystem. @@ -1194,13 +1176,6 @@ func (c *KpmClient) downloadDeps(deps pkg.Dependencies, lockDeps pkg.Dependencie depGraph := graph.New(graph.StringHash, graph.Directed()) - for _, d := range newDeps.Deps { - err := depGraph.AddVertex(fmt.Sprintf("%s@%s", d.Name, d.Version)) - if err != nil { - return nil, nil, err - } - } - // Recursively download the dependencies of the new dependencies. for _, d := range newDeps.Deps { // Load kcl.mod file of the new downloaded dependencies. @@ -1223,20 +1198,25 @@ func (c *KpmClient) downloadDeps(deps pkg.Dependencies, lockDeps pkg.Dependencie } source := fmt.Sprintf("%s@%s", d.Name, d.Version) - sourcesOfNestedDepGraph, err := FindSource(nestedDepGraph) + err = depGraph.AddVertex(source) + if err != nil && err != graph.ErrVertexAlreadyExists { + return nil, nil, err + } + + sourcesOfNestedDepGraph, err := pkgGraph.FindSources(nestedDepGraph) if err != nil { return nil, nil, err } - depGraph, err = graph.Union(depGraph, nestedDepGraph) + depGraph, err = pkgGraph.Union(depGraph, nestedDepGraph) if err != nil { return nil, nil, err } - // make an edge between the source of all nested dep graph and main dep graph + // make an edge between the source of all nested dep graph and main dep graph for _, sourceOfNestedDepGraph := range sourcesOfNestedDepGraph { err = depGraph.AddEdge(source, sourceOfNestedDepGraph) - if err != nil { + if err != nil && err != graph.ErrEdgeAlreadyExists { return nil, nil, err } } @@ -1322,22 +1302,3 @@ func check(dep pkg.Dependency, newDepPath string) bool { return dep.Sum == sum } - -func FindSource[K comparable, T any](g graph.Graph[K, T]) ([]K, error) { - if !g.Traits().IsDirected { - return nil, fmt.Errorf("cannot find source of a non-DAG graph ") - } - - predecessorMap, err := g.PredecessorMap() - if err != nil { - return nil, fmt.Errorf("failed to get predecessor map: %w", err) - } - - var sources []K - for vertex, predecessors := range predecessorMap { - if len(predecessors) == 0 { - sources = append(sources, vertex) - } - } - return sources, nil -} diff --git a/pkg/client/client_test.go b/pkg/client/client_test.go index c741e185..d7c2e597 100644 --- a/pkg/client/client_test.go +++ b/pkg/client/client_test.go @@ -12,6 +12,7 @@ import ( "strings" "testing" + "github.com/dominikbraun/graph" "github.com/otiai10/copy" "github.com/stretchr/testify/assert" "kcl-lang.io/kpm/pkg/env" @@ -116,6 +117,45 @@ func TestDownloadLatestOci(t *testing.T) { assert.Equal(t, err, nil) } +func TestDependencyGraph(t *testing.T) { + testDir := getTestDir("test_dependency_graph") + assert.Equal(t, utils.DirExists(filepath.Join(testDir, "kcl.mod.lock")), false) + kpmcli, err := NewKpmClient() + assert.Equal(t, err, nil) + kclPkg, err := kpmcli.LoadPkgFromPath(testDir) + assert.Equal(t, err, nil) + + depGraph, err := kpmcli.GetDependencyGraph(kclPkg) + assert.Equal(t, err, nil) + adjMap, err := depGraph.AdjacencyMap() + assert.Equal(t, err, nil) + + edgeProp := graph.EdgeProperties{ + Attributes: map[string]string{}, + Weight: 0, + Data: nil, + } + assert.Equal(t, adjMap, + map[string]map[string]graph.Edge[string]{ + "dependency_graph@0.0.1": { + "teleport@0.1.0": {Source: "dependency_graph@0.0.1", Target: "teleport@0.1.0", Properties: edgeProp}, + "rabbitmq@0.0.1": {Source: "dependency_graph@0.0.1", Target: "rabbitmq@0.0.1", Properties: edgeProp}, + "agent@0.1.0": {Source: "dependency_graph@0.0.1", Target: "agent@0.1.0", Properties: edgeProp}, + }, + "teleport@0.1.0": { + "k8s@1.28": {Source: "teleport@0.1.0", Target: "k8s@1.28", Properties: edgeProp}, + }, + "rabbitmq@0.0.1": { + "k8s@1.28": {Source: "rabbitmq@0.0.1", Target: "k8s@1.28", Properties: edgeProp}, + }, + "agent@0.1.0": { + "k8s@1.28": {Source: "agent@0.1.0", Target: "k8s@1.28", Properties: edgeProp}, + }, + "k8s@1.28": {}, + }, + ) +} + func TestInitEmptyPkg(t *testing.T) { testDir := initTestDir("test_init_empty_mod") kclPkg := pkg.NewKclPkg(&opt.InitOptions{Name: "test_name", InitPath: testDir}) diff --git a/pkg/client/test_data/test_dependency_graph/kcl.mod b/pkg/client/test_data/test_dependency_graph/kcl.mod new file mode 100644 index 00000000..cc4b573d --- /dev/null +++ b/pkg/client/test_data/test_dependency_graph/kcl.mod @@ -0,0 +1,9 @@ +[package] +name = "dependency_graph" +edition = "0.0.1" +version = "0.0.1" + +[dependencies] +rabbitmq = "0.0.1" +teleport = "0.1.0" +agent = "0.1.0" \ No newline at end of file diff --git a/pkg/cmd/cmd_graph.go b/pkg/cmd/cmd_graph.go index 7a421580..fae1ac1e 100644 --- a/pkg/cmd/cmd_graph.go +++ b/pkg/cmd/cmd_graph.go @@ -3,8 +3,10 @@ package cmd import ( + "fmt" "os" + "github.com/dominikbraun/graph" "github.com/urfave/cli/v2" "kcl-lang.io/kpm/pkg/client" "kcl-lang.io/kpm/pkg/env" @@ -60,7 +62,27 @@ func KpmGraph(c *cli.Context, kpmcli *client.KpmClient) error { return err } - err = kpmcli.PrintDependencyGraph(kclPkg) + depGraph, err := kpmcli.GetDependencyGraph(kclPkg) + if err != nil { + return err + } + + adjMap, err := depGraph.AdjacencyMap() + if err != nil { + return err + } + + // print the dependency graph to stdout. + root := fmt.Sprintf("%s@%s", kclPkg.GetPkgName(), kclPkg.GetPkgVersion()) + err = graph.BFS(depGraph, root, func(source string) bool { + for target := range adjMap[source] { + reporter.ReportMsgTo( + fmt.Sprint(source, " ", target), + kpmcli.GetLogWriter(), + ) + } + return false + }) if err != nil { return err } diff --git a/pkg/graph/graph.go b/pkg/graph/graph.go new file mode 100644 index 00000000..82dc7586 --- /dev/null +++ b/pkg/graph/graph.go @@ -0,0 +1,83 @@ +package graph + +import ( + "fmt" + "github.com/dominikbraun/graph" +) + +// Union combines two given graphs into a new graph. The vertex hashes in both +// graphs are expected to be unique. The two input graphs will remain unchanged. +// +// Both graphs should be either directed or undirected. All traits for the new +// graph will be derived from g. +// +// If the same vertex/edge happens to be in both g and h, then an error will not be +// thrown as happens in original Union function and successful operation takes place. +func Union[K comparable, T any](g, h graph.Graph[K, T]) (graph.Graph[K, T], error) { + union, err := g.Clone() + if err != nil { + return union, fmt.Errorf("failed to clone g: %w", err) + } + + adjacencyMap, err := h.AdjacencyMap() + if err != nil { + return union, fmt.Errorf("failed to get adjacency map: %w", err) + } + + addedEdges := make(map[K]map[K]struct{}) + + for currentHash := range adjacencyMap { + vertex, err := h.Vertex(currentHash) + if err != nil { + return union, fmt.Errorf("failed to get vertex %v: %w", currentHash, err) + } + + err = union.AddVertex(vertex) + if err != nil && err != graph.ErrVertexAlreadyExists { + return union, fmt.Errorf("failed to add vertex %v: %w", currentHash, err) + } + } + + for _, adjacencies := range adjacencyMap { + for _, edge := range adjacencies { + if _, sourceOK := addedEdges[edge.Source]; sourceOK { + if _, targetOK := addedEdges[edge.Source][edge.Target]; targetOK { + // If the edge addedEdges[source][target] exists, the edge + // has already been created and thus can be skipped here. + continue + } + } + + err = union.AddEdge(edge.Source, edge.Target) + if err != nil && err != graph.ErrEdgeAlreadyExists { + return union, fmt.Errorf("failed to add edge (%v, %v): %w", edge.Source, edge.Target, err) + } + + if _, ok := addedEdges[edge.Source]; !ok { + addedEdges[edge.Source] = make(map[K]struct{}) + } + addedEdges[edge.Source][edge.Target] = struct{}{} + } + } + + return union, nil +} + +func FindSources[K comparable, T any](g graph.Graph[K, T]) ([]K, error) { + if !g.Traits().IsDirected { + return nil, fmt.Errorf("cannot find source of a non-DAG graph ") + } + + predecessorMap, err := g.PredecessorMap() + if err != nil { + return nil, fmt.Errorf("failed to get predecessor map: %w", err) + } + + var sources []K + for vertex, predecessors := range predecessorMap { + if len(predecessors) == 0 { + sources = append(sources, vertex) + } + } + return sources, nil +} \ No newline at end of file