vphone-intg-update — progress against the migration plan
Historical snapshot (2026-09-23). This ledger records work in progress before the JB-only native pipeline was completed in PR #486. Its percentages, script names and remaining-work list are not the current project status. Start with the project README and research index for current entry points.
2026-09-23. Branch off
qof-update-26-fall@6d5ce7d.Plan:
~/Desktop/vphone-cli-migration-plan.md. Its phases are P0 → P4. This file is the ledger, because/TODO.mdis not part of this repo's workflow.Recounted against the tree on 2026-09-23, after P1 and P2 landed. The previous revision of this file still said P1 was not started and D1 stood at 7.5%; both were stale by several commits. Every number below was measured with a command, and the commands are in "How these were counted" at the end so the next person can disagree with the measurement rather than the prose.
Updated at
a908f81, after D2 and D3 landed. The D2/D3 rows below are rewritten; the D4 row is not, and the detail is inruntime_dependency_tiers.md, which is the handover for that work — what the tiers are, what each removed program was replaced by, what was verified against the real tool, and what is left open.
Where the four delivery lines stand
D4 going up is not an accounting artifact. cfw-kit/ (1,036 lines) and
Scripts/check_aux.sh (327) are both new on this branch; everything else nets
to −85. The 5,098 lines of Python that left scripts/patchers/ did not take any
shell with them, because the installers called into that Python and now call
into vphone-cli cfw instead — same scripts, different callee. cfw_install*.sh
is 2,396 lines against 2,410 at the branch base.
Phase by phase
Research/Restore/native_restore_architecture.md records what P2 decided and why, including
the behaviour table row by row.
That AMFI row went round in a circle in one day, so it is worth restating where
it landed. The amfidont scripts came out and vphone-letmein went in; then
vphone-letmein was measured killing amfid outright on a host where
vm.cs_system_enforcement reads 1, and came out again. The project ships no
bypass: vphone-cli probes with vphone-vm --help, and on a refusal prints what
to run. amfidont is what it names, installed by the user into their own Python.
This costs D1 nothing — it is not a dependency of this repo, and nothing here
imports or invokes it. research/host/host_binary_split.md has the measurement.
D1, counted
The plan's corrected inventory (§1.2.1 plus the file it missed) is 6,070
lines: 5,881 standalone across 31 .py files at the branch base, plus 189
embedded in shell heredocs. All of it is gone.
Two corrections to the earlier ledger's arithmetic:
scripts/patchers/was 5,098 lines at the branch base but 6,539 when it was deleted, still in 26 files. The set churned while the port ran —cfw_patch_hv_vmm_rootfs.pyleft,cfw_records.pyarrived, andcfw_patch_watchdogd.pyroughly doubled. The plan's 5,098 is the right denominator for D1 because that is what D1 was scoped against; 6,539 is the right number for "how much Python actually had to be ported".- The anti-fallback trap is closed.
_resolve_python3()is gone from all six scripts — the plan's §1.2.3 warning was that deleting the environment would make every one of them silently fall back to the systempython3, and that cannot happen now because there is no call site left to fall back.
D2, counted
Superseded at
a908f81. The counts below were taken when the gate had ONE flat list applied to the whole repository — which is exactly the thing that made them hard to read:xcrunin a build script andxcrunin something the.appships counted the same, so "38 registered items" did not say whether the product was any closer to standing on its own. Kept because the shape of the old list is what the tier split was a response to.Now:
zsh Scripts/check_aux.shreports gates 0, 1, 1b, 1c, 2 and 3 all green, with the dist tier's registered list empty. Seeruntime_dependency_tiers.md.
zsh Scripts/check_aux.sh --fast, as of the previous revision:
- Gate 1 (dependency closure) and gate 1b (relocation) both report
nothing. This is the change since the last revision of this file, which
recorded four failures, all
ldid:VPhoneSignreplaced it and the bundle stopped shipping it. The.appis now exactlyvphone-cli,vphone-vm,vphone-archive,signcert.p12andAppIcon.icns— no tools directory, no scripts, no interpreter. - Gate 2 (source scan): 38 registered items, 0 unregistered violations.
Three of the 38 are Python — the hardcoded
python3inVPhoneResources.swiftandsetup_machine.sh'spython3/python3.13lookups — and go out with P2's deletions, leaving 35: 6ipswand 1ldidhardcoded in Swift, and 28PATHlookups in shell (ldid,gtar,zstd,ipsw,aea,aria2c,xcrun,shasum,sha256sum,curl,wget,lsof). - Gate 3 was skipped here (
--fast). CI must not skip it. - Gate 4 — a machine with no Homebrew — still does not exist, and is still
the only thing that can support "it works elsewhere". The script says so
itself. The other gates are necessary and not sufficient. This is still
true at
a908f81.
ipsw and aea were out of scope for that round. They are not now: ipsw is
gone, and aea stayed because /usr/bin/aea is part of macOS — it is called
by absolute path, so there is nothing left to look up.
D4, counted
7,173 lines of tracked .sh in 27 files across scripts/ and cfw-kit/,
against 5,895 in 22 files at 6d5ce7d. The largest single files:
P2's deletions take setup_venv.sh (49) and setup_venv_linux.sh (52) plus the
provisioning blocks inside setup_tools.sh and setup_machine.sh. Of what is
left, vphone_jb_setup.sh (312) runs inside the guest, not on the host, so
the host-side figure P3/P4 are actually aimed at is 6,760 — a little less once
those provisioning blocks go.
Tests
Five test targets, by declaration count: FirmwarePatcherTests 341,
VPhoneCoreTests 168, VPhoneRestoreTests 67, VPhoneArchiveTests 39,
VPhoneSignTests 32. These are @Test / func test… declarations, not expanded
parameterized cases, and they are not a pass count — the suite was not run in
this pass because another stage held the build directory.
The 14 FirmwarePatcherTests failures previously recorded are pre-existing and
unrelated: they need ipsws/patch_refactor_input/, which is not in the repo.
VPhoneRestoreTests covers everything that runs without a device attached —
argument parsing, the restore-tree rules, the .shsh naming, and the C struct
the options turn into. What needs a phone in DFU is not covered, and the target's
stanza in Package.swift says so rather than shipping a test that only looks
like one.
Needs you, and a machine
Nothing below can be done without root or a real guest.
CanAnswered: yes. A guest booted and libirecovery enumerated its virtual DFU endpoint —vphone-vmstart a VM holding the entitlements alone?Research/Restore/virtual_dfu_probe.md.Answered, and the answer removed the tool. Withvphone-letmeinend to end.vm.cs_system_enforcement= 1 the patched__TEXTpage gets amfid killed (CODESIGNING, "Invalid Page") and the guest dies with it. Measured twice on macOS 27.0 (26A428) arm64e. What still needs a machine is the replacement instruction path: that theamfidontcommandvphone-cliprints on a refusal is correct as printed, on a host with nothing installed yet.- The four unverified rows of the P2 behaviour table — a real restore
(online and
--offline),--no-eraseupdate-in-place, and the exit-code contract on failure. Table and criteria inResearch/Restore/native_restore_architecture.md. Run these on a disposable VM: a failed restore leaves the guest in recovery. - FDR equivalence. The old bridge ran
Restore(..., ignore_fdr=False); idevicerestore's FDR handling is internal. Device enumeration cannot settle it, soResearch/Restore/virtual_dfu_probe.mdexplicitly did not. It shows up only during an actual restore, and it must not be assumed away. - Ownership restoration in
VPhoneArchive, which only happens as root, soARCHIVE_EXTRACT_OWNERhas never come into play. This is the blocker on switching the$TARcalls incfw_install*.sh, which write to a mounted guest volume as root — getting ownership wrong there produces a guest that will not boot. - Location and TouchID, which depend on TCC attributing the usage strings
to
vphone-vm. It isCFBundleExecutable, so it should — worth confirming. - Bridged networking, now validated at boot instead of at config time.
cfw flip-snapshotagainst a realDisk.img. The byte comparison against the Python passed on a synthetic fixture. This is now the only implementation —cfw-kit/run.shandcfw_install_host.shboth call it.
Next, in order
-
Run the four device rows of the P2 table. Everything else in P2 is built and unit-tested; these are what stands between "compiles" and "restores". Row 3,
--offline, is the one the plan's first draft missed entirely and the one with four separate criteria. -
Finish the archive switch-over. The host-side temp extractions and the IPSW unzip in
fw_prepare.shhave no ownership problem and can go first. The$TARcalls incfw_install*.shwait on item 5 above. Re-runvphone-archive fingerprint <gtar-output> <vphone-output>after each.The comparison the plan asks for has already been run on the real
cfw_input.tar.zstandcfw_jb_input.tar.zst: everything matches GNU tar except one directory mtime per archive, wherevphone-archiverestores the archive's recorded value and GNU tar leaves the extraction time. -
vm export/importontoVPhoneArchive, keeping gnutar,.tzstat zstd 3 and.txzat xz 9, checking compatibility both ways.Plan §3.9.0-0 says to move these to
VPhoneVM, which assumedvphone-cliimports the VM kit. It does not, deliberately, so that move would breakvphone-cli vm export. The right home isVPhoneArchive: aboveVPhoneCore, and reachable without Virtualization or AppKit.It is a real refactor, about twenty call sites in
BundleOpsTests. Those tests already pin the format contract (R11), so whoever does it gets told immediately if it is wrong. Worth doing in one go. -
P3 — the four
cfw_install*.shinto oneCFWInstaller. This is D4's main battle and the only thing that moves that line off 0%. Note that thecfw-kit/layer added on this branch is 1,036 lines that P3 also has to account for; the plan's §P3 was written against a tree where it did not exist. -
Gate 4 — a machine with no Homebrew.
Things the plan got wrong
Recorded because they were measured, not reasoned about.
ipsw/aea/ldidare called by absolute/opt/homebrewpath from Swift, not throughPATH. §1.4 counts the shell call sites. Gate 2 lists all of them.Sources/vphone.entitlementshas 7 keys, not the 4 the plan says or the 5CLAUDE.mdsaid. Two of them — location and BiometricKit — belong toDevices/, which is why they all landed onvphone-vm.- The Python inventory in §1.2.1 misses a file. It lists five blocks of
standalone
.py;tests/test_dropbear_plist.py(60 lines) is not among them. The total is 6,070, not 6,010. - P2 did not need the 73k-line vendoring the plan implies. §P2.1 reads as
though the whole libimobiledevice stack had to be carried in tree.
AppleMobileDeviceLibraryships all five of those libraries plus OpenSSL as prebuilt xcframeworks, so only libirecovery (4,629 lines) and idevicerestore (20,918, of which 1,399 are ours) are vendored. The argument is inResearch/Restore/native_restore_architecture.md. - The admission rule caught a bug the plan did not predict: signing
vphone-vmfirst sealed the bundle over its siblings in an earlier state, andcodesign -vreported "nested code is modified or invalid". The main executable is signed last now, and both build paths verify the seal. - D4 was never going to fall out of D1. The plan treats the Python removal as the hard part and the shell as cleanup. Porting 6,539 lines of Python changed the shell's line count by −14.
And one I nearly got wrong
The first measurement of --no-overwrite-dir said libarchive already leaves an
existing directory's mode alone, which would have made the flag decorative —
and it was taken from an extraction that was failing partway and applying
nothing. A measurement from a failing code path measures the failure. It is
0700 in, 0777 out when extraction actually works, and the flag matters.
How these were counted
Re-run these rather than trusting the tables: