github.com/F4RD1N/gomobile@v1.0.1/cmd/gomobile/bind.go (about)

     1  // Copyright 2015 The Go Authors.  All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package main
     6  
     7  import (
     8  	"bytes"
     9  	"encoding/json"
    10  	"errors"
    11  	"fmt"
    12  	"io"
    13  	"io/ioutil"
    14  	"os"
    15  	"os/exec"
    16  	"path/filepath"
    17  	"strings"
    18  
    19  	"golang.org/x/mod/modfile"
    20  	"golang.org/x/tools/go/packages"
    21  )
    22  
    23  var cmdBind = &command{
    24  	run:   runBind,
    25  	Name:  "bind",
    26  	Usage: "[-target android|ios] [-bootclasspath <path>] [-classpath <path>] [-o output] [build flags] [package]",
    27  	Short: "build a library for Android and iOS",
    28  	Long: `
    29  Bind generates language bindings for the package named by the import
    30  path, and compiles a library for the named target system.
    31  
    32  The -target flag takes a target system name, either android (the
    33  default) or ios.
    34  
    35  For -target android, the bind command produces an AAR (Android ARchive)
    36  file that archives the precompiled Java API stub classes, the compiled
    37  shared libraries, and all asset files in the /assets subdirectory under
    38  the package directory. The output is named '<package_name>.aar' by
    39  default. This AAR file is commonly used for binary distribution of an
    40  Android library project and most Android IDEs support AAR import. For
    41  example, in Android Studio (1.2+), an AAR file can be imported using
    42  the module import wizard (File > New > New Module > Import .JAR or
    43  .AAR package), and setting it as a new dependency
    44  (File > Project Structure > Dependencies).  This requires 'javac'
    45  (version 1.7+) and Android SDK (API level 15 or newer) to build the
    46  library for Android. The environment variable ANDROID_HOME must be set
    47  to the path to Android SDK. Use the -javapkg flag to specify the Java
    48  package prefix for the generated classes.
    49  
    50  By default, -target=android builds shared libraries for all supported
    51  instruction sets (arm, arm64, 386, amd64). A subset of instruction sets
    52  can be selected by specifying target type with the architecture name. E.g.,
    53  -target=android/arm,android/386.
    54  
    55  For -target ios, gomobile must be run on an OS X machine with Xcode
    56  installed. The generated Objective-C types can be prefixed with the -prefix
    57  flag.
    58  
    59  For -target android, the -bootclasspath and -classpath flags are used to
    60  control the bootstrap classpath and the classpath for Go wrappers to Java
    61  classes.
    62  
    63  The -v flag provides verbose output, including the list of packages built.
    64  
    65  The build flags -a, -n, -x, -gcflags, -ldflags, -tags, -trimpath, and -work
    66  are shared with the build command. For documentation, see 'go help build'.
    67  `,
    68  }
    69  
    70  func runBind(cmd *command) error {
    71  	cleanup, err := buildEnvInit()
    72  	if err != nil {
    73  		return err
    74  	}
    75  	defer cleanup()
    76  
    77  	args := cmd.flag.Args()
    78  
    79  	targetOS, targetArchs, err := parseBuildTarget(buildTarget)
    80  	if err != nil {
    81  		return fmt.Errorf(`invalid -target=%q: %v`, buildTarget, err)
    82  	}
    83  
    84  	if bindJavaPkg != "" && targetOS != "android" {
    85  		return fmt.Errorf("-javapkg is supported only for android target")
    86  	}
    87  	if bindPrefix != "" && targetOS != "darwin" {
    88  		return fmt.Errorf("-prefix is supported only for ios target")
    89  	}
    90  
    91  	if targetOS == "android" {
    92  		if _, err := ndkRoot(); err != nil {
    93  			return err
    94  		}
    95  	}
    96  
    97  	var gobind string
    98  	if !buildN {
    99  		gobind, err = exec.LookPath("gobind")
   100  		if err != nil {
   101  			return errors.New("gobind was not found. Please run gomobile init before trying again.")
   102  		}
   103  	} else {
   104  		gobind = "gobind"
   105  	}
   106  
   107  	if len(args) == 0 {
   108  		args = append(args, ".")
   109  	}
   110  	pkgs, err := importPackages(args, targetOS)
   111  	if err != nil {
   112  		return err
   113  	}
   114  
   115  	// check if any of the package is main
   116  	for _, pkg := range pkgs {
   117  		if pkg.Name == "main" {
   118  			return fmt.Errorf("binding 'main' package (%s) is not supported", pkg.PkgPath)
   119  		}
   120  	}
   121  
   122  	switch targetOS {
   123  	case "android":
   124  		return goAndroidBind(gobind, pkgs, targetArchs)
   125  	case "darwin":
   126  		if !xcodeAvailable() {
   127  			return fmt.Errorf("-target=ios requires XCode")
   128  		}
   129  		return goIOSBind(gobind, pkgs, targetArchs)
   130  	default:
   131  		return fmt.Errorf(`invalid -target=%q`, buildTarget)
   132  	}
   133  }
   134  
   135  func importPackages(args []string, targetOS string) ([]*packages.Package, error) {
   136  	config := packagesConfig(targetOS)
   137  	return packages.Load(config, args...)
   138  }
   139  
   140  var (
   141  	bindPrefix        string // -prefix
   142  	bindJavaPkg       string // -javapkg
   143  	bindClasspath     string // -classpath
   144  	bindBootClasspath string // -bootclasspath
   145  )
   146  
   147  func init() {
   148  	// bind command specific commands.
   149  	cmdBind.flag.StringVar(&bindJavaPkg, "javapkg", "",
   150  		"specifies custom Java package path prefix. Valid only with -target=android.")
   151  	cmdBind.flag.StringVar(&bindPrefix, "prefix", "",
   152  		"custom Objective-C name prefix. Valid only with -target=ios.")
   153  	cmdBind.flag.StringVar(&bindClasspath, "classpath", "", "The classpath for imported Java classes. Valid only with -target=android.")
   154  	cmdBind.flag.StringVar(&bindBootClasspath, "bootclasspath", "", "The bootstrap classpath for imported Java classes. Valid only with -target=android.")
   155  }
   156  
   157  func bootClasspath() (string, error) {
   158  	if bindBootClasspath != "" {
   159  		return bindBootClasspath, nil
   160  	}
   161  	apiPath, err := androidAPIPath()
   162  	if err != nil {
   163  		return "", err
   164  	}
   165  	return filepath.Join(apiPath, "android.jar"), nil
   166  }
   167  
   168  func copyFile(dst, src string) error {
   169  	if buildX {
   170  		printcmd("cp %s %s", src, dst)
   171  	}
   172  	return writeFile(dst, func(w io.Writer) error {
   173  		if buildN {
   174  			return nil
   175  		}
   176  		f, err := os.Open(src)
   177  		if err != nil {
   178  			return err
   179  		}
   180  		defer f.Close()
   181  
   182  		if _, err := io.Copy(w, f); err != nil {
   183  			return fmt.Errorf("cp %s %s failed: %v", src, dst, err)
   184  		}
   185  		return nil
   186  	})
   187  }
   188  
   189  func writeFile(filename string, generate func(io.Writer) error) error {
   190  	if buildV {
   191  		fmt.Fprintf(os.Stderr, "write %s\n", filename)
   192  	}
   193  
   194  	if err := mkdir(filepath.Dir(filename)); err != nil {
   195  		return err
   196  	}
   197  
   198  	if buildN {
   199  		return generate(ioutil.Discard)
   200  	}
   201  
   202  	f, err := os.Create(filename)
   203  	if err != nil {
   204  		return err
   205  	}
   206  	defer func() {
   207  		if cerr := f.Close(); err == nil {
   208  			err = cerr
   209  		}
   210  	}()
   211  
   212  	return generate(f)
   213  }
   214  
   215  func packagesConfig(targetOS string) *packages.Config {
   216  	config := &packages.Config{}
   217  	// Add CGO_ENABLED=1 explicitly since Cgo is disabled when GOOS is different from host OS.
   218  	config.Env = append(os.Environ(), "GOARCH=arm64", "GOOS="+targetOS, "CGO_ENABLED=1")
   219  	tags := buildTags
   220  	if targetOS == "darwin" {
   221  		tags = append(tags, "ios")
   222  	}
   223  	if len(tags) > 0 {
   224  		config.BuildFlags = []string{"-tags=" + strings.Join(tags, ",")}
   225  	}
   226  	return config
   227  }
   228  
   229  // getModuleVersions returns a module information at the directory src.
   230  func getModuleVersions(targetOS string, targetArch string, src string) (*modfile.File, error) {
   231  	cmd := exec.Command("go", "list")
   232  	cmd.Env = append(os.Environ(), "GOOS="+targetOS, "GOARCH="+targetArch)
   233  
   234  	tags := buildTags
   235  	if targetOS == "darwin" {
   236  		tags = append(tags, "ios")
   237  	}
   238  	// TODO(hyangah): probably we don't need to add all the dependencies.
   239  	cmd.Args = append(cmd.Args, "-m", "-json", "-tags="+strings.Join(tags, ","), "all")
   240  	cmd.Dir = src
   241  
   242  	output, err := cmd.Output()
   243  	if err != nil {
   244  		// Module information is not available at src.
   245  		return nil, nil
   246  	}
   247  
   248  	type Module struct {
   249  		Main    bool
   250  		Path    string
   251  		Version string
   252  		Dir     string
   253  		Replace *Module
   254  	}
   255  
   256  	f := &modfile.File{}
   257  	f.AddModuleStmt("gobind")
   258  	e := json.NewDecoder(bytes.NewReader(output))
   259  	for {
   260  		var mod *Module
   261  		err := e.Decode(&mod)
   262  		if err != nil && err != io.EOF {
   263  			return nil, err
   264  		}
   265  		if mod != nil {
   266  			if mod.Replace != nil {
   267  				p, v := mod.Replace.Path, mod.Replace.Version
   268  				if modfile.IsDirectoryPath(p) {
   269  					// replaced by a local directory
   270  					p = mod.Replace.Dir
   271  				}
   272  				f.AddReplace(mod.Path, mod.Version, p, v)
   273  			} else {
   274  				// When the version part is empty, the module is local and mod.Dir represents the location.
   275  				if v := mod.Version; v == "" {
   276  					f.AddReplace(mod.Path, mod.Version, mod.Dir, "")
   277  				} else {
   278  					f.AddRequire(mod.Path, v)
   279  				}
   280  			}
   281  		}
   282  		if err == io.EOF {
   283  			break
   284  		}
   285  	}
   286  	return f, nil
   287  }
   288  
   289  // writeGoMod writes go.mod file at $WORK/src when Go modules are used.
   290  func writeGoMod(targetOS string, targetArch string) error {
   291  	m, err := areGoModulesUsed()
   292  	if err != nil {
   293  		return err
   294  	}
   295  	// If Go modules are not used, go.mod should not be created because the dependencies might not be compatible with Go modules.
   296  	if !m {
   297  		return nil
   298  	}
   299  
   300  	return writeFile(filepath.Join(tmpdir, "src", "go.mod"), func(w io.Writer) error {
   301  		f, err := getModuleVersions(targetOS, targetArch, ".")
   302  		if err != nil {
   303  			return err
   304  		}
   305  		if f == nil {
   306  			return nil
   307  		}
   308  		bs, err := f.Format()
   309  		if err != nil {
   310  			return err
   311  		}
   312  		if _, err := w.Write(bs); err != nil {
   313  			return err
   314  		}
   315  		return nil
   316  	})
   317  }
   318  
   319  func areGoModulesUsed() (bool, error) {
   320  	out, err := exec.Command("go", "env", "GOMOD").Output()
   321  	if err != nil {
   322  		return false, err
   323  	}
   324  	outstr := strings.TrimSpace(string(out))
   325  	if outstr == "" {
   326  		return false, nil
   327  	}
   328  	return true, nil
   329  }