| // 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(`<?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 |
| } |
| |
| 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 |
| } |