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