Repository: qiniu/goc (go)

Issue:
cannot build if go.mod has relative path 'replace' directive
项目比较大，各种依赖很复杂，有十几个文件，之前都是用makefile编译的
执行goc build .报错
go: $path/$module@v0.0.0-20200804122736-5634267498a2: parsing $path/go.mod: open $path/go.mod: no such file or directory
FATA Fail to build: fail to execute: [/bin/bash -c go build  -o $path/$program .], err: exit status 1
试了下用go build .可以成功编译

Pull request description:
add support for go.mod contains replace directive
fix #92 
go.mod supports to replace some dependencies with local ones, suppose we have two projects in two directries: gomod_replace_library and gomod_replace_project. 

```
├── gomod_replace_library
│   ├── bar.go
│   └── go.mod
├── gomod_replace_project
│   ├── go.mod
│   └── main.go
```

And go.mod for gomod_replace_project is:

```
module example.com/simple-project

require qiniu.com/foo v0.0.0

replace qiniu.com/foo => ../gomod_replace_library

go 1.11
``` 

If we use goc build/install in `gomod_replace_project` directory, goc will first copy all the content in `gomod_replace_project` to a temporary directory. Then goc will call go build to build the project, but go cannot find the dependency `../gomod_replace_library` from the temporary directory, so we need to rewrite the go.mod to some thing like:

```
module example.com/simple-project

require qiniu.com/foo v0.0.0

replace qiniu.com/foo => /Users/xxx/tests/samples/gomod_replace_library

go 1.11
``` 

Patch:
diff --git a/go.mod b/go.mod
index 91b80f1..a4b18ff 100644
--- a/go.mod
+++ b/go.mod
@@ -16,6 +16,7 @@ require (
 	github.com/spf13/pflag v1.0.5
 	github.com/spf13/viper v1.6.2
 	github.com/stretchr/testify v1.5.1
+	golang.org/x/mod v0.3.0
 	golang.org/x/net v0.0.0-20200625001655-4c5254603344
 	golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
 	golang.org/x/tools v0.0.0-20200730221956-1ac65761fe2c
diff --git a/go.sum b/go.sum
index 722c912..9881f5d 100644
--- a/go.sum
+++ b/go.sum
@@ -822,6 +822,7 @@ golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKG
 golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
 golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
 golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
 golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
diff --git a/pkg/build/build.go b/pkg/build/build.go
index 50b2809..e05cf1b 100644
--- a/pkg/build/build.go
+++ b/pkg/build/build.go
@@ -41,7 +41,8 @@ type Build struct {
 	// Project Root:
 	// 1. legacy, root == GOPATH
 	// 2. mod, root == go.mod Dir
-	Target string // the binary name that go build generate
+	ModRoot string // path for go.mod
+	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]
diff --git a/pkg/build/gomodules.go b/pkg/build/gomodules.go
index 8c7e2e3..32c9b79 100644
--- a/pkg/build/gomodules.go
+++ b/pkg/build/gomodules.go
@@ -17,8 +17,12 @@
 package build
 
 import (
+	"io/ioutil"
+	"path/filepath"
+
 	"github.com/otiai10/copy"
 	log "github.com/sirupsen/logrus"
+	"golang.org/x/mod/modfile"
 )
 
 func (b *Build) cpGoModulesProject() {
@@ -36,3 +40,51 @@ func (b *Build) cpGoModulesProject() {
 		}
 	}
 }
