blob: b7aaf5580941022744e13b9bf3335e4452016b86 [file] [log] [blame]
David Crawshawd460b6e2015-03-01 13:47:54 -05001// Copyright 2015 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 program can be used as go_darwin_arm_exec by the Go tool.
6// It executes binaries on an iOS device using the XCode toolchain
7// and the ios-deploy program: https://github.com/phonegap/ios-deploy
8package main
9
10import (
11 "bytes"
12 "errors"
13 "flag"
14 "fmt"
15 "go/build"
16 "io/ioutil"
17 "log"
18 "os"
19 "os/exec"
20 "path/filepath"
21 "runtime"
22 "strings"
23 "sync"
24 "time"
25)
26
27const debug = false
28
29func main() {
30 log.SetFlags(0)
31 log.SetPrefix("go_darwin_arm_exec: ")
32 if debug {
33 log.Println(strings.Join(os.Args, " "))
34 }
35 if len(os.Args) < 2 {
36 log.Fatal("usage: go_darwin_arm_exec a.out")
37 }
38
39 if err := run(os.Args[1], os.Args[2:]); err != nil {
40 fmt.Fprintf(os.Stderr, "go_darwin_arm_exec: %v\n", err)
41 os.Exit(1)
42 }
43}
44
45func run(bin string, args []string) error {
46 defer exec.Command("killall", "ios-deploy").Run() // cleanup
47
48 exec.Command("killall", "ios-deploy").Run()
49
50 tmpdir, err := ioutil.TempDir("", "go_darwin_arm_exec_")
51 if err != nil {
52 log.Fatal(err)
53 }
54 defer os.RemoveAll(tmpdir)
55
56 appdir := filepath.Join(tmpdir, "gotest.app")
57 if err := os.MkdirAll(appdir, 0755); err != nil {
58 return err
59 }
60
61 if err := cp(filepath.Join(appdir, "gotest"), bin); err != nil {
62 return err
63 }
64
65 entitlementsPath := filepath.Join(tmpdir, "Entitlements.plist")
66 if err := ioutil.WriteFile(entitlementsPath, []byte(entitlementsPlist), 0744); err != nil {
67 return err
68 }
69 if err := ioutil.WriteFile(filepath.Join(appdir, "Info.plist"), []byte(infoPlist), 0744); err != nil {
70 return err
71 }
72 if err := ioutil.WriteFile(filepath.Join(appdir, "ResourceRules.plist"), []byte(resourceRules), 0744); err != nil {
73 return err
74 }
75
76 pkgpath, err := copyLocalData(appdir)
77 if err != nil {
78 return err
79 }
80
81 cmd := exec.Command(
82 "codesign",
83 "-f",
84 "-s", "E8BMC3FE2Z", // certificate associated with golang.org
85 "--entitlements", entitlementsPath,
86 appdir,
87 )
88 if debug {
89 log.Println(strings.Join(cmd.Args, " "))
90 }
91 cmd.Stdout = os.Stdout
92 cmd.Stderr = os.Stderr
93 if err := cmd.Run(); err != nil {
94 return fmt.Errorf("codesign: %v", err)
95 }
96
97 if err := os.Chdir(tmpdir); err != nil {
98 return err
99 }
100
101 // ios-deploy invokes lldb to give us a shell session with the app.
102 cmd = exec.Command(
103 // lldb tries to be clever with terminals.
104 // So we wrap it in script(1) and be clever
105 // right back at it.
106 "script",
107 "-q", "-t", "0",
108 "/dev/null",
109
110 "ios-deploy",
111 "--debug",
112 "-u",
113 "-r",
114 "-n",
115 `--args=`+strings.Join(args, " ")+``,
116 "--bundle", appdir,
117 )
118 if debug {
119 log.Println(strings.Join(cmd.Args, " "))
120 }
121
122 lldbr, lldb, err := os.Pipe()
123 if err != nil {
124 return err
125 }
126 w := new(bufWriter)
127 cmd.Stdout = w
128 cmd.Stderr = w // everything of interest is on stderr
129 cmd.Stdin = lldbr
130
131 if err := cmd.Start(); err != nil {
132 return fmt.Errorf("ios-deploy failed to start: %v", err)
133 }
134
135 // Manage the -test.timeout here, outside of the test. There is a lot
136 // of moving parts in an iOS test harness (notably lldb) that can
137 // swallow useful stdio or cause its own ruckus.
138 var timedout chan struct{}
139 if t := parseTimeout(args); t > 1*time.Second {
140 timedout = make(chan struct{})
141 time.AfterFunc(t-1*time.Second, func() {
142 close(timedout)
143 })
144 }
145
146 exited := make(chan error)
147 go func() {
148 exited <- cmd.Wait()
149 }()
150
151 waitFor := func(stage, str string) error {
152 select {
153 case <-timedout:
154 w.printBuf()
155 if p := cmd.Process; p != nil {
156 p.Kill()
157 }
158 return fmt.Errorf("timeout (stage %s)", stage)
159 case err := <-exited:
160 w.printBuf()
161 return fmt.Errorf("failed (stage %s): %v", stage, err)
162 case i := <-w.find(str):
163 w.clearTo(i + len(str))
164 return nil
165 }
166 }
167 do := func(cmd string) {
168 fmt.Fprintln(lldb, cmd)
169 }
170
171 // Wait for installation and connection.
172 if err := waitFor("ios-deploy before run", "(lldb) connect\r\nProcess 0 connected\r\n"); err != nil {
173 return err
174 }
175
176 // Script LLDB. Oh dear.
177 do(`process handle SIGHUP --stop false --pass true --notify false`)
178 do(`process handle SIGPIPE --stop false --pass true --notify false`)
179 do(`process handle SIGUSR1 --stop false --pass true --notify false`)
180 do(`process handle SIGSEGV --stop false --pass true --notify false`) // does not work
181 do(`process handle SIGBUS --stop false --pass true --notify false`) // does not work
David Crawshaw1fdeb6b2015-03-03 14:18:56 -0500182 if err := waitFor("handlers set", "(lldb)"); err != nil {
183 return err
184 }
David Crawshawd460b6e2015-03-01 13:47:54 -0500185
186 do(`breakpoint set -n getwd`) // in runtime/cgo/gcc_darwin_arm.go
David Crawshaw1fdeb6b2015-03-03 14:18:56 -0500187 if err := waitFor("breakpoint set", "(lldb)"); err != nil {
188 return err
189 }
190
David Crawshawd460b6e2015-03-01 13:47:54 -0500191 do(`run`)
192 if err := waitFor("br getwd", "stop reason = breakpoint"); err != nil {
193 return err
194 }
195 if err := waitFor("br getwd prompt", "(lldb)"); err != nil {
196 return err
197 }
198
199 // Move the current working directory into the faux gopath.
200 do(`breakpoint delete 1`)
201 do(`expr char* $mem = (char*)malloc(512)`)
202 do(`expr $mem = (char*)getwd($mem, 512)`)
203 do(`expr $mem = (char*)strcat($mem, "/` + pkgpath + `")`)
204 do(`expr int $res = (int)chdir($mem)`)
205 do(`print $res`)
206 if err := waitFor("move working dir", "(int) $res = 0"); err != nil {
207 return err
208 }
209
210 // Watch for SIGSEGV. Ideally lldb would never break on SIGSEGV.
211 // http://golang.org/issue/10043
212 go func() {
213 <-w.find("stop reason = EXC_BAD_ACCESS")
214 do(`bt`)
215 // The backtrace has no obvious end, so we invent one.
216 do(`expr int $dummy = 1`)
217 do(`print $dummy`)
218 <-w.find(`(int) $dummy = 1`)
219 w.printBuf()
220 if p := cmd.Process; p != nil {
221 p.Kill()
222 }
223 }()
224
225 // Run the tests.
226 w.trimSuffix("(lldb) ")
227 do(`process continue`)
228
229 // Wait for the test to complete.
230 select {
231 case <-timedout:
232 w.printBuf()
233 if p := cmd.Process; p != nil {
234 p.Kill()
235 }
236 return errors.New("timeout running tests")
237 case err := <-exited:
238 // The returned lldb error code is usually non-zero.
239 // We check for test success by scanning for the final
240 // PASS returned by the test harness, assuming the worst
241 // in its absence.
242 if w.isPass() {
243 err = nil
244 } else if err == nil {
245 err = errors.New("test failure")
246 }
247 w.printBuf()
248 return err
249 }
250}
251
252type bufWriter struct {
253 mu sync.Mutex
254 buf []byte
255 suffix []byte // remove from each Write
256
257 findTxt []byte // search buffer on each Write
258 findCh chan int // report find position
259}
260
261func (w *bufWriter) Write(in []byte) (n int, err error) {
262 w.mu.Lock()
263 defer w.mu.Unlock()
264
265 n = len(in)
266 in = bytes.TrimSuffix(in, w.suffix)
267
268 w.buf = append(w.buf, in...)
269
270 if len(w.findTxt) > 0 {
271 if i := bytes.Index(w.buf, w.findTxt); i >= 0 {
272 w.findCh <- i
273 close(w.findCh)
274 w.findTxt = nil
275 w.findCh = nil
276 }
277 }
278 return n, nil
279}
280
281func (w *bufWriter) trimSuffix(p string) {
282 w.mu.Lock()
283 defer w.mu.Unlock()
284 w.suffix = []byte(p)
285}
286
287func (w *bufWriter) printBuf() {
288 w.mu.Lock()
289 defer w.mu.Unlock()
290 fmt.Fprintf(os.Stderr, "%s", w.buf)
291 w.buf = nil
292}
293
294func (w *bufWriter) clearTo(i int) {
295 w.mu.Lock()
296 defer w.mu.Unlock()
297 if debug {
298 fmt.Fprintf(os.Stderr, "--- go_darwin_arm_exec clear ---\n%s\n--- go_darwin_arm_exec clear ---\n", w.buf[:i])
299 }
300 w.buf = w.buf[i:]
301}
302
303func (w *bufWriter) find(str string) <-chan int {
304 w.mu.Lock()
305 defer w.mu.Unlock()
306 if len(w.findTxt) > 0 {
307 panic(fmt.Sprintf("find(%s): already trying to find %s", str, w.findTxt))
308 }
309 txt := []byte(str)
310 ch := make(chan int, 1)
311 if i := bytes.Index(w.buf, txt); i >= 0 {
312 ch <- i
313 close(ch)
314 } else {
315 w.findTxt = txt
316 w.findCh = ch
317 }
318 return ch
319}
320
321func (w *bufWriter) isPass() bool {
322 w.mu.Lock()
323 defer w.mu.Unlock()
324
325 // The final stdio of lldb is non-deterministic, so we
326 // scan the whole buffer.
327 //
328 // Just to make things fun, lldb sometimes translates \n
329 // into \r\n.
330 return bytes.Contains(w.buf, []byte("\nPASS\n")) || bytes.Contains(w.buf, []byte("\nPASS\r"))
331}
332
333func parseTimeout(testArgs []string) (timeout time.Duration) {
334 var args []string
335 for _, arg := range testArgs {
336 if strings.Contains(arg, "test.timeout") {
337 args = append(args, arg)
338 }
339 }
340 f := flag.NewFlagSet("", flag.ContinueOnError)
341 f.DurationVar(&timeout, "test.timeout", 0, "")
342 f.Parse(args)
343 if debug {
344 log.Printf("parseTimeout of %s, got %s", args, timeout)
345 }
346 return timeout
347}
348
349func copyLocalDir(dst, src string) error {
350 if err := os.Mkdir(dst, 0755); err != nil {
351 return err
352 }
353
354 d, err := os.Open(src)
355 if err != nil {
356 return err
357 }
358 defer d.Close()
359 fi, err := d.Readdir(-1)
360 if err != nil {
361 return err
362 }
363
364 for _, f := range fi {
365 if f.IsDir() {
366 if f.Name() == "testdata" {
367 if err := cp(dst, filepath.Join(src, f.Name())); err != nil {
368 return err
369 }
370 }
371 continue
372 }
373 if err := cp(dst, filepath.Join(src, f.Name())); err != nil {
374 return err
375 }
376 }
377 return nil
378}
379
380func cp(dst, src string) error {
381 out, err := exec.Command("cp", "-a", src, dst).CombinedOutput()
382 if err != nil {
383 os.Stderr.Write(out)
384 }
385 return err
386}
387
388func copyLocalData(dstbase string) (pkgpath string, err error) {
389 cwd, err := os.Getwd()
390 if err != nil {
391 return "", err
392 }
393
394 finalPkgpath, underGoRoot, err := subdir()
395 if err != nil {
396 return "", err
397 }
398 cwd = strings.TrimSuffix(cwd, finalPkgpath)
399
400 // Copy all immediate files and testdata directories between
401 // the package being tested and the source root.
402 pkgpath = ""
403 for _, element := range strings.Split(finalPkgpath, string(filepath.Separator)) {
404 if debug {
405 log.Printf("copying %s", pkgpath)
406 }
407 pkgpath = filepath.Join(pkgpath, element)
408 dst := filepath.Join(dstbase, pkgpath)
409 src := filepath.Join(cwd, pkgpath)
410 if err := copyLocalDir(dst, src); err != nil {
411 return "", err
412 }
413 }
414
415 // Copy timezone file.
David Crawshaw66416c02015-03-02 16:05:11 -0500416 //
417 // Typical apps have the zoneinfo.zip in the root of their app bundle,
418 // read by the time package as the working directory at initialization.
419 // As we move the working directory to the GOROOT pkg directory, we
420 // install the zoneinfo.zip file in the pkgpath.
David Crawshawd460b6e2015-03-01 13:47:54 -0500421 if underGoRoot {
David Crawshaw66416c02015-03-02 16:05:11 -0500422 err := cp(
423 filepath.Join(dstbase, pkgpath),
424 filepath.Join(cwd, "lib", "time", "zoneinfo.zip"),
425 )
426 if err != nil {
David Crawshawd460b6e2015-03-01 13:47:54 -0500427 return "", err
428 }
429 }
430
431 return finalPkgpath, nil
432}
433
434// subdir determines the package based on the current working directory,
435// and returns the path to the package source relative to $GOROOT (or $GOPATH).
436func subdir() (pkgpath string, underGoRoot bool, err error) {
437 cwd, err := os.Getwd()
438 if err != nil {
439 return "", false, err
440 }
441 if root := runtime.GOROOT(); strings.HasPrefix(cwd, root) {
442 subdir, err := filepath.Rel(root, cwd)
443 if err != nil {
444 return "", false, err
445 }
446 return subdir, true, nil
447 }
448
449 for _, p := range filepath.SplitList(build.Default.GOPATH) {
450 if !strings.HasPrefix(cwd, p) {
451 continue
452 }
453 subdir, err := filepath.Rel(p, cwd)
454 if err == nil {
455 return subdir, false, nil
456 }
457 }
458 return "", false, fmt.Errorf(
459 "working directory %q is not in either GOROOT(%q) or GOPATH(%q)",
460 cwd,
461 runtime.GOROOT(),
462 build.Default.GOPATH,
463 )
464}
465
466const infoPlist = `<?xml version="1.0" encoding="UTF-8"?>
467<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
468<plist version="1.0">
469<dict>
470<key>CFBundleName</key><string>golang.gotest</string>
471<key>CFBundleSupportedPlatforms</key><array><string>iPhoneOS</string></array>
472<key>CFBundleExecutable</key><string>gotest</string>
473<key>CFBundleVersion</key><string>1.0</string>
474<key>CFBundleIdentifier</key><string>golang.gotest</string>
475<key>CFBundleResourceSpecification</key><string>ResourceRules.plist</string>
476<key>LSRequiresIPhoneOS</key><true/>
477<key>CFBundleDisplayName</key><string>gotest</string>
478</dict>
479</plist>
480`
481
482const devID = `YE84DJ86AZ`
483
484const entitlementsPlist = `<?xml version="1.0" encoding="UTF-8"?>
485<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
486<plist version="1.0">
487<dict>
488 <key>keychain-access-groups</key>
489 <array><string>` + devID + `.golang.gotest</string></array>
490 <key>get-task-allow</key>
491 <true/>
492 <key>application-identifier</key>
493 <string>` + devID + `.golang.gotest</string>
494 <key>com.apple.developer.team-identifier</key>
495 <string>` + devID + `</string>
496</dict>
497</plist>`
498
499const resourceRules = `<?xml version="1.0" encoding="UTF-8"?>
500<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
501<plist version="1.0">
502<dict>
503 <key>rules</key>
504 <dict>
505 <key>.*</key><true/>
506 <key>Info.plist</key>
507 <dict>
508 <key>omit</key> <true/>
509 <key>weight</key> <real>10</real>
510 </dict>
511 <key>ResourceRules.plist</key>
512 <dict>
513 <key>omit</key> <true/>
514 <key>weight</key> <real>100</real>
515 </dict>
516 </dict>
517</dict>
518</plist>
519`