Skip to content

Commit

Permalink
Merge pull request #264 from AkashKumar7902/dependency-graph
Browse files Browse the repository at this point in the history
feat: add graph subcommand to print dependency graph
  • Loading branch information
Peefy authored Feb 18, 2024
2 parents f67db1b + 6fcfb5c commit 0224b99
Show file tree
Hide file tree
Showing 9 changed files with 295 additions and 10 deletions.
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 @@ -594,7 +596,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 @@ -1095,6 +1097,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 @@ -1119,15 +1151,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 @@ -1139,7 +1171,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 @@ -1148,15 +1180,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 @@ -1169,6 +1201,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 @@ -1181,13 +1215,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 @@ -1198,7 +1256,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 @@ -117,6 +118,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

0 comments on commit 0224b99

Please sign in to comment.