summaryrefslogtreecommitdiffstats
path: root/misc/ios
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 13:15:26 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 13:15:26 +0000
commit82539ad8d59729fb45b0bb0edda8f2bddb719eb1 (patch)
tree58f0b58e6f44f0e04d4a6373132cf426fa835fa7 /misc/ios
parentInitial commit. (diff)
downloadgolang-1.17-82539ad8d59729fb45b0bb0edda8f2bddb719eb1.tar.xz
golang-1.17-82539ad8d59729fb45b0bb0edda8f2bddb719eb1.zip
Adding upstream version 1.17.13.upstream/1.17.13upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'misc/ios')
-rw-r--r--misc/ios/README57
-rwxr-xr-xmisc/ios/clangwrap.sh20
-rw-r--r--misc/ios/detect.go133
-rw-r--r--misc/ios/go_ios_exec.go911
4 files changed, 1121 insertions, 0 deletions
diff --git a/misc/ios/README b/misc/ios/README
new file mode 100644
index 0000000..0f5e9e3
--- /dev/null
+++ b/misc/ios/README
@@ -0,0 +1,57 @@
+Go on iOS
+=========
+
+To run the standard library tests, run all.bash as usual, but with the compiler
+set to the clang wrapper that invokes clang for iOS. For example, this command runs
+ all.bash on the iOS emulator:
+
+ GOOS=ios GOARCH=amd64 CGO_ENABLED=1 CC_FOR_TARGET=$(pwd)/../misc/ios/clangwrap.sh ./all.bash
+
+If CC_FOR_TARGET is not set when the toolchain is built (make.bash or all.bash), CC
+can be set on the command line. For example,
+
+ GOOS=ios GOARCH=amd64 CGO_ENABLED=1 CC=$(go env GOROOT)/misc/ios/clangwrap.sh go build
+
+Setting CC is not necessary if the toolchain is built with CC_FOR_TARGET set.
+
+To use the go tool to run individual programs and tests, put $GOROOT/bin into PATH to ensure
+the go_ios_$GOARCH_exec wrapper is found. For example, to run the archive/tar tests:
+
+ export PATH=$GOROOT/bin:$PATH
+ GOOS=ios GOARCH=amd64 CGO_ENABLED=1 go test archive/tar
+
+The go_ios_exec wrapper uses GOARCH to select the emulator (amd64) or the device (arm64).
+However, further setup is required to run tests or programs directly on a device.
+
+First make sure you have a valid developer certificate and have setup your device properly
+to run apps signed by your developer certificate. Then install the libimobiledevice and
+ideviceinstaller tools from https://www.libimobiledevice.org/. Use the HEAD versions from
+source; the stable versions have bugs that prevents the Go exec wrapper to install and run
+apps.
+
+Second, the Go exec wrapper must be told the developer account signing identity, the team
+id and a provisioned bundle id to use. They're specified with the environment variables
+GOIOS_DEV_ID, GOIOS_TEAM_ID and GOIOS_APP_ID. The detect.go program in this directory will
+attempt to auto-detect suitable values. Run it as
+
+ go run detect.go
+
+which will output something similar to
+
+ export GOIOS_DEV_ID="iPhone Developer: xxx@yyy.zzz (XXXXXXXX)"
+ export GOIOS_APP_ID=YYYYYYYY.some.bundle.id
+ export GOIOS_TEAM_ID=ZZZZZZZZ
+
+If you have multiple devices connected, specify the device UDID with the GOIOS_DEVICE_ID
+variable. Use `idevice_id -l` to list all available UDIDs. Then, setting GOARCH to arm64
+will select the device:
+
+ GOOS=ios GOARCH=arm64 CGO_ENABLED=1 CC_FOR_TARGET=$(pwd)/../misc/ios/clangwrap.sh ./all.bash
+
+Note that the go_darwin_$GOARCH_exec wrapper uninstalls any existing app identified by
+the bundle id before installing a new app. If the uninstalled app is the last app by
+the developer identity, the device might also remove the permission to run apps from
+that developer, and the exec wrapper will fail to install the new app. To avoid that,
+install another app with the same developer identity but with a different bundle id.
+That way, the permission to install apps is held on to while the primary app is
+uninstalled.
diff --git a/misc/ios/clangwrap.sh b/misc/ios/clangwrap.sh
new file mode 100755
index 0000000..dca3fcc
--- /dev/null
+++ b/misc/ios/clangwrap.sh
@@ -0,0 +1,20 @@
+#!/bin/sh
+# This uses the latest available iOS SDK, which is recommended.
+# To select a specific SDK, run 'xcodebuild -showsdks'
+# to see the available SDKs and replace iphoneos with one of them.
+if [ "$GOARCH" == "arm64" ]; then
+ SDK=iphoneos
+ PLATFORM=ios
+ CLANGARCH="arm64"
+else
+ SDK=iphonesimulator
+ PLATFORM=ios-simulator
+ CLANGARCH="x86_64"
+fi
+
+SDK_PATH=`xcrun --sdk $SDK --show-sdk-path`
+export IPHONEOS_DEPLOYMENT_TARGET=5.1
+# cmd/cgo doesn't support llvm-gcc-4.2, so we have to use clang.
+CLANG=`xcrun --sdk $SDK --find clang`
+
+exec "$CLANG" -arch $CLANGARCH -isysroot "$SDK_PATH" -m${PLATFORM}-version-min=10.0 "$@"
diff --git a/misc/ios/detect.go b/misc/ios/detect.go
new file mode 100644
index 0000000..cde5723
--- /dev/null
+++ b/misc/ios/detect.go
@@ -0,0 +1,133 @@
+// Copyright 2015 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// +build ignore
+
+// detect attempts to autodetect the correct
+// values of the environment variables
+// used by go_ios_exec.
+// detect shells out to ideviceinfo, a third party program that can
+// be obtained by following the instructions at
+// https://github.com/libimobiledevice/libimobiledevice.
+package main
+
+import (
+ "bytes"
+ "crypto/x509"
+ "fmt"
+ "os"
+ "os/exec"
+ "strings"
+)
+
+func main() {
+ udids := getLines(exec.Command("idevice_id", "-l"))
+ if len(udids) == 0 {
+ fail("no udid found; is a device connected?")
+ }
+
+ mps := detectMobileProvisionFiles(udids)
+ if len(mps) == 0 {
+ fail("did not find mobile provision matching device udids %q", udids)
+ }
+
+ fmt.Println("# Available provisioning profiles below.")
+ fmt.Println("# NOTE: Any existing app on the device with the app id specified by GOIOS_APP_ID")
+ fmt.Println("# will be overwritten when running Go programs.")
+ for _, mp := range mps {
+ fmt.Println()
+ f, err := os.CreateTemp("", "go_ios_detect_")
+ check(err)
+ fname := f.Name()
+ defer os.Remove(fname)
+
+ out := output(parseMobileProvision(mp))
+ _, err = f.Write(out)
+ check(err)
+ check(f.Close())
+
+ cert, err := plistExtract(fname, "DeveloperCertificates:0")
+ check(err)
+ pcert, err := x509.ParseCertificate(cert)
+ check(err)
+ fmt.Printf("export GOIOS_DEV_ID=\"%s\"\n", pcert.Subject.CommonName)
+
+ appID, err := plistExtract(fname, "Entitlements:application-identifier")
+ check(err)
+ fmt.Printf("export GOIOS_APP_ID=%s\n", appID)
+
+ teamID, err := plistExtract(fname, "Entitlements:com.apple.developer.team-identifier")
+ check(err)
+ fmt.Printf("export GOIOS_TEAM_ID=%s\n", teamID)
+ }
+}
+
+func detectMobileProvisionFiles(udids [][]byte) []string {
+ cmd := exec.Command("mdfind", "-name", ".mobileprovision")
+ lines := getLines(cmd)
+
+ var files []string
+ for _, line := range lines {
+ if len(line) == 0 {
+ continue
+ }
+ xmlLines := getLines(parseMobileProvision(string(line)))
+ matches := 0
+ for _, udid := range udids {
+ for _, xmlLine := range xmlLines {
+ if bytes.Contains(xmlLine, udid) {
+ matches++
+ }
+ }
+ }
+ if matches == len(udids) {
+ files = append(files, string(line))
+ }
+ }
+ return files
+}
+
+func parseMobileProvision(fname string) *exec.Cmd {
+ return exec.Command("security", "cms", "-D", "-i", string(fname))
+}
+
+func plistExtract(fname string, path string) ([]byte, error) {
+ out, err := exec.Command("/usr/libexec/PlistBuddy", "-c", "Print "+path, fname).CombinedOutput()
+ if err != nil {
+ return nil, err
+ }
+ return bytes.TrimSpace(out), nil
+}
+
+func getLines(cmd *exec.Cmd) [][]byte {
+ out := output(cmd)
+ lines := bytes.Split(out, []byte("\n"))
+ // Skip the empty line at the end.
+ if len(lines[len(lines)-1]) == 0 {
+ lines = lines[:len(lines)-1]
+ }
+ return lines
+}
+
+func output(cmd *exec.Cmd) []byte {
+ out, err := cmd.Output()
+ if err != nil {
+ fmt.Println(strings.Join(cmd.Args, "\n"))
+ fmt.Fprintln(os.Stderr, err)
+ os.Exit(1)
+ }
+ return out
+}
+
+func check(err error) {
+ if err != nil {
+ fail(err.Error())
+ }
+}
+
+func fail(msg string, v ...interface{}) {
+ fmt.Fprintf(os.Stderr, msg, v...)
+ fmt.Fprintln(os.Stderr)
+ os.Exit(1)
+}
diff --git a/misc/ios/go_ios_exec.go b/misc/ios/go_ios_exec.go
new file mode 100644
index 0000000..9e63717
--- /dev/null
+++ b/misc/ios/go_ios_exec.go
@@ -0,0 +1,911 @@
+// Copyright 2015 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// This program can be used as go_ios_$GOARCH_exec by the Go tool.
+// It executes binaries on an iOS device using the XCode toolchain
+// and the ios-deploy program: https://github.com/phonegap/ios-deploy
+//
+// This script supports an extra flag, -lldb, that pauses execution
+// just before the main program begins and allows the user to control
+// the remote lldb session. This flag is appended to the end of the
+// script's arguments and is not passed through to the underlying
+// binary.
+//
+// This script requires that three environment variables be set:
+// GOIOS_DEV_ID: The codesigning developer id or certificate identifier
+// GOIOS_APP_ID: The provisioning app id prefix. Must support wildcard app ids.
+// GOIOS_TEAM_ID: The team id that owns the app id prefix.
+// $GOROOT/misc/ios contains a script, detect.go, that attempts to autodetect these.
+package main
+
+import (
+ "bytes"
+ "encoding/xml"
+ "errors"
+ "fmt"
+ "go/build"
+ "io"
+ "log"
+ "net"
+ "os"
+ "os/exec"
+ "os/signal"
+ "path/filepath"
+ "runtime"
+ "strconv"
+ "strings"
+ "syscall"
+ "time"
+)
+
+const debug = false
+
+var tmpdir string
+
+var (
+ devID string
+ appID string
+ teamID string
+ bundleID string
+ deviceID string
+)
+
+// lock is a file lock to serialize iOS runs. It is global to avoid the
+// garbage collector finalizing it, closing the file and releasing the
+// lock prematurely.
+var lock *os.File
+
+func main() {
+ log.SetFlags(0)
+ log.SetPrefix("go_ios_exec: ")
+ if debug {
+ log.Println(strings.Join(os.Args, " "))
+ }
+ if len(os.Args) < 2 {
+ log.Fatal("usage: go_ios_exec a.out")
+ }
+
+ // For compatibility with the old builders, use a fallback bundle ID
+ bundleID = "golang.gotest"
+
+ exitCode, err := runMain()
+ if err != nil {
+ log.Fatalf("%v\n", err)
+ }
+ os.Exit(exitCode)
+}
+
+func runMain() (int, error) {
+ var err error
+ tmpdir, err = os.MkdirTemp("", "go_ios_exec_")
+ if err != nil {
+ return 1, err
+ }
+ if !debug {
+ defer os.RemoveAll(tmpdir)
+ }
+
+ appdir := filepath.Join(tmpdir, "gotest.app")
+ os.RemoveAll(appdir)
+
+ if err := assembleApp(appdir, os.Args[1]); err != nil {
+ return 1, err
+ }
+
+ // This wrapper uses complicated machinery to run iOS binaries. It
+ // works, but only when running one binary at a time.
+ // Use a file lock to make sure only one wrapper is running at a time.
+ //
+ // The lock file is never deleted, to avoid concurrent locks on distinct
+ // files with the same path.
+ lockName := filepath.Join(os.TempDir(), "go_ios_exec-"+deviceID+".lock")
+ lock, err = os.OpenFile(lockName, os.O_CREATE|os.O_RDONLY, 0666)
+ if err != nil {
+ return 1, err
+ }
+ if err := syscall.Flock(int(lock.Fd()), syscall.LOCK_EX); err != nil {
+ return 1, err
+ }
+
+ if goarch := os.Getenv("GOARCH"); goarch == "arm64" {
+ err = runOnDevice(appdir)
+ } else {
+ err = runOnSimulator(appdir)
+ }
+ if err != nil {
+ // If the lldb driver completed with an exit code, use that.
+ if err, ok := err.(*exec.ExitError); ok {
+ if ws, ok := err.Sys().(interface{ ExitStatus() int }); ok {
+ return ws.ExitStatus(), nil
+ }
+ }
+ return 1, err
+ }
+ return 0, nil
+}
+
+func runOnSimulator(appdir string) error {
+ if err := installSimulator(appdir); err != nil {
+ return err
+ }
+
+ return runSimulator(appdir, bundleID, os.Args[2:])
+}
+
+func runOnDevice(appdir string) error {
+ // e.g. B393DDEB490947F5A463FD074299B6C0AXXXXXXX
+ devID = getenv("GOIOS_DEV_ID")
+
+ // e.g. Z8B3JBXXXX.org.golang.sample, Z8B3JBXXXX prefix is available at
+ // https://developer.apple.com/membercenter/index.action#accountSummary as Team ID.
+ appID = getenv("GOIOS_APP_ID")
+
+ // e.g. Z8B3JBXXXX, available at
+ // https://developer.apple.com/membercenter/index.action#accountSummary as Team ID.
+ teamID = getenv("GOIOS_TEAM_ID")
+
+ // Device IDs as listed with ios-deploy -c.
+ deviceID = os.Getenv("GOIOS_DEVICE_ID")
+
+ parts := strings.SplitN(appID, ".", 2)
+ if len(parts) == 2 {
+ bundleID = parts[1]
+ }
+
+ if err := signApp(appdir); err != nil {
+ return err
+ }
+
+ if err := uninstallDevice(bundleID); err != nil {
+ return err
+ }
+
+ if err := installDevice(appdir); err != nil {
+ return err
+ }
+
+ if err := mountDevImage(); err != nil {
+ return err
+ }
+
+ // Kill any hanging debug bridges that might take up port 3222.
+ exec.Command("killall", "idevicedebugserverproxy").Run()
+
+ closer, err := startDebugBridge()
+ if err != nil {
+ return err
+ }
+ defer closer()
+
+ return runDevice(appdir, bundleID, os.Args[2:])
+}
+
+func getenv(envvar string) string {
+ s := os.Getenv(envvar)
+ if s == "" {
+ log.Fatalf("%s not set\nrun $GOROOT/misc/ios/detect.go to attempt to autodetect", envvar)
+ }
+ return s
+}
+
+func assembleApp(appdir, bin string) error {
+ if err := os.MkdirAll(appdir, 0755); err != nil {
+ return err
+ }
+
+ if err := cp(filepath.Join(appdir, "gotest"), bin); err != nil {
+ return err
+ }
+
+ pkgpath, err := copyLocalData(appdir)
+ if err != nil {
+ return err
+ }
+
+ entitlementsPath := filepath.Join(tmpdir, "Entitlements.plist")
+ if err := os.WriteFile(entitlementsPath, []byte(entitlementsPlist()), 0744); err != nil {
+ return err
+ }
+ if err := os.WriteFile(filepath.Join(appdir, "Info.plist"), []byte(infoPlist(pkgpath)), 0744); err != nil {
+ return err
+ }
+ if err := os.WriteFile(filepath.Join(appdir, "ResourceRules.plist"), []byte(resourceRules), 0744); err != nil {
+ return err
+ }
+ return nil
+}
+
+func signApp(appdir string) error {
+ entitlementsPath := filepath.Join(tmpdir, "Entitlements.plist")
+ cmd := exec.Command(
+ "codesign",
+ "-f",
+ "-s", devID,
+ "--entitlements", entitlementsPath,
+ appdir,
+ )
+ if debug {
+ log.Println(strings.Join(cmd.Args, " "))
+ }
+ cmd.Stdout = os.Stdout
+ cmd.Stderr = os.Stderr
+ if err := cmd.Run(); err != nil {
+ return fmt.Errorf("codesign: %v", err)
+ }
+ return nil
+}
+
+// mountDevImage ensures a developer image is mounted on the device.
+// The image contains the device lldb server for idevicedebugserverproxy
+// to connect to.
+func mountDevImage() error {
+ // Check for existing mount.
+ cmd := idevCmd(exec.Command("ideviceimagemounter", "-l", "-x"))
+ out, err := cmd.CombinedOutput()
+ if err != nil {
+ os.Stderr.Write(out)
+ return fmt.Errorf("ideviceimagemounter: %v", err)
+ }
+ var info struct {
+ Dict struct {
+ Data []byte `xml:",innerxml"`
+ } `xml:"dict"`
+ }
+ if err := xml.Unmarshal(out, &info); err != nil {
+ return fmt.Errorf("mountDevImage: failed to decode mount information: %v", err)
+ }
+ dict, err := parsePlistDict(info.Dict.Data)
+ if err != nil {
+ return fmt.Errorf("mountDevImage: failed to parse mount information: %v", err)
+ }
+ if dict["ImagePresent"] == "true" && dict["Status"] == "Complete" {
+ return nil
+ }
+ // Some devices only give us an ImageSignature key.
+ if _, exists := dict["ImageSignature"]; exists {
+ return nil
+ }
+ // No image is mounted. Find a suitable image.
+ imgPath, err := findDevImage()
+ if err != nil {
+ return err
+ }
+ sigPath := imgPath + ".signature"
+ cmd = idevCmd(exec.Command("ideviceimagemounter", imgPath, sigPath))
+ if out, err := cmd.CombinedOutput(); err != nil {
+ os.Stderr.Write(out)
+ return fmt.Errorf("ideviceimagemounter: %v", err)
+ }
+ return nil
+}
+
+// findDevImage use the device iOS version and build to locate a suitable
+// developer image.
+func findDevImage() (string, error) {
+ cmd := idevCmd(exec.Command("ideviceinfo"))
+ out, err := cmd.Output()
+ if err != nil {
+ return "", fmt.Errorf("ideviceinfo: %v", err)
+ }
+ var iosVer, buildVer string
+ lines := bytes.Split(out, []byte("\n"))
+ for _, line := range lines {
+ spl := bytes.SplitN(line, []byte(": "), 2)
+ if len(spl) != 2 {
+ continue
+ }
+ key, val := string(spl[0]), string(spl[1])
+ switch key {
+ case "ProductVersion":
+ iosVer = val
+ case "BuildVersion":
+ buildVer = val
+ }
+ }
+ if iosVer == "" || buildVer == "" {
+ return "", errors.New("failed to parse ideviceinfo output")
+ }
+ verSplit := strings.Split(iosVer, ".")
+ if len(verSplit) > 2 {
+ // Developer images are specific to major.minor ios version.
+ // Cut off the patch version.
+ iosVer = strings.Join(verSplit[:2], ".")
+ }
+ sdkBase := "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/DeviceSupport"
+ patterns := []string{fmt.Sprintf("%s (%s)", iosVer, buildVer), fmt.Sprintf("%s (*)", iosVer), fmt.Sprintf("%s*", iosVer)}
+ for _, pattern := range patterns {
+ matches, err := filepath.Glob(filepath.Join(sdkBase, pattern, "DeveloperDiskImage.dmg"))
+ if err != nil {
+ return "", fmt.Errorf("findDevImage: %v", err)
+ }
+ if len(matches) > 0 {
+ return matches[0], nil
+ }
+ }
+ return "", fmt.Errorf("failed to find matching developer image for iOS version %s build %s", iosVer, buildVer)
+}
+
+// startDebugBridge ensures that the idevicedebugserverproxy runs on
+// port 3222.
+func startDebugBridge() (func(), error) {
+ errChan := make(chan error, 1)
+ cmd := idevCmd(exec.Command("idevicedebugserverproxy", "3222"))
+ var stderr bytes.Buffer
+ cmd.Stderr = &stderr
+ if err := cmd.Start(); err != nil {
+ return nil, fmt.Errorf("idevicedebugserverproxy: %v", err)
+ }
+ go func() {
+ if err := cmd.Wait(); err != nil {
+ if _, ok := err.(*exec.ExitError); ok {
+ errChan <- fmt.Errorf("idevicedebugserverproxy: %s", stderr.Bytes())
+ } else {
+ errChan <- fmt.Errorf("idevicedebugserverproxy: %v", err)
+ }
+ }
+ errChan <- nil
+ }()
+ closer := func() {
+ cmd.Process.Kill()
+ <-errChan
+ }
+ // Dial localhost:3222 to ensure the proxy is ready.
+ delay := time.Second / 4
+ for attempt := 0; attempt < 5; attempt++ {
+ conn, err := net.DialTimeout("tcp", "localhost:3222", 5*time.Second)
+ if err == nil {
+ conn.Close()
+ return closer, nil
+ }
+ select {
+ case <-time.After(delay):
+ delay *= 2
+ case err := <-errChan:
+ return nil, err
+ }
+ }
+ closer()
+ return nil, errors.New("failed to set up idevicedebugserverproxy")
+}
+
+// findDeviceAppPath returns the device path to the app with the
+// given bundle ID. It parses the output of ideviceinstaller -l -o xml,
+// looking for the bundle ID and the corresponding Path value.
+func findDeviceAppPath(bundleID string) (string, error) {
+ cmd := idevCmd(exec.Command("ideviceinstaller", "-l", "-o", "xml"))
+ out, err := cmd.CombinedOutput()
+ if err != nil {
+ os.Stderr.Write(out)
+ return "", fmt.Errorf("ideviceinstaller: -l -o xml %v", err)
+ }
+ var list struct {
+ Apps []struct {
+ Data []byte `xml:",innerxml"`
+ } `xml:"array>dict"`
+ }
+ if err := xml.Unmarshal(out, &list); err != nil {
+ return "", fmt.Errorf("failed to parse ideviceinstaller output: %v", err)
+ }
+ for _, app := range list.Apps {
+ values, err := parsePlistDict(app.Data)
+ if err != nil {
+ return "", fmt.Errorf("findDeviceAppPath: failed to parse app dict: %v", err)
+ }
+ if values["CFBundleIdentifier"] == bundleID {
+ if path, ok := values["Path"]; ok {
+ return path, nil
+ }
+ }
+ }
+ return "", fmt.Errorf("failed to find device path for bundle: %s", bundleID)
+}
+
+// Parse an xml encoded plist. Plist values are mapped to string.
+func parsePlistDict(dict []byte) (map[string]string, error) {
+ d := xml.NewDecoder(bytes.NewReader(dict))
+ values := make(map[string]string)
+ var key string
+ var hasKey bool
+ for {
+ tok, err := d.Token()
+ if err == io.EOF {
+ break
+ }
+ if err != nil {
+ return nil, err
+ }
+ if tok, ok := tok.(xml.StartElement); ok {
+ if tok.Name.Local == "key" {
+ if err := d.DecodeElement(&key, &tok); err != nil {
+ return nil, err
+ }
+ hasKey = true
+ } else if hasKey {
+ var val string
+ var err error
+ switch n := tok.Name.Local; n {
+ case "true", "false":
+ // Bools are represented as <true/> and <false/>.
+ val = n
+ err = d.Skip()
+ default:
+ err = d.DecodeElement(&val, &tok)
+ }
+ if err != nil {
+ return nil, err
+ }
+ values[key] = val
+ hasKey = false
+ } else {
+ if err := d.Skip(); err != nil {
+ return nil, err
+ }
+ }
+ }
+ }
+ return values, nil
+}
+
+func installSimulator(appdir string) error {
+ cmd := exec.Command(
+ "xcrun", "simctl", "install",
+ "booted", // Install to the booted simulator.
+ appdir,
+ )
+ if out, err := cmd.CombinedOutput(); err != nil {
+ os.Stderr.Write(out)
+ return fmt.Errorf("xcrun simctl install booted %q: %v", appdir, err)
+ }
+ return nil
+}
+
+func uninstallDevice(bundleID string) error {
+ cmd := idevCmd(exec.Command(
+ "ideviceinstaller",
+ "-U", bundleID,
+ ))
+ if out, err := cmd.CombinedOutput(); err != nil {
+ os.Stderr.Write(out)
+ return fmt.Errorf("ideviceinstaller -U %q: %s", bundleID, err)
+ }
+ return nil
+}
+
+func installDevice(appdir string) error {
+ attempt := 0
+ for {
+ cmd := idevCmd(exec.Command(
+ "ideviceinstaller",
+ "-i", appdir,
+ ))
+ if out, err := cmd.CombinedOutput(); err != nil {
+ // Sometimes, installing the app fails for some reason.
+ // Give the device a few seconds and try again.
+ if attempt < 5 {
+ time.Sleep(5 * time.Second)
+ attempt++
+ continue
+ }
+ os.Stderr.Write(out)
+ return fmt.Errorf("ideviceinstaller -i %q: %v (%d attempts)", appdir, err, attempt)
+ }
+ return nil
+ }
+}
+
+func idevCmd(cmd *exec.Cmd) *exec.Cmd {
+ if deviceID != "" {
+ // Inject -u device_id after the executable, but before the arguments.
+ args := []string{cmd.Args[0], "-u", deviceID}
+ cmd.Args = append(args, cmd.Args[1:]...)
+ }
+ return cmd
+}
+
+func runSimulator(appdir, bundleID string, args []string) error {
+ cmd := exec.Command(
+ "xcrun", "simctl", "launch",
+ "--wait-for-debugger",
+ "booted",
+ bundleID,
+ )
+ out, err := cmd.CombinedOutput()
+ if err != nil {
+ os.Stderr.Write(out)
+ return fmt.Errorf("xcrun simctl launch booted %q: %v", bundleID, err)
+ }
+ var processID int
+ var ignore string
+ if _, err := fmt.Sscanf(string(out), "%s %d", &ignore, &processID); err != nil {
+ return fmt.Errorf("runSimulator: couldn't find processID from `simctl launch`: %v (%q)", err, out)
+ }
+ _, err = runLLDB("ios-simulator", appdir, strconv.Itoa(processID), args)
+ return err
+}
+
+func runDevice(appdir, bundleID string, args []string) error {
+ attempt := 0
+ for {
+ // The device app path reported by the device might be stale, so retry
+ // the lookup of the device path along with the lldb launching below.
+ deviceapp, err := findDeviceAppPath(bundleID)
+ if err != nil {
+ // The device app path might not yet exist for a newly installed app.
+ if attempt == 5 {
+ return err
+ }
+ attempt++
+ time.Sleep(5 * time.Second)
+ continue
+ }
+ out, err := runLLDB("remote-ios", appdir, deviceapp, args)
+ // If the program was not started it can be retried without papering over
+ // real test failures.
+ started := bytes.HasPrefix(out, []byte("lldb: running program"))
+ if started || err == nil || attempt == 5 {
+ return err
+ }
+ // Sometimes, the app was not yet ready to launch or the device path was
+ // stale. Retry.
+ attempt++
+ time.Sleep(5 * time.Second)
+ }
+}
+
+func runLLDB(target, appdir, deviceapp string, args []string) ([]byte, error) {
+ var env []string
+ for _, e := range os.Environ() {
+ // Don't override TMPDIR, HOME, GOCACHE on the device.
+ if strings.HasPrefix(e, "TMPDIR=") || strings.HasPrefix(e, "HOME=") || strings.HasPrefix(e, "GOCACHE=") {
+ continue
+ }
+ env = append(env, e)
+ }
+ lldb := exec.Command(
+ "python",
+ "-", // Read script from stdin.
+ target,
+ appdir,
+ deviceapp,
+ )
+ lldb.Args = append(lldb.Args, args...)
+ lldb.Env = env
+ lldb.Stdin = strings.NewReader(lldbDriver)
+ lldb.Stdout = os.Stdout
+ var out bytes.Buffer
+ lldb.Stderr = io.MultiWriter(&out, os.Stderr)
+ err := lldb.Start()
+ if err == nil {
+ // Forward SIGQUIT to the lldb driver which in turn will forward
+ // to the running program.
+ sigs := make(chan os.Signal, 1)
+ signal.Notify(sigs, syscall.SIGQUIT)
+ proc := lldb.Process
+ go func() {
+ for sig := range sigs {
+ proc.Signal(sig)
+ }
+ }()
+ err = lldb.Wait()
+ signal.Stop(sigs)
+ close(sigs)
+ }
+ return out.Bytes(), err
+}
+
+func copyLocalDir(dst, src string) error {
+ if err := os.Mkdir(dst, 0755); err != nil {
+ return err
+ }
+
+ d, err := os.Open(src)
+ if err != nil {
+ return err
+ }
+ defer d.Close()
+ fi, err := d.Readdir(-1)
+ if err != nil {
+ return err
+ }
+
+ for _, f := range fi {
+ if f.IsDir() {
+ if f.Name() == "testdata" {
+ if err := cp(dst, filepath.Join(src, f.Name())); err != nil {
+ return err
+ }
+ }
+ continue
+ }
+ if err := cp(dst, filepath.Join(src, f.Name())); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func cp(dst, src string) error {
+ out, err := exec.Command("cp", "-a", src, dst).CombinedOutput()
+ if err != nil {
+ os.Stderr.Write(out)
+ }
+ return err
+}
+
+func copyLocalData(dstbase string) (pkgpath string, err error) {
+ cwd, err := os.Getwd()
+ if err != nil {
+ return "", err
+ }
+
+ finalPkgpath, underGoRoot, err := subdir()
+ if err != nil {
+ return "", err
+ }
+ cwd = strings.TrimSuffix(cwd, finalPkgpath)
+
+ // Copy all immediate files and testdata directories between
+ // the package being tested and the source root.
+ pkgpath = ""
+ for _, element := range strings.Split(finalPkgpath, string(filepath.Separator)) {
+ if debug {
+ log.Printf("copying %s", pkgpath)
+ }
+ pkgpath = filepath.Join(pkgpath, element)
+ dst := filepath.Join(dstbase, pkgpath)
+ src := filepath.Join(cwd, pkgpath)
+ if err := copyLocalDir(dst, src); err != nil {
+ return "", err
+ }
+ }
+
+ if underGoRoot {
+ // Copy timezone file.
+ //
+ // Typical apps have the zoneinfo.zip in the root of their app bundle,
+ // read by the time package as the working directory at initialization.
+ // As we move the working directory to the GOROOT pkg directory, we
+ // install the zoneinfo.zip file in the pkgpath.
+ err := cp(
+ filepath.Join(dstbase, pkgpath),
+ filepath.Join(cwd, "lib", "time", "zoneinfo.zip"),
+ )
+ if err != nil {
+ return "", err
+ }
+ // Copy src/runtime/textflag.h for (at least) Test386EndToEnd in
+ // cmd/asm/internal/asm.
+ runtimePath := filepath.Join(dstbase, "src", "runtime")
+ if err := os.MkdirAll(runtimePath, 0755); err != nil {
+ return "", err
+ }
+ err = cp(
+ filepath.Join(runtimePath, "textflag.h"),
+ filepath.Join(cwd, "src", "runtime", "textflag.h"),
+ )
+ if err != nil {
+ return "", err
+ }
+ }
+
+ return finalPkgpath, nil
+}
+
+// subdir determines the package based on the current working directory,
+// and returns the path to the package source relative to $GOROOT (or $GOPATH).
+func subdir() (pkgpath string, underGoRoot bool, err error) {
+ cwd, err := os.Getwd()
+ if err != nil {
+ return "", false, err
+ }
+ cwd, err = filepath.EvalSymlinks(cwd)
+ if err != nil {
+ log.Fatal(err)
+ }
+ goroot, err := filepath.EvalSymlinks(runtime.GOROOT())
+ if err != nil {
+ return "", false, err
+ }
+ if strings.HasPrefix(cwd, goroot) {
+ subdir, err := filepath.Rel(goroot, cwd)
+ if err != nil {
+ return "", false, err
+ }
+ return subdir, true, nil
+ }
+
+ for _, p := range filepath.SplitList(build.Default.GOPATH) {
+ pabs, err := filepath.EvalSymlinks(p)
+ if err != nil {
+ return "", false, err
+ }
+ if !strings.HasPrefix(cwd, pabs) {
+ continue
+ }
+ subdir, err := filepath.Rel(pabs, cwd)
+ if err == nil {
+ return subdir, false, nil
+ }
+ }
+ return "", false, fmt.Errorf(
+ "working directory %q is not in either GOROOT(%q) or GOPATH(%q)",
+ cwd,
+ runtime.GOROOT(),
+ build.Default.GOPATH,
+ )
+}
+
+func infoPlist(pkgpath string) string {
+ return `<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+<key>CFBundleName</key><string>golang.gotest</string>
+<key>CFBundleSupportedPlatforms</key><array><string>iPhoneOS</string></array>
+<key>CFBundleExecutable</key><string>gotest</string>
+<key>CFBundleVersion</key><string>1.0</string>
+<key>CFBundleShortVersionString</key><string>1.0</string>
+<key>CFBundleIdentifier</key><string>` + bundleID + `</string>
+<key>CFBundleResourceSpecification</key><string>ResourceRules.plist</string>
+<key>LSRequiresIPhoneOS</key><true/>
+<key>CFBundleDisplayName</key><string>gotest</string>
+<key>GoExecWrapperWorkingDirectory</key><string>` + pkgpath + `</string>
+</dict>
+</plist>
+`
+}
+
+func entitlementsPlist() string {
+ return `<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>keychain-access-groups</key>
+ <array><string>` + appID + `</string></array>
+ <key>get-task-allow</key>
+ <true/>
+ <key>application-identifier</key>
+ <string>` + appID + `</string>
+ <key>com.apple.developer.team-identifier</key>
+ <string>` + teamID + `</string>
+</dict>
+</plist>
+`
+}
+
+const resourceRules = `<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>rules</key>
+ <dict>
+ <key>.*</key>
+ <true/>
+ <key>Info.plist</key>
+ <dict>
+ <key>omit</key>
+ <true/>
+ <key>weight</key>
+ <integer>10</integer>
+ </dict>
+ <key>ResourceRules.plist</key>
+ <dict>
+ <key>omit</key>
+ <true/>
+ <key>weight</key>
+ <integer>100</integer>
+ </dict>
+ </dict>
+</dict>
+</plist>
+`
+
+const lldbDriver = `
+import sys
+import os
+import signal
+
+platform, exe, device_exe_or_pid, args = sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4:]
+
+env = []
+for k, v in os.environ.items():
+ env.append(k + "=" + v)
+
+sys.path.append('/Applications/Xcode.app/Contents/SharedFrameworks/LLDB.framework/Resources/Python')
+
+import lldb
+
+debugger = lldb.SBDebugger.Create()
+debugger.SetAsync(True)
+debugger.SkipLLDBInitFiles(True)
+
+err = lldb.SBError()
+target = debugger.CreateTarget(exe, None, platform, True, err)
+if not target.IsValid() or not err.Success():
+ sys.stderr.write("lldb: failed to setup up target: %s\n" % (err))
+ sys.exit(1)
+
+listener = debugger.GetListener()
+
+if platform == 'remote-ios':
+ target.modules[0].SetPlatformFileSpec(lldb.SBFileSpec(device_exe_or_pid))
+ process = target.ConnectRemote(listener, 'connect://localhost:3222', None, err)
+else:
+ process = target.AttachToProcessWithID(listener, int(device_exe_or_pid), err)
+
+if not err.Success():
+ sys.stderr.write("lldb: failed to connect to remote target %s: %s\n" % (device_exe_or_pid, err))
+ sys.exit(1)
+
+# Don't stop on signals.
+sigs = process.GetUnixSignals()
+for i in range(0, sigs.GetNumSignals()):
+ sig = sigs.GetSignalAtIndex(i)
+ sigs.SetShouldStop(sig, False)
+ sigs.SetShouldNotify(sig, False)
+
+event = lldb.SBEvent()
+running = False
+prev_handler = None
+
+def signal_handler(signal, frame):
+ process.Signal(signal)
+
+def run_program():
+ # Forward SIGQUIT to the program.
+ prev_handler = signal.signal(signal.SIGQUIT, signal_handler)
+ # Tell the Go driver that the program is running and should not be retried.
+ sys.stderr.write("lldb: running program\n")
+ running = True
+ # Process is stopped at attach/launch. Let it run.
+ process.Continue()
+
+if platform != 'remote-ios':
+ # For the local emulator the program is ready to run.
+ # For remote device runs, we need to wait for eStateConnected,
+ # below.
+ run_program()
+
+while True:
+ if not listener.WaitForEvent(1, event):
+ continue
+ if not lldb.SBProcess.EventIsProcessEvent(event):
+ continue
+ if running:
+ # Pass through stdout and stderr.
+ while True:
+ out = process.GetSTDOUT(8192)
+ if not out:
+ break
+ sys.stdout.write(out)
+ while True:
+ out = process.GetSTDERR(8192)
+ if not out:
+ break
+ sys.stderr.write(out)
+ state = process.GetStateFromEvent(event)
+ if state in [lldb.eStateCrashed, lldb.eStateDetached, lldb.eStateUnloaded, lldb.eStateExited]:
+ if running:
+ signal.signal(signal.SIGQUIT, prev_handler)
+ break
+ elif state == lldb.eStateConnected:
+ if platform == 'remote-ios':
+ process.RemoteLaunch(args, env, None, None, None, None, 0, False, err)
+ if not err.Success():
+ sys.stderr.write("lldb: failed to launch remote process: %s\n" % (err))
+ process.Kill()
+ debugger.Terminate()
+ sys.exit(1)
+ run_program()
+
+exitStatus = process.GetExitStatus()
+exitDesc = process.GetExitDescription()
+process.Kill()
+debugger.Terminate()
+if exitStatus == 0 and exitDesc is not None:
+ # Ensure tests fail when killed by a signal.
+ exitStatus = 123
+
+sys.exit(exitStatus)
+`