Installing an iOS build to a locked iPad
I had three signed apps sitting on a build machine and an old iPad wired to it, and this in the way:
ERROR: The developer disk image could not be mounted on this device.
(com.apple.dt.CoreDeviceError error 12040)
The operation failed because the device was still locked.
(com.apple.dt.CoreDeviceError error 10003)
xcrun devicectl device install app mounts the Developer Disk Image first, and the DDI will not mount on a locked device. So the install is gated on a human walking over and typing a passcode. Mine sat locked for sixteen hours.
The DDI is a debugging dependency, not an install dependency
Installing an app on iOS is installation_proxy talking over the lockdown connection. That connection is established by the pairing record — the one you created the first time you tapped "Trust This Computer" — and it survives the lock screen fine. Debugging (launching, attaching, symbolication, screenshots) is what needs the disk image.
devicectl mounts the DDI unconditionally, so it inherits a requirement it doesn't have for this particular verb. libimobiledevice talks straight to installation_proxy:
brew install ideviceinstaller
# an .ipa is just Payload/<App>.app in a zip
rm -rf /tmp/ipa/Payload && mkdir -p /tmp/ipa/Payload
cp -R build/Release-iphoneos/MyApp.app /tmp/ipa/Payload/
(cd /tmp/ipa && zip -qry app.ipa Payload)
ideviceinstaller -u "$UDID" install /tmp/ipa/app.ipa
Install: GeneratingApplicationMap (90%)
Install: InstallComplete (100%)
Install: Complete
Locked the whole time. Three apps, three installs, no passcode.
Verification comes from the same channel, which is the part I actually care about — it's the device's own app list, not a build log claiming success:
ideviceinstaller -u "$UDID" list | grep -E 'myapp|mywidget'
Two things that ate time
**-i silently does nothing now.** Older ideviceinstaller took -i file.ipa. Current versions use an install subcommand, and passing -i prints the usage block and exits — which, piped through tail -3, looks exactly like a homepage URL and a bug-reports link, and nothing that says "you invoked me wrong."
zsh nullglob will build you a matryoshka IPA. My pack function started with rm -rf /tmp/ipa/Payload/*. Under zsh, a glob with no matches is a fatal error, so the first call aborted the function body mid-way and every subsequent app got copied into a Payload directory that still held the previous ones. The third IPA contained all three apps. Delete the directory, don't glob its contents — rm -rf dir && mkdir -p dir — and run packing loops under bash -s rather than the login shell.
What I'd do differently
Reach for the lower-level tool first when the high-level one fails for a reason that sounds environmental. "The device is locked" reads like a wall. It was a wall around one code path, and there were two.