idevice
Cross-platform device automation for end-to-end test workflows: install and manage apps on physical devices, transfer files, and drive UI interactions through a small, platform-agnostic API.
The package ships two complementary APIs:
idevice.device— local device automation: talk to a USB/network-attached device through platform CLIs (go-ios, pymobiledevice3, adb) to install apps, transfer files, and drive UI.idevice.host— keeper-backed remote orchestration: drive a memory-measurement run on a host machine that talks to the ikeeper control server and the on-device RemoteControlTest runner over HTTP.
Platform status
| Platform | Backend | App lifecycle | File transfer | Documents sandbox | Swipe | UI automation |
|---|---|---|---|---|---|---|
| iOS | go-ios (IOSDevice) |
Yes | Yes | — | — | Planned (WDA) |
| iOS | pymobiledevice3 (IOSDevice3) |
Yes | Yes (AFC + app sandbox) | Yes | — | Planned (WDA) |
| iOS | ios4 (IOSDevice4) |
Yes | — | Yes (afc --documents) |
— | Yes (WDA) |
| Android | adb (AndroidDevice) |
Yes | Yes | — | Yes | Yes (AndroidUIAuto) |
| Windows | PowerShell (WindowsDevice) |
Yes | — | Yes (local filesystem) | — | Planned |
macOS and HarmonyOS are not implemented yet.
Requirements
- Python >= 3.9
- Platform CLI tools on
PATH(or configured via environment variables below):- iOS (go-ios):
ios - iOS (pymobiledevice3):
pymobiledevice3(default:/opt/ios3/bin/pymobiledevice3on Unix,~/ios3/bin/pymobiledevice3.exeon Windows) - iOS (ios4):
ios4(or setIDEVICE_IOS4_BINARY); optionallyideviceinstallerfor installs (or setIDEVICE_IDEVICEINSTALLER_BINARY) - iOS (devicectl):
xcrunfrom Xcode, macOS only (or setIDEVICE_XCRUN_BINARY);ios4is still needed forcapture_memgraphanddocuments_rm - Android:
adb
- iOS (go-ios):
Python packages pymobiledevice3 and uiautomator2 are installed automatically with the project (see Install). IOSDevice3 uses the pymobiledevice3 Python library for Documents sandbox access (documents_*); other iOS operations go through the CLI.
Install
uv sync
# or, with dev dependencies
uv sync --group dev
Quick start
Create a device bound to a single UDID or serial, then call lifecycle methods:
from pathlib import Path
from idevice.device import Device, Platform
# iOS via go-ios
device = Device.create(Platform.IOS, device_id="00000000-0000000000000000", device_ip="")
# iOS via pymobiledevice3 (iOS 17+ tunnel support)
device = Device.create(Platform.IOS3, device_id="00000000-0000000000000000", device_ip="")
# iOS via the Rust ios4 lifecycle backend
device = Device.create(
"ios4",
device_id="00000000-0000000000000000",
device_ip="",
package_name="com.example.game",
)
# iOS via Apple's own xcrun devicectl (macOS + Xcode only)
device = Device.create(
"ios5",
device_id="00000000-0000000000000000",
device_ip="",
package_name="com.example.game",
)
# Android via adb (package_name is the default app id for stop_app())
device = Device.create(
Platform.ANDROID,
device_id="emulator-5554",
device_ip="",
package_name="com.example.app",
)
device.install(Path("MyApp.ipa"), app_id="com.example.app")
device.launch_app("com.example.app")
device.is_installed("com.example.app")
device.stop_app() # uses bound package_name
device.stop_app("com.example.app") # explicit override
device.uninstall("com.example.app")
Launch an iOS game through IOSDevice4 with malloc stack logging and ordered
command-line arguments:
from idevice.device.ios4.device import IOSDevice4
game = IOSDevice4(
"00000000-0000000000000000",
package_name="com.example.game",
)
game.launch_app(
"com.example.game",
args=["--mode", "debug", "--label", "heap capture"],
environment={"MallocStackLogging": "1"},
)
print(game.last_launch_pid)
snapshot = game.capture_memgraph("trash-dash.memgraph")
print(snapshot)
IOSDevice5 provides the same game lifecycle through Apple's
xcrun devicectl:
from idevice.device.ios5.device import IOSDevice5
game = IOSDevice5(
IOSDevice5.default_udid(), # first USB-attached device
package_name="com.example.game",
)
game.launch_app(
"com.example.game",
args=["--mode", "debug"],
environment={"MallocStackLogging": "1"},
)
game.screenshot("screen.png")
snapshot = game.capture_memgraph("trash-dash.memgraph") # shells out to ios4
Android swipe (via adb shell input swipe):
device.swipe(100, 800, 100, 200, duration_ms=300)
iOS Documents sandbox (IOSDevice3 and IOSDevice4 — requires file-sharing entitlements):
device.documents_push("com.example.app", Path("log.txt"), "Logs/log.txt")
device.documents_exists("com.example.app", "Logs/log.txt")
device.documents_ls("com.example.app", "Logs")
device.documents_pull("com.example.app", "Logs", Path("out/Logs"))
device.documents_rm("com.example.app", "Logs/log.txt")
Windows Documents sandbox (WindowsDevice — backed by the local filesystem under
%LocalAppData%/<company_name>/<package_name>). The sandbox root is fixed at
construction, so company_name and package_name are required; remote is
always resolved relative to that root, and every method works on both files and
directories:
device = Device.create(
Platform.WINDOWS,
device_id="MY-PC",
device_ip="",
company_name="MyCompany",
package_name="MyApp",
)
device.documents_push("MyApp.exe", Path("log.txt"), "Logs/log.txt")
device.documents_push("MyApp.exe", Path("assets"), "assets") # whole directory
device.documents_exists("MyApp.exe", "Logs/log.txt")
device.documents_ls("MyApp.exe", "Logs")
device.documents_pull("MyApp.exe", "Logs", Path("out/Logs"))
device.documents_rm("MyApp.exe", "Logs")
Examples
Runnable scripts under examples/ auto-detect the first connected device when no ID is passed:
# Android: launch, push/pull, swipe (optional --apk / --package)
uv run python examples/android_device.py
# iOS (pymobiledevice3): lifecycle, AFC, app sandbox, Documents API
uv run python examples/ios3_device.py
# iOS (ios4): install and launch a game
uv run python examples/ios4_device.py \
--udid 00000000-0000000000000000 \
--ipa path/to/game.ipa \
--app-id com.example.game \
--malloc-stack-logging \
--memgraph game.memgraph \
--arg=--mode --arg=debug
# iOS (devicectl): install, launch, screenshot
uv run python examples/ios5_device.py \
--udid 00000000-0000000000000000 \
--ipa path/to/game.ipa \
--app-id com.example.game \
--screenshot screen.png
# Install an IPA and exercise sandbox file transfer
uv run python examples/ios3_device.py \
--ipa path/to/app.ipa \
--app-id com.example.app \
--sandbox-app-id com.example.app
# Host orchestration: keeper-backed memory-measurement run (see "Host orchestration")
uv run python examples/host_example.py --from-env --bundle-id com.example.app
See the module docstrings in each example for prerequisites (Developer Mode, iOS 17+ tunnel, USB debugging, ikeeper reachability, etc.).
API overview
DeviceBase
Every platform implementation shares the same interface:
install(package_path, app_id=None)— install.ipa/.apkand optionally record bundle id → file nameuninstall(app_id)/is_installed(app_id)/get_installed_pkg_name(app_id)— the latter returns anInstalledAppInfo(app_id, version, path)orNonelaunch_app(app_id=None)/stop_app(app_id=None)— both use the boundpackage_namewhenapp_idis omittedlaunch(app_id=None)— optional native device-service launch, currently implemented only byIOSDevice4package_name— default app id set atDevice.create/Device.from_env(GAUTO_PACKAGE_NAME)push(local, remote, app_id=None, documents_only=False)/pull(remote, local, app_id=None, documents_only=True)— host ↔ device file transferls(remote, app_id=None, recursive=False)— list a remote directory on the devicedocuments_exists(app_id, remote)/documents_ls(app_id, remote)/documents_push(app_id, local, remote)/documents_pull(app_id, remote, local)/documents_rm(app_id, remote)— app Documents sandbox;IOSDevice5.documents_rmdelegates recursive removal to theios4AFC service because CoreDevice has no delete commandswipe(x1, y1, x2, y2, duration_ms=300)— touch gesture (Android implemented; iOS/Windows raiseNotImplementedError)tap(x, y, app_id=None)— normalized touch input, implemented byIOSDevice4through WebDriverAgent andIOSDevice5through iwda2screenshot(local)— capture the screen to a host filehost_is_running()— whether WebDriverAgent / UIAutomator2 host process is upcapture_memgraph(output, pid=None)— capture a process memory snapshot (IOSDevice4, andIOSDevice5by shelling out toios4)
Use Device.create(Platform, device_id=…, device_ip="", package_name=…) or
construct IOSDevice, IOSDevice3, IOSDevice4, IOSDevice5, AndroidDevice,
or WindowsDevice directly. Device.from_env requires GAUTO_PACKAGE_NAME on
all platforms.
UIAutoBase
Higher-level UI helpers built on top of device tooling. Currently only AndroidUIAuto is available (swipe, dismiss_post_install_dialogs, hierarchy access).
iOS backends
IOSDevice (go-ios) — lightweight CLI wrapper around go-ios for install, launch, and AFC transfers.
IOSDevice3 (pymobiledevice3) — uses pymobiledevice3 services:
- App install/uninstall/list via
apps - Process control via
developer dvt launch/pkill - File transfer via
afc push/pullorapps push/pull(app sandbox, with optional--documents) - Documents sandbox via the pymobiledevice3 Python library (House Arrest AFC):
documents_exists,documents_ls,documents_push,documents_pull,documents_rm - Developer-mode commands require a mounted DeveloperDiskImage; on iOS 17+ an active tunnel is required (
pymobiledevice3 remote start-tunnel)
IOSDevice4 (ios4) — a game lifecycle backend using the Rust
ios4 binary:
- IPA/app-directory install via the standalone
ideviceinstallerCLI when present, otherwiseios4 ideviceinstaller install - Exact bundle-id checks via
application_listing - Direct native launch via
process_control - Launch via WebDriverAgent first, falling back to
process_control; both carry orderedargvand environment values - Xcode-compatible snapshots via
memgraph, defaulting to the last launch PID - Tracks the launch PID so
memgraphcan reuse it —process_controlalways reports one, while a WDA launch resolves it best-effort from the WDA app list - Stop via WebDriverAgent first, falling back to
pkill --bundle - Normalized screen taps via WebDriverAgent
- Screen capture via
screenshot - Documents sandbox via
afc --documents <bundle-id>:documents_exists,documents_ls,documents_push,documents_pull,documents_rm, all handling files and directories (directories are walked client-side, sinceafc upload/downloadonly move single files) - Documents paths are always relative to the vended
/Documentsroot, soremotecannot escape the sandbox - Does not currently implement generic file transfer (
push/pull/ls) outside the Documents sandbox
IOSDevice5 (xcrun devicectl) — the same game lifecycle on Apple's own
CoreDevice CLI, so it needs macOS with Xcode but no third-party binary:
- Install/uninstall via
device install app/device uninstall app, caching the bundle id devicectl reports - Exact bundle-id checks via
device info apps --bundle-id - Launch via
device process launch, with the environment as a JSON dictionary andargvas real positional arguments - Stop by resolving the bundle's processes in
device info processesand terminating each withdevice process terminate --kill - Normalized screen taps through
iwda2athttp://<device-ip>:18201/api/tap; the explicitapp_idor boundpackage_nameanchors coordinates to the app's current orientation - App data container transfers via
device copy to/device copy fromand listing viadevice info files, including the Documents sandbox; directory pushes can passremove_existing_content=Trueto replace the destination contents - Screen capture via
device capture screenshoton Xcode 27+, falling back toios4 capture_memgraphshells out toios4: CoreDevice exposes no memory-graph servicedocuments_rmcontains the sameios4 afc --documentsworkflow asIOSDevice4.documents_rm: inspect withinfo, then callremovefor a file orremove_allfor a directorydelete2andswiperaiseNotImplementedError— CoreDevice has no general file-removal or touch-injection service
Every command is parsed from devicectl's JSON output, the only interface Apple
guarantees to keep stable, and errors surface as the flattened
NSLocalizedDescription chain.
Choose Platform.IOS, Platform.IOS3, Platform.IOS4, or Platform.IOS5
depending on which CLI you have deployed. The string values are ios4 and
ios5.
Host orchestration (idevice.host)
The host package drives a memory-measurement run from a host machine (mac or Windows). The host never dials the device directly: it talks to the ikeeper control server over HTTP, which in turn proxies the on-device RemoteControlTest runner. A host is always bound to a single keeper and a single target device (device_udid / device_ip) plus the app bundle_id under test.
Host status
| Host type | Implementation | Notes |
|---|---|---|
macos |
MacHost |
Real keeper-backed host |
windows |
WindowsHost |
Real keeper-backed host (HTTP-only, mirrors MacHost) |
anything else (ios, android, …) |
DummyHost |
No-op host; every operation reports unhealthy and returns an inert default |
Quick start
Build a host explicitly, or from the controller-injected GAUTO_* environment:
from idevice.host import Host
# Explicit
host = Host.create(
host_type="macos",
keeper_ip="192.168.0.10",
device_udid="00000000-0000000000000000",
device_ip="192.168.0.20",
bundle_id="com.example.app",
)
# Or from GAUTO_* environment variables (never raises; falls back to DummyHost)
host = Host.from_env()
host.health() # keeper reachable?
host.launch_app(timeout=300.0) # start run, wait for runner, launch app
host.capture_memgraph(timeout=60.0) # open a measured window that auto-closes
summary = host.export() # keeper presigns + uploads; returns download_url
host.screenshot("shot.png") # capture one screenshot via the runner proxy
host.tap(0.5, 0.5) # tap at normalized screen coordinates
host.kill() # tear down the keeper run
# Launch with command-line arguments, applied as XCUIApplication.launchArguments.
# Engine BootConfig values can be overridden this way, since argv beats boot.config:
host.launch_app(args=["-hg-mmap-allocater", "0"])
The most recently built host is also reachable anywhere via Host.Instance.
Host.create / Host.from_env return a HostBase exposing: health(), runner(), launch_app(), capture_memgraph(), export(), screenshot(), tap(), status(), kill(), and exit(). Errors are raised as HostError (with KeeperError, RunnerError, HostTimeoutError, and HostNotSupportedError subclasses).
Lower-level clients
Keeper— thin HTTP client for the ikeeper control server (/api/runsroutes):launch,launch_app,status,list_runs,kill,export,health.Runner— thin HTTP client for the on-device runner, reached through the keeper proxy:launch_app,activate,terminate,start_measuring/stop_measuring/measuring_status,dt_measuring,screenshot,tap,exit,health.
Configuration
Environment variables override default binary paths:
| Variable | Default | Used by |
|---|---|---|
IDEVICE_IOS_BINARY |
ios |
IOSDevice |
IDEVICE_IOS3_BINARY |
/opt/ios3/bin/pymobiledevice3 (Unix) / ~/ios3/bin/pymobiledevice3.exe (Windows) |
IOSDevice3 |
IDEVICE_IOS4_BINARY |
ios4 (ios4.exe on Windows) |
IOSDevice4, IOSDevice5.capture_memgraph, IOSDevice5.documents_rm |
IDEVICE_IDEVICEINSTALLER_BINARY |
ideviceinstaller (ideviceinstaller.exe on Windows) |
IOSDevice4.install (falls back to ios4 when missing) |
IDEVICE_XCRUN_BINARY |
xcrun |
IOSDevice5 |
IDEVICE_ADB_BINARY |
adb |
AndroidDevice, AndroidUIAuto |
IDEVICE_POWERSHELL_BINARY |
powershell |
WindowsDevice |
User data (e.g. installed-app cache) is stored under ~/.idevice by default.
The idevice.host orchestrator reads its configuration from the controller-injected environment (used by Host.from_env):
| Variable | Default | Meaning |
|---|---|---|
GAUTO_HOST_TYPE |
— | Host type (macos / windows run a real host; others → DummyHost) |
GAUTO_HOST_IP |
— | ikeeper control-server IP |
GAUTO_HOST_PORT |
18200 |
Keeper control-server port |
GAUTO_HOST_ID |
— | Optional keeper/controller id (informational) |
GAUTO_DEVICE_UDID |
— | Target device UDID |
GAUTO_DEVICE_IP |
— | Target device IP |
GAUTO_DEVICE_SERVER_PORT |
18100 |
On-device runner port |
GAUTO_PACKAGE_NAME |
— | Target app bundle id |
IDEVICE_HOST_TIMEOUT |
60 |
Per-request HTTP timeout (seconds) |
IDEVICE_HOST_READY_TIMEOUT |
300 |
Runner readiness timeout (seconds) |
Testing
Unit tests run without a connected device:
uv run pytest
Integration tests under tests/device/ require a physical iOS device and pymobiledevice3. They are excluded by default; run with:
export IDEVICE_IOS3_UDID="00000000-0000000000000000"
uv run pytest -m integration tests/device/
See tests/device/conftest.py for optional variables (IDEVICE_IOS3_TEST_IPA, sandbox push/pull settings, etc.).
Development
uv run ruff check src tests
uv run pytest
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file idevice-0.30.2.tar.gz.
File metadata
- Download URL: idevice-0.30.2.tar.gz
- Upload date:
- Size: 312.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b8c27c31dc987b46e34528fdca8ae24f8e014a4dd4dcef4a426b65438627c368
|
|
| MD5 |
bad722dedb70f6e7e477fc0246eeb9d3
|
|
| BLAKE2b-256 |
f551c50fc02cbee514bdd2e1e8a4e2d421d67838bd37ba26770a7ead3e882a46
|
Provenance
The following attestation bundles were made for idevice-0.30.2.tar.gz:
Publisher:
workflow.yml on nzcv/idevice
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
idevice-0.30.2.tar.gz -
Subject digest:
b8c27c31dc987b46e34528fdca8ae24f8e014a4dd4dcef4a426b65438627c368 - Sigstore transparency entry: 2532403938
- Sigstore integration time:
-
Permalink:
nzcv/idevice@91de514a5d01a59aca9d8c124409d1fbdefab234 -
Branch / Tag:
refs/heads/master - Owner: https://github.com/nzcv
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
workflow.yml@91de514a5d01a59aca9d8c124409d1fbdefab234 -
Trigger Event:
push
-
Statement type:
File details
Details for the file idevice-0.30.2-py3-none-any.whl.
File metadata
- Download URL: idevice-0.30.2-py3-none-any.whl
- Upload date:
- Size: 114.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b61bdbc5284dcf2ba2f05d0658ac6023ebadb66d4b4af50cab541b87d247a691
|
|
| MD5 |
8a3f786335e1f5dc2d59f84e16b81558
|
|
| BLAKE2b-256 |
4581aa92ee3f3d011dd323e2a7e52d138e5ee215d7dd1f01d47b72f1a76edc1f
|
Provenance
The following attestation bundles were made for idevice-0.30.2-py3-none-any.whl:
Publisher:
workflow.yml on nzcv/idevice
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
idevice-0.30.2-py3-none-any.whl -
Subject digest:
b61bdbc5284dcf2ba2f05d0658ac6023ebadb66d4b4af50cab541b87d247a691 - Sigstore transparency entry: 2532403984
- Sigstore integration time:
-
Permalink:
nzcv/idevice@91de514a5d01a59aca9d8c124409d1fbdefab234 -
Branch / Tag:
refs/heads/master - Owner: https://github.com/nzcv
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
workflow.yml@91de514a5d01a59aca9d8c124409d1fbdefab234 -
Trigger Event:
push
-
Statement type: