cmd/gomobile: enable Go modules in gomobile-bind

This CL enables Go modules in gomobile-bind command. This CL
generates go.mod at $WORK/src based on the modules state of the
working directory, and use it when executing go-build.

Updates golang/go#27234

Change-Id: I6958f29a317c0d2fb9ffa373f6e3c4cabdc4e898
Reviewed-on: https://go-review.googlesource.com/c/mobile/+/210380
Run-TryBot: Hajime Hoshi <hajimehoshi@gmail.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Hyang-Ah Hana Kim <hyangah@gmail.com>
diff --git a/cmd/gomobile/bind.go b/cmd/gomobile/bind.go
index e26d51e..2676bfe 100644
--- a/cmd/gomobile/bind.go
+++ b/cmd/gomobile/bind.go
@@ -5,6 +5,8 @@
 package main
 
 import (
+	"bytes"
+	"encoding/json"
 	"errors"
 	"fmt"
 	"io"
@@ -15,6 +17,7 @@
 	"path/filepath"
 	"strings"
 
+	"golang.org/x/mod/modfile"
 	"golang.org/x/tools/go/packages"
 )
 
@@ -102,13 +105,10 @@
 		gobind = "gobind"
 	}
 
-	var pkgs []*packages.Package
-	switch len(args) {
-	case 0:
-		pkgs, err = packages.Load(packagesConfig(targetOS), ".")
-	default:
-		pkgs, err = importPackages(args, targetOS)
+	if len(args) == 0 {
+		args = append(args, ".")
 	}
+	pkgs, err := importPackages(args, targetOS)
 	if err != nil {
 		return err
 	}
@@ -196,8 +196,7 @@
 		fmt.Fprintf(os.Stderr, "write %s\n", filename)
 	}
 
-	err := mkdir(filepath.Dir(filename))
-	if err != nil {
+	if err := mkdir(filepath.Dir(filename)); err != nil {
 		return err
 	}
 
@@ -230,3 +229,97 @@
 	}
 	return config
 }
