github.com/SpiderOak/mobile@v0.0.0-20221129182558-6f541b59af45/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  	"fmt"
    11  	"io"
    12  	"io/ioutil"
    13  	"os"
    14  	"os/exec"
    15  	"path/filepath"
    16  	"strings"
    17  	"sync"
    18  
    19  	"github.com/SpiderOak/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  	if len(args) == 0 {
   100  		args = append(args, ".")
   101  	}
   102  
   103  	// TODO(ydnar): this should work, unless build tags affect loading a single package.
   104  	// Should we try to import packages with different build tags per platform?
   105  	pkgs, err := packages.Load(packagesConfig(targets[0]), args...)
   106  	if err != nil {
   107  		return err
   108  	}
   109  
   110  	// check if any of the package is main
   111  	for _, pkg := range pkgs {
   112  		if pkg.Name == "main" {
   113  			return fmt.Errorf(`binding "main" package (%s) is not supported`, pkg.PkgPath)
   114  		}
   115  	}
   116  
   117  	switch {
   118  	case isAndroidPlatform(targets[0].platform):
   119  		return goAndroidBind(pkgs, targets)
   120  	case isApplePlatform(targets[0].platform):
   121  		if !xcodeAvailable() {
   122  			return fmt.Errorf("-target=%q requires Xcode", buildTarget)
   123  		}
   124  		return goAppleBind(pkgs, targets)
   125  	default:
   126  		return fmt.Errorf(`invalid -target=%q`, buildTarget)
   127  	}
   128  }
   129  
   130  var (
   131  	bindPrefix        string // -prefix
   132  	bindJavaPkg       string // -javapkg
   133  	bindClasspath     string // -classpath
   134  	bindBootClasspath string // -bootclasspath
   135  )
   136  
   137  func init() {
   138  	// bind command specific commands.
   139  	cmdBind.flag.StringVar(&bindJavaPkg, "javapkg", "",
   140  		"specifies custom Java package path prefix. Valid only with -target=android.")
   141  	cmdBind.flag.StringVar(&bindPrefix, "prefix", "",
   142  		"custom Objective-C name prefix. Valid only with -target=ios.")
   143  	cmdBind.flag.StringVar(&bindClasspath, "classpath", "", "The classpath for imported Java classes. Valid only with -target=android.")
   144  	cmdBind.flag.StringVar(&bindBootClasspath, "bootclasspath", "", "The bootstrap classpath for imported Java classes. Valid only with -target=android.")
   145  }
   146  
   147  func bootClasspath() (string, error) {
   148  	if bindBootClasspath != "" {
   149  		return bindBootClasspath, nil
   150  	}
   151  	apiPath, err := sdkpath.AndroidAPIPath(buildAndroidAPI)
   152  	if err != nil {
   153  		return "", err
   154  	}
   155  	return filepath.Join(apiPath, "android.jar"), nil
   156  }
   157  
   158  func copyFile(dst, src string) error {
   159  	if buildX {
   160  		printcmd("cp %s %s", src, dst)
   161  	}
   162  	return writeFile(dst, func(w io.Writer) error {
   163  		if buildN {
   164  			return nil
   165  		}
   166  		f, err := os.Open(src)
   167  		if err != nil {
   168  			return err
   169  		}
   170  		defer f.Close()
   171  
   172  		if _, err := io.Copy(w, f); err != nil {
   173  			return fmt.Errorf("cp %s %s failed: %v", src, dst, err)
   174  		}
   175  		return nil
   176  	})
   177  }
   178  
   179  func writeFile(filename string, generate func(io.Writer) error) error {
   180  	if buildV {
   181  		fmt.Fprintf(os.Stderr, "write %s\n", filename)
   182  	}
   183  
   184  	if err := mkdir(filepath.Dir(filename)); err != nil {
   185  		return err
   186  	}
   187  
   188  	if buildN {
   189  		return generate(ioutil.Discard)
   190  	}
   191  
   192  	f, err := os.Create(filename)
   193  	if err != nil {
   194  		return err
   195  	}
   196  	defer func() {
   197  		if cerr := f.Close(); err == nil {
   198  			err = cerr
   199  		}
   200  	}()
   201  
   202  	return generate(f)
   203  }
   204  
   205  func packagesConfig(t targetInfo) *packages.Config {
   206  	config := &packages.Config{}
   207  	// Add CGO_ENABLED=1 explicitly since Cgo is disabled when GOOS is different from host OS.
   208  	config.Env = append(os.Environ(), "GOARCH="+t.arch, "GOOS="+platformOS(t.platform), "CGO_ENABLED=1")
   209  	tags := append(buildTags[:], platformTags(t.platform)...)
   210  
   211  	if len(tags) > 0 {
   212  		config.BuildFlags = []string{"-tags=" + strings.Join(tags, ",")}
   213  	}
   214  	return config
   215  }
   216  
   217  // getModuleVersions returns a module information at the directory src.
   218  func getModuleVersions(targetPlatform string, targetArch string, src string) (*modfile.File, error) {
   219  	cmd := exec.Command("go", "list")
   220  	cmd.Env = append(os.Environ(), "GOOS="+platformOS(targetPlatform), "GOARCH="+targetArch)
   221  
   222  	tags := append(buildTags[:], platformTags(targetPlatform)...)
   223  
   224  	// TODO(hyangah): probably we don't need to add all the dependencies.
   225  	cmd.Args = append(cmd.Args, "-m", "-json", "-tags="+strings.Join(tags, ","), "all")
   226  	cmd.Dir = src
   227  	if buildX || buildN {
   228  		printcmd("PWD=%s GOOS=%s GOARCH=%s %s",
   229  			src, platformOS(targetPlatform), targetArch, cmd)
   230  	}
   231  
   232  	output, err := cmd.Output()
   233  	if err != nil {
   234  		// Module information is not available at src.
   235  		return nil, nil
   236  	}
   237  
   238  	type Module struct {
   239  		Main    bool
   240  		Path    string
   241  		Version string
   242  		Dir     string
   243  		Replace *Module
   244  	}
   245  
   246  	f := &modfile.File{}
   247  	f.AddModuleStmt("gobind")
   248  	e := json.NewDecoder(bytes.NewReader(output))
   249  	for {
   250  		var mod *Module
   251  		err := e.Decode(&mod)
   252  		if err != nil && err != io.EOF {
   253  			return nil, err
   254  		}
   255  		if mod != nil {
   256  			if mod.Replace != nil {
   257  				p, v := mod.Replace.Path, mod.Replace.Version
   258  				if modfile.IsDirectoryPath(p) {
   259  					// replaced by a local directory
   260  					p = mod.Replace.Dir
   261  				}
   262  				f.AddReplace(mod.Path, mod.Version, p, v)
   263  			} else {
   264  				// When the version part is empty, the module is local and mod.Dir represents the location.
   265  				if v := mod.Version; v == "" {
   266  					f.AddReplace(mod.Path, mod.Version, mod.Dir, "")
   267  				} else {
   268  					f.AddRequire(mod.Path, v)
   269  				}
   270  			}
   271  		}
   272  		if err == io.EOF {
   273  			break
   274  		}
   275  	}
   276  	return f, nil
   277  }
   278  
   279  // writeGoMod writes go.mod file at dir when Go modules are used.
   280  func writeGoMod(pkgModPath string, workSrc string) error {
   281  	m, err := areGoModulesUsed()
   282  	if err != nil {
   283  		return err
   284  	}
   285  	// If Go modules are not used, go.mod should not be created because the dependencies might not be compatible with Go modules.
   286  	if !m {
   287  		return nil
   288  	}
   289  
   290  	buf, err := os.ReadFile(pkgModPath)
   291  	if err != nil {
   292  		return err
   293  	}
   294  	f, err := modfile.Parse(pkgModPath, buf, nil)
   295  	if err != nil {
   296  		return err
   297  	}
   298  
   299  	pkgDir := filepath.Dir(pkgModPath)
   300  
   301  	// Update existing replace directives to use absolute paths.
   302  	for _, r := range f.Replace {
   303  		newPath := r.New.Path
   304  		if r.New.Version == "" &&
   305  			(!filepath.IsAbs(newPath) ||
   306  				strings.HasPrefix(newPath, "./") ||
   307  				strings.HasPrefix(newPath, "../")) {
   308  			newPath = filepath.Join(pkgDir, newPath)
   309  		}
   310  		err := f.AddReplace(r.Old.Path, r.Old.Version, newPath, r.New.Version)
   311  		if err != nil {
   312  			return err
   313  		}
   314  	}
   315  
   316  	// Add a replace directive for module itself.
   317  	v := f.Module.Mod
   318  	err = f.AddReplace(v.Path, v.Version, pkgDir, "")
   319  	if err != nil {
   320  		return err
   321  	}
   322  
   323  	// Replace the existing "module ..." statement with "module
   324  	// gobind".
   325  	err = f.AddModuleStmt("gobind")
   326  	if err != nil {
   327  		return err
   328  	}
   329  
   330  	buf, err = f.Format()
   331  	if err != nil {
   332  		return err
   333  	}
   334  	newModPath := filepath.Join(workSrc, "go.mod")
   335  	return os.WriteFile(newModPath, buf, 0644)
   336  }
   337  
   338  var (
   339  	goModPathCache struct {
   340  		path string
   341  		err  error
   342  	}
   343  	goModPathOnce sync.Once
   344  )
   345  
   346  func goModPath() (string, error) {
   347  	goModPathOnce.Do(func() {
   348  		out, err := exec.Command("go", "env", "GOMOD").Output()
   349  		if err != nil {
   350  			goModPathCache.err = err
   351  			return
   352  		}
   353  		goModPathCache.path = strings.TrimSpace(string(out))
   354  	})
   355  	return goModPathCache.path, goModPathCache.err
   356  }
   357  
   358  func areGoModulesUsed() (bool, error) {
   359  	path, err := goModPath()
   360  	if err != nil {
   361  		return false, err
   362  	}
   363  	return path != "", nil
   364  }