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. 13package main 14 15import ( 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 34func 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 65func 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 73func 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 80const ( 81 deviceRoot = "/data/local/tmp/go_android_exec" 82 deviceGoroot = deviceRoot + "/goroot" 83) 84 85func 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 95func 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 216type exitCodeFilter struct { 217 w io.Writer // Pass through to w 218 exitRe *regexp.Regexp 219 buf bytes.Buffer 220} 221 222func 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 241func (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 265func (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). 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 } 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. 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 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. 366func 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 486func 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 515func 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 523var ( 524 gorootOnce sync.Once 525 gorootPath string 526 gorootErr error 527) 528