// Copyright 2014 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // This wrapper uses syscall.Flock to prevent concurrent adb commands, // so for now it only builds on platforms that support that system call. // TODO(#33974): use a more portable library for file locking. //go:build darwin || dragonfly || freebsd || illumos || linux || netbsd || openbsd // This program can be used as go_android_GOARCH_exec by the Go tool. // It executes binaries on an android device using adb. package main import ( "bytes" "errors" "fmt" "io" "log" "os" "os/exec" "os/signal" "path" "path/filepath" "regexp" "runtime" "strconv" "strings" "sync" "syscall" ) func adbRun(args string) (int, error) { // The exit code of adb is often wrong. In theory it was fixed in 2016 // (https://code.google.com/p/android/issues/detail?id=3254), but it's // still broken on our builders in 2023. Instead, append the exitcode to // the output and parse it from there. filter, exitStr := newExitCodeFilter(os.Stdout) args += "; echo -n " + exitStr + "$?" cmd := adbCmd("exec-out", args) cmd.Stdout = filter // If the adb subprocess somehow hangs, go test will kill this wrapper // and wait for our os.Stderr (and os.Stdout) to close as a result. // However, if the os.Stderr (or os.Stdout) file descriptors are // passed on, the hanging adb subprocess will hold them open and // go test will hang forever. // // Avoid that by wrapping stderr, breaking the short circuit and // forcing cmd.Run to use another pipe and goroutine to pass // along stderr from adb. cmd.Stderr = struct{ io.Writer }{os.Stderr} err := cmd.Run() // Before we process err, flush any further output and get the exit code. exitCode, err2 := filter.Finish() if err != nil { return 0, fmt.Errorf("adb exec-out %s: %v", args, err) } return exitCode, err2 } func adb(args ...string) error { if out, err := adbCmd(args...).CombinedOutput(); err != nil { fmt.Fprintf(os.Stderr, "adb %s\n%s", strings.Join(args, " "), out) return err } return nil } func adbCmd(args ...string) *exec.Cmd { if flags := os.Getenv("GOANDROID_ADB_FLAGS"); flags != "" { args = append(strings.Split(flags, " "), args...) } return exec.Command("adb", args...) } const ( deviceRoot = "/data/local/tmp/go_android_exec" deviceGoroot = deviceRoot + "/goroot" ) func main() { log.SetFlags(0) log.SetPrefix("go_android_exec: ") exitCode, err := runMain() if err != nil { log.Fatal(err) } os.Exit(exitCode) } func runMain() (int, error) { // Concurrent use of adb is flaky, so serialize adb commands. // See https://github.com/golang/go/issues/23795 or // https://issuetracker.google.com/issues/73230216. lockPath := filepath.Join(os.TempDir(), "go_android_exec-adb-lock") lock, err := os.OpenFile(lockPath, os.O_CREATE|os.O_RDWR, 0666) if err != nil { return 0, err } defer lock.Close() if err := syscall.Flock(int(lock.Fd()), syscall.LOCK_EX); err != nil { return 0, err } // In case we're booting a device or emulator alongside all.bash, wait for // it to be ready. adb wait-for-device is not enough, we have to // wait for sys.boot_completed. if err := adb("wait-for-device", "exec-out", "while [[ -z $(getprop sys.boot_completed) ]]; do sleep 1; done;"); err != nil { return 0, err } // Done once per make.bash. if err := adbCopyGoroot(); err != nil { return 0, err } // Prepare a temporary directory that will be cleaned up at the end. // Binary names can conflict. // E.g. template.test from the {html,text}/template packages. binName := filepath.Base(os.Args[1]) deviceGotmp := fmt.Sprintf(deviceRoot+"/%s-%d", binName, os.Getpid()) deviceGopath := deviceGotmp + "/gopath" defer adb("exec-out", "rm", "-rf", deviceGotmp) // Clean up. // Determine the package by examining the current working // directory, which will look something like // "$GOROOT/src/mime/multipart" or "$GOPATH/src/golang.org/x/mobile". // We extract everything after the $GOROOT or $GOPATH to run on the // same relative directory on the target device. importPath, isStd, modPath, modDir, err := pkgPath() if err != nil { return 0, err } var deviceCwd string if isStd { // Note that we use path.Join here instead of filepath.Join: // The device paths should be slash-separated even if the go_android_exec // wrapper itself is compiled for Windows. deviceCwd = path.Join(deviceGoroot, "src", importPath) } else { deviceCwd = path.Join(deviceGopath, "src", importPath) if modDir != "" { // In module mode, the user may reasonably expect the entire module // to be present. Copy it over. deviceModDir := path.Join(deviceGopath, "src", modPath) if err := adb("exec-out", "mkdir", "-p", path.Dir(deviceModDir)); err != nil { return 0, err } // We use a single recursive 'adb push' of the module root instead of // walking the tree and copying it piecewise. If the directory tree // contains nested modules this could push a lot of unnecessary contents, // but for the golang.org/x repos it seems to be significantly (~2x) // faster than copying one file at a time (via filepath.WalkDir), // apparently due to high latency in 'adb' commands. if err := adb("push", modDir, deviceModDir); err != nil { return 0, err } } else { if err := adb("exec-out", "mkdir", "-p", deviceCwd); err != nil { return 0, err } if err := adbCopyTree(deviceCwd, importPath); err != nil { return 0, err } // Copy .go files from the package. goFiles, err := filepath.Glob("*.go") if err != nil { return 0, err } if len(goFiles) > 0 { args := append(append([]string{"push"}, goFiles...), deviceCwd) if err := adb(args...); err != nil { return 0, err } } } } deviceBin := fmt.Sprintf("%s/%s", deviceGotmp, binName) if err := adb("push", os.Args[1], deviceBin); err != nil { return 0, err } // Forward SIGQUIT from the go command to show backtraces from // the binary instead of from this wrapper. quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGQUIT) go func() { for range quit { // We don't have the PID of the running process; use the // binary name instead. adb("exec-out", "killall -QUIT "+binName) } }() cmd := `export TMPDIR="` + deviceGotmp + `"` + `; export GOROOT="` + deviceGoroot + `"` + `; export GOPATH="` + deviceGopath + `"` + `; export CGO_ENABLED=0` + `; export GOPROXY=` + os.Getenv("GOPROXY") + `; export GOCACHE="` + deviceRoot + `/gocache"` + `; export PATH="` + deviceGoroot + `/bin":$PATH` + `; cd "` + deviceCwd + `"` + "; '" + deviceBin + "' " + strings.Join(os.Args[2:], " ") code, err := adbRun(cmd) signal.Reset(syscall.SIGQUIT) close(quit) return code, err } type exitCodeFilter struct { w io.Writer // Pass through to w exitRe *regexp.Regexp buf bytes.Buffer } func newExitCodeFilter(w io.Writer) (*exitCodeFilter, string) { const exitStr = "exitcode=" // Build a regexp that matches any prefix of the exit string at the end of // the input. We do it this way to avoid assuming anything about the // subcommand output (e.g., it might not be \n-terminated). var exitReStr strings.Builder for i := 1; i <= len(exitStr); i++ { fmt.Fprintf(&exitReStr, "%s$|", exitStr[:i]) } // Finally, match the exit string along with an exit code. // This is the only case we use a group, and we'll use this // group to extract the numeric code. fmt.Fprintf(&exitReStr, "%s([0-9]+)$", exitStr) exitRe := regexp.MustCompile(exitReStr.String()) return &exitCodeFilter{w: w, exitRe: exitRe}, exitStr } func (f *exitCodeFilter) Write(data []byte) (int, error) { n := len(data) f.buf.Write(data) // Flush to w until a potential match of exitRe b := f.buf.Bytes() match := f.exitRe.FindIndex(b) if match == nil { // Flush all of the buffer. _, err := f.w.Write(b) f.buf.Reset() if err != nil { return n, err } } else { // Flush up to the beginning of the (potential) match. _, err := f.w.Write(b[:match[0]]) f.buf.Next(match[0]) if err != nil { return n, err } } return n, nil } func (f *exitCodeFilter) Finish() (int, error) { // f.buf could be empty, contain a partial match of exitRe, or // contain a full match. b := f.buf.Bytes() defer f.buf.Reset() match := f.exitRe.FindSubmatch(b) if len(match) < 2 || match[1] == nil { // Not a full match. Flush. if _, err := f.w.Write(b); err != nil { return 0, err } return 0, fmt.Errorf("no exit code (in %q)", string(b)) } // Parse the exit code. code, err := strconv.Atoi(string(match[1])) if err != nil { // Something is malformed. Flush. if _, err := f.w.Write(b); err != nil { return 0, err } return 0, fmt.Errorf("bad exit code: %v (in %q)", err, string(b)) } return code, nil } // pkgPath determines the package import path of the current working directory, // and indicates whether it is // and returns the path to the package source relative to $GOROOT (or $GOPATH). func pkgPath() (importPath string, isStd bool, modPath, modDir string, err error) { errorf := func(format string, args ...any) (string, bool, string, string, error) { return "", false, "", "", fmt.Errorf(format, args...) } goTool, err := goTool() if err != nil { return errorf("%w", err) } cmd := exec.Command(goTool, "list", "-e", "-f", "{{.ImportPath}}:{{.Standard}}{{with .Module}}:{{.Path}}:{{.Dir}}{{end}}", ".") out, err := cmd.Output() if err != nil { if ee, ok := err.(*exec.ExitError); ok && len(ee.Stderr) > 0 { return errorf("%v: %s", cmd, ee.Stderr) } return errorf("%v: %w", cmd, err) } parts := strings.SplitN(string(bytes.TrimSpace(out)), ":", 4) if len(parts) < 2 { return errorf("%v: missing ':' in output: %q", cmd, out) } importPath = parts[0] if importPath == "" || importPath == "." { return errorf("current directory does not have a Go import path") } isStd, err = strconv.ParseBool(parts[1]) if err != nil { return errorf("%v: non-boolean .Standard in output: %q", cmd, out) } if len(parts) >= 4 { modPath = parts[2] modDir = parts[3] } return importPath, isStd, modPath, modDir, nil } // adbCopyTree copies testdata, go.mod, go.sum files from subdir // and from parent directories all the way up to the root of subdir. // go.mod and go.sum files are needed for the go tool modules queries, // and the testdata directories for tests. It is common for tests to // reach out into testdata from parent packages. func adbCopyTree(deviceCwd, subdir string) error { dir := "" for { for _, name := range []string{"testdata", "go.mod", "go.sum"} { hostPath := filepath.Join(dir, name) if _, err := os.Stat(hostPath); err != nil { continue } devicePath := path.Join(deviceCwd, dir) if err := adb("exec-out", "mkdir", "-p", devicePath); err != nil { return err } if err := adb("push", hostPath, devicePath); err != nil { return err } } if subdir == "." { break } subdir = filepath.Dir(subdir) dir = path.Join(dir, "..") } return nil } // adbCopyGoroot clears deviceRoot for previous versions of GOROOT, GOPATH // and temporary data. Then, it copies relevant parts of GOROOT to the device, // including the go tool built for android. // A lock file ensures this only happens once, even with concurrent exec // wrappers. func adbCopyGoroot() error { goTool, err := goTool() if err != nil { return err } cmd := exec.Command(goTool, "version") cmd.Stderr = os.Stderr out, err := cmd.Output() if err != nil { return fmt.Errorf("%v: %w", cmd, err) } goVersion := string(out) // Also known by cmd/dist. The bootstrap command deletes the file. statPath := filepath.Join(os.TempDir(), "go_android_exec-adb-sync-status") stat, err := os.OpenFile(statPath, os.O_CREATE|os.O_RDWR, 0666) if err != nil { return err } defer stat.Close() // Serialize check and copying. if err := syscall.Flock(int(stat.Fd()), syscall.LOCK_EX); err != nil { return err } s, err := io.ReadAll(stat) if err != nil { return err } if string(s) == goVersion { return nil } goroot, err := findGoroot() if err != nil { return err } // Delete the device's GOROOT, GOPATH and any leftover test data, // and recreate GOROOT. if err := adb("exec-out", "rm", "-rf", deviceRoot); err != nil { return err } // Build Go for Android. cmd = exec.Command(goTool, "install", "cmd") out, err = cmd.CombinedOutput() if err != nil { if len(bytes.TrimSpace(out)) > 0 { log.Printf("\n%s", out) } return fmt.Errorf("%v: %w", cmd, err) } if err := adb("exec-out", "mkdir", "-p", deviceGoroot); err != nil { return err } // Copy the Android tools from the relevant bin subdirectory to GOROOT/bin. cmd = exec.Command(goTool, "list", "-f", "{{.Target}}", "cmd/go") cmd.Stderr = os.Stderr out, err = cmd.Output() if err != nil { return fmt.Errorf("%v: %w", cmd, err) } platformBin := filepath.Dir(string(bytes.TrimSpace(out))) if platformBin == "." { return errors.New("failed to locate cmd/go for target platform") } if err := adb("push", platformBin, path.Join(deviceGoroot, "bin")); err != nil { return err } // Copy only the relevant subdirectories from pkg: pkg/include and the // platform-native binaries in pkg/tool. if err := adb("exec-out", "mkdir", "-p", path.Join(deviceGoroot, "pkg", "tool")); err != nil { return err } if err := adb("push", filepath.Join(goroot, "pkg", "include"), path.Join(deviceGoroot, "pkg", "include")); err != nil { return err } cmd = exec.Command(goTool, "list", "-f", "{{.Target}}", "cmd/compile") cmd.Stderr = os.Stderr out, err = cmd.Output() if err != nil { return fmt.Errorf("%v: %w", cmd, err) } platformToolDir := filepath.Dir(string(bytes.TrimSpace(out))) if platformToolDir == "." { return errors.New("failed to locate cmd/compile for target platform") } relToolDir, err := filepath.Rel(filepath.Join(goroot), platformToolDir) if err != nil { return err } if err := adb("push", platformToolDir, path.Join(deviceGoroot, relToolDir)); err != nil { return err } // Copy all other files from GOROOT. dirents, err := os.ReadDir(goroot) if err != nil { return err } for _, de := range dirents { switch de.Name() { case "bin", "pkg": // We already created GOROOT/bin and GOROOT/pkg above; skip those. continue } if err := adb("push", filepath.Join(goroot, de.Name()), path.Join(deviceGoroot, de.Name())); err != nil { return err } } if _, err := stat.WriteString(goVersion); err != nil { return err } return nil } func findGoroot() (string, error) { gorootOnce.Do(func() { // If runtime.GOROOT reports a non-empty path, assume that it is valid. // (It may be empty if this binary was built with -trimpath.) gorootPath = runtime.GOROOT() if gorootPath != "" { return } // runtime.GOROOT is empty — perhaps go_android_exec was built with // -trimpath and GOROOT is unset. Try 'go env GOROOT' as a fallback, // assuming that the 'go' command in $PATH is the correct one. cmd := exec.Command("go", "env", "GOROOT") cmd.Stderr = os.Stderr out, err := cmd.Output() if err != nil { gorootErr = fmt.Errorf("%v: %w", cmd, err) } gorootPath = string(bytes.TrimSpace(out)) if gorootPath == "" { gorootErr = errors.New("GOROOT not found") } }) return gorootPath, gorootErr } func goTool() (string, error) { goroot, err := findGoroot() if err != nil { return "", err } return filepath.Join(goroot, "bin", "go"), nil } var ( gorootOnce sync.Once gorootPath string gorootErr error )