diff --git a/go.mod b/go.mod index eef68bf1c..4ba9e5de0 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index e87dfd217..baf30a44e 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/kpm.go b/kpm.go index 65ae09d2d..e9f988939 100644 --- a/kpm.go +++ b/kpm.go @@ -25,6 +25,7 @@ func main() { app.UsageText = "kpm [arguments]..." app.Commands = []*cli.Command{ cmd.NewInitCmd(kpmcli), + // cmd.NewGraphCmd(kpmcli), cmd.NewAddCmd(kpmcli), cmd.NewPkgCmd(kpmcli), cmd.NewMetadataCmd(kpmcli), diff --git a/pkg/client/client.go b/pkg/client/client.go index 62833d0ca..015aee4ea 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -9,6 +9,7 @@ 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" @@ -567,7 +568,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 @@ -1068,6 +1069,40 @@ func (c *KpmClient) ParseOciOptionFromString(oci string, tag string) (*opt.OciOp return ociOpt, nil } +// PrintDependencyGraph will print the dependency graph of kcl package dependencies +func (c *KpmClient) PrintDependencyGraph(kclPkg *pkg.KclPkg) error { + // create the main graph with a single root vertex. + root := fmt.Sprint(kclPkg.GetPkgName()) + mainGraph := graph.New(graph.StringHash, graph.Directed()) + err := mainGraph.AddVertex(root) + if err != nil { + return err + } + + // get the dependency graphs and merge them into the main graph at root vertex. + _, depGraphs, err := c.downloadDeps(kclPkg.Dependencies, kclPkg.ModFile.Dependencies) + if err != nil { + return err + } + + for _, g := range depGraphs { + mainGraph, err = graph.Union(mainGraph, g) + if err != nil { + return err + } + src, err := FindSource(g) + if err != nil { + return err + } + err = mainGraph.AddEdge(root, src) + if err != nil { + return err + } + } + + return nil +} + // dependencyExists will check whether the dependency exists in the local filesystem. func (c *KpmClient) dependencyExists(dep *pkg.Dependency, lockDeps *pkg.Dependencies) *pkg.Dependency { @@ -1092,15 +1127,24 @@ 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), } + depGraphs := make([]graph.Graph[string, string], len(deps.Deps)) + i := 0 + // Traverse all dependencies in kcl.mod for _, d := range deps.Deps { + depGraphs[i] = graph.New(graph.StringHash, graph.Directed()) + err := depGraphs[i].AddVertex(fmt.Sprintf("%s@%s", d.Name, d.Version)) + if err != nil { + return nil, nil, err + } + i++ if len(d.Name) == 0 { - return nil, errors.InvalidDependency + return nil, nil, errors.InvalidDependency } existDep := c.dependencyExists(&d, &lockDeps) @@ -1112,7 +1156,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) @@ -1121,7 +1165,7 @@ 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() { @@ -1129,7 +1173,7 @@ func (c *KpmClient) downloadDeps(deps pkg.Dependencies, lockDeps pkg.Dependencie 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), @@ -1142,6 +1186,7 @@ func (c *KpmClient) downloadDeps(deps pkg.Dependencies, lockDeps pkg.Dependencie lockDeps.Deps[d.Name] = *lockedDep } + i = 0 // Recursively download the dependencies of the new dependencies. for _, d := range newDeps.Deps { // Load kcl.mod file of the new downloaded dependencies. @@ -1154,13 +1199,34 @@ 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, nestedDepGraphs, err := c.downloadDeps(deppkg.ModFile.Dependencies, lockDeps) if err != nil { - return nil, err + return nil, nil, err + } + + // merge the depGraph with the nestedDepGraphs. + src, err := FindSource(depGraphs[i]) + if err != nil { + return nil, nil, err + } + + for _, g := range nestedDepGraphs { + depGraphs[i], err = graph.Union(g, depGraphs[i]) + if err != nil { + return nil, nil, err + } + srcOfNestedg, err := FindSource(g) + if err != nil { + return nil, nil, err + } + err = depGraphs[i].AddEdge(src, srcOfNestedg) + if err != nil { + return nil, nil, err + } } // Update kcl.mod. @@ -1169,9 +1235,10 @@ func (c *KpmClient) downloadDeps(deps pkg.Dependencies, lockDeps pkg.Dependencie newDeps.Deps[d.Name] = d } } + i++ } - return &newDeps, nil + return &newDeps, depGraphs, nil } // pullTarFromOci will pull a kcl package tar file from oci registry. @@ -1244,3 +1311,23 @@ func check(dep pkg.Dependency, newDepPath string) bool { return dep.Sum == sum } + +func FindSource[K comparable, T any](g graph.Graph[K, T]) (K, error) { + var src K + if !g.Traits().IsDirected { + return src, fmt.Errorf("cannot find source of a non-DAG graph ") + } + + predecessorMap, err := g.PredecessorMap() + if err != nil { + return src, fmt.Errorf("failed to get predecessor map: %w", err) + } + + for vertex, predecessors := range predecessorMap { + if len(predecessors) == 0 { + src = vertex + break + } + } + return src, nil +} diff --git a/pkg/cmd/cmd_graph.go b/pkg/cmd/cmd_graph.go new file mode 100644 index 000000000..7a421580d --- /dev/null +++ b/pkg/cmd/cmd_graph.go @@ -0,0 +1,68 @@ +// Copyright 2024 The KCL Authors. All rights reserved. + +package cmd + +import ( + "os" + + "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 + } + + err = kpmcli.PrintDependencyGraph(kclPkg) + if err != nil { + return err + } + return nil +}