Repository: qiniu/goc (go)

Issue:
unable to build project [https://github.com/enricofoltran/simple-go-server]
Steps：
1. workspace and environment
```
➜  simple-go-server git:(master) echo $GOPATH
/Users/jicarl/k8s
➜  simple-go-server git:(master) pwd
/Users/jicarl/qbox/simple-go-server
➜  simple-go-server git:(master) git remote -v
origin	git@github.com:enricofoltran/simple-go-server.git (fetch)
origin	git@github.com:enricofoltran/simple-go-server.git (push)
➜  simple-go-server git:(master)
```
2. try to use goc command to build this project, but got errors:
```
➜  simple-go-server git:(master) goc build .
2020/05/24 15:15:29 cover.go:114: go list cmd is: [go list -json . ./...]
2020/05/24 15:15:29 tmpfolder.go:65: Temp project generated in: /var/folders/_6/t59gxn4d0zz12vqlm3j3gwhr0000gn/T/goc-055cb24d8755
2020/05/24 15:15:29 tmpfolder.go:78: New working/building directory in: /var/folders/_6/t59gxn4d0zz12vqlm3j3gwhr0000gn/T/goc-055cb24d8755/Users/jicarl/qbox/simple-go-server
2020/05/24 15:15:29 tmpfolder.go:47: New GOPATH: /var/folders/_6/t59gxn4d0zz12vqlm3j3gwhr0000gn/T/goc-055cb24d8755:/Users/jicarl/k8s
2020/05/24 15:15:29 cover.go:114: go list cmd is: [go list -json . ./...]
2020/05/24 15:15:29 build.go:61: Go building in temp...
2020/05/24 15:15:29 build.go:74: Fail to execute: go build .. The error is: chdir /var/folders/_6/t59gxn4d0zz12vqlm3j3gwhr0000gn/T/goc-055cb24d8755/Users/jicarl/qbox/simple-go-server: no such file or directory, the stdout/stderr is:
```

Pull request description:
fix #14
fix #14 

add unit test `TestLegacyProjectNotInGoPATH` 

checkpoint:
1. the `b.NewGOPATH` should not change,  `b.NewGOPATH == b.OriGOPATH`
2. the `main.go` should be copied into the `b.TmpDir`

Patch:
diff --git a/cmd/build.go b/cmd/build.go
index cce6cd6..9bc6869 100644
--- a/cmd/build.go
+++ b/cmd/build.go
@@ -17,6 +17,8 @@
 package cmd
 
 import (
+	"log"
+
 	"github.com/qiniu/goc/pkg/build"
 	"github.com/qiniu/goc/pkg/cover"
 	"github.com/spf13/cobra"
@@ -27,9 +29,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,21 +38,13 @@ 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)
-		// 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()
-		return
+		runBuild()
 	},
 }
 
@@ -63,3 +55,21 @@ func init() {
 	buildCmd.Flags().StringVar(&buildOutput, "output", "", "it forces build to write the resulting executable or object to the named output file or directory")
 	rootCmd.AddCommand(buildCmd)
 }
+
+func runBuild() {
+	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
+	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..028230f 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,7 @@ 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)
-		// 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()
+		runInstall()
 	},
 }
 
@@ -56,3 +48,21 @@ func init() {
 	addBuildFlags(installCmd.Flags())
 	rootCmd.AddCommand(installCmd)
 }
+
+func runInstall() {
+	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
+	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..d8e2319 100644
--- a/pkg/build/build.go
+++ b/pkg/build/build.go
@@ -35,9 +35,12 @@ 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
+	// go 1.11, go 1.12 has no Root
+	// 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 +53,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 +87,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(): %v", 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 +120,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)
+	}
+	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.Printf("Go install successful. Binary installed in: %v", b.findWhereToInstall())
+	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..166035e 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"
@@ -47,17 +48,20 @@ func (b *Build) MvProjectsToTmp() {
 	}
 	// fix #14: unable to build project not in GOPATH in legacy mode
 	// this kind of project does not have a pkg.Root value
