cmd/gomobile: bind allows custom package path/prefix.

New option -javapkg for -target=android, and -prefix for -target=ios.
Fixes golang/go#9660.

Change-Id: I9143f30672672527876524b38f450629452a3161
Reviewed-on: https://go-review.googlesource.com/14023
Reviewed-by: David Crawshaw <crawshaw@golang.org>
diff --git a/cmd/gomobile/bind.go b/cmd/gomobile/bind.go
index f1f486f..f76cac6 100644
--- a/cmd/gomobile/bind.go
+++ b/cmd/gomobile/bind.go
@@ -49,10 +49,12 @@
 (File > Project Structure > Dependencies).  This requires 'javac'
 (version 1.7+) and Android SDK (API level 9 or newer) to build the
 library for Android. The environment variable ANDROID_HOME must be set
-to the path to Android SDK.
+to the path to Android SDK. The generated Java class is in the java
+package 'go.<package_name>' unless -javapkg flag is specified.
 
 For -target ios, gomobile must be run on an OS X machine with Xcode
-installed. Support is not complete.
+installed. Support is not complete. The generated Objective-C types
+are prefixed with 'Go' unless the -prefix flag is provided.
 
 The -v flag provides verbose output, including the list of packages built.
 
@@ -80,6 +82,13 @@
 		return fmt.Errorf(`unknown -target, %q.`, buildTarget)
 	}
 
+	if bindJavaPkg != "" && ctx.GOOS != "android" {
+		return fmt.Errorf("-javapkg is supported only for android target")
+	}
+	if bindPrefix != "" && ctx.GOOS != "darwin" {
+		return fmt.Errorf("-prefix is supported only for ios target")
+	}
+
 	var pkg *build.Package
 	switch len(args) {
 	case 0:
@@ -104,6 +113,19 @@
 	}
 }
 
