github.com/coming-chat/gomobile@v0.0.0-20220601074111-56995f7d7aba/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/mobile/internal/sdkpath"
    20  	"golang.org/x/mod/modfile"
    21  	"golang.org/x/tools/go/packages"
    22  )
    23  
    24  var cmdBind = &command{
    25  	run:   runBind,
    26  	Name:  "bind",
    27  	Usage: "[-target android|" + strings.Join(applePlatforms, "|") + "] [-bootclasspath <path>] [-classpath <path>] [-o output] [build flags] [package]",
    28  	Short: "build a library for Android and iOS",
    29  	Long: `
    30  Bind generates language bindings for the package named by the import
    31  path, and compiles a library for the named target system.
    32  
    33  The -target flag takes either android (the default), or one or more
    34  comma-delimited Apple platforms (` + strings.Join(applePlatforms, ", ") + `).
    35  
    36  For -target android, the bind command produces an AAR (Android ARchive)
    37  file that archives the precompiled Java API stub classes, the compiled
    38  shared libraries, and all asset files in the /assets subdirectory under
    39  the package directory. The output is named '<package_name>.aar' by
    40  default. This AAR file is commonly used for binary distribution of an
    41  Android library project and most Android IDEs support AAR import. For
    42  example, in Android Studio (1.2+), an AAR file can be imported using
    43  the module import wizard (File > New > New Module > Import .JAR or
    44  .AAR package), and setting it as a new dependency
    45  (File > Project Structure > Dependencies).  This requires 'javac'
    46  (version 1.7+) and Android SDK (API level 16 or newer) to build the
    47  library for Android. The ANDROID_HOME and ANDROID_NDK_HOME environment
    48  variables can be used to specify the Android SDK and NDK if they are
    49  not in the default locations. Use the -javapkg flag to specify the Java
    50  package prefix for the generated classes.
    51  
    52  By default, -target=android builds shared libraries for all supported
    53  instruction sets (arm, arm64, 386, amd64). A subset of instruction sets
    54  can be selected by specifying target type with the architecture name. E.g.,
    55  -target=android/arm,android/386.
    56  
    57  For Apple -target platforms, gomobile must be run on an OS X machine with
    58  Xcode installed. The generated Objective-C types can be prefixed with the
    59  -prefix flag.
    60  
    61  For -target android, the -bootclasspath and -classpath flags are used to
    62  control the bootstrap classpath and the classpath for Go wrappers to Java
    63  classes.
    64  
    65  The -v flag provides verbose output, including the list of packages built.
    66  
    67  The build flags -a, -n, -x, -gcflags, -ldflags, -tags, -trimpath, and -work
    68  are shared with the build command. For documentation, see 'go help build'.
    69  `,
    70  }
    71  
    72  func runBind(cmd *command) error {
    73  	cleanup, err := buildEnvInit()
    74  	if err != nil {
    75  		return err
    76  	}
    77  	defer cleanup()
    78  
    79  	args := cmd.flag.Args()
    80  
    81  	targets, err := parseBuildTarget(buildTarget)
    82  	if err != nil {
    83  		return fmt.Errorf(`invalid -target=%q: %v`, buildTarget, err)
    84  	}
    85  
    86  	if isAndroidPlatform(targets[0].platform) {
    87  		if bindPrefix != "" {
    88  			return fmt.Errorf("-prefix is supported only for Apple targets")
    89  		}
    90  		if _, err := ndkRoot(targets[0]); err != nil {
    91  			return err
    92  		}
    93  	} else {
    94  		if bindJavaPkg != "" {
    95  			return fmt.Errorf("-javapkg is supported only for android target")
    96  		}
    97  	}
    98  
    99  	var gobind string
   100  	if !buildN {
   101  		gobind, err = exec.LookPath("gobind")
   102  		if err != nil {
   103  			return errors.New("gobind was not found. Please run gomobile init before trying again")
   104  		}
   105  	} else {
   106  		gobind = "gobind"
   107  	}
   108  
   109  	if len(args) == 0 {
   110  		args = append(args, ".")
   111  	}
   112  
   113  	// TODO(ydnar): this should work, unless build tags affect loading a single package.
   114  	// Should we try to import packages with different build tags per platform?
   115  	pkgs, err := packages.Load(packagesConfig(targets[0]), args...)
   116  	if err != nil {
   117  		return err
   118  	}
   119  
   120  	// check if any of the package is main
   121  	for _, pkg := range pkgs {
   122  		if pkg.Name == "main" {
   123  			return fmt.Errorf(`binding "main" package (%s) is not supported`, pkg.PkgPath)
   124  		}
   125  	}
   126  
   127  	switch {
   128  	case isAndroidPlatform(targets[0].platform):
   129  		return goAndroidBind(gobind, pkgs, targets)
   130  	case isApplePlatform(targets[0].platform):
   131  		if !xcodeAvailable() {
   132  			return fmt.Errorf("-target=%q requires Xcode", buildTarget)
   133  		}
   134  		return goAppleBind(gobind, pkgs, targets)
   135  	default:
   136  		return fmt.Errorf(`invalid -target=%q`, buildTarget)
   137  	}
   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 := sdkpath.AndroidAPIPath(buildAndroidAPI)
   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(t targetInfo) *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="+t.arch, "GOOS="+platformOS(t.platform), "CGO_ENABLED=1")
   219  	tags := append(buildTags[:], platformTags(t.platform)...)
   220  
   221  	if len(tags) > 0 {
   222  		config.BuildFlags = []string{"-tags=" + strings.Join(tags, ",")}
   223  	}
   224  	return config
   225  }
   226  
   227  // getModuleVersions returns a module information at the directory src.
   228  func getModuleVersions(targetPlatform string, targetArch string, src string) (*modfile.File, error) {
   229  	cmd := exec.Command("go", "list")
   230  	cmd.Env = append(os.Environ(), "GOOS="+platformOS(targetPlatform), "GOARCH="+targetArch)
   231  
   232  	tags := append(buildTags[:], platformTags(targetPlatform)...)
   233  
   234  	// TODO(hyangah): probably we don't need to add all the dependencies.
   235  	cmd.Args = append(cmd.Args, "-m", "-json", "-tags="+strings.Join(tags, ","), "all")
   236  	cmd.Dir = src
   237  
   238  	output, err := cmd.Output()
   239  	if err != nil {
   240  		// Module information is not available at src.
   241  		return nil, nil
   242  	}
   243  
   244  	type Module struct {
   245  		Main    bool
   246  		Path    string
   247  		Version string
   248  		Dir     string
   249  		Replace *Module
   250  	}
   251  
   252  	f := &modfile.File{}
   253  	f.AddModuleStmt("gobind")
   254  	e := json.NewDecoder(bytes.NewReader(output))
   255  	for {
   256  		var mod *Module
   257  		err := e.Decode(&mod)
   258  		if err != nil && err != io.EOF {
   259  			return nil, err
   260  		}
   261  		if mod != nil {
   262  			if mod.Replace != nil {
   263  				p, v := mod.Replace.Path, mod.Replace.Version
   264  				if modfile.IsDirectoryPath(p) {
   265  					// replaced by a local directory
   266  					p = mod.Replace.Dir
   267  				}
   268  				f.AddReplace(mod.Path, mod.Version, p, v)
   269  			} else {
   270  				// When the version part is empty, the module is local and mod.Dir represents the location.
   271  				if v := mod.Version; v == "" {
   272  					f.AddReplace(mod.Path, mod.Version, mod.Dir, "")
   273  				} else {
   274  					f.AddRequire(mod.Path, v)
   275  				}
   276  			}
   277  		}
   278  		if err == io.EOF {
   279  			break
   280  		}
   281  	}
   282  	return f, nil
   283  }
   284  
   285  // writeGoMod writes go.mod file at $WORK/src when Go modules are used.
   286  func writeGoMod(dir, targetPlatform, targetArch string) error {
   287  	m, err := areGoModulesUsed()
   288  	if err != nil {
   289  		return err
   290  	}
   291  	// If Go modules are not used, go.mod should not be created because the dependencies might not be compatible with Go modules.
   292  	if !m {
   293  		return nil
   294  	}
   295  
   296  	return writeFile(filepath.Join(dir, "src", "go.mod"), func(w io.Writer) error {
   297  		f, err := getModuleVersions(targetPlatform, targetArch, ".")
   298  		if err != nil {
   299  			return err
   300  		}
   301  		if f == nil {
   302  			return nil
   303  		}
   304  		bs, err := f.Format()
   305  		if err != nil {
   306  			return err
   307  		}
   308  		if _, err := w.Write(bs); err != nil {
   309  			return err
   310  		}
   311  		return nil
   312  	})
   313  }
   314  
   315  func areGoModulesUsed() (bool, error) {
   316  	out, err := exec.Command("go", "env", "GOMOD").Output()
   317  	if err != nil {
   318  		return false, err
   319  	}
   320  	outstr := strings.TrimSpace(string(out))
   321  	if outstr == "" {
   322  		return false, nil
   323  	}
   324  	return true, nil
   325  }