mirror of
https://github.com/golang/go
synced 2024-11-22 20:50:05 -07:00
e4a8fb0fa0
Adds basic test scenarios for UserCacheDir and UserConfigDir. Fixes #57638 Change-Id: Ieb86e95faff44287bfa13daa0cb26e7b5401373b Reviewed-on: https://go-review.googlesource.com/c/go/+/520262 Run-TryBot: Bryan Mills <bcmills@google.com> TryBot-Bypass: Bryan Mills <bcmills@google.com> Reviewed-by: Matthew Dempsky <mdempsky@google.com> Reviewed-by: Bryan Mills <bcmills@google.com> Auto-Submit: Bryan Mills <bcmills@google.com>
528 lines
15 KiB
Go
528 lines
15 KiB
Go
// 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` +
|
|
`; export HOME="` + deviceRoot + `/home"` +
|
|
`; 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
|
|
)
|