misc/go_android_exec/main.go GO 528 lines View on github.com → Search inside
1// Copyright 2014 The Go Authors. All rights reserved.2// Use of this source code is governed by a BSD-style3// license that can be found in the LICENSE file.45// 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.89//go:build darwin || dragonfly || freebsd || illumos || linux || netbsd || openbsd1011// This program can be used as go_android_GOARCH_exec by the Go tool.12// It executes binaries on an android device using adb.13package main1415import (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)3334func adbRun(args string) (int, error) {35	// The exit code of adb is often wrong. In theory it was fixed in 201636	// (https://code.google.com/p/android/issues/detail?id=3254), but it's37	// still broken on our builders in 2023. Instead, append the exitcode to38	// the output and parse it from there.39	filter, exitStr := newExitCodeFilter(os.Stdout)40	args += "; echo -n " + exitStr + "$?"4142	cmd := adbCmd("exec-out", args)43	cmd.Stdout = filter44	// If the adb subprocess somehow hangs, go test will kill this wrapper45	// 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 are47	// passed on, the hanging adb subprocess will hold them open and48	// go test will hang forever.49	//50	// Avoid that by wrapping stderr, breaking the short circuit and51	// forcing cmd.Run to use another pipe and goroutine to pass52	// along stderr from adb.53	cmd.Stderr = struct{ io.Writer }{os.Stderr}54	err := cmd.Run()5556	// Before we process err, flush any further output and get the exit code.57	exitCode, err2 := filter.Finish()5859	if err != nil {60		return 0, fmt.Errorf("adb exec-out %s: %v", args, err)61	}62	return exitCode, err263}6465func 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 err69	}70	return nil71}7273func 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}7980const (81	deviceRoot   = "/data/local/tmp/go_android_exec"82	deviceGoroot = deviceRoot + "/goroot"83)8485func 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}9495func runMain() (int, error) {96	// Concurrent use of adb is flaky, so serialize adb commands.97	// See https://github.com/golang/go/issues/23795 or98	// 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, err103	}104	defer lock.Close()105	if err := syscall.Flock(int(lock.Fd()), syscall.LOCK_EX); err != nil {106		return 0, err107	}108109	// In case we're booting a device or emulator alongside all.bash, wait for110	// it to be ready. adb wait-for-device is not enough, we have to111	// 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, err114	}115116	// Done once per make.bash.117	if err := adbCopyGoroot(); err != nil {118		return 0, err119	}120121	// 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.128129	// Determine the package by examining the current working130	// directory, which will look something like131	// "$GOROOT/src/mime/multipart" or "$GOPATH/src/golang.org/x/mobile".132	// We extract everything after the $GOROOT or $GOPATH to run on the133	// same relative directory on the target device.134	importPath, isStd, modPath, modDir, err := pkgPath()135	if err != nil {136		return 0, err137	}138	var deviceCwd string139	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_exec142		// 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 module148			// 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, err152			}153			// We use a single recursive 'adb push' of the module root instead of154			// walking the tree and copying it piecewise. If the directory tree155			// 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, err161			}162		} else {163			if err := adb("exec-out", "mkdir", "-p", deviceCwd); err != nil {164				return 0, err165			}166			if err := adbCopyTree(deviceCwd, importPath); err != nil {167				return 0, err168			}169170			// Copy .go files from the package.171			goFiles, err := filepath.Glob("*.go")172			if err != nil {173				return 0, err174			}175			if len(goFiles) > 0 {176				args := append(append([]string{"push"}, goFiles...), deviceCwd)177				if err := adb(args...); err != nil {178					return 0, err179				}180			}181		}182	}183184	deviceBin := fmt.Sprintf("%s/%s", deviceGotmp, binName)185	if err := adb("push", os.Args[1], deviceBin); err != nil {186		return 0, err187	}188189	// Forward SIGQUIT from the go command to show backtraces from190	// 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 the196			// 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, err214}215216type exitCodeFilter struct {217	w      io.Writer // Pass through to w218	exitRe *regexp.Regexp219	buf    bytes.Buffer220}221222func newExitCodeFilter(w io.Writer) (*exitCodeFilter, string) {223	const exitStr = "exitcode="224225	// Build a regexp that matches any prefix of the exit string at the end of226	// the input. We do it this way to avoid assuming anything about the227	// subcommand output (e.g., it might not be \n-terminated).228	var exitReStr strings.Builder229	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 this234	// group to extract the numeric code.235	fmt.Fprintf(&exitReStr, "%s([0-9]+)$", exitStr)236	exitRe := regexp.MustCompile(exitReStr.String())237238	return &exitCodeFilter{w: w, exitRe: exitRe}, exitStr239}240241func (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 exitRe245	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, err253		}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, err260		}261	}262	return n, nil263}264265func (f *exitCodeFilter) Finish() (int, error) {266	// f.buf could be empty, contain a partial match of exitRe, or267	// 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, err275		}276		return 0, fmt.Errorf("no exit code (in %q)", string(b))277	}278279	// 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, err285		}286		return 0, fmt.Errorf("bad exit code: %v (in %q)", err, string(b))287	}288	return code, nil289}290291// pkgPath determines the package import path of the current working directory,292// and indicates whether it is293// and returns the path to the package source relative to $GOROOT (or $GOPATH).294func 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	}310311	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	}327328	return importPath, isStd, modPath, modDir, nil329}330331// adbCopyTree copies testdata, go.mod, go.sum files from subdir332// 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 to335// reach out into testdata from parent packages.336func 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				continue343			}344			devicePath := path.Join(deviceCwd, dir)345			if err := adb("exec-out", "mkdir", "-p", devicePath); err != nil {346				return err347			}348			if err := adb("push", hostPath, devicePath); err != nil {349				return err350			}351		}352		if subdir == "." {353			break354		}355		subdir = filepath.Dir(subdir)356		dir = path.Join(dir, "..")357	}358	return nil359}360361// adbCopyGoroot clears deviceRoot for previous versions of GOROOT, GOPATH362// 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 exec365// wrappers.366func adbCopyGoroot() error {367	goTool, err := goTool()368	if err != nil {369		return err370	}371	cmd := exec.Command(goTool, "version")372	cmd.Stderr = os.Stderr373	out, err := cmd.Output()374	if err != nil {375		return fmt.Errorf("%v: %w", cmd, err)376	}377	goVersion := string(out)378379	// 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 err384	}385	defer stat.Close()386	// Serialize check and copying.387	if err := syscall.Flock(int(stat.Fd()), syscall.LOCK_EX); err != nil {388		return err389	}390	s, err := io.ReadAll(stat)391	if err != nil {392		return err393	}394	if string(s) == goVersion {395		return nil396	}397398	goroot, err := findGoroot()399	if err != nil {400		return err401	}402403	// 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 err407	}408409	// 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 err420	}421422	// 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.Stderr425	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 err435	}436437	// Copy only the relevant subdirectories from pkg: pkg/include and the438	// platform-native binaries in pkg/tool.439	if err := adb("exec-out", "mkdir", "-p", path.Join(deviceGoroot, "pkg", "tool")); err != nil {440		return err441	}442	if err := adb("push", filepath.Join(goroot, "pkg", "include"), path.Join(deviceGoroot, "pkg", "include")); err != nil {443		return err444	}445446	cmd = exec.Command(goTool, "list", "-f", "{{.Target}}", "cmd/compile")447	cmd.Stderr = os.Stderr448	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 err459	}460	if err := adb("push", platformToolDir, path.Join(deviceGoroot, relToolDir)); err != nil {461		return err462	}463464	// Copy all other files from GOROOT.465	dirents, err := os.ReadDir(goroot)466	if err != nil {467		return err468	}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			continue474		}475		if err := adb("push", filepath.Join(goroot, de.Name()), path.Join(deviceGoroot, de.Name())); err != nil {476			return err477		}478	}479480	if _, err := stat.WriteString(goVersion); err != nil {481		return err482	}483	return nil484}485486func 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			return493		}494495		// runtime.GOROOT is empty — perhaps go_android_exec was built with496		// -trimpath and GOROOT is unset. Try 'go env GOROOT' as a fallback,497		// assuming that the 'go' command in $PATH is the correct one.498499		cmd := exec.Command("go", "env", "GOROOT")500		cmd.Stderr = os.Stderr501		out, err := cmd.Output()502		if err != nil {503			gorootErr = fmt.Errorf("%v: %w", cmd, err)504		}505506		gorootPath = string(bytes.TrimSpace(out))507		if gorootPath == "" {508			gorootErr = errors.New("GOROOT not found")509		}510	})511512	return gorootPath, gorootErr513}514515func goTool() (string, error) {516	goroot, err := findGoroot()517	if err != nil {518		return "", err519	}520	return filepath.Join(goroot, "bin", "go"), nil521}522523var (524	gorootOnce sync.Once525	gorootPath string526	gorootErr  error527)

