Skip to content

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:

  1. Hard fork pymobiledevice3 under staticwire/pymobiledevice3. Track upstream via rebase or merge. Ship pip install git+... to end users.
  2. Vendor pymobiledevice3 sources into the iosmux repo, version-pinned.
  3. 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-Note on 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 a Go-Rewrite-Note for 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 am must 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.

References

  • ADR-0001 — why we depend on pymobiledevice3 in the first place.
  • ADR-0005 — why the overlay is explicitly temporary.