bind: Android support for multiple packages.

As discussed in golang/go#12245

Usage: gomobile bind [options] a.b.c x.y.z

For java gobind and gomobile  will generate go.c.C.java and go.z.Z.java.
If -javapkg=com.example is specified they will generate
com.example.C.java and com.example.Z.java.

Tested on Darwin.

Change-Id: Ia8e57c8fec7967131d55de71cc705d9e736ccca0
Reviewed-on: https://go-review.googlesource.com/17023
Reviewed-by: Hyang-Ah Hana Kim <hyangah@gmail.com>
diff --git a/cmd/gomobile/bind.go b/cmd/gomobile/bind.go
index 4254076..ba76fab 100644
--- a/cmd/gomobile/bind.go
+++ b/cmd/gomobile/bind.go
@@ -87,15 +87,13 @@
 		return fmt.Errorf("-prefix is supported only for ios target")
 	}
 
-	var pkg *build.Package
+	var pkgs []*build.Package
 	switch len(args) {
 	case 0:
-		pkg, err = ctx.ImportDir(cwd, build.ImportComment)
-	case 1:
-		pkg, err = ctx.Import(args[0], cwd, build.ImportComment)
+		pkgs = make([]*build.Package, 1)
+		pkgs[0], err = ctx.ImportDir(cwd, build.ImportComment)
 	default:
-		cmd.usage()
-		os.Exit(1)
+		pkgs, err = importPackages(args)
 	}
 	if err != nil {
 		return err
@@ -103,14 +101,28 @@
 
 	switch buildTarget {
 	case "android":
-		return goAndroidBind(pkg)
+		return goAndroidBind(pkgs)
 	case "ios":
-		return goIOSBind(pkg)
+		if len(pkgs) > 1 {
+			return fmt.Errorf("binding multiple packages not supported for ios")
+		}
+		return goIOSBind(pkgs)
 	default:
 		return fmt.Errorf(`unknown -target, %q.`, buildTarget)
 	}
 }
 
