From 1bdd5fe29a6ac11228bdcafcce99c2c90c1ef25a Mon Sep 17 00:00:00 2001 From: zongz Date: Tue, 12 Nov 2024 17:43:40 +0800 Subject: [PATCH] refactor: refactor command graph based on resolver Signed-off-by: zongz --- pkg/client/client_test.go | 1 + pkg/client/graph.go | 193 ++++++++++++++++++ pkg/client/graph_test.go | 47 +++++ pkg/client/test_data/test_graph/dep/kcl.mod | 7 + .../test_data/test_graph/dep/kcl.mod.lock | 8 + pkg/client/test_data/test_graph/dep/main.k | 1 + pkg/client/test_data/test_graph/pkg/kcl.mod | 8 + .../test_data/test_graph/pkg/kcl.mod.lock | 12 ++ pkg/client/test_data/test_graph/pkg/main.k | 1 + 9 files changed, 278 insertions(+) create mode 100644 pkg/client/graph.go create mode 100644 pkg/client/graph_test.go create mode 100644 pkg/client/test_data/test_graph/dep/kcl.mod create mode 100644 pkg/client/test_data/test_graph/dep/kcl.mod.lock create mode 100644 pkg/client/test_data/test_graph/dep/main.k create mode 100644 pkg/client/test_data/test_graph/pkg/kcl.mod create mode 100644 pkg/client/test_data/test_graph/pkg/kcl.mod.lock create mode 100644 pkg/client/test_data/test_graph/pkg/main.k diff --git a/pkg/client/client_test.go b/pkg/client/client_test.go index 2e0abf88..f7520727 100644 --- a/pkg/client/client_test.go +++ b/pkg/client/client_test.go @@ -98,6 +98,7 @@ func TestWithGlobalLock(t *testing.T) { test.RunTestWithGlobalLock(t, "testAddRenameWithNoSpec", testAddRenameWithNoSpec) test.RunTestWithGlobalLock(t, "testPullWithOnlySpec", testPullWithOnlySpec) test.RunTestWithGlobalLock(t, "TestRunWithModSpecVersion", testRunWithModSpecVersion) + test.RunTestWithGlobalLock(t, "TestGraph", testGraph) features.Enable(features.SupportNewStorage) test.RunTestWithGlobalLock(t, "testAddWithModSpec", testAddWithModSpec) diff --git a/pkg/client/graph.go b/pkg/client/graph.go new file mode 100644 index 00000000..2ae01f96 --- /dev/null +++ b/pkg/client/graph.go @@ -0,0 +1,193 @@ +package client + +import ( + "fmt" + "path/filepath" + + "github.com/dominikbraun/graph" + "golang.org/x/mod/module" + "kcl-lang.io/kpm/pkg/downloader" + pkg "kcl-lang.io/kpm/pkg/package" + "kcl-lang.io/kpm/pkg/resolver" +) + +// GraphOptions is the options for creating a dependency graph. +type GraphOptions struct { + kMod *pkg.KclPkg +} + +type GraphOption func(*GraphOptions) error + +// WithGraphMod sets the kMod for creating a dependency graph. +func WithGraphMod(kMod *pkg.KclPkg) GraphOption { + return func(o *GraphOptions) error { + o.kMod = kMod + return nil + } +} + +// DepGraph is the dependency graph. +type DepGraph struct { + gra graph.Graph[module.Version, module.Version] +} + +// NewDepGraph creates a new dependency graph. +func NewDepGraph() *DepGraph { + return &DepGraph{ + gra: graph.New( + func(m module.Version) module.Version { return m }, + graph.Directed(), + graph.PreventCycles(), + ), + } +} + +// AddVertex adds a vertex to the dependency graph. +func (g *DepGraph) AddVertex(name, version string) (*module.Version, error) { + root := module.Version{Path: name, Version: version} + err := g.gra.AddVertex(root) + if err != nil && err != graph.ErrVertexAlreadyExists { + return nil, err + } + return &root, nil +} + +// AddEdge adds an edge to the dependency graph. +func (g *DepGraph) AddEdge(parent, child module.Version) error { + err := g.gra.AddEdge(parent, child) + if err != nil { + return err + } + return nil +} + +// DisplayGraphFromVertex displays the dependency graph from the start vertex to string. +func (g *DepGraph) DisplayGraphFromVertex(startVertex module.Version) (string, error) { + var res string + adjMap, err := g.gra.AdjacencyMap() + if err != nil { + return "", err + } + + // Print the dependency graph to string. + err = graph.BFS(g.gra, startVertex, func(source module.Version) bool { + for target := range adjMap[source] { + res += fmt.Sprint(format(source), " ", format(target)) + "\n" + } + return false + }) + if err != nil { + return "", err + } + + return res, nil +} + +// format formats the module version to string. +func format(m module.Version) string { + formattedMsg := m.Path + if m.Version != "" { + formattedMsg += "@" + m.Version + } + return formattedMsg +} + +// Graph creates a dependency graph for the given KCL Module. +func (c *KpmClient) Graph(opts ...GraphOption) (*DepGraph, error) { + options := &GraphOptions{} + for _, o := range opts { + err := o(options) + if err != nil { + return nil, err + } + } + + kMod := options.kMod + + if kMod == nil { + return nil, fmt.Errorf("kMod is required") + } + + // Create the dependency graph. + dGraph := NewDepGraph() + // Take the current KCL module as the start vertex + dGraph.AddVertex(kMod.GetPkgName(), kMod.GetPkgVersion()) + + modDeps := kMod.ModFile.Dependencies.Deps + if modDeps == nil { + return nil, fmt.Errorf("kcl.mod dependencies is nil") + } + + // ResolveFunc is the function for resolving each dependency when traversing the dependency graph. + resolverFunc := func(dep *pkg.Dependency, parentPkg *pkg.KclPkg) error { + if dep != nil && parentPkg != nil { + // Set the dep as a vertex into graph. + depVertex, err := dGraph.AddVertex(dep.Name, dep.Version) + if err != nil && err != graph.ErrVertexAlreadyExists { + return err + } + + // Create the vertex for the parent package. + parent := module.Version{ + Path: parentPkg.GetPkgName(), + Version: parentPkg.GetPkgVersion(), + } + + // Add the edge between the parent and the dependency. + err = dGraph.AddEdge(parent, *depVertex) + if err != nil { + if err == graph.ErrEdgeCreatesCycle { + return fmt.Errorf("adding %s as a dependency results in a cycle", depVertex) + } + return err + } + } + + return nil + } + + // Create a new dependency resolver + depResolver := resolver.DepsResolver{ + DefaultCachePath: c.homePath, + InsecureSkipTLSverify: c.insecureSkipTLSverify, + Downloader: c.DepDownloader, + Settings: &c.settings, + LogWriter: c.logWriter, + } + depResolver.ResolveFuncs = append(depResolver.ResolveFuncs, resolverFunc) + + for _, depName := range modDeps.Keys() { + dep, ok := modDeps.Get(depName) + if !ok { + return nil, fmt.Errorf("failed to get dependency %s", depName) + } + + // Check if the dependency is a local path and it is not an absolute path. + // If it is not an absolute path, transform the path to an absolute path. + var depSource *downloader.Source + if dep.Source.IsLocalPath() && !filepath.IsAbs(dep.Source.Local.Path) { + depSource = &downloader.Source{ + Local: &downloader.Local{ + Path: filepath.Join(kMod.HomePath, dep.Source.Local.Path), + }, + } + } else { + depSource = &dep.Source + } + + err := resolverFunc(&dep, kMod) + if err != nil { + return nil, err + } + + err = depResolver.Resolve( + resolver.WithEnableCache(true), + resolver.WithSource(depSource), + ) + if err != nil { + return nil, err + } + } + + return dGraph, nil +} diff --git a/pkg/client/graph_test.go b/pkg/client/graph_test.go new file mode 100644 index 00000000..3f3891e8 --- /dev/null +++ b/pkg/client/graph_test.go @@ -0,0 +1,47 @@ +package client + +import ( + "path/filepath" + "testing" + + "golang.org/x/mod/module" + "gotest.tools/v3/assert" + pkg "kcl-lang.io/kpm/pkg/package" + "kcl-lang.io/kpm/pkg/utils" +) + +func testGraph(t *testing.T) { + testPath := getTestDir("test_graph") + modPath := filepath.Join(testPath, "pkg") + + kpmcli, err := NewKpmClient() + if err != nil { + t.Fatalf("failed to create kpm client: %v", err) + } + + kMod, err := pkg.LoadKclPkgWithOpts( + pkg.WithPath(modPath), + pkg.WithSettings(kpmcli.GetSettings()), + ) + + if err != nil { + t.Fatalf("failed to load kcl package: %v", err) + } + + dGraph, err := kpmcli.Graph( + WithGraphMod(kMod), + ) + if err != nil { + t.Fatalf("failed to create dependency graph: %v", err) + } + + graStr, err := dGraph.DisplayGraphFromVertex( + module.Version{Path: kMod.GetPkgName(), Version: kMod.GetPkgVersion()}, + ) + + if err != nil { + t.Fatalf("failed to display graph: %v", err) + } + + assert.Equal(t, utils.RmNewline(graStr), "pkg@0.0.1 dep@0.0.1pkg@0.0.1 helloworld@0.1.4dep@0.0.1 helloworld@0.1.4") +} diff --git a/pkg/client/test_data/test_graph/dep/kcl.mod b/pkg/client/test_data/test_graph/dep/kcl.mod new file mode 100644 index 00000000..940785f4 --- /dev/null +++ b/pkg/client/test_data/test_graph/dep/kcl.mod @@ -0,0 +1,7 @@ +[package] +name = "dep" +edition = "v0.10.0" +version = "0.0.1" + +[dependencies] +helloworld = "0.1.4" diff --git a/pkg/client/test_data/test_graph/dep/kcl.mod.lock b/pkg/client/test_data/test_graph/dep/kcl.mod.lock new file mode 100644 index 00000000..1428530e --- /dev/null +++ b/pkg/client/test_data/test_graph/dep/kcl.mod.lock @@ -0,0 +1,8 @@ +[dependencies] + [dependencies.helloworld] + name = "helloworld" + full_name = "helloworld_0.1.4" + version = "0.1.4" + reg = "ghcr.io" + repo = "kcl-lang/helloworld" + oci_tag = "0.1.4" diff --git a/pkg/client/test_data/test_graph/dep/main.k b/pkg/client/test_data/test_graph/dep/main.k new file mode 100644 index 00000000..fa7048e6 --- /dev/null +++ b/pkg/client/test_data/test_graph/dep/main.k @@ -0,0 +1 @@ +The_first_kcl_program = 'Hello World!' \ No newline at end of file diff --git a/pkg/client/test_data/test_graph/pkg/kcl.mod b/pkg/client/test_data/test_graph/pkg/kcl.mod new file mode 100644 index 00000000..444eaa2e --- /dev/null +++ b/pkg/client/test_data/test_graph/pkg/kcl.mod @@ -0,0 +1,8 @@ +[package] +name = "pkg" +edition = "v0.10.0" +version = "0.0.1" + +[dependencies] +dep = { path = "../dep", version = "0.0.1" } +helloworld = "0.1.4" diff --git a/pkg/client/test_data/test_graph/pkg/kcl.mod.lock b/pkg/client/test_data/test_graph/pkg/kcl.mod.lock new file mode 100644 index 00000000..fc115dc6 --- /dev/null +++ b/pkg/client/test_data/test_graph/pkg/kcl.mod.lock @@ -0,0 +1,12 @@ +[dependencies] + [dependencies.dep] + name = "dep" + full_name = "dep_0.0.1" + version = "0.0.1" + [dependencies.helloworld] + name = "helloworld" + full_name = "helloworld_0.1.4" + version = "0.1.4" + reg = "ghcr.io" + repo = "kcl-lang/helloworld" + oci_tag = "0.1.4" diff --git a/pkg/client/test_data/test_graph/pkg/main.k b/pkg/client/test_data/test_graph/pkg/main.k new file mode 100644 index 00000000..fa7048e6 --- /dev/null +++ b/pkg/client/test_data/test_graph/pkg/main.k @@ -0,0 +1 @@ +The_first_kcl_program = 'Hello World!' \ No newline at end of file