cmd/gomobile: concurrent gomobile-bind building for Android

This speeds up gomobile-bind for Android by concurrent building for
each architecture.

Before this change (on my MacBook Pro 2020):

```
$ time go run ./cmd/gomobile/ bind -target android ./example/bind/hello/

real    0m22.555s
user    0m14.859s
sys     0m10.232s
```

After this change:

```
$ time go run ./cmd/gomobile/ bind -target android ./example/bind/hello/

real    0m9.404s
user    0m15.846s
sys     0m11.044s
```

For #54770

Change-Id: I5a709dd4422a569e9244e924bd43ad2da1ede164
Reviewed-on: https://go-review.googlesource.com/c/mobile/+/426274
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Bryan Mills <bcmills@google.com>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
Reviewed-by: Changkun Ou <mail@changkun.de>
Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org>
Run-TryBot: Hajime Hoshi <hajimehoshi@gmail.com>
diff --git a/cmd/gomobile/bind.go b/cmd/gomobile/bind.go
index c4552e3..54eaa4b 100644
--- a/cmd/gomobile/bind.go
+++ b/cmd/gomobile/bind.go
@@ -15,6 +15,7 @@
 	"os/exec"
 	"path/filepath"
 	"strings"
+	"sync"
 
 	"golang.org/x/mobile/internal/sdkpath"
 	"golang.org/x/mod/modfile"
@@ -282,7 +283,7 @@
 	return f, nil
 }
 
-// writeGoMod writes go.mod file at $WORK/src when Go modules are used.
+// writeGoMod writes go.mod file at dir when Go modules are used.
 func writeGoMod(dir, targetPlatform, targetArch string) error {
 	m, err := areGoModulesUsed()
 	if err != nil {
@@ -293,7 +294,7 @@
 		return nil
 	}
 
-	return writeFile(filepath.Join(dir, "src", "go.mod"), func(w io.Writer) error {
+	return writeFile(filepath.Join(dir, "go.mod"), func(w io.Writer) error {
 		f, err := getModuleVersions(targetPlatform, targetArch, ".")
 		if err != nil {
 			return err
@@ -312,14 +313,23 @@
 	})
 }
 
+var (
+	areGoModulesUsedResult struct {
+		used bool
+		err  error
+	}
+	areGoModulesUsedOnce sync.Once
+)
+
 func areGoModulesUsed() (bool, error) {
-	out, err := exec.Command("go", "env", "GOMOD").Output()
-	if err != nil {
-		return false, err
-	}
-	outstr := strings.TrimSpace(string(out))
-	if outstr == "" {
-		return false, nil
-	}
-	return true, nil
+	areGoModulesUsedOnce.Do(func() {
+		out, err := exec.Command("go", "env", "GOMOD").Output()
+		if err != nil {
+			areGoModulesUsedResult.err = err
+			return
+		}
+		outstr := strings.TrimSpace(string(out))
+		areGoModulesUsedResult.used = outstr != ""
+	})
+	return areGoModulesUsedResult.used, areGoModulesUsedResult.err
 }
diff --git a/cmd/gomobile/bind_androidapp.go b/cmd/gomobile/bind_androidapp.go
index a56fd82..3fa9cfa 100644
--- a/cmd/gomobile/bind_androidapp.go
+++ b/cmd/gomobile/bind_androidapp.go
@@ -15,6 +15,7 @@
 	"strings"
 
 	"golang.org/x/mobile/internal/sdkpath"
+	"golang.org/x/sync/errgroup"
 	"golang.org/x/tools/go/packages"
 )
 
@@ -52,41 +53,16 @@
 
 	androidDir := filepath.Join(tmpdir, "android")
 
-	modulesUsed, err := areGoModulesUsed()
-	if err != nil {
-		return err
-	}
-
 	// Generate binding code and java source code only when processing the first package.
+	var wg errgroup.Group
 	for _, t := range targets {
-		if err := writeGoMod(tmpdir, "android", t.arch); err != nil {
-			return err
-		}
-
-		env := androidEnv[t.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)
-
-		// Run `go mod tidy` to force to create go.sum.
-		// Without go.sum, `go build` fails as of Go 1.16.
-		if modulesUsed {
-			if err := goModTidyAt(filepath.Join(tmpdir, "src"), env); err != nil {
-				return err
-			}
-		}
-
-		toolchain := ndk.Toolchain(t.arch)
-		err := goBuildAt(
-			filepath.Join(tmpdir, "src"),
-			"./gobind",
-			env,
-			"-buildmode=c-shared",
-			"-o="+filepath.Join(androidDir, "src/main/jniLibs/"+toolchain.abi+"/libgojni.so"),
-		)
-		if err != nil {
-			return err
-		}
+		t := t
+		wg.Go(func() error {
+			return buildAndroidSO(androidDir, t.arch)
+		})
+	}
+	if err := wg.Wait(); err != nil {
+		return err
 	}
 
 	jsrc := filepath.Join(tmpdir, "java")
@@ -370,3 +346,56 @@
 	}
 	return jarw.Close()
 }