Code quality findings 17

Can cause issues on Windows consider filepath.Join instead
info correctness path-join-windows
lockPath := filepath.Join(os.TempDir(), "go_android_exec-adb-lock")
Can cause issues on Windows consider filepath.Join instead
info correctness path-join-windows
deviceCwd = path.Join(deviceGoroot, "src", importPath)
Can cause issues on Windows consider filepath.Join instead
info correctness path-join-windows
deviceCwd = path.Join(deviceGopath, "src", importPath)
Can cause issues on Windows consider filepath.Join instead
info correctness path-join-windows
deviceModDir := path.Join(deviceGopath, "src", modPath)
Regexp compiled inside function; compile once at package level to avoid recompilation on each call
info performance regexp-compile-in-func
exitRe := regexp.MustCompile(exitReStr.String())
Can cause issues on Windows consider filepath.Join instead
info correctness path-join-windows
hostPath := filepath.Join(dir, name)
Can cause issues on Windows consider filepath.Join instead
info correctness path-join-windows
devicePath := path.Join(deviceCwd, dir)
Can cause issues on Windows consider filepath.Join instead
info correctness path-join-windows
dir = path.Join(dir, "..")
Can cause issues on Windows consider filepath.Join instead
info correctness path-join-windows
statPath := filepath.Join(os.TempDir(), "go_android_exec-adb-sync-status")
Can cause issues on Windows consider filepath.Join instead
info correctness path-join-windows
if err := adb("push", platformBin, path.Join(deviceGoroot, "bin")); err != nil {
Can cause issues on Windows consider filepath.Join instead
info correctness path-join-windows
if err := adb("exec-out", "mkdir", "-p", path.Join(deviceGoroot, "pkg", "tool")); err != nil {
Can cause issues on Windows consider filepath.Join instead
info correctness path-join-windows
if err := adb("push", filepath.Join(goroot, "pkg", "include"), path.Join(deviceGoroot, "pkg", "include")); err != nil {
Can cause issues on Windows consider filepath.Join instead
info correctness path-join-windows
relToolDir, err := filepath.Rel(filepath.Join(goroot), platformToolDir)
Can cause issues on Windows consider filepath.Join instead
info correctness path-join-windows
if err := adb("push", platformToolDir, path.Join(deviceGoroot, relToolDir)); err != nil {
Can cause issues on Windows consider filepath.Join instead
info correctness path-join-windows
if err := adb("push", filepath.Join(goroot, de.Name()), path.Join(deviceGoroot, de.Name())); err != nil {
Error string starts with uppercase; per Go convention error strings should not be capitalized or end with punctuation
info maintainability error-string-format
gorootErr = errors.New("GOROOT not found")
Can cause issues on Windows consider filepath.Join instead
info correctness path-join-windows
return filepath.Join(goroot, "bin", "go"), nil

Security findings 2

Ensure restrictive umask values
security permissive-file-mode
lock, err := os.OpenFile(lockPath, os.O_CREATE|os.O_RDWR, 0666)
Ensure restrictive umask values
security permissive-file-mode
stat, err := os.OpenFile(statPath, os.O_CREATE|os.O_RDWR, 0666)

Get this view in your editor

Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.