From 57498dd11f3ec5b059602989a203bd5ed2963e4b Mon Sep 17 00:00:00 2001 From: lyyyuna Date: Tue, 16 Jun 2020 13:21:28 +0800 Subject: [PATCH] fix #14 --- cmd/build.go | 19 +++++--- cmd/install.go | 14 ++++-- cmd/run.go | 2 +- pkg/build/build.go | 50 +++++++++++++------- pkg/build/errors.go | 14 ++++++ pkg/build/gomodules.go | 2 +- pkg/build/install.go | 32 +++++++++---- pkg/build/legacy.go | 23 +++++++-- pkg/build/tmpfolder.go | 94 +++++++++++++++++++++++-------------- pkg/build/tmpfolder_test.go | 10 ++-- 10 files changed, 180 insertions(+), 80 deletions(-) create mode 100644 pkg/build/errors.go diff --git a/cmd/build.go b/cmd/build.go index cce6cd6..533110a 100644 --- a/cmd/build.go +++ b/cmd/build.go @@ -20,6 +20,7 @@ import ( "github.com/qiniu/goc/pkg/build" "github.com/qiniu/goc/pkg/cover" "github.com/spf13/cobra" + "log" ) var buildCmd = &cobra.Command{ @@ -27,9 +28,7 @@ var buildCmd = &cobra.Command{ Short: "Do cover for all go files and execute go build command", Long: ` First of all, this build command will copy the project code and its necessary dependencies to a temporary directory, then do cover for the target in this temporary directory, finally go build command will be executed and binaries generated to their original place. - -To pass original go build flags to goc command, place them after "--", see examples below for reference. - `, +`, Example: ` # Build the current binary with cover variables injected. The binary will be generated in the current folder. goc build @@ -38,20 +37,26 @@ goc build goc build --center=http://127.0.0.1:7777 # Build the current binary with cover variables injected, and redirect output to /to/this/path. -goc build -- -o /to/this/path +goc build --output /to/this/path # Build the current binary with cover variables injected, and set necessary build flags: -ldflags "-extldflags -static" -tags="embed kodo". -goc build -- -ldflags "-extldflags -static" -tags="embed kodo" +goc build --buildflags="-ldflags '-extldflags -static' -tags='embed kodo'" `, Run: func(cmd *cobra.Command, args []string) { - gocBuild := build.NewBuild(buildFlags, packages, buildOutput) + gocBuild, err := build.NewBuild(buildFlags, packages, buildOutput) + if err != nil { + log.Fatalf("Fail to NewBuild: %v", err) + } // remove temporary directory if needed defer gocBuild.Clean() // doCover with original buildFlags, with new GOPATH( tmp:original ) // in the tmp directory cover.Execute(buildFlags, gocBuild.NewGOPATH, gocBuild.TmpDir, mode, center) // do install in the temporary directory - gocBuild.Build() + err = gocBuild.Build() + if err != nil { + log.Fatalf("Fail to build: %v", err) + } return }, } diff --git a/cmd/install.go b/cmd/install.go index 66f1f5e..080a66c 100644 --- a/cmd/install.go +++ b/cmd/install.go @@ -19,6 +19,7 @@ package cmd import ( "github.com/qiniu/goc/pkg/build" "github.com/qiniu/goc/pkg/cover" + log "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) @@ -27,8 +28,6 @@ var installCmd = &cobra.Command{ Short: "Do cover for all go files and execute go install command", Long: ` First of all, this install command will copy the project code and its necessary dependencies to a temporary directory, then do cover for the target in this temporary directory, finally go install command will be executed and binaries generated to their original place. - -To pass original go build flags to goc command, place them after "--", see examples below for reference. `, Example: ` # Install all binaries with cover variables injected. The binary will be installed in $GOPATH/bin or $HOME/go/bin if directory existed. @@ -41,14 +40,21 @@ goc install --center=http://127.0.0.1:7777 goc build --buildflags="-ldflags '-extldflags -static' -tags='embed kodo'" `, Run: func(cmd *cobra.Command, args []string) { - gocBuild := build.NewInstall(buildFlags, packages) + gocBuild, err := build.NewInstall(buildFlags, packages) + if err != nil { + log.Fatalf("Fail to NewInstall: %v", err) + } // remove temporary directory if needed defer gocBuild.Clean() // doCover with original buildFlags, with new GOPATH( tmp:original ) // in the tmp directory cover.Execute(buildFlags, gocBuild.NewGOPATH, gocBuild.TmpDir, mode, center) // do install in the temporary directory - gocBuild.Install() + err = gocBuild.Install() + if err != nil { + log.Fatalf("Fail to install: %v", err) + } + return }, } diff --git a/cmd/run.go b/cmd/run.go index ad00f79..db65cd3 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -36,7 +36,7 @@ It is exactly behave as 'go run .' in addition of some internal goc features.`, goc run . `, Run: func(cmd *cobra.Command, args []string) { - gocBuild := build.NewBuild(buildFlags, packages, buildOutput) + gocBuild, _ := build.NewBuild(buildFlags, packages, buildOutput) gocBuild.GoRunExecFlag = goRunExecFlag gocBuild.GoRunArguments = goRunArguments defer gocBuild.Clean() diff --git a/pkg/build/build.go b/pkg/build/build.go index 8f62041..229625f 100644 --- a/pkg/build/build.go +++ b/pkg/build/build.go @@ -35,9 +35,11 @@ type Build struct { TmpDir string // the temporary directory to build the project TmpWorkingDir string // the working directory in the temporary directory, which is corresponding to the current directory in the project directory IsMod bool // determine whether it is a Mod project - Root string // Project Root - Target string // the binary name that go build generate - + Root string + // Project Root: + // 1. legacy, root == GOPATH + // 2. mod, root == go.mod Dir + Target string // the binary name that go build generate // keep compatible with go commands: // go run [build flags] [-exec xprog] package [arguments...] // go build [-o output] [-i] [build flags] [packages] @@ -50,26 +52,33 @@ type Build struct { // NewBuild creates a Build struct which can build from goc temporary directory, // and generate binary in current working directory -func NewBuild(buildflags string, packages string, outputDir string) *Build { +func NewBuild(buildflags string, packages string, outputDir string) (*Build, error) { // buildflags = buildflags + " -o " + outputDir b := &Build{ BuildFlags: buildflags, Packages: packages, } if false == b.validatePackageForBuild() { - log.Fatalln("packages only support \".\"") + log.Errorln(ErrWrongPackageTypeForBuild) + return nil, ErrWrongPackageTypeForBuild } b.MvProjectsToTmp() - b.Target = b.determineOutputDir(outputDir) - return b + dir, err := b.determineOutputDir(outputDir) + b.Target = dir + if err != nil { + return nil, err + } + return b, nil } -func (b *Build) Build() { +func (b *Build) Build() error { log.Infoln("Go building in temp...") // new -o will overwrite previous ones b.BuildFlags = b.BuildFlags + " -o " + b.Target cmd := exec.Command("/bin/bash", "-c", "go build "+b.BuildFlags+" "+b.Packages) cmd.Dir = b.TmpWorkingDir + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr if b.NewGOPATH != "" { // Change to temp GOPATH for go install command @@ -77,22 +86,30 @@ func (b *Build) Build() { } log.Printf("go build cmd is: %v", cmd.Args) - out, err := cmd.CombinedOutput() + err := cmd.Start() if err != nil { - log.Fatalf("Fail to execute: %v. The error is: %v, the stdout/stderr is: %v", cmd.Args, err, string(out)) + log.Errorf("Fail to execute: %v. The error is: %v", cmd.Args, err) + return fmt.Errorf("fail to execute: %v: %w", cmd.Args, err) + } + if err = cmd.Wait(); err != nil { + log.Errorf("go build failed. The error is: %v", err) + return fmt.Errorf("go build faileds: %w", err) } log.Println("Go build exit successful.") + return nil } // determineOutputDir, as we only allow . as package name, // the binary name is always same as the directory name of current directory -func (b *Build) determineOutputDir(outputDir string) string { +func (b *Build) determineOutputDir(outputDir string) (string, error) { if b.TmpDir == "" { - log.Fatalln("Can only be called after Build.MvProjectsToTmp().") + log.Errorf("Can only be called after Build.MvProjectsToTmp()", ErrWrongCallSequence) + return "", fmt.Errorf("can only be called after Build.MvProjectsToTmp(): %w", ErrWrongCallSequence) } curWorkingDir, err := os.Getwd() if err != nil { - log.Fatalf("Cannot get current working directory, the err: %v.", err) + log.Errorf("Cannot get current working directory: %v", err) + return "", fmt.Errorf("cannot get current working directory: %w", err) } if outputDir == "" { @@ -102,13 +119,14 @@ func (b *Build) determineOutputDir(outputDir string) string { // replace "_" with "-" in the import path last = strings.ReplaceAll(last, "_", "-") } - return filepath.Join(curWorkingDir, last) + return filepath.Join(curWorkingDir, last), nil } abs, err := filepath.Abs(outputDir) if err != nil { - log.Fatalf("Fail to transform the path: %v to absolute path, the error is: %v", outputDir, err) + log.Errorf("Fail to transform the path: %v to absolute path: %v", outputDir, err) + return "", fmt.Errorf("fail to transform the path %v to absolute path: %w", outputDir, err) } - return abs + return abs, nil } // validatePackageForBuild only allow . as package name diff --git a/pkg/build/errors.go b/pkg/build/errors.go new file mode 100644 index 0000000..c46cec7 --- /dev/null +++ b/pkg/build/errors.go @@ -0,0 +1,14 @@ +package build + +import ( + "errors" +) + +var ( + ErrShouldNotReached = errors.New("should never be reached") + ErrGocShouldExecInProject = errors.New("goc not executed in project directory") + ErrWrongPackageTypeForInstall = errors.New("packages only support \".\" and \"./...\"") + ErrWrongPackageTypeForBuild = errors.New("packages only support \".\"") + ErrWrongCallSequence = errors.New("function should be called in a specified sequence") + ErrNoplaceToInstall = errors.New("no go env") +) diff --git a/pkg/build/gomodules.go b/pkg/build/gomodules.go index 502a226..8c7e2e3 100644 --- a/pkg/build/gomodules.go +++ b/pkg/build/gomodules.go @@ -28,7 +28,7 @@ func (b *Build) cpGoModulesProject() { src := v.Module.Dir if err := copy.Copy(src, dst); err != nil { - log.Printf("Failed to Copy the folder from %v to %v, the error is: %v ", src, dst, err) + log.Errorf("Failed to Copy the folder from %v to %v, the error is: %v ", src, dst, err) } break } else { diff --git a/pkg/build/install.go b/pkg/build/install.go index 8ab25d3..5d20687 100644 --- a/pkg/build/install.go +++ b/pkg/build/install.go @@ -25,36 +25,50 @@ import ( ) // NewInstall creates a Build struct which can install from goc temporary directory -func NewInstall(buildflags string, packages string) *Build { +func NewInstall(buildflags string, packages string) (*Build, error) { b := &Build{ BuildFlags: buildflags, Packages: packages, } if false == b.validatePackageForInstall() { - log.Fatalln("packages only support . and ./...") + log.Errorln(ErrWrongPackageTypeForInstall) + return nil, ErrWrongPackageTypeForInstall } b.MvProjectsToTmp() - return b + return b, nil } -func (b *Build) Install() { +func (b *Build) Install() error { log.Println("Go building in temp...") cmd := exec.Command("/bin/bash", "-c", "go install "+b.BuildFlags+" "+b.Packages) cmd.Dir = b.TmpWorkingDir + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + whereToInstall, err := b.findWhereToInstall() + if err != nil { + // ignore the err + log.Errorf("No place to install: %v", err) + } // Change the temp GOBIN, to force binary install to original place - cmd.Env = append(os.Environ(), fmt.Sprintf("GOBIN=%v", b.findWhereToInstall())) + cmd.Env = append(os.Environ(), fmt.Sprintf("GOBIN=%v", whereToInstall)) if b.NewGOPATH != "" { // Change to temp GOPATH for go install command cmd.Env = append(cmd.Env, fmt.Sprintf("GOPATH=%v", b.NewGOPATH)) } - log.Printf("go install cmd is: %v", cmd.Args) - out, err := cmd.CombinedOutput() + log.Infof("go install cmd is: %v", cmd.Args) + err = cmd.Start() if err != nil { - log.Fatalf("Fail to execute: %v. The error is: %v, the stdout/stderr is: %v", cmd.Args, err, string(out)) + log.Errorf("Fail to execute: %v. The error is: %v", cmd.Args, err) + return fmt.Errorf("fail to execute: %v: %w", cmd.Args, err) } - log.Printf("Go install successful. Binary installed in: %v", b.findWhereToInstall()) + if err = cmd.Wait(); err != nil { + log.Errorf("go install failed. The error is: %v", err) + return fmt.Errorf("go install failed: %w", err) + } + log.Infof("Go install successful. Binary installed in: %v", whereToInstall) + return nil } func (b *Build) validatePackageForInstall() bool { diff --git a/pkg/build/legacy.go b/pkg/build/legacy.go index 962dea7..6627544 100644 --- a/pkg/build/legacy.go +++ b/pkg/build/legacy.go @@ -20,9 +20,10 @@ import ( "os" "path/filepath" + log "github.com/sirupsen/logrus" + "github.com/otiai10/copy" "github.com/qiniu/goc/pkg/cover" - log "github.com/sirupsen/logrus" ) func (b *Build) cpLegacyProject() { @@ -37,7 +38,7 @@ func (b *Build) cpLegacyProject() { } if err := copy.Copy(src, dst); err != nil { - log.Printf("Failed to Copy the folder from %v to %v, the error is: %v ", src, dst, err) + log.Errorf("Failed to Copy the folder from %v to %v, the error is: %v ", src, dst, err) } visited[src] = true @@ -73,9 +74,25 @@ func (b *Build) cpDepPackages(pkg *cover.Package, visited map[string]bool) { dst := filepath.Join(b.TmpDir, "src", dep) if err := copy.Copy(src, dst); err != nil { - log.Printf("Failed to Copy the folder from %v to %v, the error is: %v ", src, dst, err) + log.Errorf("Failed to Copy the folder from %v to %v, the error is: %v ", src, dst, err) } visited[src] = true } } + +func (b *Build) cpNonStandardLegacy() { + for _, v := range b.Pkgs { + if v.Name == "main" { + dst := b.TmpDir + src := v.Dir + + if err := copy.Copy(src, dst); err != nil { + log.Printf("Failed to Copy the folder from %v to %v, the error is: %v ", src, dst, err) + } + break + } else { + continue + } + } +} diff --git a/pkg/build/tmpfolder.go b/pkg/build/tmpfolder.go index d9de295..b8f227e 100644 --- a/pkg/build/tmpfolder.go +++ b/pkg/build/tmpfolder.go @@ -18,6 +18,7 @@ package build import ( "crypto/sha256" + "errors" "fmt" "os" "path/filepath" @@ -54,10 +55,11 @@ func (b *Build) MvProjectsToTmp() { return } -func (b *Build) mvProjectsToTmp() { +func (b *Build) mvProjectsToTmp() error { path, err := os.Getwd() if err != nil { - log.Fatalf("Cannot get current working directory, the error is: %v", err) + log.Errorf("Cannot get current working directory, the error is: %v", err) + return err } b.TmpDir = filepath.Join(os.TempDir(), TmpFolderName(path)) @@ -66,20 +68,39 @@ func (b *Build) mvProjectsToTmp() { // Create a new tmp folder err = os.MkdirAll(filepath.Join(b.TmpDir, "src"), os.ModePerm) if err != nil { - log.Fatalf("Fail to create the temporary build directory. The err is: %v", err) + log.Errorf("Fail to create the temporary build directory. The err is: %v", err) + return err } log.Printf("Tmp project generated in: %v", b.TmpDir) - // set Build.IsMod flag, so we don't have to call checkIfLegacyProject another time - if b.checkIfLegacyProject() { - b.cpLegacyProject() - } else { - b.IsMod = true - b.cpGoModulesProject() + // traverse pkg list to get project meta info + b.IsMod, b.Root, err = b.traversePkgsList() + if errors.Is(err, ErrShouldNotReached) { + return fmt.Errorf("mvProjectsToTmp with a empty project: %w", err) + } + // we should get corresponding working directory in temporary directory + b.TmpWorkingDir, err = b.getTmpwd() + if err != nil { + log.Errorf("fail to get workding directory in temporary directory: %v", err) + return fmt.Errorf("fail to get workding directory in temporary directory: %w", err) + } + // issue #14 + // if b.Root == "", then the project is non-standard project + // known cases: + // 1. a legacy project, but not in any GOPATH, will cause the b.Root == "" + if b.IsMod == false && b.Root != "" { + b.cpLegacyProject() + } else if b.IsMod == true && b.Root != "" { + b.cpGoModulesProject() + } else if b.IsMod == false && b.Root == "" { + b.TmpWorkingDir = b.TmpDir + b.cpNonStandardLegacy() + } else { + return fmt.Errorf("unknown project type: %w", ErrShouldNotReached) } - b.getTmpwd() - log.Printf("New workingdir in tmp directory in: %v", b.TmpWorkingDir) + log.Infof("New workingdir in tmp directory in: %v", b.TmpWorkingDir) + return nil } func TmpFolderName(path string) string { @@ -89,29 +110,35 @@ func TmpFolderName(path string) string { return "goc-" + h } -// checkIfLegacyProject Check if it is go module project -// true legacy -// false go mod -func (b *Build) checkIfLegacyProject() bool { +// traversePkgsList travse the Build.Pkgs list +// return Build.IsMod, tell if the project is a mod project +// return Build.Root: +// 1. the project root if it is a mod project, +// 2. current GOPATH if it is a legacy project, +// 3. some non-standard project, which Build.IsMod == false, Build.Root == nil +func (b *Build) traversePkgsList() (isMod bool, root string, err error) { for _, v := range b.Pkgs { // get root - b.Root = v.Root + root = v.Root if v.Module == nil { - return true + return } - return false + isMod = true + return } - log.Fatalln("Should never be reached....") - return false + log.Error("should not reach here") + err = ErrShouldNotReached + return } // getTmpwd get the corresponding working directory in the temporary working directory // and store it in the Build.tmpWorkdingDir -func (b *Build) getTmpwd() { +func (b *Build) getTmpwd() (string, error) { for _, pkg := range b.Pkgs { path, err := os.Getwd() if err != nil { - log.Fatalf("Cannot get current working directory, the error is: %v", err) + log.Errorf("cannot get current working directory: %v", err) + return "", fmt.Errorf("cannot get current working directory: %w", err) } index := -1 @@ -125,33 +152,32 @@ func (b *Build) getTmpwd() { } if index == -1 { - log.Fatalf("goc install not executed in project directory.") + return "", ErrGocShouldExecInProject } - b.TmpWorkingDir = filepath.Join(b.TmpDir, path[len(parentPath):]) - // log.Printf("New building directory in: %v", tmpwd) - return + // b.TmpWorkingDir = filepath.Join(b.TmpDir, path[len(parentPath):]) + return filepath.Join(b.TmpDir, path[len(parentPath):]), nil } - log.Fatalln("Should never be reached....") - return + return "", ErrShouldNotReached } -func (b *Build) findWhereToInstall() string { +func (b *Build) findWhereToInstall() (string, error) { if GOBIN := os.Getenv("GOBIN"); GOBIN != "" { - return GOBIN + return GOBIN, nil } // old GOPATH dir GOPATH := os.Getenv("GOPATH") if false == b.IsMod { - for _, v := range b.Pkgs { - return filepath.Join(v.Root, "bin") + if b.Root == "" { + return "", ErrNoplaceToInstall } + return filepath.Join(b.Root, "bin"), nil } if GOPATH != "" { - return filepath.Join(strings.Split(GOPATH, ":")[0], "bin") + return filepath.Join(strings.Split(GOPATH, ":")[0], "bin"), nil } - return filepath.Join(os.Getenv("HOME"), "go", "bin") + return filepath.Join(os.Getenv("HOME"), "go", "bin"), nil } // Clean clears up the temporary workspace diff --git a/pkg/build/tmpfolder_test.go b/pkg/build/tmpfolder_test.go index 1feb9bb..6804fa2 100644 --- a/pkg/build/tmpfolder_test.go +++ b/pkg/build/tmpfolder_test.go @@ -33,7 +33,7 @@ func TestNewDirParseInLegacyProject(t *testing.T) { os.Setenv("GOPATH", gopath) os.Setenv("GO111MODULE", "off") - b := NewInstall("", ".") + b, _ := NewInstall("", ".") if -1 == strings.Index(b.TmpWorkingDir, b.TmpDir) { t.Fatalf("Directory parse error. newwd: %v, tmpdir: %v", b.TmpWorkingDir, b.TmpDir) } @@ -42,7 +42,7 @@ func TestNewDirParseInLegacyProject(t *testing.T) { t.Fatalf("The New GOPATH is wrong. newgopath: %v, tmpdir: %v", b.NewGOPATH, b.TmpDir) } - b = NewBuild("", ".", "") + b, _ = NewBuild("", ".", "") if -1 == strings.Index(b.TmpWorkingDir, b.TmpDir) { t.Fatalf("Directory parse error. newwd: %v, tmpdir: %v", b.TmpWorkingDir, b.TmpDir) } @@ -61,7 +61,7 @@ func TestNewDirParseInModProject(t *testing.T) { os.Setenv("GOPATH", gopath) os.Setenv("GO111MODULE", "on") - b := NewInstall("", ".") + b, _ := NewInstall("", ".") if -1 == strings.Index(b.TmpWorkingDir, b.TmpDir) { t.Fatalf("Directory parse error. newwd: %v, tmpdir: %v", b.TmpWorkingDir, b.TmpDir) } @@ -70,7 +70,7 @@ func TestNewDirParseInModProject(t *testing.T) { t.Fatalf("The New GOPATH is wrong. newgopath: %v, tmpdir: %v", b.NewGOPATH, b.TmpDir) } - b = NewBuild("", ".", "") + b, _ = NewBuild("", ".", "") if -1 == strings.Index(b.TmpWorkingDir, b.TmpDir) { t.Fatalf("Directory parse error. newwd: %v, tmpdir: %v", b.TmpWorkingDir, b.TmpDir) } @@ -89,7 +89,7 @@ func TestLegacyProjectNotInGoPATH(t *testing.T) { fmt.Println(gopath) os.Setenv("GOPATH", gopath) os.Setenv("GO111MODULE", "off") - b := NewBuild("", ".", "") + b, _ := NewBuild("", ".", "") if b.OriGOPATH != b.NewGOPATH { t.Fatalf("New GOPATH should be same with old GOPATH, for this kind of project. New: %v, old: %v", b.NewGOPATH, b.OriGOPATH) }