]> Cypherpunks.ru repositories - gostls13.git/blob - misc/go_android_exec/main.go
os: add tests for UserCacheDir and UserConfigDir
[gostls13.git] / misc / go_android_exec / main.go
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.
13 package main
14
15 import (
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
34 func 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
65 func 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
73 func 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
80 const (
81         deviceRoot   = "/data/local/tmp/go_android_exec"
82         deviceGoroot = deviceRoot + "/goroot"
83 )
84
85 func 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
95 func 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
216 type exitCodeFilter struct {
217         w      io.Writer // Pass through to w
218         exitRe *regexp.Regexp
219         buf    bytes.Buffer
220 }
221
222 func 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
241 func (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
265 func (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).
294 func 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.
336 func 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.
366 func 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
486 func 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
515 func 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
523 var (
524         gorootOnce sync.Once
525         gorootPath string
526         gorootErr  error
527 )