To use the NDK before version r19b standalone toolchains had to be generated. Version r19b added prebuilt standalone toolchains. Use the prebuilt for gomobile build and gomobile bind and stop generating toolchains during gomobile init. gomobile init is now only necessary for building OpenAL for gomobile build programs. This change is not compatible with NDK versions < r19b, but the user is instructed how to upgrade when running gomobile build or gomobile bind. Change-Id: I96953298ecce42402459a9dd15169c09fe6b6f8b Reviewed-on: https://go-review.googlesource.com/c/163378 Reviewed-by: Hyang-Ah Hana Kim <hyangah@gmail.com>
355 lines
9.8 KiB
Go
355 lines
9.8 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/base64"
|
|
"encoding/pem"
|
|
"encoding/xml"
|
|
"errors"
|
|
"fmt"
|
|
"go/build"
|
|
"io"
|
|
"io/ioutil"
|
|
"log"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"golang.org/x/mobile/internal/binres"
|
|
)
|
|
|
|
func goAndroidBuild(pkg *build.Package, androidArchs []string) (map[string]bool, error) {
|
|
ndkRoot, err := ndkRoot()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
appName := path.Base(pkg.ImportPath)
|
|
libName := androidPkgName(appName)
|
|
manifestPath := filepath.Join(pkg.Dir, "AndroidManifest.xml")
|
|
manifestData, err := ioutil.ReadFile(manifestPath)
|
|
if err != nil {
|
|
if !os.IsNotExist(err) {
|
|
return nil, 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: strings.Title(appName),
|
|
LibName: libName,
|
|
})
|
|
if err != nil {
|
|
return nil, 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 nil, fmt.Errorf("error parsing %s: %v", manifestPath, err)
|
|
}
|
|
}
|
|
|
|
libFiles := []string{}
|
|
nmpkgs := make(map[string]map[string]bool) // map: arch -> extractPkgs' output
|
|
|
|
for _, arch := range androidArchs {
|
|
env := androidEnv[arch]
|
|
toolchain := ndk.Toolchain(arch)
|
|
libPath := "lib/" + toolchain.abi + "/lib" + libName + ".so"
|
|
libAbsPath := filepath.Join(tmpdir, libPath)
|
|
if err := mkdir(filepath.Dir(libAbsPath)); err != nil {
|
|
return nil, err
|
|
}
|
|
err = goBuild(
|
|
pkg.ImportPath,
|
|
env,
|
|
"-buildmode=c-shared",
|
|
"-o", libAbsPath,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nmpkgs[arch], err = extractPkgs(toolchain.Path(ndkRoot, "nm"), libAbsPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
libFiles = append(libFiles, libPath)
|
|
}
|
|
|
|
block, _ := pem.Decode([]byte(debugCert))
|
|
if block == nil {
|
|
return nil, errors.New("no debug cert")
|
|
}
|
|
privKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if buildO == "" {
|
|
buildO = androidPkgName(filepath.Base(pkg.Dir)) + ".apk"
|
|
}
|
|
if !strings.HasSuffix(buildO, ".apk") {
|
|
return nil, 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 nil, 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)
|
|
}
|
|
apkwWriteFile := func(dst, src string) error {
|
|
w, err := apkwCreate(dst)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !buildN {
|
|
f, err := os.Open(src)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer f.Close()
|
|
if _, err := io.Copy(w, f); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
w, err := apkwCreate("classes.dex")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
dexData, err := base64.StdEncoding.DecodeString(dexStr)
|
|
if err != nil {
|
|
log.Fatalf("internal error bad dexStr: %v", err)
|
|
}
|
|
if _, err := w.Write(dexData); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, libFile := range libFiles {
|
|
if err := apkwWriteFile(libFile, filepath.Join(tmpdir, libFile)); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
for _, arch := range androidArchs {
|
|
toolchain := ndk.Toolchain(arch)
|
|
if nmpkgs[arch]["golang.org/x/mobile/exp/audio/al"] {
|
|
dst := "lib/" + toolchain.abi + "/libopenal.so"
|
|
src := filepath.Join(gomobilepath, dst)
|
|
if _, err := os.Stat(src); err != nil {
|
|
return nil, errors.New("the Android requires the golang.org/x/mobile/exp/audio/al, but the OpenAL libraries was not found. Please run gomobile init with the -openal flag pointing to an OpenAL source directory.")
|
|
}
|
|
if err := apkwWriteFile(dst, src); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add any assets.
|
|
var arsc struct {
|
|
iconPath string
|
|
}
|
|
assetsDir := filepath.Join(pkg.Dir, "assets")
|
|
assetsDirExists := true
|
|
fi, err := os.Stat(assetsDir)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
assetsDirExists = false
|
|
} else {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
assetsDirExists = fi.IsDir()
|
|
}
|
|
if assetsDirExists {
|
|
// if assets is a symlink, follow the symlink.
|
|
assetsDir, err = filepath.EvalSymlinks(assetsDir)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
err = filepath.Walk(assetsDir, func(path string, info os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if name := filepath.Base(path); strings.HasPrefix(name, ".") {
|
|
// Do not include the hidden files.
|
|
return nil
|
|
}
|
|
if info.IsDir() {
|
|
return nil
|
|
}
|
|
|
|
if rel, err := filepath.Rel(assetsDir, path); rel == "icon.png" && err == nil {
|
|
arsc.iconPath = path
|
|
// TODO returning here does not write the assets/icon.png to the final assets output,
|
|
// making it unavailable via the assets API. Should the file be duplicated into assets
|
|
// or should assets API be able to retrieve files from the generated resource table?
|
|
return nil
|
|
}
|
|
|
|
name := "assets/" + path[len(assetsDir)+1:]
|
|
return apkwWriteFile(name, path)
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("asset %v", err)
|
|
}
|
|
}
|
|
|
|
bxml, err := binres.UnmarshalXML(bytes.NewReader(manifestData), arsc.iconPath != "")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// generate resources.arsc identifying single xxxhdpi icon resource.
|
|
if arsc.iconPath != "" {
|
|
pkgname, err := bxml.RawValueByName("manifest", xml.Name{Local: "package"})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
tbl, name := binres.NewMipmapTable(pkgname)
|
|
if err := apkwWriteFile(name, arsc.iconPath); err != nil {
|
|
return nil, err
|
|
}
|
|
w, err := apkwCreate("resources.arsc")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
bin, err := tbl.MarshalBinary()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if _, err := w.Write(bin); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
w, err = apkwCreate("AndroidManifest.xml")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
bin, err := bxml.MarshalBinary()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if _, err := w.Write(bin); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// TODO: add gdbserver to apk?
|
|
|
|
if !buildN {
|
|
if err := apkw.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// TODO: return nmpkgs
|
|
return nmpkgs[androidArchs[0]], nil
|
|
}
|
|
|
|
// androidPkgName sanitizes the go package name to be acceptable as a android
|
|
// package name part. The android package name convention is similar to the
|
|
// java package name convention described in
|
|
// https://docs.oracle.com/javase/specs/jls/se8/html/jls-6.html#jls-6.5.3.1
|
|
// but not exactly same.
|
|
func androidPkgName(name string) string {
|
|
var res []rune
|
|
for _, r := range name {
|
|
switch {
|
|
case 'a' <= r && r <= 'z', 'A' <= r && r <= 'Z', '0' <= r && r <= '9':
|
|
res = append(res, r)
|
|
default:
|
|
res = append(res, '_')
|
|
}
|
|
}
|
|
if len(res) == 0 || res[0] == '_' || ('0' <= res[0] && res[0] <= '9') {
|
|
// Android does not seem to allow the package part starting with _.
|
|
res = append([]rune{'g', 'o'}, res...)
|
|
}
|
|
s := string(res)
|
|
// Look for Java keywords that are not Go keywords, and avoid using
|
|
// them as a package name.
|
|
//
|
|
// This is not a problem for normal Go identifiers as we only expose
|
|
// exported symbols. The upper case first letter saves everything
|
|
// from accidentally matching except for the package name.
|
|
//
|
|
// Note that basic type names (like int) are not keywords in Go.
|
|
switch s {
|
|
case "abstract", "assert", "boolean", "byte", "catch", "char", "class",
|
|
"do", "double", "enum", "extends", "final", "finally", "float",
|
|
"implements", "instanceof", "int", "long", "native", "private",
|
|
"protected", "public", "short", "static", "strictfp", "super",
|
|
"synchronized", "this", "throw", "throws", "transient", "try",
|
|
"void", "volatile", "while":
|
|
s += "_"
|
|
}
|
|
return s
|
|
}
|
|
|
|
// 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-----
|
|
`
|