diff --git a/cmd/build.go b/cmd/build.go index de5e2d6..98a4c0b 100644 --- a/cmd/build.go +++ b/cmd/build.go @@ -1,14 +1,13 @@ package cmd import ( - "github.com/qiniu/goc/v2/pkg/flag" - + "github.com/qiniu/goc/v2/pkg/build" "github.com/spf13/cobra" ) var buildCmd = &cobra.Command{ Use: "build", - Run: build, + Run: buildAction, DisableFlagParsing: true, // build 命令需要用原生 go 的方式处理 flags } @@ -17,8 +16,7 @@ func init() { rootCmd.AddCommand(buildCmd) } -func build(cmd *cobra.Command, args []string) { - remainedArgs := flag.BuildCmdArgsParse(cmd, args) - where, buildName := flag.GetPackagesDir(remainedArgs) - +func buildAction(cmd *cobra.Command, args []string) { + b := build.NewBuild(cmd, args) + b.Build() } diff --git a/go.mod b/go.mod index 5cc7a2f..c19a2c5 100644 --- a/go.mod +++ b/go.mod @@ -3,19 +3,14 @@ module github.com/qiniu/goc/v2 go 1.16 require ( - github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6 // indirect github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 github.com/mattn/go-colorable v0.1.8 // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d github.com/spf13/cobra v1.1.3 github.com/spf13/pflag v1.0.5 // indirect - github.com/spf13/viper v1.7.1 - github.com/ugorji/go v1.1.4 // indirect - github.com/xlab/handysort v0.0.0-20150421192137-fb3537ed64a1 // indirect - github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77 // indirect + github.com/tongjingran/copy v1.4.2 // indirect go.uber.org/multierr v1.6.0 // indirect go.uber.org/zap v1.16.0 golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57 // indirect k8s.io/kubectl v0.20.5 // indirect - vbom.ml/util v0.0.0-20160121211510-db5cfe13f5cc // indirect ) diff --git a/go.sum b/go.sum index 1996568..9d9dcb7 100644 --- a/go.sum +++ b/go.sum @@ -299,6 +299,10 @@ github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGV github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= +github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs= +github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo= +github.com/otiai10/mint v1.3.2/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= @@ -376,6 +380,8 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/tongjingran/copy v1.4.2 h1:faPaod07yG6Z+o1B52Vu1KTvRb8il5VDNKLprC1BmsE= +github.com/tongjingran/copy v1.4.2/go.mod h1:Njma1OR5OuzB8pLAmQSzonHXzba+DDiPVmMSonpSpy4= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xlab/handysort v0.0.0-20150421192137-fb3537ed64a1/go.mod h1:QcJo0QPSfTONNIgpN5RA8prR7fF8nkF6cTWTcNerRO8= diff --git a/pkg/build/build.go b/pkg/build/build.go new file mode 100644 index 0000000..70058d5 --- /dev/null +++ b/pkg/build/build.go @@ -0,0 +1,36 @@ +package build + +import ( + "github.com/qiniu/goc/v2/pkg/flag" + "github.com/qiniu/goc/v2/pkg/log" + "github.com/spf13/cobra" +) + +// Build struct a build +// most configurations are stored in global variables: config.GocConfig & config.GoConfig +type Build struct { +} + +// NewBuild creates a Build struct +// +// consumes args, get package dirs, read project meta info. +func NewBuild(cmd *cobra.Command, args []string) *Build { + b := &Build{} + remainedArgs := flag.BuildCmdArgsParse(cmd, args) + flag.GetPackagesDir(remainedArgs) + b.readProjectMetaInfo() + b.displayProjectMetaInfo() + + return b +} + +// Build starts go build +// +// 1. copy project to temp, +// 2. inject cover variables and functions into the project, +// 3. build the project in temp. +func (b *Build) Build() { + b.copyProjectToTmp() + defer b.clean() + log.Donef("project copied to temporary directory") +} diff --git a/pkg/build/goenv.go b/pkg/build/goenv.go new file mode 100644 index 0000000..ed9a679 --- /dev/null +++ b/pkg/build/goenv.go @@ -0,0 +1,68 @@ +package build + +import ( + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/qiniu/goc/v2/pkg/config" + "github.com/qiniu/goc/v2/pkg/cover" + "github.com/qiniu/goc/v2/pkg/log" +) + +// readProjectMetaInfo reads all meta informations of the corresponding project +func (b *Build) readProjectMetaInfo() { + // get gopath & gobin + config.GocConfig.GOPATH = b.readGOPATH() + config.GocConfig.GOBIN = b.readGOBIN() + // 获取当前目录及其依赖的 package list + config.GocConfig.Pkgs = cover.ListPackages(config.GocConfig.CurPkgDir) + + // get mod info + pkgs := config.GocConfig.Pkgs + for _, pkg := range pkgs { + // check if go modules is enabled + if pkg.Module == nil { + log.Fatalf("Go module is not enabled, please set GO111MODULE=auto or on") + } + // 工程根目录 + config.GocConfig.CurModProjectDir = pkg.Root + + break + } + + // get tmp folder name + config.GocConfig.TmpModProjectDir = filepath.Join(os.TempDir(), tmpFolderName(config.GocConfig.CurModProjectDir)) + // get cur pkg dir in the corresponding tmp dir + config.GocConfig.TmpPkgDir = filepath.Join(config.GocConfig.TmpModProjectDir, config.GocConfig.CurPkgDir[len(config.GocConfig.CurModProjectDir):]) + log.Donef("project meta information parsed") +} + +// displayProjectMetaInfo prints basic infomation of this project to stdout +func (b *Build) displayProjectMetaInfo() { + log.Infof("Project Infomation") + log.Infof("GOPATH: %v", config.GocConfig.GOPATH) + log.Infof("GOBIN: %v", config.GocConfig.GOBIN) + log.Infof("Project Directory: %v", config.GocConfig.CurModProjectDir) + log.Infof("Temporary Project Directory: %v", config.GocConfig.TmpModProjectDir) + log.Infof("") +} + +// readGOPATH reads GOPATH use go env GOPATH command +func (b *Build) readGOPATH() string { + out, err := exec.Command("go", "env", "GOPATH").Output() + if err != nil { + log.Fatalf("fail to read GOPATH: %v", err) + } + return strings.TrimSpace(string(out)) +} + +// readGOBIN reads GOBIN use go env GOBIN command +func (b *Build) readGOBIN() string { + out, err := exec.Command("go", "env", "GOBIN").Output() + if err != nil { + log.Fatalf("fail to read GOBIN: %v", err) + } + return strings.TrimSpace(string(out)) +} diff --git a/pkg/build/tmpfolder.go b/pkg/build/tmpfolder.go new file mode 100644 index 0000000..966e2f9 --- /dev/null +++ b/pkg/build/tmpfolder.go @@ -0,0 +1,75 @@ +package build + +import ( + "crypto/sha256" + "fmt" + "os" + "strings" + + "github.com/qiniu/goc/v2/pkg/config" + "github.com/qiniu/goc/v2/pkg/log" + "github.com/tongjingran/copy" +) + +// copyProjectToTmp copies project files to the temporary directory +// +// It will ignore .git and irregular files, only copy source(text) files +func (b *Build) copyProjectToTmp() { + curProject := config.GocConfig.CurModProjectDir + tmpProject := config.GocConfig.TmpModProjectDir + + if _, err := os.Stat(tmpProject); !os.IsNotExist(err) { + log.Infof("find previous temporary directory, delete") + err := os.RemoveAll(tmpProject) + if err != nil { + log.Fatalf("fail to remove preivous temporary directory: %v", err) + } + } + + log.StartWait("coping project") + err := os.MkdirAll(tmpProject, os.ModePerm) + if err != nil { + log.Fatalf("fail to create temporary directory: %v", err) + } + + // copy + if err := copy.Copy(curProject, tmpProject, copy.Options{Skip: skipCopy}); err != nil { + log.Fatalf("fail to copy the folder from %v to %v, the err: %v", curProject, tmpProject, err) + } + + log.StopWait() +} + +// tmpFolderName generates a directory name according to the path +func tmpFolderName(path string) string { + sum := sha256.Sum256([]byte(path)) + h := fmt.Sprintf("%x", sum[:6]) + + return "goc-build-" + h +} + +// skipCopy skip copy .git dir and irregular files +func skipCopy(src string, info os.FileInfo) (bool, error) { + irregularModeType := os.ModeNamedPipe | os.ModeSocket | os.ModeDevice | os.ModeCharDevice | os.ModeIrregular + if strings.HasSuffix(src, "/.git") { + log.Debugf("skip .git dir [%s]", src) + return true, nil + } + if info.Mode()&irregularModeType != 0 { + log.Debugf("skip file [%s], the file mode is [%s]", src, info.Mode().String()) + return true, nil + } + return false, nil +} + +// clean clears the temporary project +func (b *Build) clean() { + if config.GocConfig.Debug != true { + if err := os.RemoveAll(config.GocConfig.TmpModProjectDir); err != nil { + log.Fatalf("fail to delete the temporary project: %v", config.GocConfig.TmpModProjectDir) + } + log.Donef("delete the temporary project") + } else { + log.Debugf("--debug is enabled, keep the temporary project") + } +} diff --git a/pkg/config/config.go b/pkg/config/config.go index b065550..631ecf0 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -1,5 +1,7 @@ package config +import "time" + type gocConfig struct { Debug bool CurPkgDir string @@ -7,8 +9,13 @@ type gocConfig struct { TmpModProjectDir string TmpPkgDir string BinaryName string + Pkgs map[string]*Package + GOPATH string + GOBIN string + IsMod bool // deprecated } +// GocConfig 全局变量,存放 goc 的各种元属性 var GocConfig gocConfig type goConfig struct { @@ -48,3 +55,72 @@ type goConfig struct { } var GoConfig goConfig + +// PackageCover holds all the generate coverage variables of a package +type PackageCover struct { + Package *Package + Vars map[string]*FileVar +} + +// FileVar holds the name of the generated coverage variables targeting the named file. +type FileVar struct { + File string + Var string +} + +// Package map a package output by go list +// this is subset of package struct in: https://github.com/golang/go/blob/master/src/cmd/go/internal/load/pkg.go#L58 +type Package struct { + Dir string `json:"Dir"` // directory containing package sources + ImportPath string `json:"ImportPath"` // import path of package in dir + Name string `json:"Name"` // package name + Target string `json:",omitempty"` // installed target for this package (may be executable) + Root string `json:",omitempty"` // Go root, Go path dir, or module root dir containing this package + + Module *ModulePublic `json:",omitempty"` // info about package's module, if any + Goroot bool `json:"Goroot,omitempty"` // is this package in the Go root? + Standard bool `json:"Standard,omitempty"` // is this package part of the standard Go library? + DepOnly bool `json:"DepOnly,omitempty"` // package is only a dependency, not explicitly listed + + // Source files + GoFiles []string `json:"GoFiles,omitempty"` // .go source files (excluding CgoFiles, TestGoFiles, XTestGoFiles) + CgoFiles []string `json:"CgoFiles,omitempty"` // .go source files that import "C" + + // Dependency information + Deps []string `json:"Deps,omitempty"` // all (recursively) imported dependencies + Imports []string `json:",omitempty"` // import paths used by this package + ImportMap map[string]string `json:",omitempty"` // map from source import to ImportPath (identity entries omitted) + + // Error information + Incomplete bool `json:"Incomplete,omitempty"` // this package or a dependency has an error + Error *PackageError `json:"Error,omitempty"` // error loading package + DepsErrors []*PackageError `json:"DepsErrors,omitempty"` // errors loading dependencies +} + +// ModulePublic represents the package info of a module +type ModulePublic struct { + Path string `json:",omitempty"` // module path + Version string `json:",omitempty"` // module version + Versions []string `json:",omitempty"` // available module versions + Replace *ModulePublic `json:",omitempty"` // replaced by this module + Time *time.Time `json:",omitempty"` // time version was created + Update *ModulePublic `json:",omitempty"` // available update (with -u) + Main bool `json:",omitempty"` // is this the main module? + Indirect bool `json:",omitempty"` // module is only indirectly needed by main module + Dir string `json:",omitempty"` // directory holding local copy of files, if any + GoMod string `json:",omitempty"` // path to go.mod file describing module, if any + GoVersion string `json:",omitempty"` // go version used in module + Error *ModuleError `json:",omitempty"` // error loading module +} + +// ModuleError represents the error loading module +type ModuleError struct { + Err string // error text +} + +// PackageError is the error info for a package when list failed +type PackageError struct { + ImportStack []string // shortest path from package named on command line to this one + Pos string // position of error (if present, file:line:col) + Err string // the error itself +} diff --git a/pkg/cover/list.go b/pkg/cover/list.go new file mode 100644 index 0000000..3b6b8ce --- /dev/null +++ b/pkg/cover/list.go @@ -0,0 +1,56 @@ +package cover + +import ( + "bytes" + "encoding/json" + "errors" + "io" + "os/exec" + + "github.com/qiniu/goc/v2/pkg/config" + "github.com/qiniu/goc/v2/pkg/log" +) + +var ( + // ErrCoverPkgFailed represents the error that fails to inject the package + ErrCoverPkgFailed = errors.New("fail to inject code to project") + // ErrCoverListFailed represents the error that fails to list package dependencies + ErrCoverListFailed = errors.New("fail to list package dependencies") +) + +// ListPackages list all packages under specific via go list command. +func ListPackages(dir string) map[string]*config.Package { + cmd := exec.Command("go", "list", "-json", "./...") + cmd.Dir = dir + + var errBuf bytes.Buffer + cmd.Stderr = &errBuf + out, err := cmd.Output() + if err != nil { + log.Fatalf("execute go list -json failed, err: %v, stdout: %v, stderr: %v", err, string(out), errBuf.String()) + } + // 有些时候 go 命令会打印一些信息到 stderr,但其实命令整体是成功运行了 + if errBuf.String() != "" { + log.Errorf("%v", errBuf.String()) + } + + dec := json.NewDecoder(bytes.NewBuffer(out)) + pkgs := make(map[string]*config.Package, 0) + + for { + var pkg config.Package + if err := dec.Decode(&pkg); err != nil { + if err == io.EOF { + break + } + log.Fatalf("reading go list output error: %v", err) + } + if pkg.Error != nil { + log.Fatalf("list package %s failed with output: %v", pkg.ImportPath, pkg.Error) + } + + pkgs[pkg.ImportPath] = &pkg + } + + return pkgs +} diff --git a/pkg/flag/build_flags.go b/pkg/flag/build_flags.go index 86abf6f..2d67896 100644 --- a/pkg/flag/build_flags.go +++ b/pkg/flag/build_flags.go @@ -16,7 +16,8 @@ However, other flags' order are same with the go official command. ` // BuildCmdArgsParse parse both go flags and goc flags, it returns all non-flag arguments. -// It will log fatal if error +// +// 吞下 [packages] 之前所有的 flags. func BuildCmdArgsParse(cmd *cobra.Command, args []string) []string { // 首先解析 cobra 定义的 flag allFlagSets := cmd.Flags() diff --git a/pkg/flag/packages.go b/pkg/flag/packages.go index 8e0b493..16ab904 100644 --- a/pkg/flag/packages.go +++ b/pkg/flag/packages.go @@ -6,17 +6,19 @@ import ( "os" "path/filepath" "strings" + + "github.com/qiniu/goc/v2/pkg/config" ) // GetPackagesDir parse [pacakges] part of args, it will fatal if error encountered // -// Return 1: [packages] 所在的目录位置,供后续插桩使用。 +// 函数获取 1: [packages] 所在的目录位置,供后续插桩使用。 // -// Return 2: 如果参数是 *.go,第一个 .go 文件的文件名。go build 中,二进制名字既可能是目录名也可能是文件名,和参数类型有关。 +// 函数获取 2: 如果参数是 *.go,第一个 .go 文件的文件名。go build 中,二进制名字既可能是目录名也可能是文件名,和参数类型有关。 // // 如果 [packages] 非法(即不符合 go 原生的定义),则返回对应错误 // 这里只考虑 go mod 的方式 -func GetPackagesDir(patterns []string) (string, string) { +func GetPackagesDir(patterns []string) { for _, p := range patterns { // patterns 只支持两种格式 // 1. 要么是直接指向某些 .go 文件的相对/绝对路径 @@ -32,7 +34,12 @@ func GetPackagesDir(patterns []string) (string, string) { if err != nil { log.Fatalf("%v", err) } - return filepath.Dir(absp), filepath.Base(absp) + + // 获取当前 [packages] 所在的目录位置,供后续插桩使用。 + config.GocConfig.CurPkgDir = filepath.Dir(absp) + // 获取二进制名字 + config.GocConfig.BinaryName = filepath.Base(absp) + return } } } @@ -43,7 +50,9 @@ func GetPackagesDir(patterns []string) (string, string) { log.Fatalf("%v", err) } - return coverWd, "" + config.GocConfig.CurPkgDir = coverWd + config.GocConfig.BinaryName = "" + return } // goFilesPackage 对一组 go 文件解析,判断是否合法