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:
David Crawshaw 2015-02-06 23:26:08 +00:00
parent c44ea393d0
commit c55730db15
3 changed files with 341 additions and 1 deletions

261
cmd/gomobile/build.go Normal file
View File

@ -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-----
`

View File

@ -87,7 +87,8 @@ func help(args []string) {
} }
var commands = []*command{ var commands = []*command{
// TODO(crawshaw): cmdBuild, cmdInstall, cmdRun // TODO(crawshaw): cmdInstall, cmdRun
cmdBuild,
cmdInit, cmdInit,
} }

78
cmd/gomobile/manifest.go Normal file
View File

@ -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>
`))