From 27c4cf69d02c5eff5355588849070e6e6514165c Mon Sep 17 00:00:00 2001 From: xhd2015 Date: Tue, 22 Oct 2024 14:30:03 +0800 Subject: [PATCH] add script/vendir --- .gitignore | 5 +- cmd/xgo/version.go | 4 +- script/vendir/README.md | 30 ++++ script/vendir/example/README.md | 20 +++ script/vendir/example/src/go.mod | 15 ++ script/vendir/example/src/go.sum | 8 + script/vendir/example/src/manifest.go | 6 + script/vendir/example/test/main.go | 23 +++ script/vendir/main.go | 242 ++++++++++++++++++++++++++ script/vendir/rewrite.go | 121 +++++++++++++ support/edit/LICENSE | 27 +++ support/edit/edit.go | 99 +++++++++++ support/edit/goedit/goedit.go | 42 +++++ support/filecopy/copy.go | 37 +++- support/goinfo/find.go | 3 + support/goinfo/mod.go | 34 +++- 16 files changed, 700 insertions(+), 16 deletions(-) create mode 100644 script/vendir/README.md create mode 100644 script/vendir/example/README.md create mode 100644 script/vendir/example/src/go.mod create mode 100644 script/vendir/example/src/go.sum create mode 100644 script/vendir/example/src/manifest.go create mode 100644 script/vendir/example/test/main.go create mode 100644 script/vendir/main.go create mode 100644 script/vendir/rewrite.go create mode 100644 support/edit/LICENSE create mode 100644 support/edit/edit.go create mode 100644 support/edit/goedit/goedit.go diff --git a/.gitignore b/.gitignore index 9e2fee3e..21d12f48 100644 --- a/.gitignore +++ b/.gitignore @@ -31,4 +31,7 @@ cover*.out /tmp -debug-compile.json \ No newline at end of file +debug-compile.json + +/script/vendir/example/src/vendor +/script/vendir/example/internal/third_party_vendir \ No newline at end of file diff --git a/cmd/xgo/version.go b/cmd/xgo/version.go index 1b3f09da..d37dbf78 100644 --- a/cmd/xgo/version.go +++ b/cmd/xgo/version.go @@ -6,8 +6,8 @@ import "fmt" // VERSION is manully updated when needed a new tag // see also runtime/core/version.go const VERSION = "1.0.49" -const REVISION = "bfe5f14d7bfaf52b5dfecb95aaaa717e0ff4d3c9+1" -const NUMBER = 310 +const REVISION = "1d8b33c2ca5fafec9439a1ec7adcdc591db705b9+1" +const NUMBER = 311 // the matching runtime/core's version // manually updated diff --git a/script/vendir/README.md b/script/vendir/README.md new file mode 100644 index 00000000..1417927f --- /dev/null +++ b/script/vendir/README.md @@ -0,0 +1,30 @@ +# Vendir +vendir is an utility helps to introduce third party vendor libraries without introducing dependencies in the go.mod. + +This way, a library itself can separate its exported APIs from its internal dependencies. + +# Usage +```sh +go run github.com/xhd2015/xgo/script/vendir@latest create ./internal/third/src ./internal/third/vendir +``` +This command will copy all dependencies in `./internal/third/src/vendor` to `./internal/third/vendir`, and rewrite all imports to that created internal package. + +Prior to running this command, you should create a go.mod, add dependency and run `go mod vendor` inside `./internal/third/src`. + +# How it works? +In general, the source directory should contain a go.mod and a vendor directory describing all it's dependencies. + +The target directory then will be created by copying all dependencies from source vendor directory and rewrite all import paths(except stdlib) to internal paths. + +This results in all third party code self-included, so go.mod does not change at all. + +Check [./example](./example) for demonstration. + +# Why? +The xgo project itself provides a range of utilities, among which the incremental coverage tool depends on heavily a lot of external APIs. + +But we don't want to expose these dependencies to the end user. Initially we took an approach that compiles the incremental coverage tool standalone and download it when required. But that has obvious shortcomings, such as bad maintenance and underperformed user experience. + +We make the step further, why binray dependency? Isn't there a way to statically compile all code together? + +So we came up with this vendir utility. \ No newline at end of file diff --git a/script/vendir/example/README.md b/script/vendir/example/README.md new file mode 100644 index 00000000..c6962ec0 --- /dev/null +++ b/script/vendir/example/README.md @@ -0,0 +1,20 @@ +# Verify vendir create +```sh +# cd to current directory if not yet +cd ./script/vendir/example + +# update dependencies +# you may need go1.22 +(cd src && go mod vendor) + +# remove target vendor +rm -rf ./internal/third_party_vendir + +# update vendor +go run github.com/xhd2015/xgo/script/vendir create ./src ./internal/third_party_vendir + +# check result +go run ./test +# output: +# github.com/xhd2015/xgo/script/vendir/example/test +``` \ No newline at end of file diff --git a/script/vendir/example/src/go.mod b/script/vendir/example/src/go.mod new file mode 100644 index 00000000..c8ee1fbd --- /dev/null +++ b/script/vendir/example/src/go.mod @@ -0,0 +1,15 @@ +module github.com/xhd2015/xgo/script/vendir/testdata/src + +go 1.22.0 + +toolchain go1.22.2 + +require ( + github.com/xhd2015/less-gen v0.0.2 + golang.org/x/tools v0.25.0 +) + +require ( + golang.org/x/mod v0.21.0 // indirect + golang.org/x/sync v0.8.0 // indirect +) diff --git a/script/vendir/example/src/go.sum b/script/vendir/example/src/go.sum new file mode 100644 index 00000000..ea49e0a1 --- /dev/null +++ b/script/vendir/example/src/go.sum @@ -0,0 +1,8 @@ +github.com/xhd2015/less-gen v0.0.2 h1:4AqksVjQoC12oqm7OPtH0zaPPwoYCPfcZgBrJFJujWE= +github.com/xhd2015/less-gen v0.0.2/go.mod h1:14sYoXpQmBGJPM5sSoxOXocW1b7WeNmCfqOSBqHI4Js= +golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= +golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE= +golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg= diff --git a/script/vendir/example/src/manifest.go b/script/vendir/example/src/manifest.go new file mode 100644 index 00000000..5763f5ef --- /dev/null +++ b/script/vendir/example/src/manifest.go @@ -0,0 +1,6 @@ +package manifest + +import ( + _ "github.com/xhd2015/less-gen/go/project" // has ref + _ "golang.org/x/tools/cover" // no ref +) diff --git a/script/vendir/example/test/main.go b/script/vendir/example/test/main.go new file mode 100644 index 00000000..95b0e074 --- /dev/null +++ b/script/vendir/example/test/main.go @@ -0,0 +1,23 @@ +package main + +import ( + "fmt" + "os" + + "github.com/xhd2015/xgo/script/vendir/example/internal/third_party_vendir/github.com/xhd2015/less-gen/go/load" + "github.com/xhd2015/xgo/script/vendir/example/internal/third_party_vendir/github.com/xhd2015/less-gen/go/project" +) + +func main() { + project, err := project.Load([]string{"./test"}, &load.LoadOptions{}) + if err != nil { + fmt.Fprintf(os.Stderr, "%v\n", err) + os.Exit(1) + } + pkg, err := project.GetOnlyEntryPackage() + if err != nil { + fmt.Fprintf(os.Stderr, "%v\n", err) + os.Exit(1) + } + fmt.Println(pkg.GoPkg().PkgPath) +} diff --git a/script/vendir/main.go b/script/vendir/main.go new file mode 100644 index 00000000..b191f355 --- /dev/null +++ b/script/vendir/main.go @@ -0,0 +1,242 @@ +package main + +import ( + "fmt" + "io/fs" + "os" + "path/filepath" + "strings" + + "github.com/xhd2015/xgo/support/filecopy" + "github.com/xhd2015/xgo/support/goinfo" +) + +const help = ` +vendir helps to create third party vendor dependency without +introducing changes to go.mod. + +Usage: + vendir create [OPTIONS] + vendir rewrite-file [OPTIONS] + vendir rewrite-path + vendir help + +Arguments: + should be either: + - the root dir containing a go.mod and a vendor dir, or + - the vendor directory + +Options: + --help show help message + +Example: + $ go run github.com/xhd2015/xgo/script/vendir create ./x/src ./x/third_party_vendir +` + +// usage: +// +// go run ./script/vendir create ./script/vendir/testdata/src ./script/vendir/testdata/third_party_vendir +func main() { + err := handle(os.Args[1:]) + if err != nil { + fmt.Fprintf(os.Stderr, "%v\n", err) + os.Exit(1) + } +} + +func handle(args []string) error { + if len(args) == 0 { + return fmt.Errorf("requires cmd") + } + cmd := args[0] + args = args[1:] + switch cmd { + case "create": + return createVendor(args) + case "rewrite-file": + return rewriteFile(args) + case "rewrite-path": + return rewritePath(args) + case "help": + fmt.Println(strings.TrimSpace(help)) + return nil + default: + return fmt.Errorf("unrecognized cmd: %s, check help", cmd) + } +} + +func createVendor(args []string) error { + var remainArgs []string + var verbose bool + n := len(args) + for i := 0; i < n; i++ { + if args[i] == "--rm" { + if i+1 >= n { + return fmt.Errorf("%v requires arg", args[i]) + } + i++ + continue + } + if args[i] == "-v" || args[i] == "--verbose" { + verbose = true + continue + } + if args[i] == "--help" { + fmt.Println(strings.TrimSpace(help)) + return nil + } + if args[i] == "--" { + remainArgs = append(remainArgs, args[i+1:]...) + break + } + if strings.HasPrefix(args[i], "-") { + return fmt.Errorf("unrecognized flag: %v", args[i]) + } + remainArgs = append(remainArgs, args[i]) + } + if len(remainArgs) < 2 { + return fmt.Errorf("usage: vendir create ") + } + + dir := remainArgs[0] + targetVendorDir := remainArgs[1] + + _, targetStatErr := os.Stat(targetVendorDir) + if !os.IsNotExist(targetStatErr) { + return fmt.Errorf("%s already exists, remove it before create", targetVendorDir) + } + goMod := filepath.Join(dir, "go.mod") + vendorDir := filepath.Join(dir, "vendor") + _, err := os.Stat(goMod) + if err != nil { + return err + } + _, err = os.Stat(vendorDir) + if err != nil { + return err + } + + err = os.MkdirAll(targetVendorDir, 0755) + if err != nil { + return err + } + + modPath, err := goinfo.ResolveModPath(targetVendorDir) + if err != nil { + return err + } + if verbose { + fmt.Fprintf(os.Stderr, "rewrite prefix: %s\n", modPath) + } + rw, err := initRewriter(modPath + "/") + if err != nil { + return err + } + + err = filecopy.Copy(vendorDir, targetVendorDir) + if err != nil { + return err + } + // traverse all go files, and rewrite + return filepath.Walk(targetVendorDir, func(path string, info fs.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() { + return nil + } + if !strings.HasSuffix(path, ".go") { + return nil + } + newCode, err := rw.rewriteFile(path) + if err != nil { + return fmt.Errorf("%s: %w", path, err) + } + return os.WriteFile(path, []byte(newCode), info.Mode()) + }) +} + +func rewriteFile(args []string) error { + var some string + + var remainArgs []string + n := len(args) + for i := 0; i < n; i++ { + if args[i] == "--some" { + if i+1 >= n { + return fmt.Errorf("%v requires arg", args[i]) + } + some = args[i+1] + i++ + continue + } + if args[i] == "--help" { + fmt.Println(strings.TrimSpace(help)) + return nil + } + if args[i] == "--" { + remainArgs = append(remainArgs, args[i+1:]...) + break + } + if strings.HasPrefix(args[i], "-") { + return fmt.Errorf("unrecognized flag: %v", args[i]) + } + remainArgs = append(remainArgs, args[i]) + } + if len(remainArgs) < 2 { + return fmt.Errorf("usage: vendir ") + } + + file := remainArgs[0] + targetDir := remainArgs[1] + + stat, err := os.Stat(file) + if err != nil { + return err + } + + if stat.IsDir() { + return fmt.Errorf("%s is not a file", file) + } + + modPath, err := goinfo.ResolveModPath(targetDir) + if err != nil { + return err + } + rw, err := initRewriter(modPath + "/") + if err != nil { + return err + } + // rewrite file + code, err := rw.rewriteFile(file) + if err != nil { + return err + } + fmt.Println(code) + + _ = some + + return nil +} + +func rewritePath(args []string) error { + remainArgs := args + if len(remainArgs) < 2 { + return fmt.Errorf("usage: vendir rewrite-path ") + } + + path := remainArgs[0] + targetDir := remainArgs[1] + + modPath, err := goinfo.ResolveModPath(targetDir) + if err != nil { + return err + } + rw, err := initRewriter(modPath + "/") + if err != nil { + return err + } + + fmt.Println(rw.rewritePath(path)) + return nil +} diff --git a/script/vendir/rewrite.go b/script/vendir/rewrite.go new file mode 100644 index 00000000..e2ece407 --- /dev/null +++ b/script/vendir/rewrite.go @@ -0,0 +1,121 @@ +package main + +import ( + "fmt" + "go/parser" + "go/token" + "os" + "path/filepath" + "strconv" + "strings" + + "github.com/xhd2015/xgo/support/cmd" + "github.com/xhd2015/xgo/support/edit/goedit" +) + +type rewriter struct { + stdPkgMapping map[string]bool + prefix string +} + +func initRewriter(prefix string) (*rewriter, error) { + if prefix == "" { + return nil, fmt.Errorf("requires prefix") + } + goroot, err := getGoroot() + if err != nil { + return nil, err + } + stdPkgs, err := listStdPkgs(goroot) + if err != nil { + return nil, err + } + stdPkgMapping := make(map[string]bool, len(stdPkgs)) + for _, pkg := range stdPkgs { + stdPkgMapping[pkg] = true + } + + return &rewriter{ + stdPkgMapping: stdPkgMapping, + prefix: prefix, + }, nil +} + +func (c *rewriter) rewritePath(path string) string { + if path == "" { + return "" + } + // relative import + switch path[0] { + case '/', '.': + return path + } + if c.stdPkgMapping[path] { + return path + } + return c.prefix + path +} + +func (c *rewriter) rewriteFile(file string) (string, error) { + code, err := os.ReadFile(file) + if err != nil { + return "", err + } + return c.rewriteCode(string(code)) +} + +func (c *rewriter) rewriteCode(code string) (string, error) { + fset := token.NewFileSet() + file, err := parser.ParseFile(fset, "", code, parser.ImportsOnly) + if err != nil { + return "", err + } + + edit := goedit.New(fset, code) + for _, imp := range file.Imports { + pkg, err := strconv.Unquote(imp.Path.Value) + if err != nil { + return "", err + } + newPkg := c.rewritePath(pkg) + if newPkg != pkg { + edit.Replace(imp.Path.Pos(), imp.Path.End(), strconv.Quote(newPkg)) + } + } + return edit.String(), nil +} + +// func rewritePath(goroot string, path string) (string, error) { +// } + +func getGoroot() (string, error) { + goroot, err := cmd.Output("go", "env", "GOROOT") + if err != nil { + return "", err + } + goroot = strings.TrimSpace(goroot) + if goroot == "" { + return "", fmt.Errorf("cannot get 'go env GOROOT'") + } + return goroot, nil +} + +func listStdPkgs(goroot string) ([]string, error) { + if goroot == "" { + return nil, fmt.Errorf("requires GOROOT") + } + res, err := cmd.Dir(filepath.Join(goroot, "src")).Output("go", "list", "./...") + if err != nil { + return nil, err + } + list := strings.Split(res, "\n") + j := 0 + for i := 0; i < len(list); i++ { + e := strings.TrimSpace(list[i]) + if e != "" { + list[j] = e + j++ + } + } + return list[:j], nil +} diff --git a/support/edit/LICENSE b/support/edit/LICENSE new file mode 100644 index 00000000..54ce0710 --- /dev/null +++ b/support/edit/LICENSE @@ -0,0 +1,27 @@ +Copyright 2009 The Go Authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google LLC nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/support/edit/edit.go b/support/edit/edit.go new file mode 100644 index 00000000..7810e2e0 --- /dev/null +++ b/support/edit/edit.go @@ -0,0 +1,99 @@ +// Copyright 2017 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package edit implements buffered position-based editing of byte slices. +package edit + +import ( + "fmt" + "sort" +) + +// A Buffer is a queue of edits to apply to a given byte slice. +type Buffer struct { + old []byte + q edits +} + +// An edit records a single text modification: change the bytes in [start,end) to new. +type edit struct { + start int + end int + new string +} + +// An edits is a list of edits that is sortable by start offset, breaking ties by end offset. +type edits []edit + +func (x edits) Len() int { return len(x) } +func (x edits) Swap(i, j int) { x[i], x[j] = x[j], x[i] } +func (x edits) Less(i, j int) bool { + if x[i].start != x[j].start { + return x[i].start < x[j].start + } + return x[i].end < x[j].end +} + +// NewBuffer returns a new buffer to accumulate changes to an initial data slice. +// The returned buffer maintains a reference to the data, so the caller must ensure +// the data is not modified until after the Buffer is done being used. +func NewBuffer(data []byte) *Buffer { + return &Buffer{old: data} +} + +func (b *Buffer) Insert(pos int, new string) { + if pos < 0 || pos > len(b.old) { + panic("invalid edit position") + } + b.q = append(b.q, edit{pos, pos, new}) +} + +func (b *Buffer) Delete(start, end int) { + if end == -1 { + end = len(b.old) + } + if end < start || start < 0 || end > len(b.old) { + panic("invalid edit position") + } + b.q = append(b.q, edit{start, end, ""}) +} + +func (b *Buffer) Replace(start, end int, new string) { + if end == -1 { + end = len(b.old) + } + if end < start || start < 0 || end > len(b.old) { + panic("invalid edit position") + } + b.q = append(b.q, edit{start, end, new}) +} + +// Bytes returns a new byte slice containing the original data +// with the queued edits applied. +func (b *Buffer) Bytes() []byte { + // Sort edits by starting position and then by ending position. + // Breaking ties by ending position allows insertions at point x + // to be applied before a replacement of the text at [x, y). + sort.Stable(b.q) + + var new []byte + offset := 0 + for i, e := range b.q { + if e.start < offset { + e0 := b.q[i-1] + panic(fmt.Sprintf("overlapping edits: [%d,%d)->%q, [%d,%d)->%q", e0.start, e0.end, e0.new, e.start, e.end, e.new)) + } + new = append(new, b.old[offset:e.start]...) + offset = e.end + new = append(new, e.new...) + } + new = append(new, b.old[offset:]...) + return new +} + +// String returns a string containing the original data +// with the queued edits applied. +func (b *Buffer) String() string { + return string(b.Bytes()) +} diff --git a/support/edit/goedit/goedit.go b/support/edit/goedit/goedit.go new file mode 100644 index 00000000..75919369 --- /dev/null +++ b/support/edit/goedit/goedit.go @@ -0,0 +1,42 @@ +package goedit + +import ( + "go/token" + + "github.com/xhd2015/xgo/support/edit" +) + +type Edit struct { + buf *edit.Buffer + fset *token.FileSet +} + +func New(fset *token.FileSet, content string) *Edit { + return &Edit{ + fset: fset, + buf: edit.NewBuffer([]byte(content)), + } +} + +func (c *Edit) Delete(start token.Pos, end token.Pos) { + c.buf.Delete(c.offsetOf(start), c.offsetOf(end)) +} + +func (c *Edit) Insert(start token.Pos, content string) { + c.buf.Insert(c.offsetOf(start), content) +} + +func (c *Edit) Replace(start token.Pos, end token.Pos, content string) { + c.buf.Replace(c.offsetOf(start), c.offsetOf(end), content) +} + +func (c *Edit) String() string { + return c.buf.String() +} + +func (c *Edit) offsetOf(pos token.Pos) int { + if pos == token.NoPos { + return -1 + } + return c.fset.Position(pos).Offset +} diff --git a/support/filecopy/copy.go b/support/filecopy/copy.go index bda9f58b..84f12d51 100644 --- a/support/filecopy/copy.go +++ b/support/filecopy/copy.go @@ -18,6 +18,8 @@ type Options struct { ignoreSubPaths []string ignoreSuffix []string includeSuffix []string + + noRm bool } func NewOptions() *Options { @@ -48,23 +50,42 @@ func (c *Options) IncludeSuffix(suffix ...string) *Options { return c } +func (c *Options) Copy(srcDir string, targetDir string) error { + var cloneOpts *Options + if c == nil { + cloneOpts = &Options{noRm: true} + } else { + clone := *c + cloneOpts = &clone + cloneOpts.noRm = true + } + return copyReplaceDir(srcDir, targetDir, cloneOpts) +} + func (c *Options) CopyReplaceDir(srcDir string, targetDir string) error { return copyReplaceDir(srcDir, targetDir, c) } +func Copy(srcDir string, targetDir string) error { + return copyReplaceDir(srcDir, targetDir, &Options{ + noRm: true, + }) +} + // Replace the target dir with files copy func CopyReplaceDir(srcDir string, targetDir string, useLink bool) error { return copyReplaceDir(srcDir, targetDir, &Options{ useLink: useLink, }) } + func copyReplaceDir(srcDir string, targetDir string, opts *Options) error { - if opts == nil { - opts = &Options{} - } if srcDir == "" { return fmt.Errorf("requires srcDir") } + if opts == nil { + opts = &Options{} + } var filterSubPath func(subPath string, isDir bool) bool if opts != nil && (len(opts.includeSuffix) > 0 || len(opts.ignoreSubPaths) > 0 || len(opts.ignoreSuffix) > 0) { @@ -116,9 +137,12 @@ func copyReplaceDir(srcDir string, targetDir string, opts *Options) error { if parent == targetAbsDir { return fmt.Errorf("unable to override %v", targetDir) } - err = os.RemoveAll(targetAbsDir) - if err != nil { - return err + if !opts.noRm { + // remove target + err = os.RemoveAll(targetAbsDir) + if err != nil { + return err + } } err = os.MkdirAll(filepath.Dir(targetAbsDir), 0755) @@ -281,6 +305,7 @@ func CopyFile(src string, dst string) error { return copyFile(src, dst, false, nil) } +// CopyFileAll copy src to dst, creates dir if necessary func CopyFileAll(src string, dst string) error { return copyFile(src, dst, true, nil) } diff --git a/support/goinfo/find.go b/support/goinfo/find.go index c05108f1..ada35238 100644 --- a/support/goinfo/find.go +++ b/support/goinfo/find.go @@ -15,6 +15,9 @@ func FindGoModDir(dir string) (string, error) { return dir, err } +// FindGoModDirSubPath find go.mod by traversing dir bottom up, +// upon finding a go.mod file, it returns the corresponding +// sub path and root dir func FindGoModDirSubPath(dir string) ([]string, string, error) { absDir, err := filepath.Abs(dir) if err != nil { diff --git a/support/goinfo/mod.go b/support/goinfo/mod.go index 0346bc6e..e5972e77 100644 --- a/support/goinfo/mod.go +++ b/support/goinfo/mod.go @@ -30,6 +30,17 @@ func ResolveMainModule(dir string) (subPaths []string, mainModule string, err er return subPaths, modPath, nil } +func ResolveModPath(dir string) (string, error) { + subPaths, mainModule, err := ResolveMainModule(dir) + if err != nil { + return "", err + } + if len(subPaths) == 0 { + return mainModule, nil + } + return mainModule + "/" + strings.Join(subPaths, "/"), nil +} + func isRelative(arg string) bool { if arg == "" { // pwd @@ -60,20 +71,22 @@ func findGoMod(dir string) (file string, subPaths []string, err error) { return "", nil, err } iterDir := absDir + var nextIterDir string init := true for { if init { init = false } else { subPaths = append(subPaths, filepath.Base(iterDir)) - nextIterDir := filepath.Dir(iterDir) - if iterDir == string(filepath.Separator) || nextIterDir == iterDir { - // until root - // TODO: what about windows? - return "", nil, ErrGoModNotFound - } iterDir = nextIterDir } + nextIterDir = filepath.Dir(iterDir) + if iterDir == string(filepath.Separator) || nextIterDir == iterDir { + // until root + // TODO: what about windows? + return "", nil, ErrGoModNotFound + } + file := filepath.Join(iterDir, "go.mod") stat, err := os.Stat(file) if err != nil { @@ -86,8 +99,15 @@ func findGoMod(dir string) (file string, subPaths []string, err error) { continue } // a valid go.mod found - return file, subPaths, nil + return file, reverse(subPaths), nil + } +} +func reverse(list []string) []string { + n := len(list) + for i, j := 0, n-1; i < j; i, j = i+1, j-1 { + list[i], list[j] = list[j], list[i] } + return list } func parseModPath(goModContent string) string {