cmd/gomobile: add build subcommand
Change-Id: I879e51834726c8713c5befeb4be2e328d5295af4 Reviewed-on: https://go-review.googlesource.com/4110 Reviewed-by: Hyang-Ah Hana Kim <hyangah@gmail.com>
This commit is contained in:
Родитель
c44ea393d0
Коммит
c55730db15
|
@ -0,0 +1,261 @@
|
|||
// 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"
|
||||
"flag"
|
||||
"fmt"
|
||||
"go/build"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var ctx = build.Default
|
||||
var pkg *build.Package
|
||||
|
||||
var cmdBuild = &command{
|
||||
run: runBuild,
|
||||
Name: "build",
|
||||
Usage: "[package]",
|
||||
Short: "compile android APK and/or iOS app",
|
||||
Long: `
|
||||
Build compiles and encodes the app named by the import path.
|
||||
|
||||
The named package must define a main function.
|
||||
|
||||
If an AndroidManifest.xml is defined in the package directory, it is
|
||||
added to the APK file. Otherwise, a default manifest is generated.
|
||||
|
||||
If the package directory contains an assets subdirectory, its contents
|
||||
are copied into the APK file.
|
||||
|
||||
These build flags are shared by the build, install, and test commands.
|
||||
For documentation, see 'go help build':
|
||||
TODO:
|
||||
-a
|
||||
-tags 'tag list'
|
||||
`,
|
||||
}
|
||||
|
||||
// TODO: -n
|
||||
// TODO: -v
|
||||
// TODO: -x
|
||||
// TODO: -mobile
|
||||
|
||||
func runBuild(cmd *command) error {
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
switch len(flag.Args()) {
|
||||
case 1:
|
||||
pkg, err = ctx.ImportDir(cwd, build.ImportComment)
|
||||
case 2:
|
||||
pkg, err = ctx.Import(flag.Args()[1], cwd, build.ImportComment)
|
||||
default:
|
||||
cmd.usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check that we are compiling an app.
|
||||
if pkg.Name != "main" {
|
||||
return fmt.Errorf(`package %q: can only build package "main"`, pkg.Name)
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
workPath, err := ioutil.TempDir("", "gobuildapk-work-")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.RemoveAll(workPath)
|
||||
|
||||
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)
|
||||
err := manifestTmpl.Execute(buf, manifestTmplData{
|
||||
// TODO(crawshaw): a better package path.
|
||||
JavaPkgPath: "org.golang.todo." + pkg.Name,
|
||||
Name: strings.ToUpper(pkg.Name[:1]) + pkg.Name[1:],
|
||||
LibName: libName,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// TODO(crawshaw): print generated manifest with -v.
|
||||
manifestData = buf.Bytes()
|
||||
} else {
|
||||
libName, err = manifestLibName(manifestData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
libPath := filepath.Join(workPath, "lib"+libName+".so")
|
||||
|
||||
gopath := goEnv("GOPATH")
|
||||
ccpath := filepath.Join(gopath, filepath.FromSlash("pkg/gomobile/android-"+ndkVersion+"/arm/bin"))
|
||||
|
||||
if _, err := os.Stat(ccpath); err != nil {
|
||||
// TODO(crawshaw): call gomobile init
|
||||
return fmt.Errorf("android %s toolchain not installed in $GOPATH/pkg/gomobile, run gomobile init", ndkVersion)
|
||||
}
|
||||
|
||||
gocmd := exec.Command(
|
||||
`go`,
|
||||
`build`,
|
||||
`-i`, // TODO(crawshaw): control with a flag
|
||||
`-ldflags="-shared"`,
|
||||
`-o`, libPath)
|
||||
gocmd.Stdout = os.Stdout
|
||||
gocmd.Stderr = os.Stderr
|
||||
gocmd.Env = []string{
|
||||
`GOOS=android`,
|
||||
`GOARCH=arm`,
|
||||
`GOARM=7`,
|
||||
`CGO_ENABLED=1`,
|
||||
`CC=` + filepath.Join(ccpath, "arm-linux-androideabi-gcc"),
|
||||
`CXX=` + filepath.Join(ccpath, "arm-linux-androideabi-g++"),
|
||||
`GOGCCFLAGS="-fPIC -marm -pthread -fmessage-length=0"`,
|
||||
`GOROOT=` + goEnv("GOROOT"),
|
||||
`GOPATH=` + gopath,
|
||||
}
|
||||
if err := gocmd.Run(); 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
|
||||
}
|
||||
|
||||
// TODO: -o
|
||||
out, err := os.Create(filepath.Base(pkg.Dir) + ".apk")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
apkw := NewWriter(out, privKey)
|
||||
|
||||
w, err := apkw.Create("AndroidManifest.xml")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := w.Write(manifestData); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r, err := os.Open(libPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
w, err = apkw.Create("lib/armeabi/lib" + libName + ".so")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := io.Copy(w, r); err != nil {
|
||||
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 := apkw.Create(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?
|
||||
|
||||
return apkw.Close()
|
||||
}
|
||||
|
||||
// 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-----
|
||||
`
|
|
@ -87,7 +87,8 @@ func help(args []string) {
|
|||
}
|
||||
|
||||
var commands = []*command{
|
||||
// TODO(crawshaw): cmdBuild, cmdInstall, cmdRun
|
||||
// TODO(crawshaw): cmdInstall, cmdRun
|
||||
cmdBuild,
|
||||
cmdInit,
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,78 @@
|
|||
// 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 (
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html/template"
|
||||
)
|
||||
|
||||
type manifestXML struct {
|
||||
Activity activityXML `xml:"application>activity"`
|
||||
}
|
||||
|
||||
type activityXML struct {
|
||||
Name string `xml:"name,attr"`
|
||||
MetaData []metaDataXML `xml:"meta-data"`
|
||||
}
|
||||
|
||||
type metaDataXML struct {
|
||||
Name string `xml:"name,attr"`
|
||||
Value string `xml:"value,attr"`
|
||||
}
|
||||
|
||||
// manifestLibName parses the AndroidManifest.xml and finds the library
|
||||
// name of the NativeActivity.
|
||||
func manifestLibName(data []byte) (string, error) {
|
||||
manifest := new(manifestXML)
|
||||
if err := xml.Unmarshal(data, manifest); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if manifest.Activity.Name != "android.app.NativeActivity" {
|
||||
return "", fmt.Errorf("can only build an .apk for NativeActivity, not %q", manifest.Activity.Name)
|
||||
}
|
||||
libName := ""
|
||||
for _, md := range manifest.Activity.MetaData {
|
||||
if md.Name == "android.app.lib_name" {
|
||||
libName = md.Value
|
||||
break
|
||||
}
|
||||
}
|
||||
if libName == "" {
|
||||
return "", errors.New("AndroidManifest.xml missing meta-data android.app.lib_name")
|
||||
}
|
||||
return libName, nil
|
||||
}
|
||||
|
||||
type manifestTmplData struct {
|
||||
JavaPkgPath string
|
||||
Name string
|
||||
LibName string
|
||||
}
|
||||
|
||||
var manifestTmpl = template.Must(template.New("manifest").Parse(`
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="{{.JavaPkgPath}}"
|
||||
android:versionCode="1"
|
||||
android:versionName="1.0">
|
||||
|
||||
<uses-sdk android:minSdkVersion="9" />
|
||||
<application android:label="{{.Name}}" android:hasCode="false" android:debuggable="true">
|
||||
<activity android:name="android.app.NativeActivity"
|
||||
android:label="{{.Name}}"
|
||||
android:configChanges="orientation|keyboardHidden">
|
||||
<meta-data android:name="android.app.lib_name" android:value="{{.LibName}}" />
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
||||
`))
|
Загрузка…
Ссылка в новой задаче