| | |
| | |
| | |
| |
|
| | |
| | |
| | |
| |
|
| | |
| |
|
| | |
| | |
| | package main |
| |
|
| | import ( |
| | "bytes" |
| | "errors" |
| | "fmt" |
| | "io" |
| | "log" |
| | "os" |
| | "os/exec" |
| | "os/signal" |
| | "path" |
| | "path/filepath" |
| | "regexp" |
| | "runtime" |
| | "strconv" |
| | "strings" |
| | "sync" |
| | "syscall" |
| | ) |
| |
|
| | func adbRun(args string) (int, error) { |
| | |
| | |
| | |
| | |
| | filter, exitStr := newExitCodeFilter(os.Stdout) |
| | args += "; echo -n " + exitStr + "$?" |
| |
|
| | cmd := adbCmd("exec-out", args) |
| | cmd.Stdout = filter |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | cmd.Stderr = struct{ io.Writer }{os.Stderr} |
| | err := cmd.Run() |
| |
|
| | |
| | exitCode, err2 := filter.Finish() |
| |
|
| | if err != nil { |
| | return 0, fmt.Errorf("adb exec-out %s: %v", args, err) |
| | } |
| | return exitCode, err2 |
| | } |
| |
|
| | func adb(args ...string) error { |
| | if out, err := adbCmd(args...).CombinedOutput(); err != nil { |
| | fmt.Fprintf(os.Stderr, "adb %s\n%s", strings.Join(args, " "), out) |
| | return err |
| | } |
| | return nil |
| | } |
| |
|
| | func adbCmd(args ...string) *exec.Cmd { |
| | if flags := os.Getenv("GOANDROID_ADB_FLAGS"); flags != "" { |
| | args = append(strings.Split(flags, " "), args...) |
| | } |
| | return exec.Command("adb", args...) |
| | } |
| |
|
| | const ( |
| | deviceRoot = "/data/local/tmp/go_android_exec" |
| | deviceGoroot = deviceRoot + "/goroot" |
| | ) |
| |
|
| | func main() { |
| | log.SetFlags(0) |
| | log.SetPrefix("go_android_exec: ") |
| | exitCode, err := runMain() |
| | if err != nil { |
| | log.Fatal(err) |
| | } |
| | os.Exit(exitCode) |
| | } |
| |
|
| | func runMain() (int, error) { |
| | |
| | |
| | |
| | lockPath := filepath.Join(os.TempDir(), "go_android_exec-adb-lock") |
| | lock, err := os.OpenFile(lockPath, os.O_CREATE|os.O_RDWR, 0666) |
| | if err != nil { |
| | return 0, err |
| | } |
| | defer lock.Close() |
| | if err := syscall.Flock(int(lock.Fd()), syscall.LOCK_EX); err != nil { |
| | return 0, err |
| | } |
| |
|
| | |
| | |
| | |
| | if err := adb("wait-for-device", "exec-out", "while [[ -z $(getprop sys.boot_completed) ]]; do sleep 1; done;"); err != nil { |
| | return 0, err |
| | } |
| |
|
| | |
| | if err := adbCopyGoroot(); err != nil { |
| | return 0, err |
| | } |
| |
|
| | |
| | |
| | |
| | binName := filepath.Base(os.Args[1]) |
| | deviceGotmp := fmt.Sprintf(deviceRoot+"/%s-%d", binName, os.Getpid()) |
| | deviceGopath := deviceGotmp + "/gopath" |
| | defer adb("exec-out", "rm", "-rf", deviceGotmp) |
| |
|
| | |
| | |
| | |
| | |
| | |
| | importPath, isStd, modPath, modDir, err := pkgPath() |
| | if err != nil { |
| | return 0, err |
| | } |
| | var deviceCwd string |
| | if isStd { |
| | |
| | |
| | |
| | deviceCwd = path.Join(deviceGoroot, "src", importPath) |
| | } else { |
| | deviceCwd = path.Join(deviceGopath, "src", importPath) |
| | if modDir != "" { |
| | |
| | |
| | deviceModDir := path.Join(deviceGopath, "src", modPath) |
| | if err := adb("exec-out", "mkdir", "-p", path.Dir(deviceModDir)); err != nil { |
| | return 0, err |
| | } |
| | |
| | |
| | |
| | |
| | |
| | |
| | if err := adb("push", modDir, deviceModDir); err != nil { |
| | return 0, err |
| | } |
| | } else { |
| | if err := adb("exec-out", "mkdir", "-p", deviceCwd); err != nil { |
| | return 0, err |
| | } |
| | if err := adbCopyTree(deviceCwd, importPath); err != nil { |
| | return 0, err |
| | } |
| |
|
| | |
| | goFiles, err := filepath.Glob("*.go") |
| | if err != nil { |
| | return 0, err |
| | } |
| | if len(goFiles) > 0 { |
| | args := append(append([]string{"push"}, goFiles...), deviceCwd) |
| | if err := adb(args...); err != nil { |
| | return 0, err |
| | } |
| | } |
| | } |
| | } |
| |
|
| | deviceBin := fmt.Sprintf("%s/%s", deviceGotmp, binName) |
| | if err := adb("push", os.Args[1], deviceBin); err != nil { |
| | return 0, err |
| | } |
| |
|
| | |
| | |
| | quit := make(chan os.Signal, 1) |
| | signal.Notify(quit, syscall.SIGQUIT) |
| | go func() { |
| | for range quit { |
| | |
| | |
| | adb("exec-out", "killall -QUIT "+binName) |
| | } |
| | }() |
| | cmd := `export TMPDIR="` + deviceGotmp + `"` + |
| | `; export GOROOT="` + deviceGoroot + `"` + |
| | `; export GOPATH="` + deviceGopath + `"` + |
| | `; export CGO_ENABLED=0` + |
| | `; export GOPROXY=` + os.Getenv("GOPROXY") + |
| | `; export GOCACHE="` + deviceRoot + `/gocache"` + |
| | `; export PATH="` + deviceGoroot + `/bin":$PATH` + |
| | `; export HOME="` + deviceRoot + `/home"` + |
| | `; cd "` + deviceCwd + `"` + |
| | "; '" + deviceBin + "' " + strings.Join(os.Args[2:], " ") |
| | code, err := adbRun(cmd) |
| | signal.Reset(syscall.SIGQUIT) |
| | close(quit) |
| | return code, err |
| | } |
| |
|
| | type exitCodeFilter struct { |
| | w io.Writer |
| | exitRe *regexp.Regexp |
| | buf bytes.Buffer |
| | } |
| |
|
| | func newExitCodeFilter(w io.Writer) (*exitCodeFilter, string) { |
| | const exitStr = "exitcode=" |
| |
|
| | |
| | |
| | |
| | var exitReStr strings.Builder |
| | for i := 1; i <= len(exitStr); i++ { |
| | fmt.Fprintf(&exitReStr, "%s$|", exitStr[:i]) |
| | } |
| | |
| | |
| | |
| | fmt.Fprintf(&exitReStr, "%s([0-9]+)$", exitStr) |
| | exitRe := regexp.MustCompile(exitReStr.String()) |
| |
|
| | return &exitCodeFilter{w: w, exitRe: exitRe}, exitStr |
| | } |
| |
|
| | func (f *exitCodeFilter) Write(data []byte) (int, error) { |
| | n := len(data) |
| | f.buf.Write(data) |
| | |
| | b := f.buf.Bytes() |
| | match := f.exitRe.FindIndex(b) |
| | if match == nil { |
| | |
| | _, err := f.w.Write(b) |
| | f.buf.Reset() |
| | if err != nil { |
| | return n, err |
| | } |
| | } else { |
| | |
| | _, err := f.w.Write(b[:match[0]]) |
| | f.buf.Next(match[0]) |
| | if err != nil { |
| | return n, err |
| | } |
| | } |
| | return n, nil |
| | } |
| |
|
| | func (f *exitCodeFilter) Finish() (int, error) { |
| | |
| | |
| | b := f.buf.Bytes() |
| | defer f.buf.Reset() |
| | match := f.exitRe.FindSubmatch(b) |
| | if len(match) < 2 || match[1] == nil { |
| | |
| | if _, err := f.w.Write(b); err != nil { |
| | return 0, err |
| | } |
| | return 0, fmt.Errorf("no exit code (in %q)", string(b)) |
| | } |
| |
|
| | |
| | code, err := strconv.Atoi(string(match[1])) |
| | if err != nil { |
| | |
| | if _, err := f.w.Write(b); err != nil { |
| | return 0, err |
| | } |
| | return 0, fmt.Errorf("bad exit code: %v (in %q)", err, string(b)) |
| | } |
| | return code, nil |
| | } |
| |
|
| | |
| | |
| | |
| | func pkgPath() (importPath string, isStd bool, modPath, modDir string, err error) { |
| | errorf := func(format string, args ...any) (string, bool, string, string, error) { |
| | return "", false, "", "", fmt.Errorf(format, args...) |
| | } |
| | goTool, err := goTool() |
| | if err != nil { |
| | return errorf("%w", err) |
| | } |
| | cmd := exec.Command(goTool, "list", "-e", "-f", "{{.ImportPath}}:{{.Standard}}{{with .Module}}:{{.Path}}:{{.Dir}}{{end}}", ".") |
| | out, err := cmd.Output() |
| | if err != nil { |
| | if ee, ok := err.(*exec.ExitError); ok && len(ee.Stderr) > 0 { |
| | return errorf("%v: %s", cmd, ee.Stderr) |
| | } |
| | return errorf("%v: %w", cmd, err) |
| | } |
| |
|
| | parts := strings.SplitN(string(bytes.TrimSpace(out)), ":", 4) |
| | if len(parts) < 2 { |
| | return errorf("%v: missing ':' in output: %q", cmd, out) |
| | } |
| | importPath = parts[0] |
| | if importPath == "" || importPath == "." { |
| | return errorf("current directory does not have a Go import path") |
| | } |
| | isStd, err = strconv.ParseBool(parts[1]) |
| | if err != nil { |
| | return errorf("%v: non-boolean .Standard in output: %q", cmd, out) |
| | } |
| | if len(parts) >= 4 { |
| | modPath = parts[2] |
| | modDir = parts[3] |
| | } |
| |
|
| | return importPath, isStd, modPath, modDir, nil |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | func adbCopyTree(deviceCwd, subdir string) error { |
| | dir := "" |
| | for { |
| | for _, name := range []string{"testdata", "go.mod", "go.sum"} { |
| | hostPath := filepath.Join(dir, name) |
| | if _, err := os.Stat(hostPath); err != nil { |
| | continue |
| | } |
| | devicePath := path.Join(deviceCwd, dir) |
| | if err := adb("exec-out", "mkdir", "-p", devicePath); err != nil { |
| | return err |
| | } |
| | if err := adb("push", hostPath, devicePath); err != nil { |
| | return err |
| | } |
| | } |
| | if subdir == "." { |
| | break |
| | } |
| | subdir = filepath.Dir(subdir) |
| | dir = path.Join(dir, "..") |
| | } |
| | return nil |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | func adbCopyGoroot() error { |
| | goTool, err := goTool() |
| | if err != nil { |
| | return err |
| | } |
| | cmd := exec.Command(goTool, "version") |
| | cmd.Stderr = os.Stderr |
| | out, err := cmd.Output() |
| | if err != nil { |
| | return fmt.Errorf("%v: %w", cmd, err) |
| | } |
| | goVersion := string(out) |
| |
|
| | |
| | statPath := filepath.Join(os.TempDir(), "go_android_exec-adb-sync-status") |
| | stat, err := os.OpenFile(statPath, os.O_CREATE|os.O_RDWR, 0666) |
| | if err != nil { |
| | return err |
| | } |
| | defer stat.Close() |
| | |
| | if err := syscall.Flock(int(stat.Fd()), syscall.LOCK_EX); err != nil { |
| | return err |
| | } |
| | s, err := io.ReadAll(stat) |
| | if err != nil { |
| | return err |
| | } |
| | if string(s) == goVersion { |
| | return nil |
| | } |
| |
|
| | goroot, err := findGoroot() |
| | if err != nil { |
| | return err |
| | } |
| |
|
| | |
| | |
| | if err := adb("exec-out", "rm", "-rf", deviceRoot); err != nil { |
| | return err |
| | } |
| |
|
| | |
| | cmd = exec.Command(goTool, "install", "cmd") |
| | out, err = cmd.CombinedOutput() |
| | if err != nil { |
| | if len(bytes.TrimSpace(out)) > 0 { |
| | log.Printf("\n%s", out) |
| | } |
| | return fmt.Errorf("%v: %w", cmd, err) |
| | } |
| | if err := adb("exec-out", "mkdir", "-p", deviceGoroot); err != nil { |
| | return err |
| | } |
| |
|
| | |
| | cmd = exec.Command(goTool, "list", "-f", "{{.Target}}", "cmd/go") |
| | cmd.Stderr = os.Stderr |
| | out, err = cmd.Output() |
| | if err != nil { |
| | return fmt.Errorf("%v: %w", cmd, err) |
| | } |
| | platformBin := filepath.Dir(string(bytes.TrimSpace(out))) |
| | if platformBin == "." { |
| | return errors.New("failed to locate cmd/go for target platform") |
| | } |
| | if err := adb("push", platformBin, path.Join(deviceGoroot, "bin")); err != nil { |
| | return err |
| | } |
| |
|
| | |
| | |
| | if err := adb("exec-out", "mkdir", "-p", path.Join(deviceGoroot, "pkg", "tool")); err != nil { |
| | return err |
| | } |
| | if err := adb("push", filepath.Join(goroot, "pkg", "include"), path.Join(deviceGoroot, "pkg", "include")); err != nil { |
| | return err |
| | } |
| |
|
| | cmd = exec.Command(goTool, "list", "-f", "{{.Target}}", "cmd/compile") |
| | cmd.Stderr = os.Stderr |
| | out, err = cmd.Output() |
| | if err != nil { |
| | return fmt.Errorf("%v: %w", cmd, err) |
| | } |
| | platformToolDir := filepath.Dir(string(bytes.TrimSpace(out))) |
| | if platformToolDir == "." { |
| | return errors.New("failed to locate cmd/compile for target platform") |
| | } |
| | relToolDir, err := filepath.Rel(filepath.Join(goroot), platformToolDir) |
| | if err != nil { |
| | return err |
| | } |
| | if err := adb("push", platformToolDir, path.Join(deviceGoroot, relToolDir)); err != nil { |
| | return err |
| | } |
| |
|
| | |
| | dirents, err := os.ReadDir(goroot) |
| | if err != nil { |
| | return err |
| | } |
| | for _, de := range dirents { |
| | switch de.Name() { |
| | case "bin", "pkg": |
| | |
| | continue |
| | } |
| | if err := adb("push", filepath.Join(goroot, de.Name()), path.Join(deviceGoroot, de.Name())); err != nil { |
| | return err |
| | } |
| | } |
| |
|
| | if _, err := stat.WriteString(goVersion); err != nil { |
| | return err |
| | } |
| | return nil |
| | } |
| |
|
| | func findGoroot() (string, error) { |
| | gorootOnce.Do(func() { |
| | |
| | |
| | gorootPath = runtime.GOROOT() |
| | if gorootPath != "" { |
| | return |
| | } |
| |
|
| | |
| | |
| | |
| |
|
| | cmd := exec.Command("go", "env", "GOROOT") |
| | cmd.Stderr = os.Stderr |
| | out, err := cmd.Output() |
| | if err != nil { |
| | gorootErr = fmt.Errorf("%v: %w", cmd, err) |
| | } |
| |
|
| | gorootPath = string(bytes.TrimSpace(out)) |
| | if gorootPath == "" { |
| | gorootErr = errors.New("GOROOT not found") |
| | } |
| | }) |
| |
|
| | return gorootPath, gorootErr |
| | } |
| |
|
| | func goTool() (string, error) { |
| | goroot, err := findGoroot() |
| | if err != nil { |
| | return "", err |
| | } |
| | return filepath.Join(goroot, "bin", "go"), nil |
| | } |
| |
|
| | var ( |
| | gorootOnce sync.Once |
| | gorootPath string |
| | gorootErr error |
| | ) |
| |
|