+func importPackages(args []string) ([]*build.Package, error) {
+	pkgs := make([]*build.Package, len(args))
+	for i, path := range args {
+		var err error
+		if pkgs[i], err = ctx.Import(path, cwd, build.ImportComment); err != nil {
+			return nil, fmt.Errorf("package %q: %v", path, err)
+		}
+	}
+	return pkgs, nil
+}
+
 var (
 	bindPrefix  string // -prefix
 	bindJavaPkg string // -javapkg
@@ -127,7 +139,7 @@
 type binder struct {
 	files []*ast.File
 	fset  *token.FileSet
-	pkg   *types.Package
+	pkgs  []*types.Package
 }
 
 func (b *binder) GenObjc(outdir string) error {
@@ -135,7 +147,7 @@
 	if bindPrefix == "" {
 		bindPrefix = bindPrefixDefault
 	}
-	name := strings.Title(b.pkg.Name())
+	name := strings.Title(b.pkgs[0].Name())
 	bindOption := "-lang=objc"
 	if bindPrefix != bindPrefixDefault {
 		bindOption += " -prefix=" + bindPrefix
@@ -146,12 +158,12 @@
 
 	generate := func(w io.Writer) error {
 		if buildX {
-			printcmd("gobind %s -outdir=%s %s", bindOption, outdir, b.pkg.Path())
+			printcmd("gobind %s -outdir=%s %s", bindOption, outdir, b.pkgs[0].Path())
 		}
 		if buildN {
 			return nil
 		}
-		return bind.GenObjc(w, b.fset, b.pkg, bindPrefix, false)
+		return bind.GenObjc(w, b.fset, b.pkgs[0], bindPrefix, false)
 	}
 	if err := writeFile(mfile, generate); err != nil {
 		return err
@@ -160,7 +172,7 @@
 		if buildN {
 			return nil
 		}
-		return bind.GenObjc(w, b.fset, b.pkg, bindPrefix, true)
+		return bind.GenObjc(w, b.fset, b.pkgs[0], bindPrefix, true)
 	}
 	if err := writeFile(hfile, generate); err != nil {
 		return err
@@ -173,8 +185,8 @@
 	return copyFile(filepath.Join(outdir, "seq.h"), filepath.Join(objcPkg.Dir, "seq.h"))
 }
 
-func (b *binder) GenJava(outdir string) error {
-	className := strings.Title(b.pkg.Name())
+func (b *binder) GenJava(pkg *types.Package, outdir string) error {
+	className := strings.Title(pkg.Name())
 	javaFile := filepath.Join(outdir, className+".java")
 	bindOption := "-lang=java"
 	if bindJavaPkg != "" {
@@ -183,12 +195,12 @@
 
 	generate := func(w io.Writer) error {
 		if buildX {
-			printcmd("gobind %s -outdir=%s %s", bindOption, outdir, b.pkg.Path())
+			printcmd("gobind %s -outdir=%s %s", bindOption, outdir, pkg.Path())
 		}
 		if buildN {
 			return nil
 		}
-		return bind.GenJava(w, b.fset, b.pkg, bindJavaPkg)
+		return bind.GenJava(w, b.fset, pkg, bindJavaPkg)
 	}
 	if err := writeFile(javaFile, generate); err != nil {
 		return err
@@ -196,19 +208,19 @@
 	return nil
 }
 
-func (b *binder) GenGo(outdir string) error {
-	pkgName := "go_" + b.pkg.Name()
+func (b *binder) GenGo(pkg *types.Package, outdir string) error {
+	pkgName := "go_" + pkg.Name()
 	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())
+			printcmd("gobind -lang=go -outdir=%s %s", outdir, pkg.Path())
 		}
 		if buildN {
 			return nil
 		}
-		return bind.GenGo(w, b.fset, b.pkg)
+		return bind.GenGo(w, b.fset, pkg)
 	}
 	if err := writeFile(goFile, generate); err != nil {
 		return err
@@ -264,11 +276,15 @@
 	return generate(f)
 }
 
-func loadExportData(importPath string, env []string, args ...string) (*types.Package, error) {
+func loadExportData(pkgs []*build.Package, env []string, args ...string) ([]*types.Package, error) {
 	// Compile the package. This will produce good errors if the package
 	// doesn't typecheck for some reason, and is a necessary step to
 	// building the final output anyway.
-	if err := goInstall(importPath, env, args...); err != nil {
+	paths := make([]string, len(pkgs))
+	for i, p := range pkgs {
+		paths[i] = p.ImportPath
+	}
+	if err := goInstall(paths, env, args...); err != nil {
 		return nil, err
 	}
 
@@ -284,33 +300,40 @@
 	if err := mkdir(filepath.Join(fakegopath, "pkg")); err != nil {
 		return nil, err
 	}
-	src := filepath.Join(pkgdir(env), importPath+".a")
-	dst := filepath.Join(fakegopath, "pkg/"+getenv(env, "GOOS")+"_"+getenv(env, "GOARCH")+"/"+importPath+".a")
-	if err := copyFile(dst, src); err != nil {
-		return nil, err
+	typePkgs := make([]*types.Package, len(pkgs))
+	for i, p := range pkgs {
+		importPath := p.ImportPath
+		src := filepath.Join(pkgdir(env), importPath+".a")
+		dst := filepath.Join(fakegopath, "pkg/"+getenv(env, "GOOS")+"_"+getenv(env, "GOARCH")+"/"+importPath+".a")
+		if err := copyFile(dst, src); err != nil {
+			return nil, err
+		}
+		if buildN {
+			typePkgs[i] = types.NewPackage(importPath, path.Base(importPath))
+			continue
+		}
+		oldDefault := build.Default
+		build.Default = ctx // copy
+		build.Default.GOPATH = fakegopath
+		p, err := importer.Default().Import(importPath)
+		build.Default = oldDefault
+		if err != nil {
+			return nil, err
+		}
+		typePkgs[i] = p
 	}
-	if buildN {
-		return types.NewPackage(importPath, path.Base(importPath)), nil
-	}
-	oldDefault := build.Default
-	build.Default = ctx // copy
-	build.Default.GOPATH = fakegopath
-	p, err := importer.Default().Import(importPath)
-	build.Default = oldDefault
-	if err != nil {
-		return nil, err
-	}
-
-	return p, nil
+	return typePkgs, nil
 }
 
-func newBinder(p *types.Package) (*binder, error) {
-	if p.Name() == "main" {
-		return nil, fmt.Errorf("package %q: can only bind a library package", p.Name())
+func newBinder(pkgs []*types.Package) (*binder, error) {
+	for _, pkg := range pkgs {
+		if pkg.Name() == "main" {
+			return nil, fmt.Errorf("package %q (%q): can only bind a library package", pkg.Name(), pkg.Path())
+		}
 	}
 	b := &binder{
 		fset: token.NewFileSet(),
-		pkg:  p,
+		pkgs: pkgs,
 	}
 	return b, nil
 }
diff --git a/cmd/gomobile/bind_androidapp.go b/cmd/gomobile/bind_androidapp.go
index 2003dc6..0694f38 100644
--- a/cmd/gomobile/bind_androidapp.go
+++ b/cmd/gomobile/bind_androidapp.go
@@ -18,7 +18,7 @@
 	"text/template"
 )
 
-func goAndroidBind(pkg *build.Package) error {
+func goAndroidBind(pkgs []*build.Package) error {
 	if sdkDir := os.Getenv("ANDROID_HOME"); sdkDir == "" {
 		return fmt.Errorf("this command requires ANDROID_HOME environment variable (path to the Android SDK)")
 	}
@@ -28,23 +28,25 @@
 		// https://golang.org/issue/13234.
 		androidArgs = []string{"-gcflags=-shared", "-ldflags=-shared"}
 	}
-	typesPkg, err := loadExportData(pkg.ImportPath, androidArmEnv, androidArgs...)
+	typesPkgs, err := loadExportData(pkgs, androidArmEnv, androidArgs...)
 	if err != nil {
 		return err
 	}
 
-	binder, err := newBinder(typesPkg)
+	binder, err := newBinder(typesPkgs)
 	if err != nil {
 		return err
 	}
 
-	if err := binder.GenGo(tmpdir); err != nil {
-		return err
+	for _, pkg := range typesPkgs {
+		if err := binder.GenGo(pkg, tmpdir); err != nil {
+			return err
+		}
 	}
 
 	mainFile := filepath.Join(tmpdir, "androidlib/main.go")
 	err = writeFile(mainFile, func(w io.Writer) error {
-		return androidMainTmpl.Execute(w, "../go_"+binder.pkg.Name())
+		return androidMainTmpl.Execute(w, binder.pkgs)
 	})
 	if err != nil {
 		return fmt.Errorf("failed to create the main package for android: %v", err)
@@ -68,12 +70,14 @@
 	}
 	repo := filepath.Clean(filepath.Join(p.Dir, "..")) // golang.org/x/mobile directory.
 
-	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
+	for _, pkg := range binder.pkgs {
+		pkgpath := strings.Replace(bindJavaPkg, ".", "/", -1)
+		if bindJavaPkg == "" {
+			pkgpath = "go/" + pkg.Name()
+		}
+		if err := binder.GenJava(pkg, filepath.Join(androidDir, "src/main/java/"+pkgpath)); err != nil {
+			return err
+		}
 	}
 
 	dst := filepath.Join(androidDir, "src/main/java/go/LoadJNI.java")
@@ -92,7 +96,7 @@
 		return err
 	}
 
-	return buildAAR(androidDir, pkg)
+	return buildAAR(androidDir, pkgs)
 }
 
 var loadSrc = `package go;
@@ -109,7 +113,8 @@
 
 import (
 	_ "golang.org/x/mobile/bind/java"
-	_ "{{.}}"
+{{range .}}	_ "../go_{{.Name}}"
+{{end}}
 )
 
 func main() {}
@@ -133,10 +138,10 @@
 //	aidl (optional, not relevant)
 //
 // javac and jar commands are needed to build classes.jar.
-func buildAAR(androidDir string, pkg *build.Package) (err error) {
+func buildAAR(androidDir string, pkgs []*build.Package) (err error) {
 	var out io.Writer = ioutil.Discard
 	if buildO == "" {
-		buildO = pkg.Name + ".aar"
+		buildO = pkgs[0].Name + ".aar"
 	}
 	if !strings.HasSuffix(buildO, ".aar") {
 		return fmt.Errorf("output file name %q does not end in '.aar'", buildO)
@@ -167,7 +172,7 @@
 	}
 	const manifestFmt = `<manifest xmlns:android="http://schemas.android.com/apk/res/android" package=%q>
 <uses-sdk android:minSdkVersion="%d"/></manifest>`
-	fmt.Fprintf(w, manifestFmt, "go."+pkg.Name+".gojni", minAndroidAPI)
+	fmt.Fprintf(w, manifestFmt, "go."+pkgs[0].Name+".gojni", minAndroidAPI)
 
 	w, err = aarwcreate("proguard.txt")
 	if err != nil {
@@ -184,39 +189,47 @@
 		return err
 	}
 
-	assetsDir := filepath.Join(pkg.Dir, "assets")
-	assetsDirExists := false
-	if fi, err := os.Stat(assetsDir); err == nil {
-		assetsDirExists = fi.IsDir()
-	} else if !os.IsNotExist(err) {
-		return err
-	}
-
-	if assetsDirExists {
-		err := filepath.Walk(
-			assetsDir, func(path string, info os.FileInfo, err error) error {
-				if err != nil {
-					return err
-				}
-				if info.IsDir() {
-					return nil
-				}
-				f, err := os.Open(path)
-				if err != nil {
-					return err
-				}
-				defer f.Close()
-				name := "assets/" + path[len(assetsDir)+1:]
-				w, err := aarwcreate(name)
-				if err != nil {
-					return nil
-				}
-				_, err = io.Copy(w, f)
-				return err
-			})
-		if err != nil {
+	files := map[string]string{}
+	for _, pkg := range pkgs {
+		assetsDir := filepath.Join(pkg.Dir, "assets")
+		assetsDirExists := false
+		if fi, err := os.Stat(assetsDir); err == nil {
+			assetsDirExists = fi.IsDir()
+		} else if !os.IsNotExist(err) {
 			return err
 		}
+
+		if assetsDirExists {
+			err := filepath.Walk(
+				assetsDir, func(path string, info os.FileInfo, err error) error {
+					if err != nil {
+						return err
+					}
+					if info.IsDir() {
+						return nil
+					}
+					f, err := os.Open(path)
+					if err != nil {
+						return err
+					}
+					defer f.Close()
+					name := "assets/" + path[len(assetsDir)+1:]
+					if orig, exists := files[name]; exists {
+						return fmt.Errorf("package %s asset name conflict: %s already added from package %s",
+							pkg.ImportPath, name, orig)
+					}
+					files[name] = pkg.ImportPath
+					w, err := aarwcreate(name)
+					if err != nil {
+						return nil
+					}
+					_, err = io.Copy(w, f)
+					return err
+				})
+			if err != nil {
+				return err
+			}
+		}
 	}
 
 	lib := "armeabi-v7a/libgojni.so"
diff --git a/cmd/gomobile/bind_iosapp.go b/cmd/gomobile/bind_iosapp.go
index 85c999f..9947334 100644
--- a/cmd/gomobile/bind_iosapp.go
+++ b/cmd/gomobile/bind_iosapp.go
@@ -15,17 +15,17 @@
 	"text/template"
 )
 
-func goIOSBind(pkg *build.Package) error {
-	typesPkg, err := loadExportData(pkg.ImportPath, darwinArmEnv)
+func goIOSBind(pkgs []*build.Package) error {
+	typesPkgs, err := loadExportData(pkgs, darwinArmEnv)
 	if err != nil {
 		return err
 	}
 
-	binder, err := newBinder(typesPkg)
+	binder, err := newBinder(typesPkgs)
 	if err != nil {
 		return err
 	}
-	name := binder.pkg.Name()
+	name := binder.pkgs[0].Name()
 	title := strings.Title(name)
 
 	if buildO != "" && !strings.HasSuffix(buildO, ".framework") {
@@ -35,7 +35,7 @@
 		buildO = title + ".framework"
 	}
 
-	if err := binder.GenGo(filepath.Join(tmpdir, "src")); err != nil {
+	if err := binder.GenGo(binder.pkgs[0], filepath.Join(tmpdir, "src")); err != nil {
 		return err
 	}
 	mainFile := filepath.Join(tmpdir, "src/iosbin/main.go")
diff --git a/cmd/gomobile/build.go b/cmd/gomobile/build.go
index d8e7523..052a959 100644
--- a/cmd/gomobile/build.go
+++ b/cmd/gomobile/build.go
@@ -248,14 +248,14 @@
 }
 
 func goBuild(src string, env []string, args ...string) error {
-	return goCmd("build", src, env, args...)
+	return goCmd("build", []string{src}, env, args...)
 }
 
-func goInstall(src string, env []string, args ...string) error {
-	return goCmd("install", src, env, args...)
+func goInstall(srcs []string, env []string, args ...string) error {
+	return goCmd("install", srcs, env, args...)
 }
 
-func goCmd(subcmd, src string, env []string, args ...string) error {
+func goCmd(subcmd string, srcs []string, env []string, args ...string) error {
 	// The -p flag is to speed up darwin/arm builds.
 	// Remove when golang.org/issue/10477 is resolved.
 	cmd := exec.Command(
@@ -284,7 +284,7 @@
 		cmd.Args = append(cmd.Args, "-work")
 	}
 	cmd.Args = append(cmd.Args, args...)
-	cmd.Args = append(cmd.Args, src)
+	cmd.Args = append(cmd.Args, srcs...)
 	cmd.Env = append([]string{}, env...)
 	return runCmd(cmd)
 }