This CL adds iOS build support to gomobile command. $ gomobile build golang.org/x/mobile/example/basic gomobile builds an .app file that is signed with the development provisioning entities. You may deploy .app files to your test device or convert them to IPA to publish on App Store or share them as an AdHoc distribution. target=ios flag requires a Darwin host machine. Fixes golang/go#11043. Change-Id: Ibc23b6d355f10b09940b20c813eb73d0f4313851 Reviewed-on: https://go-review.googlesource.com/11587 Reviewed-by: David Crawshaw <crawshaw@golang.org>
539 lines
13 KiB
Go
539 lines
13 KiB
Go
// Copyright 2015 The Go Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/x509"
|
|
"encoding/pem"
|
|
"errors"
|
|
"fmt"
|
|
"go/build"
|
|
"io"
|
|
"io/ioutil"
|
|
"os"
|
|
"os/exec"
|
|
"path"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
var ctx = build.Default
|
|
var pkg *build.Package
|
|
var gomobilepath string // $GOPATH/pkg/gomobile
|
|
var ndkccpath string // $GOPATH/pkg/gomobile/android-{{.NDK}}
|
|
var tmpdir string
|
|
|
|
var cmdBuild = &command{
|
|
run: runBuild,
|
|
Name: "build",
|
|
Usage: "[-target android|ios] [-o output] [build flags] [package]",
|
|
Short: "compile android APK and iOS app",
|
|
Long: `
|
|
Build compiles and encodes the app named by the import path.
|
|
|
|
The named package must define a main function.
|
|
|
|
The -target flag takes a target system name, either android (the
|
|
default) or ios.
|
|
|
|
For -target android, if an AndroidManifest.xml is defined in the
|
|
package directory, it is added to the APK output. Otherwise, a default
|
|
manifest is generated.
|
|
|
|
For -target ios, gomobile must be run on an OS X machine with Xcode
|
|
installed. Support is not complete.
|
|
|
|
If the package directory contains an assets subdirectory, its contents
|
|
are copied into the output.
|
|
|
|
The -o flag specifies the output file name. If not specified, the
|
|
output file name depends on the package built.
|
|
|
|
The -v flag provides verbose output, including the list of packages built.
|
|
|
|
The build flags -a, -i, -n, -x, and -tags are shared with the build command.
|
|
For documentation, see 'go help build'.
|
|
`,
|
|
}
|
|
|
|
func runBuild(cmd *command) (err error) {
|
|
cwd, err := os.Getwd()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
args := cmd.flag.Args()
|
|
|
|
switch len(args) {
|
|
case 0:
|
|
pkg, err = ctx.ImportDir(cwd, build.ImportComment)
|
|
case 1:
|
|
pkg, err = ctx.Import(args[0], cwd, build.ImportComment)
|
|
default:
|
|
cmd.usage()
|
|
os.Exit(1)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
switch buildTarget {
|
|
case "android":
|
|
// implementation is below
|
|
case "ios":
|
|
if runtime.GOOS == "darwin" {
|
|
// TODO(jbd): Handle non-main packages.
|
|
return goIOSBuild(pkg.ImportPath)
|
|
}
|
|
return fmt.Errorf("-target=ios requires darwin host")
|
|
default:
|
|
return fmt.Errorf(`unknown -target, %q.`, buildTarget)
|
|
}
|
|
|
|
if pkg.Name != "main" {
|
|
// Not an app, don't build a final package.
|
|
return goAndroidBuild(pkg.ImportPath, "")
|
|
}
|
|
|
|
// Building a program, make sure it is appropriate for mobile.
|
|
importsApp := false
|
|
for _, path := range pkg.Imports {
|
|
if path == "golang.org/x/mobile/app" {
|
|
importsApp = true
|
|
break
|
|
}
|
|
}
|
|
if !importsApp {
|
|
return fmt.Errorf(`%s does not import "golang.org/x/mobile/app"`, pkg.ImportPath)
|
|
}
|
|
|
|
if buildN {
|
|
tmpdir = "$WORK"
|
|
} else {
|
|
tmpdir, err = ioutil.TempDir("", "gobuildapk-work-")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
defer removeAll(tmpdir)
|
|
if buildX {
|
|
fmt.Fprintln(xout, "WORK="+tmpdir)
|
|
}
|
|
|
|
libName := path.Base(pkg.ImportPath)
|
|
manifestData, err := ioutil.ReadFile(filepath.Join(pkg.Dir, "AndroidManifest.xml"))
|
|
if err != nil {
|
|
if !os.IsNotExist(err) {
|
|
return err
|
|
}
|
|
buf := new(bytes.Buffer)
|
|
buf.WriteString(`<?xml version="1.0" encoding="utf-8"?>`)
|
|
err := manifestTmpl.Execute(buf, manifestTmplData{
|
|
// TODO(crawshaw): a better package path.
|
|
JavaPkgPath: "org.golang.todo." + libName,
|
|
Name: libName,
|
|
LibName: libName,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
manifestData = buf.Bytes()
|
|
if buildV {
|
|
fmt.Fprintf(os.Stderr, "generated AndroidManifest.xml:\n%s\n", manifestData)
|
|
}
|
|
} else {
|
|
libName, err = manifestLibName(manifestData)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
libPath := filepath.Join(tmpdir, "lib"+libName+".so")
|
|
|
|
if err := goAndroidBuild(pkg.ImportPath, libPath); err != nil {
|
|
return err
|
|
}
|
|
block, _ := pem.Decode([]byte(debugCert))
|
|
if block == nil {
|
|
return errors.New("no debug cert")
|
|
}
|
|
privKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if buildO == "" {
|
|
buildO = filepath.Base(pkg.Dir) + ".apk"
|
|
}
|
|
if !strings.HasSuffix(buildO, ".apk") {
|
|
return fmt.Errorf("output file name %q does not end in '.apk'", buildO)
|
|
}
|
|
var out io.Writer
|
|
if !buildN {
|
|
f, err := os.Create(buildO)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() {
|
|
if cerr := f.Close(); err == nil {
|
|
err = cerr
|
|
}
|
|
}()
|
|
out = f
|
|
}
|
|
|
|
var apkw *Writer
|
|
if !buildN {
|
|
apkw = NewWriter(out, privKey)
|
|
}
|
|
apkwcreate := func(name string) (io.Writer, error) {
|
|
if buildV {
|
|
fmt.Fprintf(os.Stderr, "apk: %s\n", name)
|
|
}
|
|
if buildN {
|
|
return ioutil.Discard, nil
|
|
}
|
|
return apkw.Create(name)
|
|
}
|
|
|
|
w, err := apkwcreate("AndroidManifest.xml")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, err := w.Write(manifestData); err != nil {
|
|
return err
|
|
}
|
|
|
|
w, err = apkwcreate("lib/armeabi/lib" + libName + ".so")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !buildN {
|
|
r, err := os.Open(libPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, err := io.Copy(w, r); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
importsAL := pkgImportsAL(pkg)
|
|
if importsAL {
|
|
alDir := filepath.Join(ndkccpath, "openal/lib")
|
|
filepath.Walk(alDir, func(path string, info os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if info.IsDir() {
|
|
return nil
|
|
}
|
|
name := "lib/" + path[len(alDir)+1:]
|
|
w, err := apkwcreate(name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !buildN {
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer f.Close()
|
|
_, err = io.Copy(w, f)
|
|
}
|
|
return err
|
|
})
|
|
}
|
|
|
|
// Add any assets.
|
|
assetsDir := filepath.Join(pkg.Dir, "assets")
|
|
assetsDirExists := true
|
|
fi, err := os.Stat(assetsDir)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
assetsDirExists = false
|
|
} else {
|
|
return err
|
|
}
|
|
} else {
|
|
assetsDirExists = fi.IsDir()
|
|
}
|
|
if assetsDirExists {
|
|
filepath.Walk(assetsDir, func(path string, info os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if info.IsDir() {
|
|
return nil
|
|
}
|
|
name := "assets/" + path[len(assetsDir)+1:]
|
|
w, err := apkwcreate(name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer f.Close()
|
|
_, err = io.Copy(w, f)
|
|
return err
|
|
})
|
|
}
|
|
|
|
// TODO: add gdbserver to apk?
|
|
|
|
if !buildN {
|
|
if err := apkw.Close(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
var xout io.Writer = os.Stderr
|
|
|
|
func printcmd(format string, args ...interface{}) {
|
|
cmd := fmt.Sprintf(format+"\n", args...)
|
|
if tmpdir != "" {
|
|
cmd = strings.Replace(cmd, tmpdir, "$WORK", -1)
|
|
}
|
|
if gomobilepath != "" {
|
|
cmd = strings.Replace(cmd, gomobilepath, "$GOMOBILE", -1)
|
|
}
|
|
if goroot := goEnv("GOROOT"); goroot != "" {
|
|
cmd = strings.Replace(cmd, goroot, "$GOROOT", -1)
|
|
}
|
|
if gopath := goEnv("GOPATH"); gopath != "" {
|
|
cmd = strings.Replace(cmd, gopath, "$GOPATH", -1)
|
|
}
|
|
if env := os.Getenv("HOME"); env != "" {
|
|
cmd = strings.Replace(cmd, env, "$HOME", -1)
|
|
}
|
|
if env := os.Getenv("HOMEPATH"); env != "" {
|
|
cmd = strings.Replace(cmd, env, "$HOMEPATH", -1)
|
|
}
|
|
fmt.Fprint(xout, cmd)
|
|
}
|
|
|
|
// "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
|
|
buildTarget string // -target
|
|
)
|
|
|
|
func addBuildFlags(cmd *command) {
|
|
cmd.flag.StringVar(&buildO, "o", "", "")
|
|
cmd.flag.StringVar(&buildTarget, "target", "android", "")
|
|
|
|
cmd.flag.BoolVar(&buildA, "a", false, "")
|
|
cmd.flag.BoolVar(&buildI, "i", false, "")
|
|
cmd.flag.Var((*stringsFlag)(&ctx.BuildTags), "tags", "")
|
|
}
|
|
|
|
func addBuildFlagsNVX(cmd *command) {
|
|
cmd.flag.BoolVar(&buildN, "n", false, "")
|
|
cmd.flag.BoolVar(&buildV, "v", false, "")
|
|
cmd.flag.BoolVar(&buildX, "x", false, "")
|
|
}
|
|
|
|
// goAndroidBuild builds a package.
|
|
// If libPath is specified then it builds as a shared library.
|
|
func goAndroidBuild(src, libPath string) error {
|
|
version, err := goVersion()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
gopath := goEnv("GOPATH")
|
|
for _, p := range filepath.SplitList(gopath) {
|
|
gomobilepath = filepath.Join(p, "pkg", "gomobile")
|
|
if _, err = os.Stat(gomobilepath); err == nil {
|
|
break
|
|
}
|
|
}
|
|
if err != nil || gomobilepath == "" {
|
|
return errors.New("toolchain not installed, run:\n\tgomobile init")
|
|
}
|
|
verpath := filepath.Join(gomobilepath, "version")
|
|
installedVersion, err := ioutil.ReadFile(verpath)
|
|
if err != nil {
|
|
return errors.New("toolchain partially installed, run:\n\tgomobile init")
|
|
}
|
|
if !bytes.Equal(installedVersion, version) {
|
|
return errors.New("toolchain out of date, run:\n\tgomobile init")
|
|
}
|
|
|
|
ndkccpath = filepath.Join(gomobilepath, "android-"+ndkVersion)
|
|
ndkccbin := filepath.Join(ndkccpath, "arm", "bin")
|
|
if buildX {
|
|
fmt.Fprintln(xout, "GOMOBILE="+gomobilepath)
|
|
}
|
|
|
|
gocmd := exec.Command(
|
|
`go`,
|
|
`build`,
|
|
`-tags=`+strconv.Quote(strings.Join(ctx.BuildTags, ",")),
|
|
`-toolexec=`+filepath.Join(ndkccbin, "toolexec"))
|
|
if buildV {
|
|
gocmd.Args = append(gocmd.Args, "-v")
|
|
}
|
|
if buildI {
|
|
gocmd.Args = append(gocmd.Args, "-i")
|
|
}
|
|
if buildX {
|
|
gocmd.Args = append(gocmd.Args, "-x")
|
|
}
|
|
if libPath == "" {
|
|
if buildO != "" {
|
|
gocmd.Args = append(gocmd.Args, `-o`, buildO)
|
|
}
|
|
} else {
|
|
gocmd.Args = append(gocmd.Args, "-buildmode=c-shared", "-o", libPath)
|
|
}
|
|
|
|
gocmd.Args = append(gocmd.Args, src)
|
|
|
|
gocmd.Stdout = os.Stdout
|
|
gocmd.Stderr = os.Stderr
|
|
gocmd.Env = []string{
|
|
`GOOS=android`,
|
|
`GOARCH=arm`,
|
|
`GOARM=7`,
|
|
`CGO_ENABLED=1`,
|
|
`CC=` + filepath.Join(ndkccbin, "arm-linux-androideabi-gcc"),
|
|
`CXX=` + filepath.Join(ndkccbin, "arm-linux-androideabi-g++"),
|
|
`GOGCCFLAGS="-fPIC -marm -pthread -fmessage-length=0"`,
|
|
`GOROOT=` + goEnv("GOROOT"),
|
|
`GOPATH=` + gopath,
|
|
`GOMOBILEPATH=` + ndkccbin, // for toolexec
|
|
}
|
|
if buildX {
|
|
printcmd("%s", strings.Join(gocmd.Env, " ")+" "+strings.Join(gocmd.Args, " "))
|
|
}
|
|
if !buildN {
|
|
gocmd.Env = environ(gocmd.Env)
|
|
if err := gocmd.Run(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
var importsALPkg = make(map[string]struct{})
|
|
|
|
// pkgImportsAL returns true if the given package or one of its
|
|
// dependencies imports the x/mobile/exp/audio/al package.
|
|
func pkgImportsAL(pkg *build.Package) bool {
|
|
for _, path := range pkg.Imports {
|
|
if path == "C" {
|
|
continue
|
|
}
|
|
if _, ok := importsALPkg[path]; ok {
|
|
continue
|
|
}
|
|
importsALPkg[path] = struct{}{}
|
|
if strings.HasPrefix(path, "golang.org/x/mobile/exp/audio/al") {
|
|
return true
|
|
}
|
|
dPkg, err := ctx.Import(path, "", build.ImportComment)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "error reading OpenAL library: %v", err)
|
|
os.Exit(2)
|
|
}
|
|
if pkgImportsAL(dPkg) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func init() {
|
|
addBuildFlags(cmdBuild)
|
|
addBuildFlagsNVX(cmdBuild)
|
|
|
|
addBuildFlags(cmdInstall)
|
|
addBuildFlagsNVX(cmdInstall)
|
|
|
|
addBuildFlagsNVX(cmdInit)
|
|
|
|
addBuildFlags(cmdBind)
|
|
addBuildFlagsNVX(cmdBind)
|
|
}
|
|
|
|
// A random uninteresting private key.
|
|
// Must be consistent across builds so newer app versions can be installed.
|
|
const debugCert = `
|
|
-----BEGIN RSA PRIVATE KEY-----
|
|
MIIEowIBAAKCAQEAy6ItnWZJ8DpX9R5FdWbS9Kr1U8Z7mKgqNByGU7No99JUnmyu
|
|
NQ6Uy6Nj0Gz3o3c0BXESECblOC13WdzjsH1Pi7/L9QV8jXOXX8cvkG5SJAyj6hcO
|
|
LOapjDiN89NXjXtyv206JWYvRtpexyVrmHJgRAw3fiFI+m4g4Qop1CxcIF/EgYh7
|
|
rYrqh4wbCM1OGaCleQWaOCXxZGm+J5YNKQcWpjZRrDrb35IZmlT0bK46CXUKvCqK
|
|
x7YXHgfhC8ZsXCtsScKJVHs7gEsNxz7A0XoibFw6DoxtjKzUCktnT0w3wxdY7OTj
|
|
9AR8mobFlM9W3yirX8TtwekWhDNTYEu8dwwykwIDAQABAoIBAA2hjpIhvcNR9H9Z
|
|
BmdEecydAQ0ZlT5zy1dvrWI++UDVmIp+Ve8BSd6T0mOqV61elmHi3sWsBN4M1Rdz
|
|
3N38lW2SajG9q0fAvBpSOBHgAKmfGv3Ziz5gNmtHgeEXfZ3f7J95zVGhlHqWtY95
|
|
JsmuplkHxFMyITN6WcMWrhQg4A3enKLhJLlaGLJf9PeBrvVxHR1/txrfENd2iJBH
|
|
FmxVGILL09fIIktJvoScbzVOneeWXj5vJGzWVhB17DHBbANGvVPdD5f+k/s5aooh
|
|
hWAy/yLKocr294C4J+gkO5h2zjjjSGcmVHfrhlXQoEPX+iW1TGoF8BMtl4Llc+jw
|
|
lKWKfpECgYEA9C428Z6CvAn+KJ2yhbAtuRo41kkOVoiQPtlPeRYs91Pq4+NBlfKO
|
|
2nWLkyavVrLx4YQeCeaEU2Xoieo9msfLZGTVxgRlztylOUR+zz2FzDBYGicuUD3s
|
|
EqC0Wv7tiX6dumpWyOcVVLmR9aKlOUzA9xemzIsWUwL3PpyONhKSq7kCgYEA1X2F
|
|
f2jKjoOVzglhtuX4/SP9GxS4gRf9rOQ1Q8DzZhyH2LZ6Dnb1uEQvGhiqJTU8CXxb
|
|
7odI0fgyNXq425Nlxc1Tu0G38TtJhwrx7HWHuFcbI/QpRtDYLWil8Zr7Q3BT9rdh
|
|
moo4m937hLMvqOG9pyIbyjOEPK2WBCtKW5yabqsCgYEAu9DkUBr1Qf+Jr+IEU9I8
|
|
iRkDSMeusJ6gHMd32pJVCfRRQvIlG1oTyTMKpafmzBAd/rFpjYHynFdRcutqcShm
|
|
aJUq3QG68U9EAvWNeIhA5tr0mUEz3WKTt4xGzYsyWES8u4tZr3QXMzD9dOuinJ1N
|
|
+4EEumXtSPKKDG3M8Qh+KnkCgYBUEVSTYmF5EynXc2xOCGsuy5AsrNEmzJqxDUBI
|
|
SN/P0uZPmTOhJIkIIZlmrlW5xye4GIde+1jajeC/nG7U0EsgRAV31J4pWQ5QJigz
|
|
0+g419wxIUFryGuIHhBSfpP472+w1G+T2mAGSLh1fdYDq7jx6oWE7xpghn5vb9id
|
|
EKLjdwKBgBtz9mzbzutIfAW0Y8F23T60nKvQ0gibE92rnUbjPnw8HjL3AZLU05N+
|
|
cSL5bhq0N5XHK77sscxW9vXjG0LJMXmFZPp9F6aV6ejkMIXyJ/Yz/EqeaJFwilTq
|
|
Mc6xR47qkdzu0dQ1aPm4XD7AWDtIvPo/GG2DKOucLBbQc2cOWtKS
|
|
-----END RSA PRIVATE KEY-----
|
|
`
|
|
|
|
// environ merges os.Environ and the given "key=value" pairs.
|
|
func environ(kv []string) []string {
|
|
envs := map[string]string{}
|
|
|
|
cur := os.Environ()
|
|
new := make([]string, 0, len(cur)+len(kv))
|
|
for _, ev := range cur {
|
|
elem := strings.SplitN(ev, "=", 2)
|
|
if len(elem) != 2 || elem[0] == "" {
|
|
// pass the env var of unusual form untouched.
|
|
// e.g. Windows may have env var names starting with "=".
|
|
new = append(new, ev)
|
|
continue
|
|
}
|
|
if goos == "windows" {
|
|
elem[0] = strings.ToUpper(elem[0])
|
|
}
|
|
envs[elem[0]] = elem[1]
|
|
}
|
|
for _, ev := range kv {
|
|
elem := strings.SplitN(ev, "=", 2)
|
|
if len(elem) != 2 || elem[0] == "" {
|
|
panic(fmt.Sprintf("malformed env var %q from input", ev))
|
|
}
|
|
if goos == "windows" {
|
|
elem[0] = strings.ToUpper(elem[0])
|
|
}
|
|
envs[elem[0]] = elem[1]
|
|
}
|
|
for k, v := range envs {
|
|
new = append(new, k+"="+v)
|
|
}
|
|
return new
|
|
}
|