-	if b.Root == "" {
+	// go 1.11, 1.12 has no pkg.Root,
+	// so add b.IsMod == false as secondary judgement
+	if b.Root == "" && b.IsMod == false {
 		b.NewGOPATH = b.OriGOPATH
 	}
 	log.Printf("New GOPATH: %v", b.NewGOPATH)
 	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 +70,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() {
+	// 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 {
-		b.IsMod = true
+	} else if b.IsMod == true { // go 1.11, 1.12 has no Build.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 +112,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 +154,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


Test patch:
diff --git a/cmd/build_test.go b/cmd/build_test.go
new file mode 100644
index 0000000..db005d6
--- /dev/null
+++ b/cmd/build_test.go
@@ -0,0 +1,61 @@
+/*
+ Copyright 2020 Qiniu Cloud (qiniu.com)
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+     http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package cmd
+
+import (
+	"github.com/stretchr/testify/assert"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"strings"
+	"testing"
+	"time"
+)
+
+var baseDir string
+
+func init() {
+	baseDir, _ = os.Getwd()
+}
+
+func TestGeneratedBinary(t *testing.T) {
+	startTime := time.Now()
+
+	workingDir := filepath.Join(baseDir, "../tests/samples/simple_project")
+	gopath := ""
+
+	os.Chdir(workingDir)
+	os.Setenv("GOPATH", gopath)
+	os.Setenv("GO111MODULE", "on")
+
+	buildFlags, packages, buildOutput = "", ".", ""
+	runBuild()
+
+	obj := filepath.Join(".", "simple-project")
+	fInfo, err := os.Lstat(obj)
+	assert.Equal(t, err, nil, "the binary should be generated.")
+	assert.Equal(t, startTime.Before(fInfo.ModTime()), true, obj+"new binary should be generated, not the old one")
+
+	cmd := exec.Command("go", "tool", "objdump", "simple-project")
+	cmd.Dir = workingDir
+	out, _ := cmd.CombinedOutput()
+	cnt := strings.Count(string(out), "main.registerSelf")
+	assert.Equal(t, cnt > 0, true, "main.registerSelf function should be in the binary")
+
+	cnt = strings.Count(string(out), "GoCover")
+	assert.Equal(t, cnt > 0, true, "GoCover varibale should be in the binary")
+}
diff --git a/cmd/install_test.go b/cmd/install_test.go
new file mode 100644
index 0000000..150d098
--- /dev/null
+++ b/cmd/install_test.go
@@ -0,0 +1,83 @@
+/*
+ Copyright 2020 Qiniu Cloud (qiniu.com)
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+     http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package cmd
+
+import (
+	"github.com/stretchr/testify/assert"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"strings"
+	"testing"
+	"time"
+)
+
+func TestInstalledBinaryForMod(t *testing.T) {
+	startTime := time.Now()
+
+	workingDir := filepath.Join(baseDir, "../tests/samples/simple_project")
+	gopath := filepath.Join(baseDir, "../tests/samples/simple_project", "testhome")
+
+	os.Chdir(workingDir)
+	os.Setenv("GOPATH", gopath)
+	os.Setenv("GO111MODULE", "on")
+
+	buildFlags, packages, buildOutput = "", ".", ""
+	runInstall()
+
+	obj := filepath.Join(gopath, "bin", "simple-project")
+	fInfo, err := os.Lstat(obj)
+	assert.Equal(t, err, nil, "the binary should be generated.")
+	assert.Equal(t, startTime.Before(fInfo.ModTime()), true, obj+"new binary should be generated, not the old one")
+
+	cmd := exec.Command("go", "tool", "objdump", "simple-project")
+	cmd.Dir = workingDir
+	out, _ := cmd.CombinedOutput()
+	cnt := strings.Count(string(out), "main.registerSelf")
+	assert.Equal(t, cnt > 0, true, "main.registerSelf function should be in the binary")
+
+	cnt = strings.Count(string(out), "GoCover")
+	assert.Equal(t, cnt > 0, true, "GoCover varibale should be in the binary")
+}
+
+func TestInstalledBinaryForLegacy(t *testing.T) {
+	startTime := time.Now()
+
+	workingDir := filepath.Join(baseDir, "../tests/samples/simple_gopath_project/src/qiniu.com/simple_gopath_project")
+	gopath := filepath.Join(baseDir, "../tests/samples/simple_gopath_project")
+
+	os.Chdir(workingDir)
+	os.Setenv("GOPATH", gopath)
+	os.Setenv("GO111MODULE", "off")
+
+	buildFlags, packages, buildOutput = "", ".", ""
+	runInstall()
+
+	obj := filepath.Join(gopath, "bin", "simple_gopath_project")
+	fInfo, err := os.Lstat(obj)
+	assert.Equal(t, err, nil, "the binary should be generated.")
+	assert.Equal(t, startTime.Before(fInfo.ModTime()), true, obj+"new binary should be generated, not the old one")
+
+	cmd := exec.Command("go", "tool", "objdump", obj)
+	cmd.Dir = workingDir
+	out, _ := cmd.CombinedOutput()
+	cnt := strings.Count(string(out), "main.registerSelf")
+	assert.Equal(t, cnt > 0, true, "main.registerSelf function should be in the binary")
+
+	cnt = strings.Count(string(out), "GoCover")
+	assert.Equal(t, cnt > 0, true, "GoCover varibale should be in the binary")
+}
diff --git a/pkg/build/build_test.go b/pkg/build/build_test.go
new file mode 100644
index 0000000..fa25263
--- /dev/null
+++ b/pkg/build/build_test.go
@@ -0,0 +1,53 @@
+/*
+ Copyright 2020 Qiniu Cloud (qiniu.com)
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+     http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package build
+
+import (
+	"github.com/stretchr/testify/assert"
+	"os"
+	"path/filepath"
+	"testing"
+)
+
+func TestInvalidPackage(t *testing.T) {
+
+	workingDir := filepath.Join(baseDir, "../../tests/samples/simple_project")
+	gopath := ""
+
+	os.Chdir(workingDir)
+	os.Setenv("GOPATH", gopath)
+	os.Setenv("GO111MODULE", "on")
+
+	_, err := NewBuild("", "example.com/simple-project", "")
+	assert.Equal(t, err, ErrWrongPackageTypeForBuild, "the package name should be invalid")
+}
+
+func TestBasicBuildForModProject(t *testing.T) {
+	workingDir := filepath.Join(baseDir, "../tests/samples/simple_project")
+	gopath := ""
+
+	os.Chdir(workingDir)
+	os.Setenv("GOPATH", gopath)
+	os.Setenv("GO111MODULE", "on")
+
+	buildFlags, packages, buildOutput := "", ".", ""
+	gocBuild, err := NewBuild(buildFlags, packages, buildOutput)
+	assert.Equal(t, err, nil, "should create temporary directory successfully")
+
+	err = gocBuild.Build()
+	assert.Equal(t, err, nil, "temporary directory should build successfully")
+}
diff --git a/pkg/build/install_test.go b/pkg/build/install_test.go
new file mode 100644
index 0000000..9108b3f
--- /dev/null
+++ b/pkg/build/install_test.go
@@ -0,0 +1,24 @@
+package build
+
+import (
+	"github.com/stretchr/testify/assert"
+	"os"
+	"path/filepath"
+	"testing"
+)
+
+func TestBasicInstallForModProject(t *testing.T) {
+	workingDir := filepath.Join(baseDir, "../tests/samples/simple_project")
+	gopath := filepath.Join(baseDir, "../tests/samples/simple_project", "testhome")
+
+	os.Chdir(workingDir)
+	os.Setenv("GOPATH", gopath)
+	os.Setenv("GO111MODULE", "on")
+
+	buildFlags, packages := "", "."
+	gocBuild, err := NewInstall(buildFlags, packages)
+	assert.Equal(t, err, nil, "should create temporary directory successfully")
+
+	err = gocBuild.Install()
+	assert.Equal(t, err, nil, "temporary directory should build successfully")
+}
diff --git a/pkg/build/tmpfolder_test.go b/pkg/build/tmpfolder_test.go
index 1feb9bb..ed4f4b8 100644
--- a/pkg/build/tmpfolder_test.go
+++ b/pkg/build/tmpfolder_test.go
@@ -24,16 +24,22 @@ import (
 	"testing"
 )
 
+var baseDir string
+
+func init() {
+	baseDir, _ = os.Getwd()
+}
+
 func TestNewDirParseInLegacyProject(t *testing.T) {
-	workingDir := "../../tests/samples/simple_gopath_project/src/qiniu.com/simple_gopath_project"
-	gopath, _ := filepath.Abs("../../tests/samples/simple_gopath_project")
+	workingDir := filepath.Join(baseDir, "../../tests/samples/simple_gopath_project/src/qiniu.com/simple_gopath_project")
+	gopath := filepath.Join(baseDir, "../../tests/samples/simple_gopath_project")
 
 	os.Chdir(workingDir)
-	fmt.Println(gopath)
+
 	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 +48,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)
 	}
@@ -53,7 +59,7 @@ func TestNewDirParseInLegacyProject(t *testing.T) {
 }
 
 func TestNewDirParseInModProject(t *testing.T) {
-	workingDir := "../../tests/samples/simple_project"
+	workingDir := filepath.Join(baseDir, "../../tests/samples/simple_project")
 	gopath := ""
 
 	os.Chdir(workingDir)
@@ -61,7 +67,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 +76,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)
 	}
@@ -82,15 +88,21 @@ func TestNewDirParseInModProject(t *testing.T) {
 
 // Test #14
 func TestLegacyProjectNotInGoPATH(t *testing.T) {
-	workingDir := "../../tests/samples/simple_gopath_project/src/qiniu.com/simple_gopath_project"
+	workingDir := filepath.Join(baseDir, "../../tests/samples/simple_gopath_project/src/qiniu.com/simple_gopath_project")
 	gopath := ""
 
 	os.Chdir(workingDir)
 	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)
 	}
+
+	_, err := os.Stat(filepath.Join(b.TmpDir, "main.go"))
+	if err != nil {
+		t.Fatalf("There should be a main.go in temporary directory directly, the error: %v", err)
+	}
 }
diff --git a/tests/e2e/simple_project_test.go b/tests/e2e/simple_project_test.go
index 139d05a..b189289 100644
--- a/tests/e2e/simple_project_test.go
+++ b/tests/e2e/simple_project_test.go
@@ -56,6 +56,7 @@ var _ = Describe("E2E", func() {
 			testProjDir := filepath.Join(TESTS_ROOT, "samples/simple_project")
 			cmd := exec.Command("goc", "build", "--debug")
 			cmd.Dir = testProjDir
+			cmd.Env = append(os.Environ(), "GO111MODULE=on")
 
 			out, err := cmd.CombinedOutput()
 			Expect(err).To(BeNil(), "goc build on this project should be successful", string(out))
diff --git a/tests/run-ci-actions.sh b/tests/run-ci-actions.sh
index 62d0085..ac44500 100755
--- a/tests/run-ci-actions.sh
+++ b/tests/run-ci-actions.sh
@@ -22,4 +22,4 @@ chmod +x /home/runner/tools/e2e.test/e2e.test
 export PATH=/home/runner/tools/e2e.test:$PATH
 
 cd e2e
-e2e.test ./...
\ No newline at end of file
+e2e.test -test.v ./...
\ No newline at end of file