Skip to content

Commit

Permalink
Merge pull request #540 from kcl-lang/refactor-graph
Browse files Browse the repository at this point in the history
refactor: refactor command graph based on resolver
  • Loading branch information
Peefy authored Nov 12, 2024
2 parents 29013ca + 1bdd5fe commit 8cf5a37
Show file tree
Hide file tree
Showing 9 changed files with 278 additions and 0 deletions.
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), "[email protected] [email protected]@0.0.1 [email protected]@0.0.1 [email protected]")
}
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!'

0 comments on commit 8cf5a37

Please sign in to comment.