Can cause issues on Windows consider filepath.Join instead
lockPath := filepath.Join(os.TempDir(), "go_android_exec-adb-lock")
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)
Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.