+
+// getModuleVersions returns a module information at the directory src.
+func getModuleVersions(targetOS string, targetArch string, src string) (*modfile.File, error) {
+	cmd := exec.Command(goBin(), "list")
+	cmd.Env = append(os.Environ(), "GOOS="+targetOS, "GOARCH="+targetArch)
+
+	tags := buildTags
+	if targetOS == "darwin" {
+		tags = append(tags, "ios")
+	}
+	cmd.Args = append(cmd.Args, "-m", "-json", "-tags="+strings.Join(tags, ","), "all")
+	cmd.Dir = src
+
+	output, err := cmd.Output()
+	if err != nil {
+		// Module information is not available at src.
+		return nil, nil
+	}
+
+	type Module struct {
+		Path    string
+		Version string
+		Dir     string
+		Replace *Module
+	}
+
+	f := &modfile.File{}
+	f.AddModuleStmt("gobind")
+	e := json.NewDecoder(bytes.NewReader(output))
+	for {
+		var mod *Module
+		err := e.Decode(&mod)
+		if err != nil && err != io.EOF {
+			return nil, err
+		}
+		if mod != nil {
+			switch {
+			case mod.Replace != nil:
+				f.AddReplace(mod.Path, mod.Version, mod.Replace.Path, mod.Replace.Version)
+			case mod.Version == "":
+				// When the version part is empty, the module is local and mod.Dir represents the location.
+				f.AddReplace(mod.Path, "", mod.Dir, "")
+			default:
+				f.AddRequire(mod.Path, mod.Version)
+			}
+		}
+		if err == io.EOF {
+			break
+		}
+	}
+	return f, nil
+}
+
+// writeGoMod writes go.mod file at $WORK/src when Go modules are used.
+func writeGoMod(targetOS string, targetArch string) error {
+	m, err := areGoModulesUsed()
+	if err != nil {
+		return err
+	}
+	// If Go modules are not used, go.mod should not be created because the dependencies might not be compatible with Go modules.
+	if !m {
+		return nil
+	}
+
+	return writeFile(filepath.Join(tmpdir, "src", "go.mod"), func(w io.Writer) error {
+		f, err := getModuleVersions(targetOS, targetArch, ".")
+		if err != nil {
+			return err
+		}
+		if f == nil {
+			return nil
+		}
+		bs, err := f.Format()
+		if err != nil {
+			return err
+		}
+		if _, err := w.Write(bs); err != nil {
+			return err
+		}
+		return nil
+	})
+}
+
+func areGoModulesUsed() (bool, error) {
+	out, err := exec.Command(goBin(), "env", "GOMOD").Output()
+	if err != nil {
+		return false, err
+	}
+	outstr := strings.TrimSpace(string(out))
+	if outstr == "" {
+		return false, nil
+	}
+	return true, nil
+}
diff --git a/cmd/gomobile/bind_androidapp.go b/cmd/gomobile/bind_androidapp.go
index 31b2e56..8fe9179 100644
--- a/cmd/gomobile/bind_androidapp.go
+++ b/cmd/gomobile/bind_androidapp.go
@@ -54,12 +54,14 @@
 
 	// Generate binding code and java source code only when processing the first package.
 	for _, arch := range androidArchs {
+		if err := writeGoMod("android", arch); err != nil {
+			return err
+		}
+
 		env := androidEnv[arch]
 		// Add the generated packages to GOPATH for reverse bindings.
 		gopath := fmt.Sprintf("GOPATH=%s%c%s", tmpdir, filepath.ListSeparator, goEnv("GOPATH"))
 		env = append(env, gopath)
-		// gomobile-bind does not support modules yet.
-		env = append(env, "GO111MODULE=off")
 		toolchain := ndk.Toolchain(arch)
 
 		err := goBuildAt(
diff --git a/cmd/gomobile/bind_iosapp.go b/cmd/gomobile/bind_iosapp.go
index 924a474..f49a068 100644
--- a/cmd/gomobile/bind_iosapp.go
+++ b/cmd/gomobile/bind_iosapp.go
@@ -57,6 +57,10 @@
 	cmd = exec.Command("xcrun", "lipo", "-create")
 
 	for _, arch := range archs {
+		if err := writeGoMod("darwin", arch); err != nil {
+			return err
+		}
+
 		env := darwinEnv[arch]
 		// Add the generated packages to GOPATH for reverse bindings.
 		gopath := fmt.Sprintf("GOPATH=%s%c%s", tmpdir, filepath.ListSeparator, goEnv("GOPATH"))
@@ -178,8 +182,6 @@
 func goIOSBindArchive(name string, env []string, gosrc string) (string, error) {
 	arch := getenv(env, "GOARCH")
 	archive := filepath.Join(tmpdir, name+"-"+arch+".a")
-	// gobind-bind does not support modules yet.
-	env = append(env, "GO111MODULE=off")
 	err := goBuildAt(gosrc, "./gobind", env, "-buildmode=c-archive", "-o", archive)
 	if err != nil {
 		return "", err
diff --git a/cmd/gomobile/bind_test.go b/cmd/gomobile/bind_test.go
index d31c4fd..d6ca3f6 100644
--- a/cmd/gomobile/bind_test.go
+++ b/cmd/gomobile/bind_test.go
@@ -6,7 +6,9 @@
 
 import (
 	"bytes"
+	"io/ioutil"
 	"os"
+	"os/exec"
 	"path"
 	"path/filepath"
 	"runtime"
@@ -179,7 +181,8 @@
 var bindAndroidTmpl = template.Must(template.New("output").Parse(`GOMOBILE={{.GOPATH}}/pkg/gomobile
 WORK=$WORK
 GOOS=android CGO_ENABLED=1 gobind -lang=go,java -outdir=$WORK{{if .JavaPkg}} -javapkg={{.JavaPkg}}{{end}} golang.org/x/mobile/asset
-PWD=$WORK/src GOOS=android GOARCH=arm CC=$NDK_PATH/toolchains/llvm/prebuilt/{{.NDKARCH}}/bin/armv7a-linux-androideabi16-clang CXX=$NDK_PATH/toolchains/llvm/prebuilt/{{.NDKARCH}}/bin/armv7a-linux-androideabi16-clang++ CGO_ENABLED=1 GOARM=7 GOPATH=$WORK:$GOPATH GO111MODULE=off go build -x -buildmode=c-shared -o=$WORK/android/src/main/jniLibs/armeabi-v7a/libgojni.so ./gobind
+mkdir -p $WORK/src
+PWD=$WORK/src GOOS=android GOARCH=arm CC=$NDK_PATH/toolchains/llvm/prebuilt/{{.NDKARCH}}/bin/armv7a-linux-androideabi16-clang CXX=$NDK_PATH/toolchains/llvm/prebuilt/{{.NDKARCH}}/bin/armv7a-linux-androideabi16-clang++ CGO_ENABLED=1 GOARM=7 GOPATH=$WORK:$GOPATH go build -x -buildmode=c-shared -o=$WORK/android/src/main/jniLibs/armeabi-v7a/libgojni.so ./gobind
 PWD=$WORK/java javac -d $WORK/javac-output -source 1.7 -target 1.7 -bootclasspath {{.AndroidPlatform}}/android.jar *.java
 jar c -C $WORK/javac-output .
 `))
@@ -187,7 +190,8 @@
 var bindIOSTmpl = template.Must(template.New("output").Parse(`GOMOBILE={{.GOPATH}}/pkg/gomobile
 WORK=$WORK
 GOOS=darwin CGO_ENABLED=1 gobind -lang=go,objc -outdir=$WORK -tags=ios{{if .Prefix}} -prefix={{.Prefix}}{{end}} golang.org/x/mobile/asset
-PWD=$WORK/src GOARM=7 GOOS=darwin GOARCH=arm CC=iphoneos-clang CXX=iphoneos-clang++ CGO_CFLAGS=-isysroot=iphoneos -miphoneos-version-min=7.0 -fembed-bitcode -arch armv7 CGO_CXXFLAGS=-isysroot=iphoneos -miphoneos-version-min=7.0 -fembed-bitcode -arch armv7 CGO_LDFLAGS=-isysroot=iphoneos -miphoneos-version-min=7.0 -fembed-bitcode -arch armv7 CGO_ENABLED=1 GOPATH=$WORK:$GOPATH GO111MODULE=off go build -tags ios -x -buildmode=c-archive -o $WORK/asset-arm.a ./gobind
+mkdir -p $WORK/src
+PWD=$WORK/src GOARM=7 GOOS=darwin GOARCH=arm CC=iphoneos-clang CXX=iphoneos-clang++ CGO_CFLAGS=-isysroot=iphoneos -miphoneos-version-min=7.0 -fembed-bitcode -arch armv7 CGO_CXXFLAGS=-isysroot=iphoneos -miphoneos-version-min=7.0 -fembed-bitcode -arch armv7 CGO_LDFLAGS=-isysroot=iphoneos -miphoneos-version-min=7.0 -fembed-bitcode -arch armv7 CGO_ENABLED=1 GOPATH=$WORK:$GOPATH go build -tags ios -x -buildmode=c-archive -o $WORK/asset-arm.a ./gobind
 rm -r -f "Asset.framework"
 mkdir -p Asset.framework/Versions/A/Headers
 ln -s A Asset.framework/Versions/Current
@@ -207,3 +211,60 @@
 mkdir -p Asset.framework/Versions/A/Modules
 ln -s Versions/Current/Modules Asset.framework/Modules
 `))
+
+func TestBindWithGoModules(t *testing.T) {
+	if runtime.GOOS == "android" {
+		t.Skipf("gomobile and gobind are not available on %s", runtime.GOOS)
+	}
+
+	dir, err := ioutil.TempDir("", "")
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer os.RemoveAll(dir)
+
+	if out, err := exec.Command(goBin(), "build", "-o="+dir, "golang.org/x/mobile/cmd/gobind").CombinedOutput(); err != nil {
+		t.Fatalf("%v: %s", err, string(out))
+	}
+	if out, err := exec.Command(goBin(), "build", "-o="+dir, "golang.org/x/mobile/cmd/gomobile").CombinedOutput(); err != nil {
+		t.Fatalf("%v: %s", err, string(out))
+	}
+	path := dir
+	if p := os.Getenv("PATH"); p != "" {
+		path += string(filepath.ListSeparator) + p
+	}
+
+	for _, target := range []string{"android", "ios"} {
+		t.Run(target, func(t *testing.T) {
+			switch target {
+			case "android":
+				androidHome := os.Getenv("ANDROID_HOME")
+				if androidHome == "" {
+					t.Skip("ANDROID_HOME not found, skipping bind")
+				}
+				if _, err := androidAPIPath(); err != nil {
+					t.Skip("No android API platform found in $ANDROID_HOME, skipping bind")
+				}
+			case "ios":
+				if !xcodeAvailable() {
+					t.Skip("Xcode is missing")
+				}
+			}
+
+			var out string
+			switch target {
+			case "android":
+				out = filepath.Join(dir, "cgopkg.aar")
+			case "ios":
+				out = filepath.Join(dir, "Cgopkg.framework")
+			}
+			cmd := exec.Command(filepath.Join(dir, "gomobile"), "bind", "-target="+target, "-o="+out, "golang.org/x/mobile/bind/testdata/cgopkg")
+			cmd.Env = append(os.Environ(), "PATH="+path, "GO111MODULE=on")
+			var b bytes.Buffer
+			cmd.Stderr = &b
+			if err := cmd.Run(); err != nil {
+				t.Errorf("%v: %s", err, string(b.Bytes()))
+			}
+		})
+	}
+}
diff --git a/go.mod b/go.mod
index 13a3b3b..807e703 100644
--- a/go.mod
+++ b/go.mod
@@ -5,5 +5,6 @@
 require (
 	golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56
 	golang.org/x/image v0.0.0-20190802002840-cff245a6509b
-	golang.org/x/tools v0.0.0-20190909214602-067311248421
+	golang.org/x/mod v0.1.1-0.20191209134235-331c550502dd
+	golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e
 )
diff --git a/go.sum b/go.sum
index 664c53c..c387242 100644
--- a/go.sum
+++ b/go.sum
@@ -1,13 +1,17 @@
 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 h1:estk1glOnSVeJ9tdEZZc5mAMDZk5lNJNyJ6DvrBkTEU=
 golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4=
 golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
 golang.org/x/image v0.0.0-20190802002840-cff245a6509b h1:+qEpEAPhDZ1o0x3tHzZTQDArnOixOzGD9HUJfcg0mb4=
 golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
 golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
+golang.org/x/mod v0.1.0 h1:sfUMP1Gu8qASkorDVjnMuvgJzwFbTZSeXFiGBYAVdl4=
 golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
+golang.org/x/mod v0.1.1-0.20191209134235-331c550502dd h1:ePuNC7PZ6O5BzgPn9bZayERXBdfZjUYoXEf5BTfDfh8=
+golang.org/x/mod v0.1.1-0.20191209134235-331c550502dd/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -17,6 +21,8 @@
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
-golang.org/x/tools v0.0.0-20190909214602-067311248421 h1:NmmWqJbt02YJHmp4A4gBXvsXXIzzixjzE1y6PKUyIjk=
-golang.org/x/tools v0.0.0-20190909214602-067311248421/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e h1:aZzprAO9/8oim3qStq3wc1Xuxx4QmAGriC4VU4ojemQ=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898 h1:/atklqdjdhuosWIl6AIbOeHJjicWYPqR9bpxqxYG2pA=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=