+
+// buildAndroidSO generates an Android libgojni.so file to outputDir.
+// buildAndroidSO is concurrent-safe.
+func buildAndroidSO(outputDir string, arch string) error {
+	// Copy the environment variables to make this function concurrent-safe.
+	env := make([]string, len(androidEnv[arch]))
+	copy(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)
+
+	modulesUsed, err := areGoModulesUsed()
+	if err != nil {
+		return err
+	}
+
+	srcDir := filepath.Join(tmpdir, "src")
+
+	if modulesUsed {
+		// Copy the source directory for each architecture for concurrent building.
+		newSrcDir := filepath.Join(tmpdir, "src-android-"+arch)
+		if !buildN {
+			if err := doCopyAll(newSrcDir, srcDir); err != nil {
+				return err
+			}
+		}
+		srcDir = newSrcDir
+
+		if err := writeGoMod(srcDir, "android", arch); err != nil {
+			return err
+		}
+
+		// Run `go mod tidy` to force to create go.sum.
+		// Without go.sum, `go build` fails as of Go 1.16.
+		if err := goModTidyAt(srcDir, env); err != nil {
+			return err
+		}
+	}
+
+	toolchain := ndk.Toolchain(arch)
+	if err := goBuildAt(
+		srcDir,
+		"./gobind",
+		env,
+		"-buildmode=c-shared",
+		"-o="+filepath.Join(outputDir, "src", "main", "jniLibs", toolchain.abi, "libgojni.so"),
+	); err != nil {
+		return err
+	}
+
+	return nil
+}
diff --git a/cmd/gomobile/bind_iosapp.go b/cmd/gomobile/bind_iosapp.go
index bf0f37d..b9beaea 100644
--- a/cmd/gomobile/bind_iosapp.go
+++ b/cmd/gomobile/bind_iosapp.go
@@ -93,7 +93,7 @@
 		gopath := fmt.Sprintf("GOPATH=%s%c%s", outDir, filepath.ListSeparator, goEnv("GOPATH"))
 		env = append(env, gopath)
 
-		if err := writeGoMod(outDir, t.platform, t.arch); err != nil {
+		if err := writeGoMod(filepath.Join(outDir, "src"), t.platform, t.arch); err != nil {
 			return err
 		}
 
diff --git a/cmd/gomobile/bind_test.go b/cmd/gomobile/bind_test.go
index cac3511..318ff62 100644
--- a/cmd/gomobile/bind_test.go
+++ b/cmd/gomobile/bind_test.go
@@ -187,9 +187,9 @@
 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
-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 mod tidy
-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
+mkdir -p $WORK/src-android-arm
+PWD=$WORK/src-android-arm 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 mod tidy
+PWD=$WORK/src-android-arm 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 .
 `))
diff --git a/go.mod b/go.mod
index 765a365..fd0ee23 100644
--- a/go.mod
+++ b/go.mod
@@ -6,5 +6,6 @@
 	golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56
 	golang.org/x/image v0.0.0-20190802002840-cff245a6509b
 	golang.org/x/mod v0.4.2
+	golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde
 	golang.org/x/tools v0.1.8-0.20211022200916-316ba0b74098
 )
diff --git a/go.sum b/go.sum
index d4d3ebf..b20722d 100644
--- a/go.sum
+++ b/go.sum
@@ -18,6 +18,8 @@
 golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde h1:ejfdSekXMDxDLbRrJMwUk6KnSLZ2McaUCVcIKM+N6jc=
+golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=