ADR-0003 — pymobiledevice3 changes live as a patch overlay, not a GitHub fork¶
Status¶
accepted
Context¶
Phase C of Stage 2 needed a small but intrusive change to
pymobiledevice3: an IOSMUX_SPIKE=1 env-var gate that makes
tunneld advertise a spike listener address ([::1]:IOSMUX_SPIKE_PORT)
instead of the real iPhone tunnel endpoint, so we could capture what
bytes CoreDeviceService sends once it believes it has a tunnel.
The options for maintaining this kind of change were:
- Hard fork pymobiledevice3 under
staticwire/pymobiledevice3. Track upstream via rebase or merge. Shippip install git+...to end users. - Vendor pymobiledevice3 sources into the iosmux repo, version-pinned.
- Patch overlay: pin an exact upstream version tag and apply numbered patches on top at install time, document each patch with its rationale and a "Go-Rewrite-Note" pointing at what the Go backend will replace it with.
Option 1 is the industry-default, but it has two costs specific to our situation:
- We are planning to delete all production Python over the course of Phase D. A long-lived fork creates emotional inertia that works against that plan — the fork becomes something to maintain for its own sake.
- Every rebase of the fork against upstream is a surface for regressions that are hard to attribute.
Option 2 loses upstream bug fixes automatically and makes security updates our problem. Not acceptable for a library that speaks a trust-sensitive Apple protocol.
Option 3 — the patch overlay — is cheaper in both directions:
- Each patch is a self-contained document of a single change. The
Go-Rewrite-Noteon each patch is a forward-looking promise that the patch will be replaced, not merged into a long-lived fork. - Upstream bumps are a one-field change (
UPSTREAM_VERSION) plus a patch rebase if needed. Any patch that no longer applies is a loud signal that upstream changed something semantically. - End users run a single idempotent script
(
scripts/iosmux-install-pymobiledevice3.sh) that clones the pinned upstream, applies the patches in order, and installs the result into the embedded virtualenv.
Decision¶
pymobiledevice3 local changes live as a patch overlay under
docs/patches/pymobiledevice3/. The overlay consists of:
UPSTREAM_VERSION— pins an exact upstream tag.README.md— workflow for adding, bumping, and dropping patches.PATCHES.md— source-of-truth index with aGo-Rewrite-Notefor each patch.NNNN-short-title.patch— numbered patch files in the order they must be applied.
The overlay is installed by scripts/iosmux-install-pymobiledevice3.sh
and lives until Phase D's Go backend makes Python unnecessary, at
which point the entire docs/patches/ tree is deleted.
Consequences¶
Wanted:
- Every local change to pymobiledevice3 is a first-class documented artifact instead of a drive-by fork commit.
- Upstream stays authoritative — we are not maintaining a parallel truth.
- Deleting the Python dependency is a well-defined operation: delete the overlay, delete the install script, done.
Accepted as cost:
- Slightly more complex install workflow than
pip install git+ours. Offset by the install script being idempotent and dealing with stale site-packages shadowing automatically. git ammust apply the patches cleanly against the pinned upstream. When upstream drifts, somebody has to rebase the patches. This happens rarely and is worth the friction to avoid a long-lived fork.
Evidence¶
docs/patches/pymobiledevice3/README.md— the workflow that implements this decision.scripts/iosmux-install-pymobiledevice3.sh— the idempotent installer (clone + checkout tag +git am+pip install -e).docs/patches/pymobiledevice3/0001-iosmux-spike-tunneld-endpoint-override.patch— the first and currently only patch; documents the Spike endpoint override used by the Phase C self-experiment.