cmd/gomobile: add the -bundleid flag

The current gomobile build command fails when attempting to sign the application
indicating that a development team should be selected. These changes fix that
by auto-detecting the developer team ID and setting the
"allowProvisioningUpdates" flag on the xcodebuild command. The bundle ID of
the application is also allowed to be changed via another command line
parameter "bundleid".

Fixes golang/go#17407

Change-Id: Ib0b878424a95a0cd49f3655ed4de56b2b91ff7a0
Reviewed-on: https://go-review.googlesource.com/77070
Reviewed-by: Elias Naur <elias.naur@gmail.com>
diff --git a/cmd/gomobile/build.go b/cmd/gomobile/build.go
index 423da7f..7543a2b 100644
--- a/cmd/gomobile/build.go
+++ b/cmd/gomobile/build.go
@@ -24,7 +24,7 @@
 var cmdBuild = &command{
 	run:   runBuild,
 	Name:  "build",
-	Usage: "[-target android|ios] [-o output] [build flags] [package]",
+	Usage: "[-target android|ios] [-o output] [-bundleid bundleId] [build flags] [package]",
 	Short: "compile android APK and iOS app",
 	Long: `
 Build compiles and encodes the app named by the import path.
@@ -47,6 +47,9 @@
 If the package directory contains an assets subdirectory, its contents
 are copied into the output.
 
+The -bundleid flag is for -target ios only and sets the bundle ID to use 
+with the app; defaults to "org.golang.todo".
+
 The -o flag specifies the output file name. If not specified, the
 output file name depends on the package built.
 
@@ -118,7 +121,7 @@
 			}
 			return goBuild(pkg.ImportPath, darwinArm64Env)
 		}
-		nmpkgs, err = goIOSBuild(pkg)
+		nmpkgs, err = goIOSBuild(pkg, buildBundleID)
 		if err != nil {
 			return err
 		}
@@ -205,16 +208,17 @@
 
 // "Build flags", used by multiple commands.
 var (
-	buildA       bool   // -a
-	buildI       bool   // -i
-	buildN       bool   // -n
-	buildV       bool   // -v
-	buildX       bool   // -x
-	buildO       string // -o
-	buildGcflags string // -gcflags
-	buildLdflags string // -ldflags
-	buildTarget  string // -target
-	buildWork    bool   // -work
+	buildA        bool   // -a
+	buildI        bool   // -i
+	buildN        bool   // -n
+	buildV        bool   // -v
+	buildX        bool   // -x
+	buildO        string // -o
+	buildGcflags  string // -gcflags
+	buildLdflags  string // -ldflags
+	buildTarget   string // -target
+	buildWork     bool   // -work
+	buildBundleID string // -bundleid
 )
 
 func addBuildFlags(cmd *command) {
@@ -222,6 +226,7 @@
 	cmd.flag.StringVar(&buildGcflags, "gcflags", "", "")
 	cmd.flag.StringVar(&buildLdflags, "ldflags", "", "")
 	cmd.flag.StringVar(&buildTarget, "target", "android", "")
+	cmd.flag.StringVar(&buildBundleID, "bundleid", "org.golang.todo", "")
 
 	cmd.flag.BoolVar(&buildA, "a", false, "")
 	cmd.flag.BoolVar(&buildI, "i", false, "")
diff --git a/cmd/gomobile/build_darwin_test.go b/cmd/gomobile/build_darwin_test.go
index 0cb6754..00097a0 100644
--- a/cmd/gomobile/build_darwin_test.go
+++ b/cmd/gomobile/build_darwin_test.go
@@ -33,7 +33,29 @@
 		t.Fatal(err)
 	}
 
-	diff, err := diffOutput(buf.String(), iosBuildTmpl)
+	teamID, err := detectTeamID()
+	if err != nil {
+		t.Fatalf("detecting team ID failed: %v", err)
+	}
+
+	data := struct {
+		outputData
+		TeamID string
+	}{
+		outputData: defaultOutputData(),
+		TeamID:     teamID,
+	}
+
+	got := filepath.ToSlash(buf.String())
+
+	wantBuf := new(bytes.Buffer)
+
+	if err := iosBuildTmpl.Execute(wantBuf, data); err != nil {
+		t.Fatalf("computing diff failed: %v", err)
+	}
+
+	diff, err := diff(got, wantBuf.String())
+
 	if err != nil {
 		t.Fatalf("computing diff failed: %v", err)
 	}
@@ -54,6 +76,6 @@
 GOOS=darwin GOARCH=arm64 CC=clang-iphoneos CXX=clang-iphoneos CGO_CFLAGS=-isysroot=iphoneos -miphoneos-version-min=6.1 -arch arm64 CGO_LDFLAGS=-isysroot=iphoneos -miphoneos-version-min=6.1 -arch arm64 CGO_ENABLED=1 go build -pkgdir=$GOMOBILE/pkg_darwin_arm64 -tags tag1 ios -x -o=$WORK/arm64 golang.org/x/mobile/example/basic
 xcrun lipo -create $WORK/arm $WORK/arm64 -o $WORK/main/main
 mkdir -p $WORK/main/assets
-xcrun xcodebuild -configuration Release -project $WORK/main.xcodeproj
+xcrun xcodebuild -configuration Release -project $WORK/main.xcodeproj -allowProvisioningUpdates DEVELOPMENT_TEAM={{.TeamID}}
 mv $WORK/build/Release-iphoneos/main.app basic.app
 `))
