github.com/SpiderOak/mobile@v0.0.0-20221129182558-6f541b59af45/cmd/gomobile/build.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  //go:generate go run gendex.go -o dex.go
     6  
     7  package main
     8  
     9  import (
    10  	"bufio"
    11  	"errors"
    12  	"fmt"
    13  	"io"
    14  	"os"
    15  	"os/exec"
    16  	"regexp"
    17  	"strconv"
    18  	"strings"
    19  
    20  	"github.com/SpiderOak/mobile/internal/sdkpath"
    21  	"golang.org/x/tools/go/packages"
    22  )
    23  
    24  var tmpdir string
    25  
    26  var cmdBuild = &command{
    27  	run:   runBuild,
    28  	Name:  "build",
    29  	Usage: "[-target android|" + strings.Join(applePlatforms, "|") + "] [-o output] [-bundleid bundleID] [build flags] [package]",
    30  	Short: "compile android APK and iOS app",
    31  	Long: `
    32  Build compiles and encodes the app named by the import path.
    33  
    34  The named package must define a main function.
    35  
    36  The -target flag takes either android (the default), or one or more
    37  comma-delimited Apple platforms (` + strings.Join(applePlatforms, ", ") + `).
    38  
    39  For -target android, if an AndroidManifest.xml is defined in the
    40  package directory, it is added to the APK output. Otherwise, a default
    41  manifest is generated. By default, this builds a fat APK for all supported
    42  instruction sets (arm, 386, amd64, arm64). A subset of instruction sets can
    43  be selected by specifying target type with the architecture name. E.g.
    44  -target=android/arm,android/386.
    45  
    46  For Apple -target platforms, gomobile must be run on an OS X machine with
    47  Xcode installed.
    48  
    49  By default, -target ios will generate an XCFramework for both ios
    50  and iossimulator. Multiple Apple targets can be specified, creating a "fat"
    51  XCFramework with each slice. To generate a fat XCFramework that supports
    52  iOS, macOS, and macCatalyst for all supportec architectures (amd64 and arm64),
    53  specify -target ios,macos,maccatalyst. A subset of instruction sets can be
    54  selectged by specifying the platform with an architecture name. E.g.
    55  -target=ios/arm64,maccatalyst/arm64.
    56  
    57  If the package directory contains an assets subdirectory, its contents
    58  are copied into the output.
    59  
    60  Flag -iosversion sets the minimal version of the iOS SDK to compile against.
    61  The default version is 13.0.
    62  
    63  Flag -androidapi sets the Android API version to compile against.
    64  The default and minimum is 16.
    65  
    66  The -bundleid flag is required for -target ios and sets the bundle ID to use
    67  with the app.
    68  
    69  The -o flag specifies the output file name. If not specified, the
    70  output file name depends on the package built.
    71  
    72  The -v flag provides verbose output, including the list of packages built.
    73  
    74  The build flags -a, -i, -n, -x, -gcflags, -ldflags, -tags, -trimpath, and -work are
    75  shared with the build command. For documentation, see 'go help build'.
    76  `,
    77  }
    78  
    79  func runBuild(cmd *command) (err error) {
    80  	_, err = runBuildImpl(cmd)
    81  	return
    82  }
    83  
    84  // runBuildImpl builds a package for mobiles based on the given commands.
    85  // runBuildImpl returns a built package information and an error if exists.
    86  func runBuildImpl(cmd *command) (*packages.Package, error) {
    87  	cleanup, err := buildEnvInit()
    88  	if err != nil {
    89  		return nil, err
    90  	}
    91  	defer cleanup()
    92  
    93  	args := cmd.flag.Args()
    94  
    95  	targets, err := parseBuildTarget(buildTarget)
    96  	if err != nil {
    97  		return nil, fmt.Errorf(`invalid -target=%q: %v`, buildTarget, err)
    98  	}
    99  
   100  	var buildPath string
   101  	switch len(args) {
   102  	case 0:
   103  		buildPath = "."
   104  	case 1:
   105  		buildPath = args[0]
   106  	default:
   107  		cmd.usage()
   108  		os.Exit(1)
   109  	}
   110  
   111  	// TODO(ydnar): this should work, unless build tags affect loading a single package.
   112  	// Should we try to import packages with different build tags per platform?
   113  	pkgs, err := packages.Load(packagesConfig(targets[0]), buildPath)
   114  	if err != nil {
   115  		return nil, err
   116  	}
   117  
   118  	// len(pkgs) can be more than 1 e.g., when the specified path includes `...`.
   119  	if len(pkgs) != 1 {
   120  		cmd.usage()
   121  		os.Exit(1)
   122  	}
   123  
   124  	pkg := pkgs[0]
   125  
   126  	if pkg.Name != "main" && buildO != "" {
   127  		return nil, fmt.Errorf("cannot set -o when building non-main package")
   128  	}
   129  
   130  	var nmpkgs map[string]bool
   131  	switch {
   132  	case isAndroidPlatform(targets[0].platform):
   133  		if pkg.Name != "main" {
   134  			for _, t := range targets {
   135  				if err := goBuild(pkg.PkgPath, androidEnv[t.arch]); err != nil {
   136  					return nil, err
   137  				}
   138  			}
   139  			return pkg, nil
   140  		}
   141  		nmpkgs, err = goAndroidBuild(pkg, targets)
   142  		if err != nil {
   143  			return nil, err
   144  		}
   145  	case isApplePlatform(targets[0].platform):
   146  		if !xcodeAvailable() {
   147  			return nil, fmt.Errorf("-target=%s requires XCode", buildTarget)
   148  		}
   149  		if pkg.Name != "main" {
   150  			for _, t := range targets {
   151  				// Catalyst support requires iOS 13+
   152  				v, _ := strconv.ParseFloat(buildIOSVersion, 64)
   153  				if t.platform == "maccatalyst" && v < 13.0 {
   154  					return nil, errors.New("catalyst requires -iosversion=13 or higher")
   155  				}
   156  				if err := goBuild(pkg.PkgPath, appleEnv[t.String()]); err != nil {
   157  					return nil, err
   158  				}
   159  			}
   160  			return pkg, nil
   161  		}
   162  		if buildBundleID == "" {
   163  			return nil, fmt.Errorf("-target=ios requires -bundleid set")
   164  		}
   165  		nmpkgs, err = goAppleBuild(pkg, buildBundleID, targets)
   166  		if err != nil {
   167  			return nil, err
   168  		}
   169  	}
   170  
   171  	if !nmpkgs["github.com/SpiderOak/mobile/app"] {
   172  		return nil, fmt.Errorf(`%s does not import "github.com/SpiderOak/mobile/app"`, pkg.PkgPath)
   173  	}
   174  
   175  	return pkg, nil
   176  }
   177  
   178  var nmRE = regexp.MustCompile(`[0-9a-f]{8} t _?(?:.*/vendor/)?(golang.org/x.*/[^.]*)`)
   179  
   180  func extractPkgs(nm string, path string) (map[string]bool, error) {
   181  	if buildN {
   182  		return map[string]bool{"github.com/SpiderOak/mobile/app": true}, nil
   183  	}
   184  	r, w := io.Pipe()
   185  	cmd := exec.Command(nm, path)
   186  	cmd.Stdout = w
   187  	cmd.Stderr = os.Stderr
   188  
   189  	nmpkgs := make(map[string]bool)
   190  	errc := make(chan error, 1)
   191  	go func() {
   192  		s := bufio.NewScanner(r)
   193  		for s.Scan() {
   194  			if res := nmRE.FindStringSubmatch(s.Text()); res != nil {
   195  				nmpkgs[res[1]] = true
   196  			}
   197  		}
   198  		errc <- s.Err()
   199  	}()
   200  
   201  	err := cmd.Run()
   202  	w.Close()
   203  	if err != nil {
   204  		return nil, fmt.Errorf("%s %s: %v", nm, path, err)
   205  	}
   206  	if err := <-errc; err != nil {
   207  		return nil, fmt.Errorf("%s %s: %v", nm, path, err)
   208  	}
   209  	return nmpkgs, nil
   210  }
   211  
   212  var xout io.Writer = os.Stderr
   213  
   214  func printcmd(format string, args ...interface{}) {
   215  	cmd := fmt.Sprintf(format+"\n", args...)
   216  	if tmpdir != "" {
   217  		cmd = strings.Replace(cmd, tmpdir, "$WORK", -1)
   218  	}
   219  	if androidHome, err := sdkpath.AndroidHome(); err == nil {
   220  		cmd = strings.Replace(cmd, androidHome, "$ANDROID_HOME", -1)
   221  	}
   222  	if gomobilepath != "" {
   223  		cmd = strings.Replace(cmd, gomobilepath, "$GOMOBILE", -1)
   224  	}
   225  	if gopath := goEnv("GOPATH"); gopath != "" {
   226  		cmd = strings.Replace(cmd, gopath, "$GOPATH", -1)
   227  	}
   228  	if env := os.Getenv("HOMEPATH"); env != "" {
   229  		cmd = strings.Replace(cmd, env, "$HOMEPATH", -1)
   230  	}
   231  	fmt.Fprint(xout, cmd)
   232  }
   233  
   234  // "Build flags", used by multiple commands.
   235  var (
   236  	buildA          bool        // -a
   237  	buildI          bool        // -i
   238  	buildN          bool        // -n
   239  	buildV          bool        // -v
   240  	buildX          bool        // -x
   241  	buildO          string      // -o
   242  	buildGcflags    string      // -gcflags
   243  	buildLdflags    string      // -ldflags
   244  	buildTarget     string      // -target
   245  	buildTrimpath   bool        // -trimpath
   246  	buildWork       bool        // -work
   247  	buildBundleID   string      // -bundleid
   248  	buildIOSVersion string      // -iosversion
   249  	buildAndroidAPI int         // -androidapi
   250  	buildTags       stringsFlag // -tags
   251  )
   252  
   253  func addBuildFlags(cmd *command) {
   254  	cmd.flag.StringVar(&buildO, "o", "", "")
   255  	cmd.flag.StringVar(&buildGcflags, "gcflags", "", "")
   256  	cmd.flag.StringVar(&buildLdflags, "ldflags", "", "")
   257  	cmd.flag.StringVar(&buildTarget, "target", "android", "")
   258  	cmd.flag.StringVar(&buildBundleID, "bundleid", "", "")
   259  	cmd.flag.StringVar(&buildIOSVersion, "iosversion", "13.0", "")
   260  	cmd.flag.IntVar(&buildAndroidAPI, "androidapi", minAndroidAPI, "")
   261  
   262  	cmd.flag.BoolVar(&buildA, "a", false, "")
   263  	cmd.flag.BoolVar(&buildI, "i", false, "")
   264  	cmd.flag.BoolVar(&buildTrimpath, "trimpath", false, "")
   265  	cmd.flag.Var(&buildTags, "tags", "")
   266  }
   267  
   268  func addBuildFlagsNVXWork(cmd *command) {
   269  	cmd.flag.BoolVar(&buildN, "n", false, "")
   270  	cmd.flag.BoolVar(&buildV, "v", false, "")
   271  	cmd.flag.BoolVar(&buildX, "x", false, "")
   272  	cmd.flag.BoolVar(&buildWork, "work", false, "")
   273  }
   274  
   275  func init() {
   276  	addBuildFlags(cmdBuild)
   277  	addBuildFlagsNVXWork(cmdBuild)
   278  
   279  	addBuildFlags(cmdInstall)
   280  	addBuildFlagsNVXWork(cmdInstall)
   281  
   282  	addBuildFlagsNVXWork(cmdInit)
   283  
   284  	addBuildFlags(cmdBind)
   285  	addBuildFlagsNVXWork(cmdBind)
   286  
   287  	addBuildFlagsNVXWork(cmdClean)
   288  }
   289  
   290  func goBuild(src string, env []string, args ...string) error {
   291  	return goCmd("build", []string{src}, env, args...)
   292  }
   293  
   294  func goBuildAt(at string, src string, env []string, args ...string) error {
   295  	return goCmdAt(at, "build", []string{src}, env, args...)
   296  }
   297  
   298  func goInstall(srcs []string, env []string, args ...string) error {
   299  	return goCmd("install", srcs, env, args...)
   300  }
   301  
   302  func goCmd(subcmd string, srcs []string, env []string, args ...string) error {
   303  	return goCmdAt("", subcmd, srcs, env, args...)
   304  }
   305  
   306  func goCmdAt(at string, subcmd string, srcs []string, env []string, args ...string) error {
   307  	cmd := exec.Command("go", subcmd)
   308  	tags := buildTags
   309  	if len(tags) > 0 {
   310  		cmd.Args = append(cmd.Args, "-tags", strings.Join(tags, ","))
   311  	}
   312  	if buildV {
   313  		cmd.Args = append(cmd.Args, "-v")
   314  	}
   315  	if subcmd != "install" && buildI {
   316  		cmd.Args = append(cmd.Args, "-i")
   317  	}
   318  	if buildX {
   319  		cmd.Args = append(cmd.Args, "-x")
   320  	}
   321  	if buildGcflags != "" {
   322  		cmd.Args = append(cmd.Args, "-gcflags", buildGcflags)
   323  	}
   324  	if buildLdflags != "" {
   325  		cmd.Args = append(cmd.Args, "-ldflags", buildLdflags)
   326  	}
   327  	if buildTrimpath {
   328  		cmd.Args = append(cmd.Args, "-trimpath")
   329  	}
   330  	if buildWork {
   331  		cmd.Args = append(cmd.Args, "-work")
   332  	}
   333  	cmd.Args = append(cmd.Args, args...)
   334  	cmd.Args = append(cmd.Args, srcs...)
   335  
   336  	// Specify GOMODCACHE explicitly. The default cache path is GOPATH[0]/pkg/mod,
   337  	// but the path varies when GOPATH is specified at env, which results in cold cache.
   338  	if gmc, err := goModCachePath(); err == nil {
   339  		env = append([]string{"GOMODCACHE=" + gmc}, env...)
   340  	} else {
   341  		env = append([]string{}, env...)
   342  	}
   343  	cmd.Env = env
   344  	cmd.Dir = at
   345  	return runCmd(cmd)
   346  }
   347  
   348  func goModTidyAt(at string, env []string) error {
   349  	cmd := exec.Command("go", "mod", "tidy")
   350  	if buildV {
   351  		cmd.Args = append(cmd.Args, "-v")
   352  	}
   353  
   354  	// Specify GOMODCACHE explicitly. The default cache path is GOPATH[0]/pkg/mod,
   355  	// but the path varies when GOPATH is specified at env, which results in cold cache.
   356  	if gmc, err := goModCachePath(); err == nil {
   357  		env = append([]string{"GOMODCACHE=" + gmc}, env...)
   358  	} else {
   359  		env = append([]string{}, env...)
   360  	}
   361  	cmd.Env = env
   362  	cmd.Dir = at
   363  	return runCmd(cmd)
   364  }
   365  
   366  // parseBuildTarget parses buildTarget into 1 or more platforms and architectures.
   367  // Returns an error if buildTarget contains invalid input.
   368  // Example valid target strings:
   369  //
   370  //	android
   371  //	android/arm64,android/386,android/amd64
   372  //	ios,iossimulator,maccatalyst
   373  //	macos/amd64
   374  func parseBuildTarget(buildTarget string) ([]targetInfo, error) {
   375  	if buildTarget == "" {
   376  		return nil, fmt.Errorf(`invalid target ""`)
   377  	}
   378  
   379  	targets := []targetInfo{}
   380  	targetsAdded := make(map[targetInfo]bool)
   381  
   382  	addTarget := func(platform, arch string) {
   383  		t := targetInfo{platform, arch}
   384  		if targetsAdded[t] {
   385  			return
   386  		}
   387  		targets = append(targets, t)
   388  		targetsAdded[t] = true
   389  	}
   390  
   391  	addPlatform := func(platform string) {
   392  		for _, arch := range platformArchs(platform) {
   393  			addTarget(platform, arch)
   394  		}
   395  	}
   396  
   397  	var isAndroid, isApple bool
   398  	for _, target := range strings.Split(buildTarget, ",") {
   399  		tuple := strings.SplitN(target, "/", 2)
   400  		platform := tuple[0]
   401  		hasArch := len(tuple) == 2
   402  
   403  		if isAndroidPlatform(platform) {
   404  			isAndroid = true
   405  		} else if isApplePlatform(platform) {
   406  			isApple = true
   407  		} else {
   408  			return nil, fmt.Errorf("unsupported platform: %q", platform)
   409  		}
   410  		if isAndroid && isApple {
   411  			return nil, fmt.Errorf(`cannot mix android and Apple platforms`)
   412  		}
   413  
   414  		if hasArch {
   415  			arch := tuple[1]
   416  			if !isSupportedArch(platform, arch) {
   417  				return nil, fmt.Errorf(`unsupported platform/arch: %q`, target)
   418  			}
   419  			addTarget(platform, arch)
   420  		} else {
   421  			addPlatform(platform)
   422  		}
   423  	}
   424  
   425  	// Special case to build iossimulator if -target=ios
   426  	if buildTarget == "ios" {
   427  		addPlatform("iossimulator")
   428  	}
   429  
   430  	return targets, nil
   431  }
   432  
   433  type targetInfo struct {
   434  	platform string
   435  	arch     string
   436  }
   437  
   438  func (t targetInfo) String() string {
   439  	return t.platform + "/" + t.arch
   440  }
   441  
   442  func goModCachePath() (string, error) {
   443  	out, err := exec.Command("go", "env", "GOMODCACHE").Output()
   444  	if err != nil {
   445  		return "", err
   446  	}
   447  	return strings.TrimSpace(string(out)), nil
   448  }