From 90224dfd0651e9d31cb4c4485b2bba00999a868d Mon Sep 17 00:00:00 2001 From: pjay Date: Tue, 9 Jun 2026 18:38:52 +0530 Subject: [PATCH] Physical-device iOS support (#64) (#66) * feat(companion): add appState, eraseText, pressKey runner handlers The Go runner transport already calls these methods; the in-device runner implemented them only latently. They become load-bearing on the device path, where the hybrid's legacy-companion fallback is absent. Backward compatible: the simulator hybrid never calls them. * feat(ios): resolve physical devices from devicectl ResolveDevice parses xcrun devicectl list devices into Device{Name, HardwareUDID, CoreDeviceID}: the hardware UDID feeds xcodebuild/iproxy and the CoreDevice id feeds devicectl install. Matches by name or either id; errors list candidates on none/ambiguous. Fixes the stale sidecar comment on ResolveTarget. * feat(ioscompanion): runner-only device driver mode NewDevice reuses Driver with d.companion set to the runner dialed over an iproxy usbmux tunnel, hybrid=false, runnerClient=nil. The existing accessor seams then route launch/snapshot/text/gesture to the runner with no new DeviceDriver methods. Device seams swap clear-state to a devicectl reinstall, container reset to a warn-once no-op, and paste grant to a no-op. realSpawnDeviceRunner builds and signs the runner at run time via the App Store Connect API key (no Xcode UI), caching on a source hash. * test(ioscompanion): cover device wiring, routing, and shell-out argv Seam-driven NewDevice wiring + gesture/text routing (asserting no keyboard HID), devicectl/build/test/iproxy argv builders, xctestrun test-target dict name parsing, signing-credential env checks, and source-hash cache keying. * feat(testrun): route physical-device iOS runs to the device driver Execute resolves a non-simulator iOS target through ios.ResolveDevice into its hardware UDID and CoreDevice id; buildDriver constructs NewDevice via a seam instead of rejecting the device. Generalizes the --ios-device and --ios-app-path help to cover the device path; signing stays env-read, never a flag. * feat(doctor): device prereqs replace java/sidecar for ios-device iosDeviceChecks now verifies devicectl, iproxy on PATH, a connected+paired device (via ios.ConnectedDevices), and App Store Connect signing creds (via ioscompanion.VerifyDeviceSigning). The retired JVM sidecar checks stay only under android. * feat(conformance): device backend uses iphoneos app and tunnel orphan checks The device backend now builds via just ios-device, points --ios-app-path at the Debug-iphoneos bundle, and reinstalls each run for clear-state. The G5 orphan scan replaces the retired sidecar.jar check with lingering iproxy and device test-without-building sessions (destination platform=iOS,id=). * feat(folio): device build linking the iosArm64 framework project.yml selects the Kotlin framework slice by SDK (iosArm64 for iphoneos, iosSimulatorArm64 for simulator) and links via -framework Shared on the SDK-conditional search path. New ios-device/test-ios-device recipes mirror ios/test-ios, signing the Debug-iphoneos build with the .env API key. * docs(cli): document ios-device doctor checks and the device flags The --ios-device flag now also selects a connected device; --ios-app-path covers the device install; the doctor gains an ios-device platform whose checks are devicectl, iproxy, a paired device, and signing credentials. Corrects the --clear-data default to true. * fix(ioscompanion): resolve signing key path to absolute xcodebuild's -authenticationKeyPath requires an absolute path, but .env files commonly carry a repo-relative one. Resolve it against the working directory before the stat so a relative ASC_API_KEY_PATH still signs. * fix(ioscompanion): re-enable signing for the device runner build companion/project.yml disables code signing for the simulator build, so the device build inherited it and produced an unsigned runner that the device rejected at install (0xe8008018). build-for-testing now forces CODE_SIGNING_ALLOWED/REQUIRED=YES so automatic provisioning signs it. * fix(ioscompanion): key the device build cache on signing identity The cache marker hashed only sources, so switching signing team or key reused a runner signed with the stale identity, which the device rejects at install (0xe8008018). Fold team + key id into the cache key so a signing change forces a rebuild. * docs(getting-started): document physical iOS device setup Lists the iproxy requirement and the App Store Connect signing env vars (SANDERLING_IOS_TEAM, ASC_API_*) a device run needs, plus the test-ios-device recipe and the doctor check. * feat(ios): native usbmux client and in-process tunnel forwarder Talk to macOS usbmuxd directly instead of shelling out to iproxy, so the device path depends on nothing beyond macOS + Xcode. * refactor(ios): drive device tunnel via io.Closer seam Replace the tunnelChild *exec.Cmd and spawnTunnel seam with a tunnel io.Closer and startTunnel seam backed by the in-process usbmux forwarder. * refactor(ios): remove iproxy spawn from device runner * test(ios): cover tunnel close via io.Closer not child process * feat(doctor): check usbmuxd socket instead of iproxy on PATH * chore(conformance): drop iproxy orphan check; tunnel is in-process * docs(ios): device tunnel uses native usbmux, nothing to install * chore: gitignore the signing keys directory * feat(folio): add Android launcher icon (black bg, white dot) * feat(folio): add iOS app icon (black bg, white dot) * feat(folio): add web favicon (black bg, white dot) * docs(ioscompanion): fix stale const comments * refactor(ioscompanion): inline single-use devicectl argv builders * refactor(ioscompanion): inline xcodegenArgs, drop tautological argv tests * refactor(ioscompanion): inline firstNonEmpty * refactor(doctor): dedup usbmuxd socket path via ioscompanion seam * test(doctor): trim redundant signing-check test * refactor(ioscompanion): deliver COMPANION_PORT via TEST_RUNNER_ env * fix(testrun): seam preflight so iOS routing tests pass on CI without xcrun --- .gitignore | 3 +- cmd/sanderling/doctor.go | 63 ++- cmd/sanderling/doctor_test.go | 59 ++- cmd/sanderling/main.go | 4 +- companion/Sources/AppLifecycle.swift | 23 + companion/Sources/Server.swift | 13 + companion/Sources/TextInput.swift | 34 ++ conformance/gates.sh | 39 +- docs/manual/cli.md | 10 +- docs/manual/getting-started.md | 15 + .../androidApp/src/main/AndroidManifest.xml | 1 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 709 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 563 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 924 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 1290 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 2375 bytes .../AppIcon.appiconset/Contents.json | 14 + .../AppIcon.appiconset/icon-1024.png | Bin 0 -> 13430 bytes .../iosApp/Assets.xcassets/Contents.json | 6 + examples/folio/app/iosApp/project.yml | 20 +- .../src/wasmJsMain/resources/index.html | 1 + examples/folio/justfile | 37 ++ internal/driver/ioscompanion/device.go | 219 +++++++++ internal/driver/ioscompanion/device_test.go | 197 ++++++++ internal/driver/ioscompanion/devicerunner.go | 335 ++++++++++++++ .../driver/ioscompanion/devicerunner_test.go | 129 ++++++ internal/driver/ioscompanion/driver.go | 22 + internal/driver/ioscompanion/usbmux.go | 433 ++++++++++++++++++ internal/driver/ioscompanion/usbmux_test.go | 228 +++++++++ internal/ios/device.go | 129 ++++++ internal/ios/device_test.go | 158 +++++++ internal/ios/ios.go | 4 +- internal/testrun/driver.go | 85 +++- internal/testrun/driver_test.go | 147 +++++- internal/testrun/testrun.go | 37 +- 35 files changed, 2377 insertions(+), 88 deletions(-) create mode 100644 examples/folio/app/androidApp/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 examples/folio/app/androidApp/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 examples/folio/app/androidApp/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 examples/folio/app/androidApp/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 examples/folio/app/androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 examples/folio/app/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 examples/folio/app/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/icon-1024.png create mode 100644 examples/folio/app/iosApp/iosApp/Assets.xcassets/Contents.json create mode 100644 internal/driver/ioscompanion/device.go create mode 100644 internal/driver/ioscompanion/device_test.go create mode 100644 internal/driver/ioscompanion/devicerunner.go create mode 100644 internal/driver/ioscompanion/devicerunner_test.go create mode 100644 internal/driver/ioscompanion/usbmux.go create mode 100644 internal/driver/ioscompanion/usbmux_test.go create mode 100644 internal/ios/device.go create mode 100644 internal/ios/device_test.go diff --git a/.gitignore b/.gitignore index a7d7fea..a42af9c 100644 --- a/.gitignore +++ b/.gitignore @@ -72,4 +72,5 @@ examples/folio-web/.vite/ # Personal research notes /research/ -/talk/ \ No newline at end of file +/talk/ +keys/* diff --git a/cmd/sanderling/doctor.go b/cmd/sanderling/doctor.go index 40072ce..d642c3e 100644 --- a/cmd/sanderling/doctor.go +++ b/cmd/sanderling/doctor.go @@ -14,6 +14,8 @@ import ( "github.com/chromedp/chromedp" + "github.com/priyanshujain/sanderling/internal/driver/ioscompanion" + "github.com/priyanshujain/sanderling/internal/ios" "github.com/priyanshujain/sanderling/internal/sidecarassets" ) @@ -59,8 +61,9 @@ func androidChecks() []doctorCheck { // iosChecks covers the simulator path, which the native companion drives with // no JVM. A simulator host with no Java still passes. Physical-device runs -// additionally need java and the sidecar JAR, covered by iosDeviceChecks and -// surfaced through the "all" union. +// additionally need devicectl, the usbmuxd socket, a connected device, and +// signing credentials, covered by iosDeviceChecks and surfaced through the +// "all" union. func iosChecks() []doctorCheck { return []doctorCheck{ {Name: "xcrun on PATH (ios simulator)", Run: checkExecutableOnPath("xcrun")}, @@ -68,12 +71,18 @@ func iosChecks() []doctorCheck { } } -// iosDeviceChecks covers the extra prerequisites a physical iOS device needs: -// the JVM and a real sidecar JAR for the sidecar driver path. +// iosDeviceChecks covers the prerequisites a physical iOS device needs: the +// runner is built and driven over a native usbmux tunnel, so devicectl installs +// the app, the macOS usbmuxd socket carries the tunnel, a device must be +// connected and paired, and App Store Connect signing credentials must be +// present for the no-UI build. Everything here is part of macOS + Xcode; nothing +// is installed. func iosDeviceChecks() []doctorCheck { return []doctorCheck{ - {Name: "java 17+ on PATH (ios physical device)", Run: checkJavaVersion}, - {Name: "sidecar JAR is real (ios physical device)", Run: checkSidecarJAR}, + {Name: "devicectl available (ios physical device)", Run: checkDevicectl}, + {Name: "usbmuxd socket present (ios physical device)", Run: checkUsbmuxd}, + {Name: "an iOS device is connected and paired", Run: checkDeviceConnected}, + {Name: "App Store Connect signing credentials present", Run: checkDeviceSigning}, } } @@ -120,6 +129,48 @@ func checkSimctl(ctx context.Context) error { return nil } +// Device-check seams: package-level so the doctor's device checks run against +// canned results instead of a real device. +var ( + doctorConnectedDevices = ios.ConnectedDevices + doctorVerifySigning = ioscompanion.VerifyDeviceSigning + doctorVerifyUsbmuxd = ioscompanion.VerifyUsbmuxdSocket +) + +// checkDevicectl exercises `xcrun devicectl --version`: devicectl is an xcrun +// subcommand, so a PATH lookup cannot find it. +func checkDevicectl(ctx context.Context) error { + if err := exec.CommandContext(ctx, "xcrun", "devicectl", "--version").Run(); err != nil { + return fmt.Errorf("xcrun devicectl --version: %w", err) + } + return nil +} + +// checkUsbmuxd confirms the macOS usbmuxd socket is present: the native device +// tunnel speaks to it directly instead of shelling out to a third-party client. +func checkUsbmuxd(_ context.Context) error { + return doctorVerifyUsbmuxd() +} + +// checkDeviceConnected confirms at least one physical iOS device is connected +// and paired, the prerequisite for the tunnel and the install. +func checkDeviceConnected(ctx context.Context) error { + devices, err := doctorConnectedDevices(ctx) + if err != nil { + return err + } + if len(devices) == 0 { + return fmt.Errorf("no connected iOS device; connect and pair an iPhone") + } + return nil +} + +// checkDeviceSigning confirms the App Store Connect signing environment is +// complete and the key file exists, so the no-UI device build can sign. +func checkDeviceSigning(_ context.Context) error { + return doctorVerifySigning() +} + func checkSidecarJAR(_ context.Context) error { if sidecarassets.IsPlaceholder() { return fmt.Errorf("placeholder JAR embedded; run `make sidecar && make sanderling` to embed the real fat JAR") diff --git a/cmd/sanderling/doctor_test.go b/cmd/sanderling/doctor_test.go index d6c464d..3155180 100644 --- a/cmd/sanderling/doctor_test.go +++ b/cmd/sanderling/doctor_test.go @@ -8,6 +8,8 @@ import ( "io" "strings" "testing" + + "github.com/priyanshujain/sanderling/internal/ios" ) func TestRunDoctorChecks_AllPass(t *testing.T) { @@ -142,15 +144,58 @@ func TestDoctorChecksFor_iOSSimulator_OmitsJava(t *testing.T) { } } -func TestDoctorChecksFor_iOSDevice_IncludesJava(t *testing.T) { - found := false - for _, c := range doctorChecksFor("ios-device") { - if strings.Contains(c.Name, "java") { - found = true +func TestDoctorChecksFor_iOSDevice_CoversDevicePrereqs(t *testing.T) { + checks := doctorChecksFor("ios-device") + for _, c := range checks { + if strings.Contains(c.Name, "java") || strings.Contains(c.Name, "sidecar") { + t.Errorf("device checks must not include the retired %q", c.Name) } } - if !found { - t.Error("ios-device checks must include java for the sidecar path") + for _, want := range []string{"devicectl", "usbmuxd", "connected and paired", "signing credentials"} { + found := false + for _, c := range checks { + if strings.Contains(c.Name, want) { + found = true + } + } + if !found { + t.Errorf("ios-device checks missing %q: %+v", want, checks) + } + } +} + +func TestCheckDeviceConnected(t *testing.T) { + original := doctorConnectedDevices + t.Cleanup(func() { doctorConnectedDevices = original }) + + doctorConnectedDevices = func(context.Context) ([]ios.Device, error) { + return []ios.Device{{Name: "iPhone"}}, nil + } + if err := checkDeviceConnected(context.Background()); err != nil { + t.Fatalf("a connected device must pass: %v", err) + } + + doctorConnectedDevices = func(context.Context) ([]ios.Device, error) { return nil, nil } + if err := checkDeviceConnected(context.Background()); err == nil { + t.Fatal("no device must fail") + } +} + +func TestCheckDeviceSigning_SurfacesSeamResult(t *testing.T) { + // checkDeviceSigning is a passthrough to the driver's credential check; the + // credential logic itself is covered by TestReadSigningCredentials* in the + // ioscompanion package. Here we only confirm the wiring through the seam. + original := doctorVerifySigning + t.Cleanup(func() { doctorVerifySigning = original }) + + doctorVerifySigning = func() error { return nil } + if err := checkDeviceSigning(context.Background()); err != nil { + t.Fatalf("a passing signing check must surface nil: %v", err) + } + + doctorVerifySigning = func() error { return errors.New("missing credentials") } + if err := checkDeviceSigning(context.Background()); err == nil { + t.Fatal("a failing signing check must surface the error") } } diff --git a/cmd/sanderling/main.go b/cmd/sanderling/main.go index 16b0088..33d8589 100644 --- a/cmd/sanderling/main.go +++ b/cmd/sanderling/main.go @@ -52,8 +52,8 @@ func parseTestArgs(args []string, stderr io.Writer) (testOptions, error) { flagSet.StringVar(&options.bundleID, "bundle-id", "", "target app bundle ID (required)") flagSet.StringVar(&options.platform, "platform", "android", "target platform: android, ios, web") flagSet.StringVar(&options.avd, "avd", "", "Android AVD name to boot if no device is connected") - flagSet.StringVar(&options.iosDevice, "ios-device", "", "iOS simulator name or UDID to boot if none is running") - flagSet.StringVar(&options.iosAppPath, "ios-app-path", "", "path to the .app bundle for iOS simulator clear-state reinstall") + flagSet.StringVar(&options.iosDevice, "ios-device", "", "iOS target: a simulator name/UDID to boot, or a connected device's name, UDID, or CoreDevice id") + flagSet.StringVar(&options.iosAppPath, "ios-app-path", "", "path to the .app bundle for iOS clear-state reinstall (simulator: simctl; device: devicectl)") flagSet.DurationVar(&options.duration, "duration", 5*time.Minute, "total test duration") flagSet.Int64Var(&options.seed, "seed", 0, "RNG seed (0 = random)") flagSet.StringVar(&options.output, "output", "./runs", "output directory for traces") diff --git a/companion/Sources/AppLifecycle.swift b/companion/Sources/AppLifecycle.swift index d7f8770..065abc5 100644 --- a/companion/Sources/AppLifecycle.swift +++ b/companion/Sources/AppLifecycle.swift @@ -31,6 +31,29 @@ enum AppLifecycle { } } + // state reports the app's run state in the vocabulary the Go transport maps + // to a process state: foreground, background, or notRunning. On the + // simulator the hybrid never calls it; on device it backs ForegroundApp. + static func state(bundleIdentifier: String) -> String { + var result = "notRunning" + let collect = { + switch XCUIApplication(bundleIdentifier: bundleIdentifier).state { + case .runningForeground: + result = "foreground" + case .runningBackground, .runningBackgroundSuspended: + result = "background" + default: + result = "notRunning" + } + } + if Thread.isMainThread { + collect() + } else { + DispatchQueue.main.sync(execute: collect) + } + return result + } + // onMainCatching runs automation work on the main thread and converts a // framework assertion into a thrown error so the server survives it. private static func onMainCatching(_ work: @escaping () -> Void) throws { diff --git a/companion/Sources/Server.swift b/companion/Sources/Server.swift index 1d67f94..0f84cc4 100644 --- a/companion/Sources/Server.swift +++ b/companion/Sources/Server.swift @@ -122,6 +122,19 @@ final class Server { let bundleIdentifier = params["bundleId"] as? String ?? currentBundleIdentifier try TextInput.type(text: text, replace: replace, bundleIdentifier: bundleIdentifier) return ["ok": true] + case "eraseText": + let count = params["count"] as? Int ?? 0 + let bundleIdentifier = params["bundleId"] as? String ?? currentBundleIdentifier + try TextInput.erase(count: count, bundleIdentifier: bundleIdentifier) + return ["ok": true] + case "pressKey": + let key = params["key"] as? String ?? "" + let bundleIdentifier = params["bundleId"] as? String ?? currentBundleIdentifier + try TextInput.pressKey(key: key, bundleIdentifier: bundleIdentifier) + return ["ok": true] + case "appState": + let bundleIdentifier = params["bundleId"] as? String ?? currentBundleIdentifier + return ["state": AppLifecycle.state(bundleIdentifier: bundleIdentifier)] case "screenshot": return try screenshot() default: diff --git a/companion/Sources/TextInput.swift b/companion/Sources/TextInput.swift index 0efcf3f..2641fae 100644 --- a/companion/Sources/TextInput.swift +++ b/companion/Sources/TextInput.swift @@ -37,6 +37,40 @@ enum TextInput { } } + // erase deletes count characters from the focused field by typing that many + // delete keys. Used on device, where EraseText routes to the runner instead + // of the legacy companion's HID stream. + static func erase(count: Int, bundleIdentifier: String) throws { + guard count > 0 else { return } + let deletes = String(repeating: XCUIKeyboardKey.delete.rawValue, count: count) + try typeOnFocus(deletes, bundleIdentifier: bundleIdentifier) + } + + // pressKey types a single logical key into the focused field. Only return is + // supported, matching the simulator companion's key surface. + static func pressKey(key: String, bundleIdentifier: String) throws { + switch key { + case "return", "enter", "Return", "Enter": + try typeOnFocus(XCUIKeyboardKey.return.rawValue, bundleIdentifier: bundleIdentifier) + default: + throw TextInputError.typingFailed("unsupported key \(key)") + } + } + + private static func typeOnFocus(_ payload: String, bundleIdentifier: String) throws { + let application = XCUIApplication(bundleIdentifier: bundleIdentifier) + var caughtError: NSError? + var completed = false + runOnMain { + completed = CompanionRunCatching({ + application.typeText(payload) + }, &caughtError) + } + if !completed { + throw TextInputError.typingFailed(caughtError?.localizedDescription ?? "unknown") + } + } + private static func runOnMain(_ work: () -> Void) { if Thread.isMainThread { work() diff --git a/conformance/gates.sh b/conformance/gates.sh index 81d580b..947b82d 100755 --- a/conformance/gates.sh +++ b/conformance/gates.sh @@ -7,8 +7,8 @@ # Backends: # BACKEND=simulator (default) drive the booted iOS simulator # BACKEND=device drive an attached physical iPhone via the -# native sidecar; select it with -# IOS_DEVICE="" (passed as --ios-device) +# driver's runner-only device path; select it +# with IOS_DEVICE="" (passed as --ios-device) # # Usage: # ./gates.sh run the simulator gates @@ -37,7 +37,13 @@ IOS_DEVICE="${IOS_DEVICE:-iPhone 17 Pro}" bundle_id="app.folio" spec_path="${folio_directory}/sanderling/spec.ts" -ios_app="${folio_directory}/app/iosApp/build/Build/Products/Debug-iphonesimulator/iosApp.app" +# The built app bundle differs by SDK: the simulator build lands under +# Debug-iphonesimulator, the device build under Debug-iphoneos. +if [[ "$BACKEND" == "device" ]]; then + ios_app="${folio_directory}/app/iosApp/build/Build/Products/Debug-iphoneos/iosApp.app" +else + ios_app="${folio_directory}/app/iosApp/build/Build/Products/Debug-iphonesimulator/iosApp.app" +fi # The companion binary, embedded for simulator runs. Referenced by file name # only for the orphan-process check; prose elsewhere says "the companion". @@ -187,7 +193,8 @@ print(samples[rank - 1]) # G5 orphan check: report any lingering companion, runner session (the hybrid # simulator driver hosts an in-simulator runner), and, on the device backend, -# the XCTest runner java sidecar. Empty output means clean. +# the device runner session. The usbmux tunnel is an in-process forwarder that +# dies with sanderling, so it leaves no process to check. Empty output is clean. orphan_processes() { local found="" if pgrep -f "$companion_process_name" >/dev/null 2>&1; then @@ -200,9 +207,10 @@ orphan_processes() { found+="runner-app " fi if [[ "$BACKEND" == "device" ]]; then - # The native sidecar that drives the XCTest runner for physical devices. - if pgrep -f "sanderling.*sidecar.jar" >/dev/null 2>&1; then - found+="sidecar " + # The device test session that hosts the runner. Its destination carries + # platform=iOS,id=. + if pgrep -f "xctestrun.*platform=iOS,id=" >/dev/null 2>&1; then + found+="device-session " fi fi printf '%s' "$found" @@ -215,15 +223,11 @@ invoke_sanderling() { local output_log="$2" local exit_status_file="$3" - local target_flags=() - if [[ "$BACKEND" == "device" ]]; then - target_flags=(--ios-device "$IOS_DEVICE") - else - # Simulator: build + install the current app so each run starts from the - # current build, matching the test-ios recipe. clear-state reinstall uses - # --ios-app-path. just ios boots IOS_DEVICE if nothing is booted. - target_flags=(--ios-device "$IOS_DEVICE" --ios-app-path "$ios_app") - fi + # Both backends pass --ios-app-path so each run reinstalls the current build + # for a clean clear-state start (device install via devicectl, simulator via + # simctl). The device backend selects the connected iPhone by name; the + # simulator backend boots IOS_DEVICE if nothing is booted. + local target_flags=(--ios-device "$IOS_DEVICE" --ios-app-path "$ios_app") local status=0 "$SANDERLING" test \ @@ -254,6 +258,9 @@ run_gates() { if [[ "$BACKEND" == "simulator" ]]; then echo "preparing folio build for the simulator backend" ( cd "$folio_directory" && just ios >/dev/null ) + else + echo "preparing folio device build" + ( cd "$folio_directory" && just ios-device >/dev/null ) fi local timestamp diff --git a/docs/manual/cli.md b/docs/manual/cli.md index 0faf506..73c15da 100644 --- a/docs/manual/cli.md +++ b/docs/manual/cli.md @@ -19,11 +19,12 @@ Run a spec against an app for a fixed duration. | `--launcher-activity` | resolved | Optional `/` to launch. Overrides default resolution. | | `--platform` | `android` | Target platform: `android`, `ios`, or `web`. | | `--avd` | optional (android) | Android AVD name to boot if no device is connected. Required only when no device is connected and multiple AVDs exist. | -| `--ios-device` | optional (ios) | iOS simulator name or UDID to boot if none is running. | +| `--ios-device` | optional (ios) | iOS target: a simulator name/UDID to boot, or a connected device's name, UDID, or CoreDevice id. | +| `--ios-app-path` | optional (ios) | Path to the `.app` bundle for clear-state reinstall (simulator via `simctl`, device via `devicectl`). | | `--duration` | `5m` | Total test duration (`30s`, `5m`, `2h`, `1d`). | | `--seed` | `0` | PRNG seed. `0` uses a random seed and records it in `meta.json`. | | `--output` | `./runs` | Output directory for traces. | -| `--clear-data` | `false` | Clear app data before launching so the run starts from a fresh install. | +| `--clear-data` | `true` | Clear app data before launching so the run starts from a fresh install. Pass `--clear-data=false` to resume prior state. | ## `sanderling replay [run-or-runs-dir]` @@ -42,7 +43,7 @@ See [the replay UI page](./replay/) for the panel reference and keyboard shortcu Check the host environment for a working sanderling setup. ``` -sanderling doctor [--platform web|android|ios|all] +sanderling doctor [--platform web|android|ios|ios-device|all] ``` `--platform` defaults to `all`, which runs every platform's checks (deduped). Pass a specific platform to scope the output. @@ -51,7 +52,8 @@ sanderling doctor [--platform web|android|ios|all] |---|---| | `web` | headless Chromium can launch (the bundled CDP surface boots a real browser). | | `android` | `adb` on PATH; `emulator` on PATH or under `ANDROID_HOME`; Java 17+; embedded native sidecar JAR is real. | -| `ios` | `xcrun` on PATH; `simctl` on PATH; Java 17+; embedded native sidecar JAR is real. | +| `ios` | `xcrun` on PATH; `simctl` on PATH. The simulator path drives the native companion with no JVM. | +| `ios-device` | the `ios` checks plus `devicectl`; the macOS `usbmuxd` socket; a connected, paired device; App Store Connect signing credentials present. | ## `sanderling version` diff --git a/docs/manual/getting-started.md b/docs/manual/getting-started.md index b0fe8e4..fde52dd 100644 --- a/docs/manual/getting-started.md +++ b/docs/manual/getting-started.md @@ -65,6 +65,21 @@ IOS_DEVICE="iPhone 15" just test-ios # pick a different simulator `just test-ios` boots the simulator if needed, builds and installs the app, then runs `sanderling test --platform ios`. +#### Physical device + +A connected iPhone is driven over a usbmux tunnel by a runner the driver builds and signs at run time. The tunnel talks to macOS's own `usbmuxd`, so nothing extra is installed beyond Xcode. It needs App Store Connect signing credentials in the environment (a gitignored `.env` is loaded by `just`): + +```sh +SANDERLING_IOS_TEAM=<10-char team id> +ASC_API_KEY_ID= +ASC_API_ISSUER_ID= +ASC_API_KEY_PATH= + +IOS_DEVICE="iPhone" just test-ios-device # name, UDID, or CoreDevice id +``` + +Run `sanderling doctor --platform ios-device` to check `devicectl`, the `usbmuxd` socket, a connected and paired device, and the signing credentials before a run. + ### Web From either example. For the KMP wasmJs build, use `examples/folio`: diff --git a/examples/folio/app/androidApp/src/main/AndroidManifest.xml b/examples/folio/app/androidApp/src/main/AndroidManifest.xml index c94068c..0001cc9 100644 --- a/examples/folio/app/androidApp/src/main/AndroidManifest.xml +++ b/examples/folio/app/androidApp/src/main/AndroidManifest.xml @@ -2,6 +2,7 @@ z45bDP46hOx7_4S6Fo+k-*%fF5lweBoc6a&zUu3o6Ss;(S#M9T6{S_N04;#mg8~^qJ zg;YyiBT7;dOH!?pi&B9UgOP!enXUm4S%w%ISs9pF8CYl=7+4t?{LKhngrXrgKP5A* z61RpP)7Nr94H|G8N-}d(i%Sx73vlbXt#Ilf0|VnqPZ!6KjC*gdpY}TJAky$K*nF4a z1GfMj-;Hrm%^l1^cdsPeS6aJhebBNOi@ts`h%6136`k%B@6<7YtLed`9RlHEwllp; z3jdW(`f22>$9$kr<~Ru5k$<-JP>9|Ng}XpbyEHBoY@hK&llpOn5ebcg| z`0t;Sx3uuvpgX z9Jgti!S01>A78t%nLaz~&}(^mLBcT}ku#V2v;*`z@(ps`v?d%=I)7nD<}pe63w_GX zZu)5pCca&sGh;?$oBuS&=82LSA2Q1RZQnEP;p|Z>AP<0uG|6cr0-t-G9>^ga_^WB_628I+(Y4@->MGOn{v?VQ4pZ_)jrkutd5j)?+ zFftr?|9=~Epy_`l^KTOC_CJl<^S>@yF5T($eeTzzttLS+rK{_-)&^wG z|GOeqU+Gc%F^0I8Oh4`}DPL~2x1-~9^0c)T2Yy7vpQ%WZys&v6^FI@o7TH<{i>^qv zj|`DX(edXLlCo|%PIdN8-8u6|X0z;`V>-ODXT*OcJu`M@*Sr5fW{w2Y7w1RfljT0N z?_$`W&%>}H=lbQjk^BrtQ#bG1@oouwRNR*1=c*V!NY<~9%+0N1zxME4#;JJ4$)OTO kzT5Zy(-P`*c{KkgWBKOBGuEt}pm<>LboFyt=akR{01ckb!2kdN literal 0 HcmV?d00001 diff --git a/examples/folio/app/androidApp/src/main/res/mipmap-xhdpi/ic_launcher.png b/examples/folio/app/androidApp/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..ecf584010584e87efeee4e7d8b7e22bdceba3a7f GIT binary patch literal 924 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 literal 0 HcmV?d00001 diff --git a/examples/folio/app/iosApp/iosApp/Assets.xcassets/Contents.json b/examples/folio/app/iosApp/iosApp/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/examples/folio/app/iosApp/iosApp/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/examples/folio/app/iosApp/project.yml b/examples/folio/app/iosApp/project.yml index 10a632d..34b918e 100644 --- a/examples/folio/app/iosApp/project.yml +++ b/examples/folio/app/iosApp/project.yml @@ -19,25 +19,33 @@ targets: sources: - iosApp dependencies: - - framework: ../shared/build/bin/iosSimulatorArm64/debugFramework/Shared.framework - embed: false - sdk: libsqlite3.tbd settings: base: PRODUCT_BUNDLE_IDENTIFIER: app.folio PRODUCT_NAME: iosApp INFOPLIST_FILE: iosApp/Info.plist + ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon + # The Kotlin framework slice differs by SDK: device builds link the + # iosArm64 framework, simulator builds the iosSimulatorArm64 one. The + # search path and the prebuild gradle task both follow this variable. + KOTLIN_FRAMEWORK_DIR[sdk=iphoneos*]: iosArm64 + KOTLIN_FRAMEWORK_DIR[sdk=iphonesimulator*]: iosSimulatorArm64 FRAMEWORK_SEARCH_PATHS: - $(SRCROOT)/../shared/build/bin/$(KOTLIN_FRAMEWORK_DIR)/debugFramework OTHER_LDFLAGS: - -ObjC - KOTLIN_FRAMEWORK_DIR: iosSimulatorArm64 + - -framework + - Shared preBuildScripts: - name: Build Kotlin framework script: | cd "$SRCROOT/../.." + if [[ "$PLATFORM_NAME" == iphoneos ]]; then + task=linkDebugFrameworkIosArm64 + else + task=linkDebugFrameworkIosSimulatorArm64 + fi ANDROID_HOME="${ANDROID_HOME:-$HOME/Library/Android/sdk}" \ - ./gradlew :app:shared:linkDebugFrameworkIosSimulatorArm64 - outputFiles: - - $(SRCROOT)/../shared/build/bin/iosSimulatorArm64/debugFramework/Shared.framework/Shared + ./gradlew :app:shared:$task basedOnDependencyAnalysis: false diff --git a/examples/folio/app/webApp/src/wasmJsMain/resources/index.html b/examples/folio/app/webApp/src/wasmJsMain/resources/index.html index 6927ef9..2af076a 100644 --- a/examples/folio/app/webApp/src/wasmJsMain/resources/index.html +++ b/examples/folio/app/webApp/src/wasmJsMain/resources/index.html @@ -4,6 +4,7 @@ Folio +