Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: refactor command graph based on resolver #540

Merged
merged 1 commit into from
Nov 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pkg/client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
193 changes: 193 additions & 0 deletions pkg/client/graph.go
Original file line number Diff line number Diff line change
@@ -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
}
47 changes: 47 additions & 0 deletions pkg/client/graph_test.go
Original file line number Diff line number Diff line change
@@ -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")
}
7 changes: 7 additions & 0 deletions pkg/client/test_data/test_graph/dep/kcl.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[package]
name = "dep"
edition = "v0.10.0"
version = "0.0.1"

[dependencies]
helloworld = "0.1.4"
8 changes: 8 additions & 0 deletions pkg/client/test_data/test_graph/dep/kcl.mod.lock
Original file line number Diff line number Diff line change
@@ -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"
1 change: 1 addition & 0 deletions pkg/client/test_data/test_graph/dep/main.k
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The_first_kcl_program = 'Hello World!'
8 changes: 8 additions & 0 deletions pkg/client/test_data/test_graph/pkg/kcl.mod
Original file line number Diff line number Diff line change
@@ -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"
12 changes: 12 additions & 0 deletions pkg/client/test_data/test_graph/pkg/kcl.mod.lock
Original file line number Diff line number Diff line change
@@ -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"
1 change: 1 addition & 0 deletions pkg/client/test_data/test_graph/pkg/main.k
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The_first_kcl_program = 'Hello World!'