diff --git a/cmd/gomobile/build_iosapp.go b/cmd/gomobile/build_iosapp.go
index 0b2a923..3146173 100644
--- a/cmd/gomobile/build_iosapp.go
+++ b/cmd/gomobile/build_iosapp.go
@@ -6,6 +6,8 @@
 
 import (
 	"bytes"
+	"crypto/x509"
+	"encoding/pem"
 	"fmt"
 	"go/build"
 	"io/ioutil"
@@ -17,7 +19,7 @@
 	"text/template"
 )
 
-func goIOSBuild(pkg *build.Package) (map[string]bool, error) {
+func goIOSBuild(pkg *build.Package, bundleID string) (map[string]bool, error) {
 	src := pkg.ImportPath
 	if buildO != "" && !strings.HasSuffix(buildO, ".app") {
 		return nil, fmt.Errorf("-o must have an .app for -target=ios")
@@ -31,7 +33,7 @@
 	infoplist := new(bytes.Buffer)
 	if err := infoplistTmpl.Execute(infoplist, infoplistTmplData{
 		// TODO: better bundle id.
-		BundleID: "org.golang.todo." + productName,
+		BundleID: bundleID + "." + productName,
 		Name:     strings.Title(path.Base(pkg.ImportPath)),
 	}); err != nil {
 		return nil, err
@@ -94,12 +96,22 @@
 		return nil, err
 	}
 
+	// Detect the team ID
+	teamID, err := detectTeamID()
+	if err != nil {
+		return nil, err
+	}
+
 	// Build and move the release build to the output directory.
-	cmd = exec.Command(
-		"xcrun", "xcodebuild",
+	cmdStrings := []string{
+		"xcodebuild",
 		"-configuration", "Release",
-		"-project", tmpdir+"/main.xcodeproj",
-	)
+		"-project", tmpdir + "/main.xcodeproj",
+		"-allowProvisioningUpdates",
+		"DEVELOPMENT_TEAM=" + teamID,
+	}
+
+	cmd = exec.Command("xcrun", cmdStrings...)
 	if err := runCmd(cmd); err != nil {
 		return nil, err
 	}
@@ -133,6 +145,39 @@
 	return nmpkgs, nil
 }
 
+func detectTeamID() (string, error) {
+	// Grabs the first certificate for "iPhone Developer"; will not work if there
+	// are multiple certificates and the first is not desired.
+	cmd := exec.Command(
+		"security", "find-certificate",
+		"-c", "iPhone Developer", "-p",
+	)
+	pemString, err := cmd.Output()
+	if err != nil {
+		err = fmt.Errorf("failed to pull the signing certificate to determine your team ID: %v", err)
+		return "", err
+	}
+
+	block, _ := pem.Decode(pemString)
+	if block == nil {
+		err = fmt.Errorf("failed to decode the PEM to determine your team ID: %s", pemString)
+		return "", err
+	}
+
+	cert, err := x509.ParseCertificate(block.Bytes)
+	if err != nil {
+		err = fmt.Errorf("failed to parse your signing certificate to determine your team ID: %v", err)
+		return "", err
+	}
+
+	if len(cert.Subject.OrganizationalUnit) == 0 {
+		err = fmt.Errorf("the signing certificate has no organizational unit (team ID).")
+		return "", err
+	}
+
+	return cert.Subject.OrganizationalUnit[0], nil
+}
+
 func iosCopyAssets(pkg *build.Package, xcodeProjDir string) error {
 	dstAssets := xcodeProjDir + "/main/assets"
 	if err := mkdir(dstAssets); err != nil {