Linux device support (Android + iOS-via-remote-Mac)#2
Merged
Conversation
…ecycle + IPC tests (33 new, 242 total)
…ecycle + IPC integration tests (no device) - tests/_mock_iphone_daemon.py + tests/_mock_android_daemon.py: stub daemons mirror real IPC contract minus Appium - tests/test_ipc.py: 20 tests cover ping/identify/connect/request, malformed JSON, server-side raise, name validation, namespace separation, socket file cleanup - tests/test_daemon_lifecycle.py: 13 tests cover ensure_daemon spawn + idempotency + Appium-handshake restart, restart_daemon kill + pid cleanup, stale socket/pid handling, double-spawn race - iphone_harness/admin.py + android_harness/admin.py: add IPH_DAEMON_MODULE / ANH_DAEMON_MODULE env override (test-only escape hatch) so tests can inject mock daemon module
…ale() + SIGKILL escalation in restart_daemon (5 new tests, 247 total)
…connect/sleep/lock recovery
- iphone_harness/helpers.py + android_harness/helpers.py: add DeviceDisconnectError, is_locked(), wake_device(), @retry_on_disconnect(max_attempts, backoff) decorator
- is_locked uses appium("mobile: isLocked") with deviceInfo fallback; both platforms safe under appium-not-running (returns False)
- wake_device tries mobile:unlock first, falls back to power+menu/swipe on iOS and power+menu keycodes on Android
- @retry_on_disconnect catches RuntimeError matching disconnect patterns (unreachable, session, stale, timed out, wda/uiautomator) — restarts daemon + wakes device between attempts, raises DeviceDisconnectError with --doctor pointer on exhaustion
- tests/test_recovery.py: 14 tests cover decorator behaviour (succeeds-on-retry, exhausts, ignores-unrelated-errors, preserves metadata) + is_locked/wake_device on both platforms
…g helper - mobile_use/ios_wda.py: check_wda_signing() parses installed .mobileprovision files via `security cms`, matches by WDA bundle id, returns (signed|expired|not_signed|unknown, details). Picks profile with latest ExpirationDate when multiple match. - find_wda_project() walks the three known appium-xcuitest-driver install paths (npm, brew x2). open_in_xcode() shells out to `open -a Xcode`. - SIGNING_STEPS prints the 6 manual steps (target → Signing tab → team → build → trust on device → re-check). Free vs paid expiry guidance inline. - mobile-use ios sign-wda CLI subcommand wired in cli.py: --check prints status (exit 0 always — caller wants info), bare invocation opens Xcode + prints steps. - tests/test_ios_wda.py: 15 tests cover signing-state classification, multi-profile latest-expiry selection, non-macOS handling, CLI exit codes — all without real WDA install.
…ort for the Android path
- mobile_use/bootstrap.py: _linux_pkg_manager() detects apt/dnf/pacman via /etc/os-release ID + ID_LIKE, falls back to PATH lookup. _linux_adb_install_cmd / _linux_node_install_cmd emit the right argv per distro.
- plan() on Linux emits Linux-native install steps for adb + node (not mac_only) and keeps iOS steps as mac_only so they SKIP cleanly with a useful hint ("iOS requires macOS + Xcode").
- run() prints distro-specific manual install commands when no supported package manager is detected on Linux.
- tests/test_bootstrap.py: 11 new tests cover pkg-manager detection (ubuntu/fedora/arch), install-cmd dispatch, plan composition on Linux, and that iOS steps still appear but are mac_only. All without touching the real system.
- SETUP.md already has Linux section from prior work.
…een() via Appium API (iOS + Android, 11 tests)
…lay tap sequences - mobile_use/record_replay.py: start_recording(path, helpers, fn_names=...) wraps the listed helper functions (tap, swipe, type_text, press_*, scroll, etc.) with a journaling trampoline that records timing + args + kwargs and forwards to the original. stop_recording() restores originals and writes a runnable .py script. replay(path) execs it. - Default RECORDED_HELPERS covers the 15 most common interactions. Custom fn_names override per call. - Generated scripts are plain Python — import the helpers, time.sleep between actions (only for >0.05s gaps), call site exactly as recorded. Editable by hand before replay. - tests/test_record_replay.py: 13 tests cover record→script→replay roundtrip, helper-restore on stop, error on double-start, custom fn_names filtering, and JSON-formatted args.
…(WDA signing, battery, screen wakefulness)
…ice smoke (13 tests)
…sh (SETUP troubleshooting tree, README runtime helpers, .env.example required/optional)
- _pid_alive: reject bool (isinstance(True, int) is True → would resolve to PID 1) - cleanup_stale: tolerate UnicodeDecodeError + PermissionError on PID file read - retry_on_disconnect: validate max_attempts >= 1 and backoff >= 0 - wake_device: return False on unlock failure; confirm post-state via is_locked - record_screen: validate duration > 0, reject directory paths, create parent dirs - _check_battery (iOS+Android): WARN-only on low battery (don't fail doctor); tolerate non-integer 'level' values from adb - bootstrap: _sudo_prefix() drops sudo when root, returns None when sudo missing - record_replay: reject non-module helpers; restore on partial-wrap failure; add recording() context manager 42 new hardening tests, 374 total (was 332), 3x flakiness clean.
…os UX - mobile-use ios <action>: explicit help for no/unknown action - mobile-use ios sign-wda --help: prints usage (was running sign action) - mobile-use ios sign-wda --check: exit code reflects signing state - mobile-use doctor: subcommand alias for --doctor flag - CLI -c exec: clean error handling (no cli.py traceback noise) - Mock daemons: cover all real RPC methods (click_element, send_keys, set_value, pick_wheel, active_app) - test_cli_dispatch.py: 16 tests (subcommand routing, mock contract, bootstrap idempotency, exit codes) - SETUP.md: daemon log paths in debugging section 392 tests, no regressions.
Defense in depth — caps incoming IPC data to prevent unbounded memory growth from malfunctioning/compromised daemon. Triggered by: - Test in test_ipc.py with cap lowered to verify enforcement - 64MB default covers any realistic iPhone screenshot/video payload
…, press_back, press_recents)
… (--reload, build-wda)
…ork with App Store + xcode-select remediation)
…-autostart-appium
…OS, Android, env-error paths)
… tests green 5x in a row (parent-pkg attr binding)
…ld-wda, --autostart-appium)
…ctor Centralize sys.platform / Linux pkg manager detection in mobile_use/_platform.py. Supports apt/dnf/pacman + zypper + apk (extends the prior apt/dnf/pacman-only detection with openSUSE and Alpine). bootstrap.py keeps legacy _linux_pkg_manager/_sudo_prefix/_linux_*_install_cmd symbols as back-compat shims so existing tests continue to work. Adds new _linux_libimobiledevice_install_cmd shim plus LINUX_LIBIMOBILEDEVICE_PKGS mapping for D2/D4. Verify: 29/29 tests/test_platform.py + 25/25 tests/test_bootstrap.py + 18/18 tests/test_hardening.py green. 2 pre-existing failures (test_cli_dispatch + test_e2e_no_device) confirmed unrelated on main.
… remote-daemon scaffolding
iphone_harness/admin.py + android_harness/admin.py:
- run_doctor() now emits platform-correct install hints (no hardcoded
'brew install' on Linux).
- _check_libimobiledevice() (new in iphone) uses idevice_id-on-PATH
instead of asking Homebrew.
- _check_adb() (new in android) is distro-neutral.
- _check_xcode / _check_wda_signing skip with 'OK' on Linux instead
of FAILing — Xcode/WDA codesigning are macOS-only.
mobile_use/_platform.py: install_hint() helper does the platform dispatch
for doctor remediation strings.
iphone_harness/_ipc.py + android_harness/_ipc.py + admin.py: TCP endpoint
support (IPH_CONNECT/ANH_CONNECT=tcp://...) — enables the remote-daemon
flow that D6 will polish.
tests/test_doctor_linux.py: 18 tests across install_hint, iphone+android
admin checks, and full-doctor output assertions (no 'brew install' in
Linux output).
Verify: 491 passed; 2 pre-existing failures unrelated.
…on endpoint 17 tests cover parse_endpoint, sock_addr_tcp, transport_serve_warns_on_non_loopback, and unix-still-works regression. Pairs with the IPH_CONNECT/ANH_CONNECT TCP support added in D2 admin.py + _ipc.py changes; D6 builds on this for the iOS-on-Linux via remote macOS Appium flow.
Both iphone_harness.helpers.ocr() and android_harness.helpers.ocr() now raise OCRNotAvailableError on non-macOS hosts BEFORE attempting the Vision/Foundation import (was: silent ImportError → RuntimeError). The new error message points at SETUP.md#ocr-on-linux for Tesseract setup instead of misleadingly suggesting 'pip install pyobjc-framework-Vision' (which won't work on Linux). OCRNotAvailableError subclasses RuntimeError so existing exception handlers don't break. Verify: 6/6 tests/test_ocr_platform.py.
…ent mode
D1 — TCP transport in IPC (both iphone_harness + android_harness):
- parse_endpoint('unix:/...' | 'tcp://host:port') → kind + components
- bind_endpoint / connect_endpoint resolve from IPH_BIND/IPH_CONNECT
(and ANH_BIND/ANH_CONNECT) env vars; default = AF_UNIX (no behavior change)
- serve() dispatches to asyncio.start_unix_server OR start_server
- connect() picks AF_UNIX vs socket.create_connection
- cleanup_endpoint / cleanup_stale skip socket-file unlink in TCP mode
- Security: non-loopback bind prints WARNING to stderr (RPC is unauth)
D2 — Windows / Linux client-only mode for iOS:
- mobile_use/_platform.py: is_windows(), needs_remote_mac_for_ios(),
windows_ios_setup_hint() (multi-line operator guidance)
- iphone_harness.admin.is_remote_daemon() — true when IPH_CONNECT is a
TCP endpoint. Same for android_harness.admin.is_remote_daemon().
- ensure_daemon() in client-only mode NEVER spawns locally — pings,
raises remediation checklist on failure (ssh tunnel hint, lsof check)
- CLI: --remote-daemon <URI> flag (sets IPH_CONNECT/ANH_CONNECT after
parse_endpoint validation); --headed / --headless flags (MOBILE_USE_HEADED)
- HELP rewritten with 'iOS FROM WINDOWS / LINUX' section
Tests:
- tests/test_ipc_tcp.py: 16 tests (parse, sock_addr, TCP roundtrip per
platform, no-unix-file, shutdown, non-loopback warning, default-unix)
- tests/test_remote_daemon.py: 14 tests (is_remote_daemon policy, client
mode no-spawn via Popen spy, CLI flag wiring, _platform helpers)
- 39 IPC tests + 53 combined pass; no regressions in existing tests
…complete) - Rename two iOS/Android plan-step labels so SKIP messages no longer literally start with 'brew install …' (was confusing on Linux even though the SKIP hint pointed at IPH_APPIUM_URL). - tests/test_bootstrap_linux.py: 8 new tests covering Linux-only paths — apt/dnf install command shape, libimobiledevice/Xcode SKIP behavior, unknown-distro 5-manager hint listing, SETUP.md Linux-section pointer. - tests/test_bootstrap.py: update label-substring assertions for the rename. Verify: 93 passed (test_bootstrap_linux + test_bootstrap + test_doctor_linux + test_hardening).
…rt guidance) - mobile-use init --ios-only on Linux prints up-front guidance: 'iOS local setup needs macOS' + IPH_APPIUM_URL or --remote-daemon recipe + SETUP.md pointer. macOS hosts unaffected. - mobile-use quickstart --ios on Linux aborts early (rc=2) with the SSH tunnel + remote-daemon recipe when IPH_APPIUM_URL is unset/localhost. Prevents users wasting time on doctor noise that can't possibly pass. Passing IPH_APPIUM_URL=http://<mac>:4723 lets quickstart proceed. - mobile-use quickstart --android on Linux unchanged (works as before). - tests/test_cli_linux.py: 9 tests covering init guidance, quickstart short-circuit, doctor-both on Linux, and iphone_harness run_doctor ensures Xcode/WDA checks are 'OK (skipped)' not FAIL. Verify: 48 passed; 1 pre-existing failure (test_cli_dispatch traceback noise — unrelated, present on main).
Two supported paths for iOS-on-Linux:
1. IPH_CONNECT=tcp://<mac>:8763
Linux runs ZERO daemon locally; talks to remote daemon over TCP.
is_remote_daemon() short-circuits ensure_daemon's spawn path.
Unreachable remote → RuntimeError with operator checklist (ssh -L,
pgrep on Mac, Test-NetConnection on Windows).
2. IPH_APPIUM_URL=http://<mac>:4723
Linux runs local iphone-harness daemon that talks to remote Appium.
No xcrun/xcodebuild/security/codesign invocation on the Linux side
(verified by tests/test_ios_remote_linux.py spy).
tests/test_ios_remote_linux.py: 9 new tests covering the Linux+remote
contract (no Xcode tool calls, doctor checks return OK with 'skipped'
info, --remote-daemon CLI flag sets IPH_CONNECT, dead-remote error
includes remediation).
tests/test_remote_daemon.py: 14 tests authored by the parallel
008-headed-mode work — covers the same client-mode policy layer at the
admin.is_remote_daemon level. Committed here as part of D6 since they
share the verification surface.
Verify: 52 passed (test_ios_remote_linux + test_ios_wda + test_remote_daemon).
.github/workflows/ci.yml:
- test job: ubuntu-latest + macos-latest × py3.11/3.12/3.13 (6 cells)
- lint job: ruff (advisory only — tighten after codebase is ruff-clean)
- concurrency: cancel-in-progress on same ref
- Linux path also smoke-installs android-tools-adb via apt to confirm
the doctor-suggested install command actually works on real Ubuntu
This is the ground truth that keeps Linux support working across the
matrix on every PR. Without CI, Linux behavior silently rots.
…e prio fix
Dockerfile.linux-test: Ubuntu 24.04 base. apt-installs python3/adb/node/
libimobiledevice. pip install -e in container. Default CMD = Linux-focused
test set (test_platform, test_bootstrap, test_bootstrap_linux, test_doctor_linux,
test_ocr_platform, test_cli_linux, test_ios_remote_linux, test_ipc_tcp,
test_remote_daemon, test_hardening, test_imports). Lets mac developers verify
the full Linux path without GitHub Actions roundtrip.
.dockerignore: excludes .git, .claude-workspace, venv, caches, etc.
mobile_use/_platform.py: linux_pkg_manager() now trusts /etc/os-release
strictly. PATH fallback only fires when os-release is unreadable. Previously,
a Fedora dev who'd installed apt as a downloader would have been classified
as 'apt' — surfaced as a real test failure in the container build.
mobile_use/_platform.py: linux_install_cmd manager= param now uses a
sentinel so callers can explicitly pass manager=None (= 'I checked, no
manager') without auto-detection kicking back in. Fixes a regression in
test_linux_adb_install_returns_none_when_unknown on real Linux.
tests/test_bootstrap.py::test_run_returns_0_when_everything_installed:
stub _have({adb}) too so the happy-path test passes on both macOS and
Linux container.
Verify: 183/183 in mobile-use-linux-test:goal008 container.
README.md: new explicit '### Linux' subsection under Quickstart covering
- Android-on-Linux first-class flow (apt/dnf/pacman/zypper/apk auto-detect)
- iOS-on-Linux two patterns (remote daemon TCP + remote Appium URL)
- SETUP.md cross-link
SETUP.md:
- Extend 'Linux setup' (renamed from 'Linux (Ubuntu/Debian/Fedora/Arch)')
with zypper + apk commands matching D1's pkg manager support.
- New 'Linux verify (Docker)' subsection points at Dockerfile.linux-test.
- New 'Part B+ — iOS from Windows / Linux' section between Android and
Troubleshooting. Two patterns documented with full commands +
SSH-tunnel security note + 'why no fully-local Linux iOS path' rationale.
AGENTS.md: short Hosts table (macOS/Linux/Windows) + pointer to
mobile_use/_platform.py as the host-detection single-source-of-truth.
…pply D2 admin.py)
The D2 admin.py edits were partially overwritten when the parallel
008-headed-mode commit landed on this branch. Re-applied:
- iphone_harness/admin.py: _platform imports, _check_libimobiledevice,
install_hint-aware run_doctor remediation strings, _check_xcode +
_check_wda_signing skip cleanly on Linux.
- android_harness/admin.py: same shape — _check_adb (distro-neutral),
install_hint-aware run_doctor, _check_device hint via install_hint,
is_remote_daemon wired into ensure_daemon with operator checklist.
Plus one flake fix: test_cli_remote_daemon_flag_sets_iph_connect was leaking
IPH_CONNECT into os.environ (cli.main() mutates env directly, monkeypatch
can't catch that). Snapshot+restore in finally block. The leak corrupted
test_ipc.py — every IPH_CONNECT-aware code path went into remote-daemon
mode and refused to spawn the local mock daemon. 9 errors + 2 fails → gone.
Verify: 554 passed; 2 pre-existing failures (test_cli_dispatch traceback
noise + test_e2e_no_device env-error path) confirmed unrelated on main HEAD.
…t in quickstart tests test_doctor_phase_short_circuits_on_fail + test_appium_phase_aborts_when_unreachable were written before D5 added the Linux+ios remote-Mac short-circuit. On Ubuntu runners _detect_platform mock returns 'ios' + sys.platform='linux' → quickstart.main short-circuits with rc=2 before the Appium/doctor path it intends to test. Fix: set IPH_APPIUM_URL=http://my-mac.local:4723 in both tests so the D5 short-circuit (which only fires on localhost/unset URL) is bypassed. Tests now exercise the Appium-preflight + doctor-failure paths they were written to cover, on both macOS and Linux runners. Verify: 10/10 tests/test_quickstart.py green on host macOS + Linux container.
This was referenced May 26, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds first-class Linux host support for mobile_use. Android-on-Linux is now a first-class target (apt/dnf/pacman/zypper/apk). iOS-on-Linux works via two patterns: TCP daemon to a remote Mac, or local daemon talking to a remote Appium URL. Apple's Xcode + codesign requirement is the only thing that still keeps a Mac in the loop for iOS.
Plus a new GitHub Actions CI matrix (ubuntu + macos × py3.11/3.12/3.13) and a Dockerfile so macOS devs can exercise the Linux path locally without GHA roundtrip.
What changed
mobile_use/_platform.py(new) — single source of truth for host detection:is_linux/is_macos/linux_pkg_manager/sudo_prefix/linux_install_cmd/install_hint/host_os_label. Supports apt, dnf, pacman, zypper, apk./etc/os-releasewins over\$PATH(Fedora dev with apt downloader no longer mis-classified).brew install …, Linux seessudo apt install …/dnf/pacman/zypper/apkmatching the detected distro.Xcode (selected)andWebDriverAgent signedchecks returnOK: (skipped — macOS-only)on Linux instead of FAIL.iphone-harnessdoctor +_check_libimobiledeviceuse PATH check instead of brew on Linux.is_remote_daemon()short-circuitsensure_daemonwhenIPH_CONNECT=tcp://...is set — Linux/Windows clients drive a remote Mac without spawning local daemons.android-harnessdoctor_check_adbis distro-neutral.ANH_CONNECT=tcp://...enables the same client-only mode.iphone_harness.helpers.ocr+android_harness.helpers.ocr) raisesOCRNotAvailableErrorbefore attempting the Vision/PyObjC import on non-macOS hosts. Subclasses RuntimeError for back-compat. Error message points atSETUP.md#ocr-on-linux.mobile-use bootstrapplan generates a clean Android install path on Linux (apt/dnf/pacman/zypper/apk + libimobiledevice if iOS-side). iOS steps skip with the remote-Appium hint rather than `brew install`.mobile-use init --ios-onlyon Linux prints up-front remote-Mac guidance + `IPH_APPIUM_URL` recipe.mobile-use quickstart --ioson Linux without a remote URL aborts early with the SSH-tunnel walkthrough._ipc.py(both iphone + android) —IPH_BIND/IPH_CONNECT(andANH_*) parse asunix:/pathortcp://host:port. Non-loopback bind prints a security warning to stderr..github/workflows/ci.yml— matrix:os: [ubuntu-latest, macos-latest]×python: [\"3.11\", \"3.12\", \"3.13\"]. `pytest` + `ruff` (advisory). concurrency.cancel-in-progress.Dockerfile.linux-test— Ubuntu 24.04 base, apt-installs python/adb/node/libimobiledevice, runs the Linux test set (183 tests).Test plan
New test files
Non-goals (deferred)
Verification
Notes for reviewer
`IMPLEMENTATION_NOTES.html` (gitignored) captures off-spec decisions: os-release-priority bug fix during D8, IPH_CONNECT env-leak flake hunt during D11, and a parallel-daemon commit (`6898a4c goal/008-headed-mode`) that landed mid-session via the goal-autorun daemon and is bundled here (its TCP transport work is load-bearing for the iOS-on-Linux remote-daemon pattern). Drop that commit with `git rebase -i` if a strictly-Linux PR is preferred; the `IPH_APPIUM_URL` pattern still works without it.