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