+
+// updateGoModFile rewrites the go.mod file in the temporary directory,
+// if it has a 'replace' directive, and the directive has a relative local path
+// it will be rewritten with a absolute path.
+// ex.
+// suppose original project is located at /path/to/aa/bb/cc, go.mod contains a directive:
+// 'replace github.com/qiniu/bar => ../home/foo/bar'
+// after the project is copied to temporary directory, it should be rewritten as
+// 'replace github.com/qiniu/bar => /path/to/aa/bb/home/foo/bar'
+func (b *Build) updateGoModFile() (updateFlag bool, newModFile []byte, err error) {
+	tempModfile := filepath.Join(b.TmpDir, "go.mod")
+	buf, err := ioutil.ReadFile(tempModfile)
+	if err != nil {
+		return
+	}
+	oriGoModFile, err := modfile.Parse(tempModfile, buf, nil)
+	if err != nil {
+		return
+	}
+
+	updateFlag = false
+	for index := range oriGoModFile.Replace {
+		replace := oriGoModFile.Replace[index]
+		oldPath := replace.Old.Path
+		oldVersion := replace.Old.Version
+		newPath := replace.New.Path
+		newVersion := replace.New.Version
+		// replace to a local filesystem does not have a version
+		// absolute path no need to rewrite
+		if newVersion == "" && !filepath.IsAbs(newPath) {
+			var absPath string
+			fullPath := filepath.Join(b.ModRoot, newPath)
+			absPath, _ = filepath.Abs(fullPath)
+			// DropReplace & AddReplace will not return error
+			// so no need to check the error
+			_ = oriGoModFile.DropReplace(oldPath, oldVersion)
+			_ = oriGoModFile.AddReplace(oldPath, oldVersion, absPath, newVersion)
+			updateFlag = true
+		}
+	}
+	oriGoModFile.Cleanup()
+	// Format will not return error, so ignore the returned error
+	// func (f *File) Format() ([]byte, error) {
+	//     return Format(f.Syntax), nil
+	// }
+	newModFile, _ = oriGoModFile.Format()
+	return
+}
diff --git a/pkg/build/tmpfolder.go b/pkg/build/tmpfolder.go
index f9a027c..cbfeced 100644
--- a/pkg/build/tmpfolder.go
+++ b/pkg/build/tmpfolder.go
@@ -20,6 +20,7 @@ import (
 	"crypto/sha256"
 	"errors"
 	"fmt"
+	"io/ioutil"
 	"os"
 	"path/filepath"
 	"strings"
@@ -75,10 +76,9 @@ func (b *Build) mvProjectsToTmp() error {
 	// Create a new tmp folder
 	err := os.MkdirAll(filepath.Join(b.TmpDir, "src"), os.ModePerm)
 	if err != nil {
-		log.Errorf("Fail to create the temporary build directory. The err is: %v", err)
-		return err
+		return fmt.Errorf("Fail to create the temporary build directory. The err is: %v", err)
 	}
-	log.Printf("Tmp project generated in: %v", b.TmpDir)
+	log.Infof("Tmp project generated in: %v", b.TmpDir)
 
 	// traverse pkg list to get project meta info
 	b.IsMod, b.Root, err = b.traversePkgsList()
@@ -89,7 +89,6 @@ func (b *Build) mvProjectsToTmp() error {
 	// 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("getTmpwd failed with error: %w", err)
 	}
 	// issue #14
@@ -100,6 +99,18 @@ func (b *Build) mvProjectsToTmp() error {
 		b.cpLegacyProject()
 	} else if b.IsMod == true { // go 1.11, 1.12 has no Build.Root
 		b.cpGoModulesProject()
+		updated, newGoModContent, err := b.updateGoModFile()
+		if err != nil {
+			return fmt.Errorf("fail to generate new go.mod: %v", err)
+		}
+		if updated {
+			log.Infoln("go.mod needs rewrite")
+			tmpModFile := filepath.Join(b.TmpDir, "go.mod")
+			err := ioutil.WriteFile(tmpModFile, newGoModContent, os.ModePerm)
+			if err != nil {
+				return fmt.Errorf("fail to update go.mod: %v", err)
+			}
+		}
 	} else if b.IsMod == false && b.Root == "" {
 		b.TmpWorkingDir = b.TmpDir
 		b.cpNonStandardLegacy()
@@ -134,6 +145,7 @@ func (b *Build) traversePkgsList() (isMod bool, root string, err error) {
 			return
 		}
 		isMod = true
+		b.ModRoot = v.Module.Dir
 		return
 	}
 	log.Error(ErrShouldNotReached)


Test patch:
diff --git a/pkg/build/gomodules_test.go b/pkg/build/gomodules_test.go
index 12276bd..e0e6a6d 100644
--- a/pkg/build/gomodules_test.go
+++ b/pkg/build/gomodules_test.go
@@ -18,7 +18,9 @@ package build
 
 import (
 	"bytes"
+	"errors"
 	"os"
+	"path/filepath"
 	"strings"
 	"testing"
 
@@ -53,3 +55,49 @@ func TestModProjectCopyWithUnexistedDir(t *testing.T) {
 	output := captureOutput(b.cpGoModulesProject)
 	assert.Equal(t, strings.Contains(output, "Failed to Copy"), true)
 }
+
+// test go mod file udpate
+func TestUpdateModFileIfContainsReplace(t *testing.T) {
+	workingDir := filepath.Join(baseDir, "../../tests/samples/gomod_samples/a")
+	b := &Build{
+		TmpDir:  workingDir,
+		ModRoot: "/aa/bb/cc",
+	}
+
+	// replace with relative local file path should be rewrite
+	updated, newmod, err := b.updateGoModFile()
+	assert.Equal(t, err, nil)
+	assert.Equal(t, updated, true)
+	assert.Contains(t, string(newmod), "replace github.com/qiniu/bar => /aa/bb/home/foo/bar")
+
+	// old replace should be removed
+	assert.NotContains(t, string(newmod), "github.com/qiniu/bar => ../home/foo/bar")
+
+	// normal replace should not be rewrite
+	assert.Contains(t, string(newmod), "github.com/qiniu/bar2 => github.com/baniu/bar3 v1.2.3")
+}
+
+// test wrong go mod file
+func TestWithWrongGoModFile(t *testing.T) {
+	// go.mod not exist
+	workingDir := filepath.Join(baseDir, "../../tests/samples/xxxxxxxxxxxx/a")
+	b := &Build{
+		TmpDir:  workingDir,
+		ModRoot: "/aa/bb/cc",
+	}
+
+	updated, _, err := b.updateGoModFile()
+	assert.Equal(t, errors.Is(err, os.ErrNotExist), true)
+	assert.Equal(t, updated, false)
+
+	// a wrong format go mod
+	workingDir = filepath.Join(baseDir, "../../tests/samples/gomod_samples/b")
+	b = &Build{
+		TmpDir:  workingDir,
+		ModRoot: "/aa/bb/cc",
+	}
+
+	updated, _, err = b.updateGoModFile()
+	assert.NotEqual(t, err, nil)
+	assert.Equal(t, updated, false)
+}
diff --git a/tests/build.bats b/tests/build.bats
index e2f17a0..b4d8567 100755
--- a/tests/build.bats
+++ b/tests/build.bats
@@ -72,5 +72,18 @@ setup() {
     info build3 output: $output
     [ "$status" -eq 0 ]
 
+    wait $profile_pid
+}
+
+@test "test goc build with go.mod project which contains replace directive" {
+    cd samples/gomod_replace_project
+
+    wait_profile_backend "build4" &
+    profile_pid=$!
+
+    run gocc build --debug --debugcisyncfile ci-sync.bak;
+    info build4 output: $output
+    [ "$status" -eq 0 ]
+
     wait $profile_pid
 }
\ No newline at end of file
diff --git a/tests/samples/gomod_replace_library/bar.go b/tests/samples/gomod_replace_library/bar.go
new file mode 100644
index 0000000..7307d25
--- /dev/null
+++ b/tests/samples/gomod_replace_library/bar.go
@@ -0,0 +1,7 @@
+package foo
+
+import "fmt"
+
+func Bar() {
+	fmt.Println("foo bar")
+}
diff --git a/tests/samples/gomod_replace_library/go.mod b/tests/samples/gomod_replace_library/go.mod
new file mode 100644
index 0000000..b16fdd4
--- /dev/null
+++ b/tests/samples/gomod_replace_library/go.mod
@@ -0,0 +1,4 @@
+module qiniu.com/foo
+
+
+go 1.11
diff --git a/tests/samples/gomod_replace_project/go.mod b/tests/samples/gomod_replace_project/go.mod
new file mode 100644
index 0000000..d92ad34
--- /dev/null
+++ b/tests/samples/gomod_replace_project/go.mod
@@ -0,0 +1,7 @@
+module example.com/simple-project
+
+require qiniu.com/foo v0.0.0
+
+replace qiniu.com/foo => ../gomod_replace_library
+
+go 1.11
diff --git a/tests/samples/gomod_replace_project/main.go b/tests/samples/gomod_replace_project/main.go
new file mode 100644
index 0000000..d370f07
--- /dev/null
+++ b/tests/samples/gomod_replace_project/main.go
@@ -0,0 +1,9 @@
+package main
+
+import (
+	"qiniu.com/foo"
+)
+
+func main() {
+	foo.Bar()
+}
diff --git a/tests/samples/gomod_samples/a/go.mod b/tests/samples/gomod_samples/a/go.mod
new file mode 100644
index 0000000..d47b8c2
--- /dev/null
+++ b/tests/samples/gomod_samples/a/go.mod
@@ -0,0 +1,11 @@
+module example.com/gg/a
+
+replace (
+    github.com/qiniu/bar => ../home/foo/bar
+    github.com/qiniu/bar2 => github.com/baniu/bar3 v1.2.3
+)
+
+require (
+	github.com/qiniu/bar v1.0.0
+    github.com/qiniu/bar2 v1.2.0
+)
diff --git a/tests/samples/gomod_samples/b/go.mod b/tests/samples/gomod_samples/b/go.mod
new file mode 100644
index 0000000..ca70ddd
--- /dev/null
+++ b/tests/samples/gomod_samples/b/go.mod
@@ -0,0 +1,7 @@
+module example.com/gg/a
+
+replerace github.com/qiniu/bar => ../home/foo/bar
+
+eggrr (
+	github.com/qiniu/bar v1.0.0
+)