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