blob: 6d40267f846fe721ccdb84a62774c50abe4aa812 [file] [log] [blame]
// 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"
"go/types"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
"golang.org/x/mobile/bind"
"golang.org/x/mobile/internal/loader"
)
// ctx, pkg, tmpdir in build.go
var cmdBind = &command{
run: runBind,
Name: "bind",
Usage: "[-target android|ios] [-o output] [build flags] [package]",
Short: "build a library for Android and iOS",
Long: `
Bind generates language bindings for the package named by the import
path, and compiles a library for the named target system.
The -target flag takes a target system name, either android (the
default) or ios.
For -target android, the bind command produces an AAR (Android ARchive)
file that archives the precompiled Java API stub classes, the compiled
shared libraries, and all asset files in the /assets subdirectory under
the package directory. The output is named '<package_name>.aar' by
default. This AAR file is commonly used for binary distribution of an
Android library project and most Android IDEs support AAR import. For
example, in Android Studio (1.2+), an AAR file can be imported using
the module import wizard (File > New > New Module > Import .JAR or
.AAR package), and setting it as a new dependency
(File > Project Structure > Dependencies). This requires 'javac'
(version 1.7+) and Android SDK (API level 9 or newer) to build the
library for Android. The environment variable ANDROID_HOME must be set
to the path to Android SDK. The generated Java class is in the java
package 'go.<package_name>' unless -javapkg flag is specified.
For -target ios, gomobile must be run on an OS X machine with Xcode
installed. Support is not complete. The generated Objective-C types
are prefixed with 'Go' unless the -prefix flag is provided.
The -v flag provides verbose output, including the list of packages built.
The build flags -a, -i, -n, -x, -gcflags, -ldflags, -tags, and -work
are shared with the build command. For documentation, see 'go help build'.
`,
}
func runBind(cmd *command) error {
cleanup, err := buildEnvInit()
if err != nil {
return err
}
defer cleanup()
args := cmd.flag.Args()
ctx.GOARCH = "arm"
switch buildTarget {
case "android":
ctx.GOOS = "android"
case "ios":
ctx.GOOS = "darwin"
default:
return fmt.Errorf(`unknown -target, %q.`, buildTarget)
}
if bindJavaPkg != "" && ctx.GOOS != "android" {
return fmt.Errorf("-javapkg is supported only for android target")
}
if bindPrefix != "" && ctx.GOOS != "darwin" {
return fmt.Errorf("-prefix is supported only for ios target")
}
var pkg *build.Package
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":
return goAndroidBind(pkg)
case "ios":
return goIOSBind(pkg)
default:
return fmt.Errorf(`unknown -target, %q.`, buildTarget)
}
}
var (
bindPrefix string // -prefix
bindJavaPkg string // -javapkg
)
func init() {
// bind command specific commands.
cmdBind.flag.StringVar(&bindJavaPkg, "javapkg", "",
"specifies custom Java package path used instead of the default 'go.<go package name>'. Valid only with -target=android.")
cmdBind.flag.StringVar(&bindPrefix, "prefix", "",
"custom Objective-C name prefix used instead of the default 'Go'. Valid only with -lang=ios.")
}
type binder struct {
files []*ast.File
fset *token.FileSet
pkg *types.Package
}
func (b *binder) GenObjc(outdir string) error {
const bindPrefixDefault = "Go"
if bindPrefix == "" {
bindPrefix = bindPrefixDefault
}
name := strings.Title(b.pkg.Name())
bindOption := "-lang=objc"
if bindPrefix != bindPrefixDefault {
bindOption += " -prefix=" + bindPrefix
}
mfile := filepath.Join(outdir, bindPrefix+name+".m")
hfile := filepath.Join(outdir, bindPrefix+name+".h")
generate := func(w io.Writer) error {
if buildX {
printcmd("gobind %s -outdir=%s %s", bindOption, outdir, b.pkg.Path())
}
return bind.GenObjc(w, b.fset, b.pkg, bindPrefix, false)
}
if err := writeFile(mfile, generate); err != nil {
return err
}
generate = func(w io.Writer) error {
return bind.GenObjc(w, b.fset, b.pkg, bindPrefix, true)
}
if err := writeFile(hfile, generate); err != nil {
return err
}
objcPkg, err := ctx.Import("golang.org/x/mobile/bind/objc", "", build.FindOnly)
if err != nil {
return err
}
return copyFile(filepath.Join(outdir, "seq.h"), filepath.Join(objcPkg.Dir, "seq.h"))
}
func (b *binder) GenJava(outdir string) error {
className := strings.Title(b.pkg.Name())
javaFile := filepath.Join(outdir, className+".java")
bindOption := "-lang=java"
if bindJavaPkg != "" {
bindOption += " -javapkg=" + bindJavaPkg
}
generate := func(w io.Writer) error {
if buildX {
printcmd("gobind %s -outdir=%s %s", bindOption, outdir, b.pkg.Path())
}
return bind.GenJava(w, b.fset, b.pkg, bindJavaPkg)
}
if err := writeFile(javaFile, generate); err != nil {
return err
}
return nil
}
func (b *binder) GenGo(outdir string) error {
pkgName := "go_" + b.pkg.Name()
outdir = filepath.Join(outdir, pkgName)
goFile := filepath.Join(outdir, pkgName+"main.go")
generate := func(w io.Writer) error {
if buildX {
printcmd("gobind -lang=go -outdir=%s %s", outdir, b.pkg.Path())
}
return bind.GenGo(w, b.fset, b.pkg)
}
if err := writeFile(goFile, generate); err != nil {
return err
}
return nil
}
func copyFile(dst, src string) error {
if buildX {
printcmd("cp %s %s", src, dst)
}
return writeFile(dst, func(w io.Writer) error {
if buildN {
return nil
}
f, err := os.Open(src)
if err != nil {
return err
}
defer f.Close()
if _, err := io.Copy(w, f); err != nil {
return fmt.Errorf("cp %s %s failed: %v", src, dst, 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)
}
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{
Fset: fset,
AllowErrors: true,
}
conf.TypeChecker.IgnoreFuncBodies = true
conf.TypeChecker.FakeImportC = true
conf.TypeChecker.DisableUnusedImportCheck = true
var tcErrs []error
conf.TypeChecker.Error = func(err error) {
tcErrs = append(tcErrs, err)
}
conf.CreateFromFiles(bindPkg.ImportPath, files...)
program, err := conf.Load()
if err != nil {
for _, err := range tcErrs {
fmt.Fprintln(os.Stderr, err)
}
return nil, err
}
b := &binder{
files: files,
fset: fset,
pkg: program.Created[0].Pkg,
}
return b, nil
}