mobile/cmd/gomobile/build.go

538 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, ",")),
)
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,
}
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
}