// 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" { if pkg.Name != "main" { return fmt.Errorf("cannot build non-main packages") } if err := importsApp(pkg); err != nil { return err } 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, "") } if err := importsApp(pkg); err != nil { return err } 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(``) 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 } func importsApp(pkg *build.Package) error { // Building a program, make sure it is appropriate for mobile. for _, path := range pkg.Imports { if path == "golang.org/x/mobile/app" { return nil } } return fmt.Errorf(`%s does not import "golang.org/x/mobile/app"`, pkg.ImportPath) } 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 }