diff --git a/misc/ios/clangwrap.sh b/misc/ios/clangwrap.sh index 8f7b439315..16a35f409e 100755 --- a/misc/ios/clangwrap.sh +++ b/misc/ios/clangwrap.sh @@ -1,19 +1,22 @@ #!/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. + +# This script configures clang to target the iOS simulator. If you'd like to +# build for real iOS devices, change SDK to "iphoneos" and PLATFORM to "ios". +# 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 +# iphonesimulator with one of them. + +SDK=iphonesimulator +PLATFORM=ios-simulator + 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` diff --git a/misc/ios/go_ios_exec.go b/misc/ios/go_ios_exec.go index c275dd339c..b21ce1aaf3 100644 --- a/misc/ios/go_ios_exec.go +++ b/misc/ios/go_ios_exec.go @@ -1,44 +1,21 @@ -// Copyright 2015 The Go Authors. All rights reserved. +// Copyright 2024 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. +// This program can be used as go_ios_$GOARCH_exec by the Go tool. It executes +// binaries on the iOS Simulator using the XCode toolchain. 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 @@ -110,18 +87,8 @@ func runMain() (int, error) { return 1, err } - if goarch := os.Getenv("GOARCH"); goarch == "arm64" { - err = runOnDevice(appdir) - } else { - err = runOnSimulator(appdir) - } + 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 @@ -135,61 +102,6 @@ func runOnSimulator(appdir string) error { 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") - - if _, id, ok := strings.Cut(appID, "."); ok { - bundleID = id - } - - 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 @@ -217,236 +129,6 @@ func assembleApp(appdir, bin string) error { 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 { - key, val, ok := strings.Cut(string(line), ": ") - if !ok { - continue - } - 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 and . - 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", @@ -460,138 +142,20 @@ func installSimulator(appdir string) error { 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", + xcrunArgs := []string{"simctl", "spawn", "booted", - bundleID, - ) - out, err := cmd.CombinedOutput() + appdir + "/gotest", + } + xcrunArgs = append(xcrunArgs, args...) + cmd := exec.Command("xcrun", xcrunArgs...) + cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr + err := cmd.Run() 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 + return nil } func copyLocalDir(dst, src string) error { @@ -800,112 +364,3 @@ const resourceRules = ` ` - -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) -`