Thanks for helping! Issues and pull requests are welcome in English or Chinese (欢迎用中文提 issue 和 PR).
Most of the code comments, design documents and commit messages in this repository are written in Chinese. Follow the language of the code you are editing; English is fine for new files and for commit messages.
- Bugs: include the iPhone or iPad model, iOS version, how you installed it (install.sh, Homebrew, .deb or source; Linux or macOS), and the output of
iphone-control doctor, with private data removed. The issue template asks for these. - Larger changes: open an issue first. The key design decisions and their reasons are in docs/decisions.md; the regression cases every change must keep passing are in docs/dev/regression-cases.md.
- Tests: every pull request must pass the tests in Required tests; read it before you start, since the device part needs a phone.
- Security problems: don't open an issue — see SECURITY.md.
You need Rust 1.88 or later (CI pins 1.99.1) and a C compiler. Everything except the device tests runs offline:
# --all-features also builds the debug-cmds feature, as CI does
cargo test --workspace --all-features --locked
# Linux/macOS PTY regression; temporary settings only, no daemon or device actions
python3 tests/terminal.py --binary target/debug/iphone-control --test-dir target/debug/deps
cargo clippy --workspace --all-targets --all-features --locked -- -D warnings
# vendor/ is upstream code and isn't formatted; check only this repository's packages:
for p in ictl-l10n ictl-webinspector ictl-device ictl-wda ictl-sign ictl-unlock ictl-daemon ictl-mcp ictl-protocol iphone-control; do cargo fmt -p $p -- --check; doneOr, without Rust installed:
docker run --rm -v "$PWD":/src -w /src rust:1-trixie sh -c \
'rustup component add clippy && cargo test --workspace --all-features --locked && cargo clippy --workspace --all-targets --all-features --locked -- -D warnings'Behind a firewall add --network host -e HTTP_PROXY -e HTTPS_PROXY (idevice is a git dependency). cargo test -p <one crate> can fail to compile plume_store because of feature unification; test the whole workspace instead.
The pure logic — element-tree parsing, occlusion, the auto-unlock rules (against a fake WebDriverAgent), the daemon state machine, MCP argument handling — is covered by unit tests. Real iOS 27 element trees used as fixtures are in crates/ictl-wda/tests/fixtures.
Anything that touches the device, timing or WebDriverAgent behaviour needs a real phone. With the daemon running:
python3 -m venv .venv && .venv/bin/pip install -r tests/device/requirements.txt
export MCP_CMD="$PWD/target/release/iphone-control mcp" # your build; the daemon must be the same build
.venv/bin/python tests/device/run.py # default steps: harmless (Home, Clock tabs, Spotlight search)
.venv/bin/python tests/device/run.py '[["iphone_launch_app", {"app": "设置"}], ["iphone_screenshot", {}]]'run.py prints every result with its wall time, ends with a line counting calls and failures, saves screenshots to ./shots and exits with 1 if any call failed. MCP_CMD defaults to iphone-control mcp on your PATH. To test your branch, run its daemon too (stop the installed service with iphone-control service stop, then start target/release/iphone-control daemon in another terminal; service start afterwards). What a pull request has to run on the device is in Required tests.
AI tasks (anything in crates/ictl-daemon/src/tasks.rs, task, the viewer's task API, the MCP server's task handling) have their own scenarios in tests/device/tasks.py, each check a PASS / FAIL line:
export ICTL_BIN="$PWD/target/release/iphone-control" # your build (the daemon too)
.venv/bin/python tests/device/tasks.py # every scenario but restart
.venv/bin/python tests/device/tasks.py pause confirm # only theseWith MCP_URL (the viewer's /mcp) the agents connect over HTTP instead of stdio, and ICTL_VIEWER_TOKEN is the viewer's login token (iphone-control viewer token); ICTL_VIEWER (the viewer's address) adds the checks through its API, and the remote scenario, which plays a platform on another machine, needs all three. MCP_CA trusts the viewer's self-signed certificate for an https:// address. The scenarios are harmless (Home, Settings, Clock, screenshots) and stop any task they leave behind.
On macOS, the full checklist (setup, debug checks, coexistence with Finder, reconnect) is in docs/dev/macos-verification.md.
Notes:
- Wait until
iphone-control statussays 就绪 (ready) before calling tools after a restart. Restarting the daemon several times in quick succession can make the phone ask for the passcode to enable UI automation again. - Never run two daemons, or the hidden
iphone-control debug …diagnostics, against the same phone at once: only one usbmuxd and one XCTest session can own it.debugis not compiled by default; build it withcargo build -p iphone-control --features debug-cmdsorcargo install --path apps/iphone-control --locked --features debug-cmds. - Don't automate anything that sends messages, pays, deletes data or changes account or privacy settings in tests.
iphone-control wda-bridge --port 18100(stop it afterwards — WebDriverAgent has no authentication).-
curl -s "127.0.0.1:18100/source?format=xml&excluded_attributes=visible,accessible,focused,enabled,selected,index,traits,placeholderValue,hittable,customActions,nativeFrame" \ | python3 -c 'import sys, json; print(json.load(sys.stdin)["value"])' > fixture.xml
- Scrub it: names, phone numbers, email addresses, locations, notification contents, the device name. Check with
grep -o 'label="[^"]*"' fixture.xml | sort -u.
- Pin exact versions (
=x.y.z) and keepCargo.lockcommitted; upgrade dependencies in separate commits (D9). - Don't break the MCP interface: tool names, parameters and default outputs stay compatible; add optional parameters instead (D10).
- Vendored code (
vendor/) keeps its license. Record any change in THIRD_PARTY.md; changed MPL-2.0 files stay MPL-2.0. - Never commit
data/,~/.config/iphone-control/envor other settings files with secrets, certificates, provisioning profiles, pairing records or anything Apple ships (Developer Disk Images, frameworks). - Comments explain why, especially device quirks; put measured numbers next to timeouts and thresholds.
- Errors: the daemon returns
crate::error::Result(ictl_daemon::error::Error): WDA's, the device's and unlocking's errors pass through?as they are, the daemon's own messages areError::Msg; small pure helpers may keepResult<_, String>. The caller gets the text in its language, so word messages for it. The CLI usesanyhow(with context for people reading it), the control page's JSON carries amessagestring. New facts a client acts on go into a structured field (Outcome,Data,retry_as_gesture), never into wording it has to match. - Environment variables are listed in one place,
crates/ictl-protocol/src/env.rs: add a new one there (anIPHONE_CONTROL_…name, with its kind) and read it through the constant. Its tests fail when code uses a name that isn't there, or when a setting is missing from the configuration page (both languages) orpackaging/env.example. Don't rename existing ones: they are in people's settings files.
Every pull request must pass the tests below, and its description must show it: the pull request template has a section for each. A pull request without them is asked for them before review. The rules apply to maintainers too. 每个 PR 都必须通过下面的测试,并在描述里写明结果。
Every bug fix and every new feature must be verified on a real device, with the results attached to the pull request (part 4). Without them it is not merged. 所有 bug 修复和新功能都必须经过真机测试验证,并在 PR 里附上测试结果,否则不予合并。
cargo test, clippy with-D warningsandcargo fmt --check(commands above) pass on your machine. CI runs them on Linux and macOS, plus the MSRV check, gitleaks and the package checks; a pull request with a failing check isn't merged.- New behaviour and every bug fix come with a unit test when the logic can run without a phone (parsing, decisions, state, arguments, protocol). For a fix, the test fails without it. Name the tests in the pull request, or say why there can't be one.
- Don't delete,
#[ignore]or loosen an existing test to make a change pass. If a test is wrong, fix it and explain why in the pull request. - Tests marked
#[ignore]are run by hand when their area changes, and named in the pull request:slow_page_stays_close_to_live(apps/iphone-control/src/video/, needs ffmpeg) andmeasure_recording(video/vt.rs, macOS only). Run one withcargo test --workspace --all-features --locked -- --ignored --nocapture <name>.
Needed when the change touches code that talks to a phone or changes what it does or how long it takes: anything under crates/ and apps/ except pure text changes (comments, translations that don't change meaning). Not needed for changes limited to documentation, the website, CI, packaging or other tests; say so in the template. A bug fix or a feature always needs part 4, wherever the change is.
- Default steps: build your branch, run its daemon and
tests/device/run.pywith the default steps on a real iPhone or iPad (Testing on a real phone). Every call succeeds and the exit code is 0. - No slower, no less stable: on the same device, run the default steps once more on
main(or the latest release), and paste both outputs. A step that became clearly slower (more than about 20 %, or by more than 0.5 s) needs a reason. A change that could affect long-running behaviour (WDA start-up and recovery, reconnecting, unlocking, the live picture, timers) also needs a longer run: repeat the default steps for at least an hour, e.g.for i in $(seq 12); do .venv/bin/python tests/device/run.py || break; sleep 300; done, and report failures and how the times developed. - What you changed: also run steps that exercise the change itself (pass them as a JSON list) and paste the output.
- The right hardware: when the change concerns iPad, landscape, an iOS version or a platform, test on that. Changes to macOS-only code go through docs/dev/macos-verification.md.
- In the pull request: device model, iOS version, Linux or macOS, and the outputs (the
===lines and the last line are enough; remove private data).
docs/dev/regression-cases.md lists the problems this project has run into, with the right behaviour and how each is tested. Every pull request must keep all of them passing.
- Cases tested by unit tests are covered by part 1.
- Cases tested on a device (真机, headless Chrome, measured by hand) in the area you changed: run them again and list their numbers in the pull request. To find them, search the file for the module, tool or behaviour you changed; if none apply, say so.
- A pull request that fixes a problem not in the list adds a row: the problem, the right behaviour, and how it is tested (the unit test's name, or the device steps).
Part 2 checks that a change doesn't break what already works; this part shows that the fix or the feature itself works, on real hardware. It applies to every bug fix and every new feature, whatever part of the code it touches (daemon, MCP tools, CLI, Viewer, installer, updater, packaging). A bug fix or feature without it is not merged.
- Real device, real installation: an iPhone or iPad connected over USB to the host, running your build. Unit tests, a fake or mocked daemon, simulated device states, fixtures and headless browsers are welcome in addition, but they are not the proof. Host-side changes (CLI, installer, updater, packages) are run on a real installation on a real host, the way a user runs them, with a phone connected.
- Bug fix: before and after. Reproduce the bug on
main(or the release that has it) and show it gone with your branch, on the same device and with the same steps. If the bug can only be reproduced under conditions you can't create, say what you did instead and why. - Feature: end to end. Exercise the new behaviour the way a user or an agent would (the MCP tool call, the CLI command, the action on the Viewer page), including the error and edge cases it is meant to handle (refused, not trusted, locked, disconnected, landscape, …).
- The right hardware and conditions: the iOS version, device kind (iPhone, iPad), orientation and host OS (Linux, macOS) the change is about. If you don't have them, say which part is untested; the maintainers test it before merging, or the pull request waits.
- Attach the results in the pull request: device model, iOS version, host OS, the exact steps, and their output (tool or command output,
run.pylines, relevant daemon log lines, a screenshot or recording for anything visible). Remove private data.
- Keep them focused; describe what changed and why.
- The tests above pass and are reported in the template (Required tests). Bug fixes and features without real-device results are not merged.
- CI must pass: format, clippy with
-D warnings, tests (on Linux and macOS), the MSRV check, gitleaks and the package checks (.deb,install.sh, the Homebrew formula). - User-visible changes get a line in CHANGELOG.md and CHANGELOG.zh-CN.md under Unreleased (a Chinese line is welcome but optional for outside contributors; maintainers add it before release).
The user guide at derrors.github.io/iphone-control is a VitePress site in website/ (Chinese at the root, English under en/; keep both in step). Preview it with cd website && pnpm install && pnpm docs:dev. docs.yml builds it on pull requests and pushes to main that change website/ (dead links fail the build) and deploys it from main. Changes elsewhere, including docs/, don't trigger it.
- Move the Unreleased entries in CHANGELOG.md and CHANGELOG.zh-CN.md under
## [x.y.z] - YYYY-MM-DDand update the compare links at the bottom. Both files need the section — the release notes are built from the two (English, then Chinese) and the workflow fails if either is missing. Keep entries short: one line per user-visible change, no implementation details. - Set
versionin the rootCargo.toml([workspace.package]) and runcargo check --workspaceto updateCargo.lock. - Configure the publisher and signing keys before tagging (any newer signed stable release can be installed from any version; there is no per-release upgrade path to set). Commit, tag
vx.y.zand push the tag. The release workflow builds the Linux archives and Debian packages, creates the GitHub Release from both changelogs, then adds the Apple Silicon archive. Stable releases also publish the installer and signed platform manifests. If a tap is configured, the workflow updates its formula after signing all three archives. See the release configuration and release workflow. - Update Glama: on glama.ai, the build steps under this server's Admin → Dockerfile pin the version. Change it and rebuild.
By contributing you agree that your contributions are licensed under the MIT license, except for changes to vendored files, which stay under their original licenses.