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