Source file misc/go_android_exec/main.go

     1  // Copyright 2014 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  // This wrapper uses syscall.Flock to prevent concurrent adb commands,
     6  // so for now it only builds on platforms that support that system call.
     7  // TODO(#33974): use a more portable library for file locking.
     8  
     9  //go:build darwin || dragonfly || freebsd || illumos || linux || netbsd || openbsd
    10  
    11  // This program can be used as go_android_GOARCH_exec by the Go tool.
    12  // It executes binaries on an android device using adb.
    13  package main
    14  
    15  import (
    16  	"bytes"
    17  	"errors"
    18  	"fmt"
    19  	"io"
    20  	"log"
    21  	"os"
    22  	"os/exec"
    23  	"os/signal"
    24  	"path"
    25  	"path/filepath"
    26  	"regexp"
    27  	"runtime"
    28  	"strconv"
    29  	"strings"
    30  	"sync"
    31  	"syscall"
    32  )
    33  
    34  func adbRun(args string) (int, error) {
    35  	// The exit code of adb is often wrong. In theory it was fixed in 2016
    36  	// (https://code.google.com/p/android/issues/detail?id=3254), but it's
    37  	// still broken on our builders in 2023. Instead, append the exitcode to
    38  	// the output and parse it from there.
    39  	filter, exitStr := newExitCodeFilter(os.Stdout)
    40  	args += "; echo -n " + exitStr + "$?"
    41  
    42  	cmd := adbCmd("exec-out", args)
    43  	cmd.Stdout = filter
    44  	// If the adb subprocess somehow hangs, go test will kill this wrapper
    45  	// and wait for our os.Stderr (and os.Stdout) to close as a result.
    46  	// However, if the os.Stderr (or os.Stdout) file descriptors are
    47  	// passed on, the hanging adb subprocess will hold them open and
    48  	// go test will hang forever.
    49  	//
    50  	// Avoid that by wrapping stderr, breaking the short circuit and
    51  	// forcing cmd.Run to use another pipe and goroutine to pass
    52  	// along stderr from adb.
    53  	cmd.Stderr = struct{ io.Writer }{os.Stderr}
    54  	err := cmd.Run()
    55  
    56  	// Before we process err, flush any further output and get the exit code.
    57  	exitCode, err2 := filter.Finish()
    58  
    59  	if err != nil {
    60  		return 0, fmt.Errorf("adb exec-out %s: %v", args, err)
    61  	}
    62  	return exitCode, err2
    63  }
    64  
    65  func adb(args ...string) error {
    66  	if out, err := adbCmd(args...).CombinedOutput(); err != nil {
    67  		fmt.Fprintf(os.Stderr, "adb %s\n%s", strings.Join(args, " "), out)
    68  		return err
    69  	}
    70  	return nil
    71  }
    72  
    73  func adbCmd(args ...string) *exec.Cmd {
    74  	if flags := os.Getenv("GOANDROID_ADB_FLAGS"); flags != "" {
    75  		args = append(strings.Split(flags, " "), args...)
    76  	}
    77  	return exec.Command("adb", args...)
    78  }
    79  
    80  const (
    81  	deviceRoot   = "/data/local/tmp/go_android_exec"
    82  	deviceGoroot = deviceRoot + "/goroot"
    83  )
    84  
    85  func main() {
    86  	log.SetFlags(0)
    87  	log.SetPrefix("go_android_exec: ")
    88  	exitCode, err := runMain()
    89  	if err != nil {
    90  		log.Fatal(err)
    91  	}
    92  	os.Exit(exitCode)
    93  }
    94  
    95  func runMain() (int, error) {
    96  	// Concurrent use of adb is flaky, so serialize adb commands.
    97  	// See https://github.com/golang/go/issues/23795 or
    98  	// https://issuetracker.google.com/issues/73230216.
    99  	lockPath := filepath.Join(os.TempDir(), "go_android_exec-adb-lock")
   100  	lock, err := os.OpenFile(lockPath, os.O_CREATE|os.O_RDWR, 0666)
   101  	if err != nil {
   102  		return 0, err
   103  	}
   104  	defer lock.Close()
   105  	if err := syscall.Flock(int(lock.Fd()), syscall.LOCK_EX); err != nil {
   106  		return 0, err
   107  	}
   108  
   109  	// In case we're booting a device or emulator alongside all.bash, wait for
   110  	// it to be ready. adb wait-for-device is not enough, we have to
   111  	// wait for sys.boot_completed.
   112  	if err := adb("wait-for-device", "exec-out", "while [[ -z $(getprop sys.boot_completed) ]]; do sleep 1; done;"); err != nil {
   113  		return 0, err
   114  	}
   115  
   116  	// Done once per make.bash.
   117  	if err := adbCopyGoroot(); err != nil {
   118  		return 0, err
   119  	}
   120  
   121  	// Prepare a temporary directory that will be cleaned up at the end.
   122  	// Binary names can conflict.
   123  	// E.g. template.test from the {html,text}/template packages.
   124  	binName := filepath.Base(os.Args[1])
   125  	deviceGotmp := fmt.Sprintf(deviceRoot+"/%s-%d", binName, os.Getpid())
   126  	deviceGopath := deviceGotmp + "/gopath"
   127  	defer adb("exec-out", "rm", "-rf", deviceGotmp) // Clean up.
   128  
   129  	// Determine the package by examining the current working
   130  	// directory, which will look something like
   131  	// "$GOROOT/src/mime/multipart" or "$GOPATH/src/golang.org/x/mobile".
   132  	// We extract everything after the $GOROOT or $GOPATH to run on the
   133  	// same relative directory on the target device.
   134  	importPath, isStd, modPath, modDir, err := pkgPath()
   135  	if err != nil {
   136  		return 0, err
   137  	}
   138  	var deviceCwd string
   139  	if isStd {
   140  		// Note that we use path.Join here instead of filepath.Join:
   141  		// The device paths should be slash-separated even if the go_android_exec
   142  		// wrapper itself is compiled for Windows.
   143  		deviceCwd = path.Join(deviceGoroot, "src", importPath)
   144  	} else {
   145  		deviceCwd = path.Join(deviceGopath, "src", importPath)
   146  		if modDir != "" {
   147  			// In module mode, the user may reasonably expect the entire module
   148  			// to be present. Copy it over.
   149  			deviceModDir := path.Join(deviceGopath, "src", modPath)
   150  			if err := adb("exec-out", "mkdir", "-p", path.Dir(deviceModDir)); err != nil {
   151  				return 0, err
   152  			}
   153  			// We use a single recursive 'adb push' of the module root instead of
   154  			// walking the tree and copying it piecewise. If the directory tree
   155  			// contains nested modules this could push a lot of unnecessary contents,
   156  			// but for the golang.org/x repos it seems to be significantly (~2x)
   157  			// faster than copying one file at a time (via filepath.WalkDir),
   158  			// apparently due to high latency in 'adb' commands.
   159  			if err := adb("push", modDir, deviceModDir); err != nil {
   160  				return 0, err
   161  			}
   162  		} else {
   163  			if err := adb("exec-out", "mkdir", "-p", deviceCwd); err != nil {
   164  				return 0, err
   165  			}
   166  			if err := adbCopyTree(deviceCwd, importPath); err != nil {
   167  				return 0, err
   168  			}
   169  
   170  			// Copy .go files from the package.
   171  			goFiles, err := filepath.Glob("*.go")
   172  			if err != nil {
   173  				return 0, err
   174  			}
   175  			if len(goFiles) > 0 {
   176  				args := append(append([]string{"push"}, goFiles...), deviceCwd)
   177  				if err := adb(args...); err != nil {
   178  					return 0, err
   179  				}
   180  			}
   181  		}
   182  	}
   183  
   184  	deviceBin := fmt.Sprintf("%s/%s", deviceGotmp, binName)
   185  	if err := adb("push", os.Args[1], deviceBin); err != nil {
   186  		return 0, err
   187  	}
   188  
   189  	// Forward SIGQUIT from the go command to show backtraces from
   190  	// the binary instead of from this wrapper.
   191  	quit := make(chan os.Signal, 1)
   192  	signal.Notify(quit, syscall.SIGQUIT)
   193  	go func() {
   194  		for range quit {
   195  			// We don't have the PID of the running process; use the
   196  			// binary name instead.
   197  			adb("exec-out", "killall -QUIT "+binName)
   198  		}
   199  	}()
   200  	cmd := `export TMPDIR="` + deviceGotmp + `"` +
   201  		`; export GOROOT="` + deviceGoroot + `"` +
   202  		`; export GOPATH="` + deviceGopath + `"` +
   203  		`; export CGO_ENABLED=0` +
   204  		`; export GOPROXY=` + os.Getenv("GOPROXY") +
   205  		`; export GOCACHE="` + deviceRoot + `/gocache"` +
   206  		`; export PATH="` + deviceGoroot + `/bin":$PATH` +
   207  		`; export HOME="` + deviceRoot + `/home"` +
   208  		`; cd "` + deviceCwd + `"` +
   209  		"; '" + deviceBin + "' " + strings.Join(os.Args[2:], " ")
   210  	code, err := adbRun(cmd)
   211  	signal.Reset(syscall.SIGQUIT)
   212  	close(quit)
   213  	return code, err
   214  }
   215  
   216  type exitCodeFilter struct {
   217  	w      io.Writer // Pass through to w
   218  	exitRe *regexp.Regexp
   219  	buf    bytes.Buffer
   220  }
   221  
   222  func newExitCodeFilter(w io.Writer) (*exitCodeFilter, string) {
   223  	const exitStr = "exitcode="
   224  
   225  	// Build a regexp that matches any prefix of the exit string at the end of
   226  	// the input. We do it this way to avoid assuming anything about the
   227  	// subcommand output (e.g., it might not be \n-terminated).
   228  	var exitReStr strings.Builder
   229  	for i := 1; i <= len(exitStr); i++ {
   230  		fmt.Fprintf(&exitReStr, "%s$|", exitStr[:i])
   231  	}
   232  	// Finally, match the exit string along with an exit code.
   233  	// This is the only case we use a group, and we'll use this
   234  	// group to extract the numeric code.
   235  	fmt.Fprintf(&exitReStr, "%s([0-9]+)$", exitStr)
   236  	exitRe := regexp.MustCompile(exitReStr.String())
   237  
   238  	return &exitCodeFilter{w: w, exitRe: exitRe}, exitStr
   239  }
   240  
   241  func (f *exitCodeFilter) Write(data []byte) (int, error) {
   242  	n := len(data)
   243  	f.buf.Write(data)
   244  	// Flush to w until a potential match of exitRe
   245  	b := f.buf.Bytes()
   246  	match := f.exitRe.FindIndex(b)
   247  	if match == nil {
   248  		// Flush all of the buffer.
   249  		_, err := f.w.Write(b)
   250  		f.buf.Reset()
   251  		if err != nil {
   252  			return n, err
   253  		}
   254  	} else {
   255  		// Flush up to the beginning of the (potential) match.
   256  		_, err := f.w.Write(b[:match[0]])
   257  		f.buf.Next(match[0])
   258  		if err != nil {
   259  			return n, err
   260  		}
   261  	}
   262  	return n, nil
   263  }
   264  
   265  func (f *exitCodeFilter) Finish() (int, error) {
   266  	// f.buf could be empty, contain a partial match of exitRe, or
   267  	// contain a full match.
   268  	b := f.buf.Bytes()
   269  	defer f.buf.Reset()
   270  	match := f.exitRe.FindSubmatch(b)
   271  	if len(match) < 2 || match[1] == nil {
   272  		// Not a full match. Flush.
   273  		if _, err := f.w.Write(b); err != nil {
   274  			return 0, err
   275  		}
   276  		return 0, fmt.Errorf("no exit code (in %q)", string(b))
   277  	}
   278  
   279  	// Parse the exit code.
   280  	code, err := strconv.Atoi(string(match[1]))
   281  	if err != nil {
   282  		// Something is malformed. Flush.
   283  		if _, err := f.w.Write(b); err != nil {
   284  			return 0, err
   285  		}
   286  		return 0, fmt.Errorf("bad exit code: %v (in %q)", err, string(b))
   287  	}
   288  	return code, nil
   289  }
   290  
   291  // pkgPath determines the package import path of the current working directory,
   292  // and indicates whether it is
   293  // and returns the path to the package source relative to $GOROOT (or $GOPATH).
   294  func pkgPath() (importPath string, isStd bool, modPath, modDir string, err error) {
   295  	errorf := func(format string, args ...any) (string, bool, string, string, error) {
   296  		return "", false, "", "", fmt.Errorf(format, args...)
   297  	}
   298  	goTool, err := goTool()
   299  	if err != nil {
   300  		return errorf("%w", err)
   301  	}
   302  	cmd := exec.Command(goTool, "list", "-e", "-f", "{{.ImportPath}}:{{.Standard}}{{with .Module}}:{{.Path}}:{{.Dir}}{{end}}", ".")
   303  	out, err := cmd.Output()
   304  	if err != nil {
   305  		if ee, ok := err.(*exec.ExitError); ok && len(ee.Stderr) > 0 {
   306  			return errorf("%v: %s", cmd, ee.Stderr)
   307  		}
   308  		return errorf("%v: %w", cmd, err)
   309  	}
   310  
   311  	parts := strings.SplitN(string(bytes.TrimSpace(out)), ":", 4)
   312  	if len(parts) < 2 {
   313  		return errorf("%v: missing ':' in output: %q", cmd, out)
   314  	}
   315  	importPath = parts[0]
   316  	if importPath == "" || importPath == "." {
   317  		return errorf("current directory does not have a Go import path")
   318  	}
   319  	isStd, err = strconv.ParseBool(parts[1])
   320  	if err != nil {
   321  		return errorf("%v: non-boolean .Standard in output: %q", cmd, out)
   322  	}
   323  	if len(parts) >= 4 {
   324  		modPath = parts[2]
   325  		modDir = parts[3]
   326  	}
   327  
   328  	return importPath, isStd, modPath, modDir, nil
   329  }
   330  
   331  // adbCopyTree copies testdata, go.mod, go.sum files from subdir
   332  // and from parent directories all the way up to the root of subdir.
   333  // go.mod and go.sum files are needed for the go tool modules queries,
   334  // and the testdata directories for tests.  It is common for tests to
   335  // reach out into testdata from parent packages.
   336  func adbCopyTree(deviceCwd, subdir string) error {
   337  	dir := ""
   338  	for {
   339  		for _, name := range []string{"testdata", "go.mod", "go.sum"} {
   340  			hostPath := filepath.Join(dir, name)
   341  			if _, err := os.Stat(hostPath); err != nil {
   342  				continue
   343  			}
   344  			devicePath := path.Join(deviceCwd, dir)
   345  			if err := adb("exec-out", "mkdir", "-p", devicePath); err != nil {
   346  				return err
   347  			}
   348  			if err := adb("push", hostPath, devicePath); err != nil {
   349  				return err
   350  			}
   351  		}
   352  		if subdir == "." {
   353  			break
   354  		}
   355  		subdir = filepath.Dir(subdir)
   356  		dir = path.Join(dir, "..")
   357  	}
   358  	return nil
   359  }
   360  
   361  // adbCopyGoroot clears deviceRoot for previous versions of GOROOT, GOPATH
   362  // and temporary data. Then, it copies relevant parts of GOROOT to the device,
   363  // including the go tool built for android.
   364  // A lock file ensures this only happens once, even with concurrent exec
   365  // wrappers.
   366  func adbCopyGoroot() error {
   367  	goTool, err := goTool()
   368  	if err != nil {
   369  		return err
   370  	}
   371  	cmd := exec.Command(goTool, "version")
   372  	cmd.Stderr = os.Stderr
   373  	out, err := cmd.Output()
   374  	if err != nil {
   375  		return fmt.Errorf("%v: %w", cmd, err)
   376  	}
   377  	goVersion := string(out)
   378  
   379  	// Also known by cmd/dist. The bootstrap command deletes the file.
   380  	statPath := filepath.Join(os.TempDir(), "go_android_exec-adb-sync-status")
   381  	stat, err := os.OpenFile(statPath, os.O_CREATE|os.O_RDWR, 0666)
   382  	if err != nil {
   383  		return err
   384  	}
   385  	defer stat.Close()
   386  	// Serialize check and copying.
   387  	if err := syscall.Flock(int(stat.Fd()), syscall.LOCK_EX); err != nil {
   388  		return err
   389  	}
   390  	s, err := io.ReadAll(stat)
   391  	if err != nil {
   392  		return err
   393  	}
   394  	if string(s) == goVersion {
   395  		return nil
   396  	}
   397  
   398  	goroot, err := findGoroot()
   399  	if err != nil {
   400  		return err
   401  	}
   402  
   403  	// Delete the device's GOROOT, GOPATH and any leftover test data,
   404  	// and recreate GOROOT.
   405  	if err := adb("exec-out", "rm", "-rf", deviceRoot); err != nil {
   406  		return err
   407  	}
   408  
   409  	// Build Go for Android.
   410  	cmd = exec.Command(goTool, "install", "cmd")
   411  	out, err = cmd.CombinedOutput()
   412  	if err != nil {
   413  		if len(bytes.TrimSpace(out)) > 0 {
   414  			log.Printf("\n%s", out)
   415  		}
   416  		return fmt.Errorf("%v: %w", cmd, err)
   417  	}
   418  	if err := adb("exec-out", "mkdir", "-p", deviceGoroot); err != nil {
   419  		return err
   420  	}
   421  
   422  	// Copy the Android tools from the relevant bin subdirectory to GOROOT/bin.
   423  	cmd = exec.Command(goTool, "list", "-f", "{{.Target}}", "cmd/go")
   424  	cmd.Stderr = os.Stderr
   425  	out, err = cmd.Output()
   426  	if err != nil {
   427  		return fmt.Errorf("%v: %w", cmd, err)
   428  	}
   429  	platformBin := filepath.Dir(string(bytes.TrimSpace(out)))
   430  	if platformBin == "." {
   431  		return errors.New("failed to locate cmd/go for target platform")
   432  	}
   433  	if err := adb("push", platformBin, path.Join(deviceGoroot, "bin")); err != nil {
   434  		return err
   435  	}
   436  
   437  	// Copy only the relevant subdirectories from pkg: pkg/include and the
   438  	// platform-native binaries in pkg/tool.
   439  	if err := adb("exec-out", "mkdir", "-p", path.Join(deviceGoroot, "pkg", "tool")); err != nil {
   440  		return err
   441  	}
   442  	if err := adb("push", filepath.Join(goroot, "pkg", "include"), path.Join(deviceGoroot, "pkg", "include")); err != nil {
   443  		return err
   444  	}
   445  
   446  	cmd = exec.Command(goTool, "list", "-f", "{{.Target}}", "cmd/compile")
   447  	cmd.Stderr = os.Stderr
   448  	out, err = cmd.Output()
   449  	if err != nil {
   450  		return fmt.Errorf("%v: %w", cmd, err)
   451  	}
   452  	platformToolDir := filepath.Dir(string(bytes.TrimSpace(out)))
   453  	if platformToolDir == "." {
   454  		return errors.New("failed to locate cmd/compile for target platform")
   455  	}
   456  	relToolDir, err := filepath.Rel(filepath.Join(goroot), platformToolDir)
   457  	if err != nil {
   458  		return err
   459  	}
   460  	if err := adb("push", platformToolDir, path.Join(deviceGoroot, relToolDir)); err != nil {
   461  		return err
   462  	}
   463  
   464  	// Copy all other files from GOROOT.
   465  	dirents, err := os.ReadDir(goroot)
   466  	if err != nil {
   467  		return err
   468  	}
   469  	for _, de := range dirents {
   470  		switch de.Name() {
   471  		case "bin", "pkg":
   472  			// We already created GOROOT/bin and GOROOT/pkg above; skip those.
   473  			continue
   474  		}
   475  		if err := adb("push", filepath.Join(goroot, de.Name()), path.Join(deviceGoroot, de.Name())); err != nil {
   476  			return err
   477  		}
   478  	}
   479  
   480  	if _, err := stat.WriteString(goVersion); err != nil {
   481  		return err
   482  	}
   483  	return nil
   484  }
   485  
   486  func findGoroot() (string, error) {
   487  	gorootOnce.Do(func() {
   488  		// If runtime.GOROOT reports a non-empty path, assume that it is valid.
   489  		// (It may be empty if this binary was built with -trimpath.)
   490  		gorootPath = runtime.GOROOT()
   491  		if gorootPath != "" {
   492  			return
   493  		}
   494  
   495  		// runtime.GOROOT is empty — perhaps go_android_exec was built with
   496  		// -trimpath and GOROOT is unset. Try 'go env GOROOT' as a fallback,
   497  		// assuming that the 'go' command in $PATH is the correct one.
   498  
   499  		cmd := exec.Command("go", "env", "GOROOT")
   500  		cmd.Stderr = os.Stderr
   501  		out, err := cmd.Output()
   502  		if err != nil {
   503  			gorootErr = fmt.Errorf("%v: %w", cmd, err)
   504  		}
   505  
   506  		gorootPath = string(bytes.TrimSpace(out))
   507  		if gorootPath == "" {
   508  			gorootErr = errors.New("GOROOT not found")
   509  		}
   510  	})
   511  
   512  	return gorootPath, gorootErr
   513  }
   514  
   515  func goTool() (string, error) {
   516  	goroot, err := findGoroot()
   517  	if err != nil {
   518  		return "", err
   519  	}
   520  	return filepath.Join(goroot, "bin", "go"), nil
   521  }
   522  
   523  var (
   524  	gorootOnce sync.Once
   525  	gorootPath string
   526  	gorootErr  error
   527  )
   528  

View as plain text