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

feat: add graph subcommand to print dependency graph #264

Merged
merged 3 commits into from
Feb 18, 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 go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
1 change: 1 addition & 0 deletions kpm.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ func main() {
app.UsageText = "kpm <command> [arguments]..."
app.Commands = []*cli.Command{
cmd.NewInitCmd(kpmcli),
cmd.NewGraphCmd(kpmcli),
cmd.NewAddCmd(kpmcli),
cmd.NewPkgCmd(kpmcli),
cmd.NewMetadataCmd(kpmcli),
Expand Down
78 changes: 68 additions & 10 deletions pkg/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@ 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"
"kcl-lang.io/kpm/pkg/constants"
"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"
Expand Down Expand Up @@ -567,7 +569,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
Expand Down Expand Up @@ -1068,6 +1070,36 @@ func (c *KpmClient) ParseOciOptionFromString(oci string, tag string) (*opt.OciOp
return ociOpt, nil
}

// 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 nil, err
}

sources, err := pkgGraph.FindSources(depGraph)
if err != nil {
return nil, err
}

// 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 nil, err
}

// make an edge between the root vertex and all the sources of the dependency graph.
for _, source := range sources {
err = depGraph.AddEdge(root, source)
if err != nil {
return nil, err
}
}

return depGraph, nil
}

// dependencyExists will check whether the dependency exists in the local filesystem.
func (c *KpmClient) dependencyExists(dep *pkg.Dependency, lockDeps *pkg.Dependencies) *pkg.Dependency {

Expand All @@ -1092,15 +1124,15 @@ 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),
}

// Traverse all dependencies in kcl.mod
for _, d := range deps.Deps {
if len(d.Name) == 0 {
return nil, errors.InvalidDependency
return nil, nil, errors.InvalidDependency
}

existDep := c.dependencyExists(&d, &lockDeps)
Expand All @@ -1112,7 +1144,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)
Expand All @@ -1121,15 +1153,15 @@ 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() {
if !c.noSumCheck && expectedSum != "" &&
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),
Expand All @@ -1142,6 +1174,8 @@ func (c *KpmClient) downloadDeps(deps pkg.Dependencies, lockDeps pkg.Dependencie
lockDeps.Deps[d.Name] = *lockedDep
}

depGraph := graph.New(graph.StringHash, graph.Directed())

// Recursively download the dependencies of the new dependencies.
for _, d := range newDeps.Deps {
// Load kcl.mod file of the new downloaded dependencies.
Expand All @@ -1154,13 +1188,37 @@ 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, nestedDepGraph, err := c.downloadDeps(deppkg.ModFile.Dependencies, lockDeps)
if err != nil {
return nil, err
return nil, nil, err
}

source := fmt.Sprintf("%s@%s", d.Name, d.Version)
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 = 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
for _, sourceOfNestedDepGraph := range sourcesOfNestedDepGraph {
err = depGraph.AddEdge(source, sourceOfNestedDepGraph)
if err != nil && err != graph.ErrEdgeAlreadyExists {
return nil, nil, err
}
}

// Update kcl.mod.
Expand All @@ -1171,7 +1229,7 @@ func (c *KpmClient) downloadDeps(deps pkg.Dependencies, lockDeps pkg.Dependencie
}
}

return &newDeps, nil
return &newDeps, depGraph, nil
}

// pullTarFromOci will pull a kcl package tar file from oci registry.
Expand Down
40 changes: 40 additions & 0 deletions pkg/client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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})
Expand Down
9 changes: 9 additions & 0 deletions pkg/client/test_data/test_dependency_graph/kcl.mod
Original file line number Diff line number Diff line change
@@ -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"
90 changes: 90 additions & 0 deletions pkg/cmd/cmd_graph.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// Copyright 2024 The KCL Authors. All rights reserved.

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"
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
}

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
}
return nil
}
Loading
Loading