+var (
+	bindPrefix  string // -prefix
+	bindJavaPkg string // -javapkg
+)
+
+func init() {
+	// bind command specific commands.
+	cmdBind.flag.StringVar(&bindJavaPkg, "javapkg", "",
+		"specifies custom Java package path used instead of the default 'go.<go package name>'. Valid only with -target=android.")
+	cmdBind.flag.StringVar(&bindPrefix, "prefix", "",
+		"custom Objective-C name prefix used instead of the default 'Go'. Valid only with -lang=ios.")
+}
+
 type binder struct {
 	files []*ast.File
 	fset  *token.FileSet
@@ -112,23 +134,27 @@
 
 func (b *binder) GenObjc(outdir string) error {
 	name := strings.Title(b.pkg.Name())
-	mfile := filepath.Join(outdir, "Go"+name+".m")
-	hfile := filepath.Join(outdir, "Go"+name+".h")
-
-	if buildX {
-		printcmd("gobind -lang=objc %s > %s", b.pkg.Path(), mfile)
+	bindOption := "-lang=objc"
+	prefix := "Go"
+	if bindPrefix != "" {
+		prefix = bindPrefix
+		bindOption += " -prefix=" + bindPrefix
 	}
 
-	const objcPrefix = "" // TODO(hyangah): -prefix
+	mfile := filepath.Join(outdir, prefix+name+".m")
+	hfile := filepath.Join(outdir, prefix+name+".h")
 
 	generate := func(w io.Writer) error {
-		return bind.GenObjc(w, b.fset, b.pkg, objcPrefix, false)
+		if buildX {
+			printcmd("gobind %s -outdir=%s %s", bindOption, outdir, b.pkg.Path())
+		}
+		return bind.GenObjc(w, b.fset, b.pkg, prefix, false)
 	}
 	if err := writeFile(mfile, generate); err != nil {
 		return err
 	}
 	generate = func(w io.Writer) error {
-		return bind.GenObjc(w, b.fset, b.pkg, objcPrefix, true)
+		return bind.GenObjc(w, b.fset, b.pkg, prefix, true)
 	}
 	if err := writeFile(hfile, generate); err != nil {
 		return err
@@ -144,14 +170,16 @@
 func (b *binder) GenJava(outdir string) error {
 	className := strings.Title(b.pkg.Name())
 	javaFile := filepath.Join(outdir, className+".java")
-
-	if buildX {
-		printcmd("gobind -lang=java %s > %s", b.pkg.Path(), javaFile)
+	bindOption := "-lang=java"
+	if bindJavaPkg != "" {
+		bindOption += " -javapkg=" + bindJavaPkg
 	}
 
-	const javaPkg = "" // TODO(hyangah): -javapkg
 	generate := func(w io.Writer) error {
-		return bind.GenJava(w, b.fset, b.pkg, javaPkg)
+		if buildX {
+			printcmd("gobind %s -outdir=%s %s", bindOption, outdir, b.pkg.Path())
+		}
+		return bind.GenJava(w, b.fset, b.pkg, bindJavaPkg)
 	}
 	if err := writeFile(javaFile, generate); err != nil {
 		return err
@@ -161,13 +189,13 @@
 
 func (b *binder) GenGo(outdir string) error {
 	pkgName := "go_" + b.pkg.Name()
-	goFile := filepath.Join(outdir, pkgName, pkgName+"main.go")
-
-	if buildX {
-		printcmd("gobind -lang=go %s > %s", b.pkg.Path(), goFile)
-	}
+	outdir = filepath.Join(outdir, pkgName)
+	goFile := filepath.Join(outdir, pkgName+"main.go")
 
 	generate := func(w io.Writer) error {
+		if buildX {
+			printcmd("gobind -lang=go -outdir=%s %s", outdir, b.pkg.Path())
+		}
 		return bind.GenGo(w, b.fset, b.pkg)
 	}
 	if err := writeFile(goFile, generate); err != nil {
diff --git a/cmd/gomobile/bind_androidapp.go b/cmd/gomobile/bind_androidapp.go
index 5a639be..667066a 100644
--- a/cmd/gomobile/bind_androidapp.go
+++ b/cmd/gomobile/bind_androidapp.go
@@ -58,8 +58,11 @@
 	}
 	repo := filepath.Clean(filepath.Join(p.Dir, "..")) // golang.org/x/mobile directory.
 
-	// TODO(crawshaw): use a better package path derived from the go package.
-	if err := binder.GenJava(filepath.Join(androidDir, "src/main/java/go/"+binder.pkg.Name())); err != nil {
+	pkgpath := strings.Replace(bindJavaPkg, ".", "/", -1)
+	if bindJavaPkg == "" {
+		pkgpath = "go/" + binder.pkg.Name()
+	}
+	if err := binder.GenJava(filepath.Join(androidDir, "src/main/java/"+pkgpath)); err != nil {
 		return err
 	}
 
diff --git a/cmd/gomobile/bind_test.go b/cmd/gomobile/bind_test.go
index 9964d7e..5ab697d 100644
--- a/cmd/gomobile/bind_test.go
+++ b/cmd/gomobile/bind_test.go
@@ -8,6 +8,7 @@
 	"bytes"
 	"os"
 	"path/filepath"
+	"strings"
 	"testing"
 	"text/template"
 )
@@ -15,52 +16,100 @@
 // TODO(crawshaw): TestBindIOS
 
 func TestBindAndroid(t *testing.T) {
-	if os.Getenv("ANDROID_HOME") == "" {
+	androidHome := os.Getenv("ANDROID_HOME")
+	if androidHome == "" {
 		t.Skip("ANDROID_HOME not found, skipping bind")
 	}
+	platform, err := androidAPIPath()
+	if err != nil {
+		t.Skip("No android API platform found in $ANDROID_HOME, skipping bind")
+	}
+	platform = strings.Replace(platform, androidHome, "$ANDROID_HOME", -1)
 
-	buf := new(bytes.Buffer)
 	defer func() {
 		xout = os.Stderr
 		buildN = false
 		buildX = false
+		buildO = ""
+		buildTarget = ""
 	}()
-	xout = buf
 	buildN = true
 	buildX = true
 	buildO = "asset.aar"
 	buildTarget = "android"
-	gopath = filepath.SplitList(os.Getenv("GOPATH"))[0]
-	if goos == "windows" {
-		os.Setenv("HOMEDRIVE", "C:")
-	}
-	cmdBind.flag.Parse([]string{"golang.org/x/mobile/asset"})
-	err := runBind(cmdBind)
-	if err != nil {
-		t.Log(buf.String())
-		t.Fatal(err)
-	}
 
-	diff, err := diffOutput(buf.String(), bindAndroidTmpl)
-	if err != nil {
-		t.Fatalf("computing diff failed: %v", err)
+	tests := []struct {
+		javaPkg    string
+		wantGobind string
+		wantPkgDir string
+	}{
+		{
+			wantGobind: "gobind -lang=java",
+			wantPkgDir: "go/asset",
+		},
+		{
+			javaPkg:    "com.example.foo",
+			wantGobind: "gobind -lang=java -javapkg=com.example.foo",
+			wantPkgDir: "com/example/foo",
+		},
 	}
-	if diff != "" {
-		t.Errorf("unexpected output:\n%s", diff)
+	for _, tc := range tests {
+		bindJavaPkg = tc.javaPkg
+
+		buf := new(bytes.Buffer)
+		xout = buf
+		gopath = filepath.SplitList(os.Getenv("GOPATH"))[0]
+		if goos == "windows" {
+			os.Setenv("HOMEDRIVE", "C:")
+		}
+		cmdBind.flag.Parse([]string{"golang.org/x/mobile/asset"})
+		err := runBind(cmdBind)
+		if err != nil {
+			t.Log(buf.String())
+			t.Fatal(err)
+		}
+		got := filepath.ToSlash(buf.String())
+
+		data := struct {
+			outputData
+			AndroidPlatform string
+			GobindJavaCmd   string
+			JavaPkgDir      string
+		}{
+			outputData:      defaultOutputData(),
+			AndroidPlatform: platform,
+			GobindJavaCmd:   tc.wantGobind,
+			JavaPkgDir:      tc.wantPkgDir,
+		}
+
+		wantBuf := new(bytes.Buffer)
+		if err := bindAndroidTmpl.Execute(wantBuf, data); err != nil {
+			t.Errorf("%+v: computing diff failed: %v", tc, err)
+			continue
+		}
+
+		diff, err := diff(got, wantBuf.String())
+		if err != nil {
+			t.Errorf("%+v: computing diff failed: %v", tc, err)
+			continue
+		}
+		if diff != "" {
+			t.Errorf("%+v: unexpected output:\n%s", tc, diff)
+		}
 	}
 }
 
 var bindAndroidTmpl = template.Must(template.New("output").Parse(`GOMOBILE={{.GOPATH}}/pkg/gomobile
 WORK=$WORK
-gobind -lang=go golang.org/x/mobile/asset > $WORK/go_asset/go_assetmain.go
 mkdir -p $WORK/go_asset
+gobind -lang=go -outdir=$WORK/go_asset golang.org/x/mobile/asset
 mkdir -p $WORK/androidlib
 GOOS=android GOARCH=arm GOARM=7 CC=$GOMOBILE/android-{{.NDK}}/arm/bin/arm-linux-androideabi-gcc{{.EXE}} CXX=$GOMOBILE/android-{{.NDK}}/arm/bin/arm-linux-androideabi-g++{{.EXE}} CGO_ENABLED=1 go build -p={{.NumCPU}} -pkgdir=$GOMOBILE/pkg_android_arm -tags="" -x -buildmode=c-shared -o=$WORK/android/src/main/jniLibs/armeabi-v7a/libgojni.so $WORK/androidlib/main.go
-gobind -lang=java golang.org/x/mobile/asset > $WORK/android/src/main/java/go/asset/Asset.java
-mkdir -p $WORK/android/src/main/java/go/asset
+mkdir -p $WORK/android/src/main/java/{{.JavaPkgDir}}
+{{.GobindJavaCmd}} -outdir=$WORK/android/src/main/java/{{.JavaPkgDir}} golang.org/x/mobile/asset
 mkdir -p $WORK/android/src/main/java/go
 rm $WORK/android/src/main/java/go/Seq.java
 ln -s $GOPATH/src/golang.org/x/mobile/bind/java/Seq.java $WORK/android/src/main/java/go/Seq.java
-PWD=$WORK/android/src/main/java javac -d $WORK/javac-output -source 1.7 -target 1.7 -bootclasspath $ANDROID_HOME/platforms/android-22/android.jar *.java
+PWD=$WORK/android/src/main/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/cmd/gomobile/init_test.go b/cmd/gomobile/init_test.go
index e122fb6..8d73a28 100644
--- a/cmd/gomobile/init_test.go
+++ b/cmd/gomobile/init_test.go
@@ -64,20 +64,7 @@
 	got = filepath.ToSlash(got)
 
 	wantBuf := new(bytes.Buffer)
-	data := outputData{
-		NDK:       ndkVersion,
-		GOOS:      goos,
-		GOARCH:    goarch,
-		GOPATH:    gopath,
-		NDKARCH:   ndkarch,
-		Xproj:     projPbxproj,
-		Xcontents: contentsJSON,
-		Xinfo:     infoplistTmplData{BundleID: "org.golang.todo.basic", Name: "Basic"},
-		NumCPU:    strconv.Itoa(runtime.NumCPU()),
-	}
-	if goos == "windows" {
-		data.EXE = ".exe"
-	}
+	data := defaultOutputData()
 	if err := wantTmpl.Execute(wantBuf, data); err != nil {
 		return "", err
 	}
@@ -101,6 +88,24 @@
 	NumCPU    string
 }
 
+func defaultOutputData() outputData {
+	data := outputData{
+		NDK:       ndkVersion,
+		GOOS:      goos,
+		GOARCH:    goarch,
+		GOPATH:    gopath,
+		NDKARCH:   ndkarch,
+		Xproj:     projPbxproj,
+		Xcontents: contentsJSON,
+		Xinfo:     infoplistTmplData{BundleID: "org.golang.todo.basic", Name: "Basic"},
+		NumCPU:    strconv.Itoa(runtime.NumCPU()),
+	}
+	if goos == "windows" {
+		data.EXE = ".exe"
+	}
+	return data
+}
+
 var initTmpl = template.Must(template.New("output").Parse(`GOMOBILE={{.GOPATH}}/pkg/gomobile
 mkdir -p $GOMOBILE/android-{{.NDK}}
 WORK={{.GOPATH}}/pkg/gomobile/work