1
2
3
4
5
6
7
8
9
10
11
12
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
36
37
38
39 filter, exitStr := newExitCodeFilter(os.Stdout)
40 args += "; echo -n " + exitStr + "$?"
41
42 cmd := adbCmd("exec-out", args)
43 cmd.Stdout = filter
44
45
46
47
48
49
50
51
52
53 cmd.Stderr = struct{ io.Writer }{os.Stderr}
54 err := cmd.Run()
55
56
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
97
98
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
110
111
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
117 if err := adbCopyGoroot(); err != nil {
118 return 0, err
119 }
120
121
122
123
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)
128
129
130
131
132
133
134 importPath, isStd, modPath, modDir, err := pkgPath()
135 if err != nil {
136 return 0, err
137 }
138 var deviceCwd string
139 if isStd {
140
141
142
143 deviceCwd = path.Join(deviceGoroot, "src", importPath)
144 } else {
145 deviceCwd = path.Join(deviceGopath, "src", importPath)
146 if modDir != "" {
147
148
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
154
155
156
157
158
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
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
190
191 quit := make(chan os.Signal, 1)
192 signal.Notify(quit, syscall.SIGQUIT)
193 go func() {
194 for range quit {
195
196
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
218 exitRe *regexp.Regexp
219 buf bytes.Buffer
220 }
221
222 func newExitCodeFilter(w io.Writer) (*exitCodeFilter, string) {
223 const exitStr = "exitcode="
224
225
226
227
228 var exitReStr strings.Builder
229 for i := 1; i <= len(exitStr); i++ {
230 fmt.Fprintf(&exitReStr, "%s$|", exitStr[:i])
231 }
232
233
234
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
245 b := f.buf.Bytes()
246 match := f.exitRe.FindIndex(b)
247 if match == nil {
248
249 _, err := f.w.Write(b)
250 f.buf.Reset()
251 if err != nil {
252 return n, err
253 }
254 } else {
255
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
267
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
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
280 code, err := strconv.Atoi(string(match[1]))
281 if err != nil {
282
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
292
293
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
332
333
334
335
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
362
363
364
365
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
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
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
404
405 if err := adb("exec-out", "rm", "-rf", deviceRoot); err != nil {
406 return err
407 }
408
409
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
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
438
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
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
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
489
490 gorootPath = runtime.GOROOT()
491 if gorootPath != "" {
492 return
493 }
494
495
496
497
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 )
528
View as plain text