Skip to content

Commit

Permalink
Merge pull request #110 from kcmvp/deps
Browse files Browse the repository at this point in the history
Deps
  • Loading branch information
kcmvp authored May 20, 2024
2 parents f500d81 + 4118493 commit 1e55d87
Show file tree
Hide file tree
Showing 8 changed files with 95 additions and 159 deletions.
1 change: 0 additions & 1 deletion cmd/gbc/artifact/internal_plugin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,6 @@ func (suite *InternalPluginTestSuit) TestNewPlugin() {
assert.True(t, test.wantErr == (err != nil))
if !test.wantErr {
assert.Equal(t, test.module, plugin.module)
assert.True(t, lo.Contains([]string{"v1.58.1", "v1.57.2", "v1.1.1", "v1.11.0"}, plugin.Version()))
}
})
}
Expand Down
103 changes: 44 additions & 59 deletions cmd/gbc/artifact/project.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@ import (
"github.com/kcmvp/gob/utils"
"github.com/samber/lo" //nolint
"github.com/spf13/viper" //nolint
"io/fs"
"go/types"
"golang.org/x/mod/modfile"
"golang.org/x/tools/go/packages"
"log"
"os"
"os/exec"
"path/filepath"
"regexp"
"runtime"
"strings"
"sync"
Expand All @@ -30,10 +31,10 @@ var (
)

type Project struct {
root string
module string
deps []string
cfgs sync.Map // store all the configuration
root string
mod *modfile.File
cfgs sync.Map // store all the configuration
pkgs []*packages.Package
}

func (project *Project) load() *viper.Viper {
Expand Down Expand Up @@ -77,32 +78,28 @@ func (project *Project) HookDir() string {
}

func init() {
cmd := exec.Command("go", "list", "-m", "-f", "{{.Dir}}_:_{{.Path}}")
output, err := cmd.Output()
output, err := exec.Command("go", "list", "-m", "-f", "{{.Dir}}_:_{{.Path}}").CombinedOutput()
if err != nil || len(string(output)) == 0 {
log.Fatal(color.RedString("Error: please execute command in project root directory %s", string(output)))
log.Fatal(color.RedString("please execute command in project root directory %s", string(output)))
}

item := strings.Split(strings.TrimSpace(string(output)), "_:_")
project = Project{
root: item[0],
module: item[1],
cfgs: sync.Map{},
project = Project{cfgs: sync.Map{}, root: item[0]}
data, err := os.ReadFile(filepath.Join(project.root, "go.mod"))
if err != nil {
log.Fatal(color.RedString(err.Error()))
}
cmd = exec.Command("go", "list", "-f", "{{if not .Standard}}{{.ImportPath}}{{end}}", "-deps", "./...")
output, err = cmd.Output()
project.mod, err = modfile.Parse("go.mod", data, nil)
if err != nil {
log.Fatal(color.RedString("Error: please execute command in project root directory"))
log.Fatal(color.RedString("please execute command in project root directory %s", string(output)))
}
scanner := bufio.NewScanner(strings.NewReader(string(output)))
var deps []string
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if len(line) > 0 {
deps = append(deps, line)
}
cfg := &packages.Config{
Mode: packages.NeedName | packages.NeedTypes | packages.NeedTypesInfo | packages.NeedFiles | packages.NeedTypesInfo | packages.NeedDeps | packages.NeedImports | packages.NeedSyntax,
Dir: project.root,
}
project.pkgs, err = packages.Load(cfg, "./...")
if err != nil {
log.Fatal(color.RedString("failed to load project %s", err.Error()))
}
project.deps = deps
}

// CurProject return Project struct
Expand All @@ -117,7 +114,7 @@ func (project *Project) Root() string {

// Module return current project module name
func (project *Project) Module() string {
return project.module
return project.mod.Module.Mod.Path
}

func (project *Project) Target() string {
Expand Down Expand Up @@ -148,37 +145,22 @@ func (project *Project) sourceFileInPkg(pkg string) ([]string, error) {
}

func (project *Project) MainFiles() []string {
var mainFiles []string
dirs, _ := project.sourceFileInPkg("main")
re := regexp.MustCompile(`func\s+main\s*\(\s*\)`)
lo.ForEach(dirs, func(dir string, _ int) {
_ = filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() && dir != path {
return filepath.SkipDir
}
if d.IsDir() || !strings.HasSuffix(d.Name(), ".go") || strings.HasSuffix(d.Name(), "_test.go") {
return nil
}
file, err := os.Open(path)
if err != nil {
return err
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if re.MatchString(line) {
mainFiles = append(mainFiles, path)
return filepath.SkipDir
return lo.FilterMap(project.pkgs, func(pkg *packages.Package, _ int) (string, bool) {
if pkg.Name != "main" {
return "", false
}
scope := pkg.Types.Scope()
for _, name := range scope.Names() {
obj := scope.Lookup(name)
if f, ok := obj.(*types.Func); ok {
signature := f.Type().(*types.Signature)
if f.Name() == "main" && signature.Params().Len() == 0 && signature.Results().Len() == 0 {
return pkg.Fset.Position(obj.Pos()).Filename, true
}
}
return scanner.Err()
})
}
return "", false
})
return mainFiles
}

func (project *Project) Plugins() []Plugin {
Expand All @@ -201,15 +183,18 @@ func (project *Project) Plugins() []Plugin {
}
}

func (project *Project) Dependencies() []string {
return project.deps
func (project *Project) Dependencies() []*modfile.Require {
return project.mod.Require
}

func (project *Project) InstallDependency(dep string) error {
if !lo.Contains(project.deps, dep) {
exec.Command("go", "get", "-u", dep).CombinedOutput() //nolint
var err error
if lo.NoneBy(project.mod.Require, func(r *modfile.Require) bool {
return lo.Contains(r.Syntax.Token, dep)
}) {
_, err = exec.Command("go", "get", "-u", dep).CombinedOutput() //nolint
}
return nil
return err
}

func (project *Project) InstallPlugin(plugin Plugin) error {
Expand Down
7 changes: 5 additions & 2 deletions cmd/gbc/artifact/project_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"github.com/samber/lo"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
"golang.org/x/mod/modfile"
"io"
"os"
"path/filepath"
Expand Down Expand Up @@ -49,8 +50,10 @@ func TestBasic(t *testing.T) {

func (suite *ProjectTestSuite) TestDeps() {
deps := CurProject().Dependencies()
assert.Equal(suite.T(), 58, len(deps))
assert.True(suite.T(), lo.Contains(deps, "github.com/spf13/viper"))
assert.Equal(suite.T(), 50, len(deps))
assert.True(suite.T(), lo.ContainsBy(deps, func(require *modfile.Require) bool {
return require.Mod.Path == "github.com/spf13/viper"
}))
}

func (suite *ProjectTestSuite) TestPlugins() {
Expand Down
99 changes: 28 additions & 71 deletions cmd/gbc/command/deps.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package command

import (
"bufio"
"fmt"
"golang.org/x/mod/modfile" //nolint
"os"
"os/exec"
"path/filepath"
Expand All @@ -21,93 +21,50 @@ var (
yellow = color.New(color.FgYellow)
)

// parseMod return a tuple which the fourth element is the indicator of direct or indirect reference
func parseMod(mod *os.File) (string, string, []*lo.Tuple4[string, string, string, int], error) {
scanner := bufio.NewScanner(mod)
var deps []*lo.Tuple4[string, string, string, int]
var module, version string
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if len(line) == 0 || line == ")" || line == "//" || strings.HasPrefix(line, "require") {
continue
}
if strings.HasPrefix(line, "module ") {
module = strings.Split(line, " ")[1]
} else if strings.HasPrefix(line, "go ") {
version = strings.Split(line, " ")[1]
} else {
entry := strings.Split(line, " ")
m := strings.TrimSpace(entry[0])
v := strings.TrimSpace(entry[1])
dep := lo.T4(m, v, v, lo.If(len(entry) > 2, 0).Else(1))
deps = append(deps, &dep)
}
}
return module, version, deps, scanner.Err()
}

// dependencyTree build dependency tree of the project, an empty tree returns when runs into error
func dependencyTree() (treeprint.Tree, error) {
mod, err := os.Open(filepath.Join(artifact.CurProject().Root(), "go.mod"))
if err != nil {
return nil, fmt.Errorf(color.RedString(err.Error()))
}
exec.Command("go", "mod", "tidy").CombinedOutput() //nolint
if output, err := exec.Command("go", "build", "./...").CombinedOutput(); err != nil {
return nil, fmt.Errorf(color.RedString(string(output)))
}
module, _, dependencies, err := parseMod(mod)
if len(dependencies) < 1 {
return nil, nil
}
if err != nil {
return nil, fmt.Errorf(err.Error())
}
tree := treeprint.New()
tree.SetValue(module)
direct := lo.FilterMap(dependencies, func(item *lo.Tuple4[string, string, string, int], _ int) (string, bool) {
return item.A, item.D == 1
tree.SetValue(artifact.CurProject().Module())
directs := lo.FilterMap(artifact.CurProject().Dependencies(), func(item *modfile.Require, _ int) (lo.Tuple2[string, string], bool) {
return lo.Tuple2[string, string]{A: item.Mod.Path, B: item.Mod.Version}, !item.Indirect
})
// get the latest version
versions := artifact.LatestVersion(direct...)
for _, dep := range dependencies {
if version, ok := lo.Find(versions, func(t lo.Tuple2[string, string]) bool {
return dep.A == t.A && dep.B != t.B
}); ok {
dep.C = version.B
}
}
versions := artifact.LatestVersion(lo.Map(directs, func(item lo.Tuple2[string, string], _ int) string {
return item.A
})...)
// parse the dependency tree
cache := []string{os.Getenv("GOPATH"), "pkg", "mod", "cache", "download"}
for _, dependency := range dependencies {
if dependency.D == 1 {
label := lo.IfF(dependency.B == dependency.C, func() string {
return fmt.Sprintf("%s@%s", dependency.A, dependency.B)
for _, dependency := range artifact.CurProject().Dependencies() {
if !dependency.Indirect {
m, ok := lo.Find(versions, func(item lo.Tuple2[string, string]) bool {
return dependency.Mod.Path == item.A && dependency.Mod.Version != item.B
})
label := lo.IfF(!ok, func() string {
return dependency.Mod.String()
}).ElseF(func() string {
return yellow.Sprintf("* %s@%s (%s)", dependency.A, dependency.B, dependency.C)
return yellow.Sprintf("* %s (%s)", dependency.Mod.String(), m.B)
})
child := tree.AddBranch(label)
dir := append(cache, strings.Split(dependency.A, "/")...)
dir = append(dir, []string{"@v", fmt.Sprintf("%s.mod", dependency.B)}...)
mod, err = os.Open(filepath.Join(dir...))
if err != nil {
return tree, fmt.Errorf(color.RedString(err.Error()))
}
_, _, cDeps, err := parseMod(mod)
direct := tree.AddBranch(label)
dir := append(cache, strings.Split(dependency.Mod.Path, "/")...)
dir = append(dir, []string{"@v", fmt.Sprintf("%s.mod", dependency.Mod.Version)}...)
data, err := os.ReadFile(filepath.Join(dir...))
if err != nil {
return tree, fmt.Errorf(color.RedString(err.Error()))
color.Yellow("failed to get latest version of %s", dependency.Mod.Path)
continue
}
inter := lo.Filter(cDeps, func(c *lo.Tuple4[string, string, string, int], _ int) bool {
return lo.ContainsBy(dependencies, func(p *lo.Tuple4[string, string, string, int]) bool {
return p.A == c.A
mod, _ := modfile.Parse("go.mod", data, nil)
children := lo.Filter(artifact.CurProject().Dependencies(), func(p *modfile.Require, _ int) bool {
return p.Indirect && lo.ContainsBy(mod.Require, func(c *modfile.Require) bool {
return !c.Indirect && p.Mod.Path == c.Mod.Path
})
})
for _, l := range inter {
child.AddNode(fmt.Sprintf("%s@%s", l.A, l.B))
for _, c := range children {
direct.AddNode(c.Mod.String())
}
}
}
return tree, err
return tree, nil
}

// depCmd represents the dep command
Expand Down
20 changes: 3 additions & 17 deletions cmd/gbc/command/deps_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,33 +6,19 @@ import (
"github.com/samber/lo"
"github.com/stretchr/testify/assert"
"github.com/xlab/treeprint"
"golang.org/x/mod/modfile"
"os"
"path/filepath"
"strings"
"testing"
)

func TestParseMod(t *testing.T) {
os.Chdir(artifact.CurProject().Root())
mod, _ := os.Open(filepath.Join(artifact.CurProject().Root(), "go.mod"))
m, _, deps, err := parseMod(mod)
assert.NoError(t, err)
assert.Equal(t, m, "github.com/kcmvp/gob")
assert.Equal(t, 15, len(lo.Filter(deps, func(item *lo.Tuple4[string, string, string, int], _ int) bool {
return item.D == 1
})))
assert.Equal(t, 48, len(deps))
}

func TestDependency(t *testing.T) {
os.Chdir(artifact.CurProject().Root())
mod, _ := os.Open(filepath.Join(artifact.CurProject().Root(), "go.mod"))
_, _, deps, _ := parseMod(mod)
tree, err := dependencyTree()
assert.NoError(t, err)
tree.VisitAll(func(item *treeprint.Node) {
contains := lo.ContainsBy(deps, func(dep *lo.Tuple4[string, string, string, int]) bool {
return strings.Contains(fmt.Sprintf("%s", item.Value), fmt.Sprintf("%s", dep.A))
contains := lo.ContainsBy(artifact.CurProject().Dependencies(), func(dep *modfile.Require) bool {
return strings.Contains(fmt.Sprintf("%s", item.Value), dep.Mod.Path)
})
assert.True(t, contains)
})
Expand Down
16 changes: 7 additions & 9 deletions cmd/gbc/command/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,21 +94,19 @@ func installPlugins(cmd *cobra.Command, args []string) error {

func installDeps(cmd *cobra.Command, args []string) error {
result, err := parseArtifacts(cmd, args, "deps")
if err != nil {
return err
}
if result.Exists() {
var cfgDeps []string
err = json.Unmarshal([]byte(result.Raw), &cfgDeps)
for _, dep := range lo.Filter(cfgDeps, func(url string, _ int) bool {
return !lo.Contains(artifact.CurProject().Dependencies(), url)
}) {
if err = artifact.CurProject().InstallDependency(dep); err != nil {
break
for _, dep := range cfgDeps {
if err := artifact.CurProject().InstallDependency(dep); err != nil {
return err
}
}
}
if err != nil {
return errors.New(color.RedString(err.Error()))
}
return err
return nil
}

// rootCmd represents the base command when called without any subcommands
Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ require (
github.com/stretchr/testify v1.9.0
github.com/tidwall/gjson v1.17.1
github.com/xlab/treeprint v1.2.0
golang.org/x/mod v0.12.0
golang.org/x/tools v0.13.0
)

require (
Expand Down
Loading

0 comments on commit 1e55d87

Please sign in to comment.