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

Deps #110

Merged
merged 2 commits into from
May 20, 2024
Merged

Deps #110

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: 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
Loading