cmd/gomobile: bind command for running gobind and building the libraries.
Change-Id: Ic4bf580b9c3f9757611ec7644eaf998d60f0fafc Reviewed-on: https://go-review.googlesource.com/4833 Reviewed-by: David Crawshaw <crawshaw@golang.org>
This commit is contained in:
parent
8b708def2f
commit
968871a5be
307
cmd/gomobile/bind.go
Normal file
307
cmd/gomobile/bind.go
Normal file
@ -0,0 +1,307 @@
|
||||
// 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 (
|
||||
"errors"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/build"
|
||||
"go/parser"
|
||||
"go/scanner"
|
||||
"go/token"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"text/template"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"golang.org/x/mobile/bind"
|
||||
"golang.org/x/tools/go/loader"
|
||||
"golang.org/x/tools/go/types"
|
||||
)
|
||||
|
||||
// ctx, pkg, ndkccpath, tmpdir in build.go
|
||||
|
||||
var cmdBind = &command{
|
||||
run: runBind,
|
||||
Name: "bind",
|
||||
Usage: "[package]",
|
||||
Short: "build a shared library for android APK and/or iOS app",
|
||||
Long: `
|
||||
Bind generates language bindings like gobind (golang.org/x/mobile/cmd/gobind)
|
||||
for a package and builds a shared library for each platform from the go binding
|
||||
code.
|
||||
|
||||
The -outdir flag specifies the output directory and is required.
|
||||
|
||||
For Android, the bind command will place the generated Java API stubs and the
|
||||
compiled shared libraries in the android subdirectory of the following layout.
|
||||
|
||||
<outdir>/android
|
||||
libs/
|
||||
armeabi-v7a/libgojni.so
|
||||
...
|
||||
src/main/java/go/
|
||||
Seq.java
|
||||
Go.java
|
||||
mypackage/Mypackage.java
|
||||
|
||||
The -v flag provides verbose output, including the list of packages built.
|
||||
|
||||
These build flags are shared by the build command.
|
||||
For documentation, see 'go help build':
|
||||
-a
|
||||
-i
|
||||
-n
|
||||
-x
|
||||
-tags 'tag list'
|
||||
`,
|
||||
}
|
||||
|
||||
// TODO: -mobile
|
||||
|
||||
var bindOutdir *string // -outdir
|
||||
func init() {
|
||||
bindOutdir = cmdBind.flag.String("outdir", "", "output directory. Default is the current directory.")
|
||||
}
|
||||
|
||||
func runBind(cmd *command) error {
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
args := cmd.flag.Args()
|
||||
|
||||
var bindPkg *build.Package
|
||||
switch len(args) {
|
||||
case 0:
|
||||
bindPkg, err = ctx.ImportDir(cwd, build.ImportComment)
|
||||
case 1:
|
||||
bindPkg, err = ctx.Import(args[0], cwd, build.ImportComment)
|
||||
default:
|
||||
cmd.usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if *bindOutdir == "" {
|
||||
return fmt.Errorf("-outdir is required")
|
||||
}
|
||||
|
||||
if !buildN {
|
||||
sentinel := filepath.Join(*bindOutdir, "gomobile-bind-sentinel")
|
||||
if err := ioutil.WriteFile(sentinel, []byte("write test"), 0644); err != nil {
|
||||
return fmt.Errorf("output directory %q not writable", *bindOutdir)
|
||||
}
|
||||
os.Remove(sentinel)
|
||||
}
|
||||
|
||||
if buildN {
|
||||
tmpdir = "$WORK"
|
||||
} else {
|
||||
tmpdir, err = ioutil.TempDir("", "gomobile-bind-work-")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
defer removeAll(tmpdir)
|
||||
if buildX {
|
||||
fmt.Fprintln(os.Stderr, "WORK="+tmpdir)
|
||||
}
|
||||
|
||||
binder, err := newBinder(bindPkg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := binder.GenGo(tmpdir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pathJoin := func(a, b string) string {
|
||||
return filepath.Join(a, filepath.FromSlash(b))
|
||||
}
|
||||
|
||||
mainFile := pathJoin(tmpdir, "androidlib/main.go")
|
||||
err = writeFile(mainFile, func(w io.Writer) error {
|
||||
return androidMainTmpl.Execute(w, "../go_"+binder.pkg.Name())
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create the main package for android: %v", err)
|
||||
}
|
||||
|
||||
androidOutdir := pathJoin(*bindOutdir, "android")
|
||||
|
||||
err = gobuild(mainFile, pathJoin(androidOutdir, "libs/armeabi-v7a/libgojni.so"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p, err := ctx.Import("golang.org/x/mobile/app", cwd, build.ImportComment)
|
||||
if err != nil {
|
||||
return fmt.Errorf(`"golang.org/x/mobile/app" is not found; run go get golang.org/x/mobile/app`)
|
||||
}
|
||||
repo := filepath.Clean(filepath.Join(p.Dir, "..")) // golang.org/x/mobile directory.
|
||||
|
||||
// TODO(crawshaw): use a better package path derived from the go package.
|
||||
if err := binder.GenJava(pathJoin(androidOutdir, "src/main/java/go/"+binder.pkg.Name())); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
src := pathJoin(repo, "app/Go.java")
|
||||
dst := pathJoin(androidOutdir, "src/main/java/go/Go.java")
|
||||
if err := symlink(src, dst); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
src = pathJoin(repo, "bind/java/Seq.java")
|
||||
dst = pathJoin(androidOutdir, "src/main/java/go/Seq.java")
|
||||
if err := symlink(src, dst); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type binder struct {
|
||||
files []*ast.File
|
||||
fset *token.FileSet
|
||||
pkg *types.Package
|
||||
}
|
||||
|
||||
func (b *binder) GenJava(outdir string) error {
|
||||
firstRune, size := utf8.DecodeRuneInString(b.pkg.Name())
|
||||
className := string(unicode.ToUpper(firstRune)) + b.pkg.Name()[size:]
|
||||
javaFile := filepath.Join(outdir, className+".java")
|
||||
|
||||
if buildX {
|
||||
printcmd("gobind -lang=java %s > %s", b.pkg.Path(), javaFile)
|
||||
}
|
||||
|
||||
generate := func(w io.Writer) error {
|
||||
return bind.GenJava(w, b.fset, b.pkg)
|
||||
}
|
||||
if err := writeFile(javaFile, generate); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *binder) GenGo(outdir string) error {
|
||||
pkgName := "go_" + b.pkg.Name()
|
||||
goFile := filepath.Join(outdir, pkgName, pkgName+".go")
|
||||
|
||||
if buildX {
|
||||
printcmd("gobind -lang=go %s > %s", b.pkg.Path(), goFile)
|
||||
}
|
||||
|
||||
generate := func(w io.Writer) error {
|
||||
return bind.GenGo(w, b.fset, b.pkg)
|
||||
}
|
||||
if err := writeFile(goFile, generate); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeFile(filename string, generate func(io.Writer) error) error {
|
||||
if buildV {
|
||||
fmt.Fprintf(os.Stderr, "write %s\n", filename)
|
||||
}
|
||||
|
||||
err := mkdir(filepath.Dir(filename))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if buildN {
|
||||
return generate(ioutil.Discard)
|
||||
}
|
||||
|
||||
f, err := os.Create(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if cerr := f.Close(); err == nil {
|
||||
err = cerr
|
||||
}
|
||||
}()
|
||||
|
||||
return generate(f)
|
||||
}
|
||||
|
||||
func newBinder(bindPkg *build.Package) (*binder, error) {
|
||||
if bindPkg.Name == "main" {
|
||||
return nil, fmt.Errorf("package %q: can only bind a library package", bindPkg.Name)
|
||||
}
|
||||
|
||||
if len(bindPkg.CgoFiles) > 0 {
|
||||
return nil, fmt.Errorf("cannot use cgo-dependent package as service definition: %s", bindPkg.CgoFiles[0])
|
||||
}
|
||||
|
||||
fset := token.NewFileSet()
|
||||
|
||||
hasErr := false
|
||||
var files []*ast.File
|
||||
for _, filename := range bindPkg.GoFiles {
|
||||
p := filepath.Join(bindPkg.Dir, filename)
|
||||
file, err := parser.ParseFile(fset, p, nil, parser.AllErrors)
|
||||
if err != nil {
|
||||
hasErr = true
|
||||
if list, _ := err.(scanner.ErrorList); len(list) > 0 {
|
||||
for _, err := range list {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
}
|
||||
} else {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
}
|
||||
}
|
||||
files = append(files, file)
|
||||
}
|
||||
|
||||
if hasErr {
|
||||
return nil, errors.New("package parsing failed.")
|
||||
}
|
||||
|
||||
conf := loader.Config{
|
||||
SourceImports: true,
|
||||
Fset: fset,
|
||||
}
|
||||
conf.TypeChecker.Error = func(err error) {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
}
|
||||
|
||||
conf.CreateFromFiles(bindPkg.ImportPath, files...)
|
||||
program, err := conf.Load()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
b := &binder{
|
||||
files: files,
|
||||
fset: fset,
|
||||
pkg: program.Created[0].Pkg,
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
var androidMainTmpl = template.Must(template.New("android.go").Parse(`
|
||||
package main
|
||||
|
||||
import (
|
||||
"golang.org/x/mobile/app"
|
||||
|
||||
_ "golang.org/x/mobile/bind/java"
|
||||
_ "{{.}}"
|
||||
)
|
||||
|
||||
func main() {
|
||||
app.Run(app.Callbacks{})
|
||||
}
|
||||
`))
|
@ -137,58 +137,9 @@ func runBuild(cmd *command) error {
|
||||
}
|
||||
libPath := filepath.Join(tmpdir, "lib"+libName+".so")
|
||||
|
||||
gopath := goEnv("GOPATH")
|
||||
for _, p := range filepath.SplitList(gopath) {
|
||||
ndkccpath = filepath.Join(p, filepath.FromSlash("pkg/gomobile/android-"+ndkVersion))
|
||||
if _, err = os.Stat(filepath.Join(ndkccpath, "arm", "bin")); err == nil {
|
||||
break
|
||||
}
|
||||
if err := gobuild(pkg.ImportPath, libPath); err != nil {
|
||||
return err
|
||||
}
|
||||
if err != nil || ndkccpath == "" {
|
||||
// TODO(crawshaw): call gomobile init
|
||||
return fmt.Errorf("android toolchain not installed in $GOPATH/pkg/gomobile, run:\n\tgomobile init")
|
||||
}
|
||||
if buildX {
|
||||
fmt.Fprintln(os.Stderr, "NDKCCPATH="+ndkccpath)
|
||||
}
|
||||
|
||||
gocmd := exec.Command(
|
||||
`go`,
|
||||
`build`,
|
||||
`-ldflags="-shared"`,
|
||||
`-tags=`+strconv.Quote(strings.Join(ctx.BuildTags, ",")),
|
||||
`-o`, libPath)
|
||||
if buildV {
|
||||
gocmd.Args = append(gocmd.Args, "-v")
|
||||
}
|
||||
if buildI {
|
||||
gocmd.Args = append(gocmd.Args, "-i")
|
||||
}
|
||||
|
||||
gocmd.Args = append(gocmd.Args, pkg.ImportPath)
|
||||
|
||||
gocmd.Stdout = os.Stdout
|
||||
gocmd.Stderr = os.Stderr
|
||||
gocmd.Env = []string{
|
||||
`GOOS=android`,
|
||||
`GOARCH=arm`,
|
||||
`GOARM=7`,
|
||||
`CGO_ENABLED=1`,
|
||||
`CC=` + filepath.Join(ndkccpath, "arm", "bin", "arm-linux-androideabi-gcc"),
|
||||
`CXX=` + filepath.Join(ndkccpath, "arm", "bin", "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 {
|
||||
if err := gocmd.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
block, _ := pem.Decode([]byte(debugCert))
|
||||
if block == nil {
|
||||
return errors.New("no debug cert")
|
||||
@ -322,6 +273,62 @@ func addBuildFlagsNVX(cmd *command) {
|
||||
cmd.flag.BoolVar(&buildX, "x", false, "")
|
||||
}
|
||||
|
||||
func gobuild(src, libPath string) error {
|
||||
var err error
|
||||
gopath := goEnv("GOPATH")
|
||||
for _, p := range filepath.SplitList(gopath) {
|
||||
ndkccpath = filepath.Join(p, filepath.FromSlash("pkg/gomobile/android-"+ndkVersion))
|
||||
if _, err = os.Stat(filepath.Join(ndkccpath, "arm", "bin")); err == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
if err != nil || ndkccpath == "" {
|
||||
// TODO(crawshaw): call gomobile init
|
||||
return fmt.Errorf("android toolchain not installed in $GOPATH/pkg/gomobile, run:\n\tgomobile init")
|
||||
}
|
||||
if buildX {
|
||||
fmt.Fprintln(os.Stderr, "NDKCCPATH="+ndkccpath)
|
||||
}
|
||||
|
||||
gocmd := exec.Command(
|
||||
`go`,
|
||||
`build`,
|
||||
`-ldflags="-shared"`,
|
||||
`-tags=`+strconv.Quote(strings.Join(ctx.BuildTags, ",")),
|
||||
`-o`, libPath)
|
||||
if buildV {
|
||||
gocmd.Args = append(gocmd.Args, "-v")
|
||||
}
|
||||
if buildI {
|
||||
gocmd.Args = append(gocmd.Args, "-i")
|
||||
}
|
||||
|
||||
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(ndkccpath, "arm", "bin", "arm-linux-androideabi-gcc"),
|
||||
`CXX=` + filepath.Join(ndkccpath, "arm", "bin", "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 {
|
||||
if err := gocmd.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
buildO = cmdBuild.flag.String("o", "", "output file")
|
||||
addBuildFlags(cmdBuild)
|
||||
@ -331,6 +338,9 @@ func init() {
|
||||
addBuildFlagsNVX(cmdInstall)
|
||||
|
||||
addBuildFlagsNVX(cmdInit)
|
||||
|
||||
addBuildFlags(cmdBind)
|
||||
addBuildFlagsNVX(cmdBind)
|
||||
}
|
||||
|
||||
// A random uninteresting private key.
|
||||
|
@ -90,6 +90,7 @@ func help(args []string) {
|
||||
|
||||
var commands = []*command{
|
||||
// TODO(crawshaw): cmdRun
|
||||
cmdBind,
|
||||
cmdBuild,
|
||||
cmdInit,
|
||||
cmdInstall,
|
||||
|
Loading…
x
Reference in New Issue
Block a user