181 lines
8.3 KiB
Markdown
181 lines
8.3 KiB
Markdown
# Deploying the managed iOS app
|
||
|
||
VoiceCat's supported iOS client is the .NET 10 UIKit application in
|
||
`clients/apple/VoiceCat.iOS`. The checked-in scripts build its native Opus/RNNoise
|
||
dependency, compile and sign the managed app and Swift ReplayKit extension, stage the bundle,
|
||
install it, and launch it on a paired physical device.
|
||
|
||
## Prerequisites
|
||
|
||
- macOS 27, Xcode 27, .NET SDK 10.0.401, and the matching iOS workload.
|
||
- An unlocked, trusted iPhone connected by USB or visible to Xcode over the network.
|
||
- Apple Development profiles for `me.iamtalon.voicecat` and
|
||
`me.iamtalon.voicecat.broadcast`, both with App Group `group.me.iamtalon.voicecat`.
|
||
|
||
List available devices:
|
||
|
||
```bash
|
||
clients/apple/deploy-ios-device.sh --list
|
||
```
|
||
|
||
## Build, install, and launch
|
||
|
||
Set the Apple developer team explicitly. The .NET host and Xcode-built extension use separate
|
||
build systems, and Xcode needs the team to select the extension profile.
|
||
|
||
```bash
|
||
export VOICECAT_DEVELOPMENT_TEAM=FJV8L966W4
|
||
clients/apple/build-ios-device.sh --configuration Debug
|
||
clients/apple/deploy-ios-device.sh \
|
||
--device "Talon’s iPhone" \
|
||
--configuration Debug \
|
||
--no-build
|
||
```
|
||
|
||
The verified bundle is staged at `dist/ios-managed-device/VoiceCat.iOS.app`. Omit `--no-build`
|
||
to build and deploy in one command. Add `--console` to attach the launch to device logs.
|
||
|
||
The device build first validates the checked-in managed dependency locks, then generates
|
||
`obj/packages.device.lock.json` for the single `ios-arm64` app restore. This keeps a preceding
|
||
simulator restore from making the device build fail with `NU1004`; root-level generated lock
|
||
files under `clients/apple/VoiceCat.iOS` are not inputs to the device build.
|
||
|
||
.NET's iOS linker reports `IL2104` aggregate warnings for the pinned BouncyCastle and
|
||
Google.Protobuf assemblies. The iOS project leaves those aggregate warnings visible but does not
|
||
promote them to errors. All other warnings remain subject to the repository-wide
|
||
`TreatWarningsAsErrors` policy.
|
||
|
||
Confirm that the process stayed alive:
|
||
|
||
```bash
|
||
xcrun devicectl device info processes --device "Talon’s iPhone" | rg VoiceCat
|
||
```
|
||
|
||
## Signing findings
|
||
|
||
The development team ID is `FJV8L966W4`. Do not infer it from the parenthesized suffix in an
|
||
Apple Development certificate name; that value can identify the certificate holder and need
|
||
not equal a profile's `TeamIdentifier`.
|
||
|
||
The extension wrapper uses automatic signing with installed profiles. If a profile is absent
|
||
and Xcode has a valid signed-in developer account, permit Xcode to create or download it:
|
||
|
||
```bash
|
||
export VOICECAT_ALLOW_PROVISIONING_UPDATES=1
|
||
```
|
||
|
||
Leave that variable unset when valid profiles are already installed. A stale command-line Xcode
|
||
account can otherwise make provisioning updates fail even though offline signing can succeed.
|
||
|
||
## Troubleshooting
|
||
|
||
### Stale CMake source path
|
||
|
||
An old CMake cache can report a source-directory mismatch. Move the generated directories
|
||
aside and rebuild:
|
||
|
||
```bash
|
||
mv artifacts/native-build-ios-arm64-cmake /tmp/
|
||
mv artifacts/native-build-iossimulator-arm64-cmake /tmp/
|
||
```
|
||
|
||
### Locked restore reports changed runtime identifiers
|
||
|
||
The managed libraries' lock files must include `ios-arm64` and `iossimulator-arm64`. Regenerate
|
||
them after runtime changes, then verify that dependency versions did not change:
|
||
|
||
```bash
|
||
/usr/local/share/dotnet/dotnet restore \
|
||
clients/apple/VoiceCat.iOS/VoiceCat.iOS.csproj \
|
||
-p:VoiceCatIosStatic=true \
|
||
--force-evaluate
|
||
```
|
||
|
||
The device script should not require this command merely because a simulator was built first. If
|
||
it does, confirm that it is using `obj/packages.device.lock.json` for its `ios-arm64` restore.
|
||
|
||
### Command-line signing identity appears missing
|
||
|
||
Xcode being signed in and `security find-identity -v -p codesigning` returning no identities can
|
||
be a shell sandbox or keychain-access artifact. Run the identity check from an ordinary Terminal
|
||
session before changing certificates. A successful build prints the selected Apple Development
|
||
identity, provisioning profile, bundle ID, and app ID before linking.
|
||
|
||
### Install fails with `MissingBundleVersion`
|
||
|
||
CoreDevice error 3002 with `does not have a CFBundleVersion key with a non-zero length string
|
||
value` means the ReplayKit extension was built without `CURRENT_PROJECT_VERSION`. The extension
|
||
manifest expands that placeholder, and an empty value drops `CFBundleVersion` entirely, which
|
||
installd rejects.
|
||
|
||
`build-broadcast-extension.sh` now always passes both version placeholders, defaulting to `1`
|
||
and `0.0.1` to match `ApplicationVersion` and `ApplicationDisplayVersion` in the csproj, so a
|
||
plain Debug deployment needs no release environment variables. If this reappears, confirm the
|
||
script is not making `CURRENT_PROJECT_VERSION` conditional again, and check the staged bundle:
|
||
|
||
```bash
|
||
/usr/libexec/PlistBuddy -c 'Print :CFBundleVersion' \
|
||
dist/ios-managed-device/VoiceCat.iOS.app/PlugIns/VoiceCatBroadcast.appex/Info.plist
|
||
```
|
||
|
||
### Install fails on a dropped connection
|
||
|
||
CoreDevice error 4000 with `Connection reset by peer` is a transport failure, not a bundle or
|
||
signing problem. Rerun the deployment with `--no-build`.
|
||
|
||
### Developer disk image cannot be mounted
|
||
|
||
CoreDevice errors 10003 or 12040 mean the phone locked. Unlock it, keep the display awake, and
|
||
rerun deployment with `--no-build`.
|
||
|
||
CoreDevice can also install the app successfully and then reject only the launch with error
|
||
10002 and reason `Locked`. In that case, unlock the phone and rerun the same `--no-build`
|
||
deployment; rebuilding is unnecessary. Occasional CoreDeviceService initialization timeouts while
|
||
listing devices or processes do not imply that an already-confirmed install or launch failed.
|
||
|
||
## Simulator connection and audio gate
|
||
|
||
The Debug app has an opt-in simulator smoke gate. Start a local VoiceCat server and create an
|
||
account named `smoke`, then build the `iossimulator-arm64` app. The simulator build needs an
|
||
App Group entitlement for the saved-password and screen-ring checks; sign the built app
|
||
ad hoc with `clients/apple/VoiceCat.iOS/Entitlements.plist` before installing it. A clean
|
||
build avoids stale simulator AOT modules after managed code changes.
|
||
|
||
```bash
|
||
dotnet restore clients/apple/VoiceCat.iOS/VoiceCat.iOS.csproj \
|
||
-p:RuntimeIdentifier=iossimulator-arm64 -p:VoiceCatIosStatic=true --force-evaluate
|
||
dotnet clean clients/apple/VoiceCat.iOS/VoiceCat.iOS.csproj -c Debug \
|
||
-p:RuntimeIdentifier=iossimulator-arm64
|
||
dotnet build clients/apple/VoiceCat.iOS/VoiceCat.iOS.csproj -c Debug --no-restore \
|
||
-p:RuntimeIdentifier=iossimulator-arm64
|
||
codesign --force --sign - --entitlements clients/apple/VoiceCat.iOS/Entitlements.plist \
|
||
clients/apple/VoiceCat.iOS/bin/Debug/net10.0-ios27.0/iossimulator-arm64/VoiceCat.iOS.app
|
||
xcrun simctl install booted \
|
||
clients/apple/VoiceCat.iOS/bin/Debug/net10.0-ios27.0/iossimulator-arm64/VoiceCat.iOS.app
|
||
```
|
||
|
||
Grant microphone access with `xcrun simctl privacy booted grant microphone
|
||
me.iamtalon.voicecat`. Launch with `SIMCTL_CHILD_VOICECAT_SIM_SMOKE` set to
|
||
`127.0.0.1:<port>:<smoke-account-password>` and
|
||
`SIMCTL_CHILD_VOICECAT_SIM_SMOKE_MODE=share` using `xcrun simctl launch --console-pty booted
|
||
me.iamtalon.voicecat`. The `share` mode checks that a stale ring stays unmapped, then exercises
|
||
synthetic screen audio with the microphone off and during a voice call, voice join, disconnect,
|
||
and reconnect. Look for
|
||
`VC_SIM phase=screenSharePassed` and `VC_SIM phase=passed`. Terminate the app, then launch
|
||
again with mode `resume` to verify the saved account after a process restart. The simulator
|
||
gate also accepts mode `stereo` to select the StereoMicrophone preset before login and voice join;
|
||
the simulator exposes only a mono microphone, so check stereo capture on an iPhone. It also
|
||
does not validate Bluetooth hardware, the iOS 27 ScreenCaptureKit picker, or suspension on
|
||
a physical device.
|
||
|
||
The simulator restore can rewrite `packages.ios.lock.json`; retain both checked-in iOS
|
||
runtime entries when reviewing the diff.
|
||
|
||
## Verified hardware result
|
||
|
||
On 2026-09-22, the Debug build completed with .NET 10.0.401 and Xcode 27.0. The host and
|
||
ReplayKit extension were signed under team `FJV8L966W4` with matching versions, installed on
|
||
Talon's iPhone (iPhone 16 Pro Max), and launched as `me.iamtalon.voicecat`; the process was
|
||
confirmed running on the device. Audio, background/lock behavior, Bluetooth, ReplayKit,
|
||
ScreenCaptureKit, and VoiceOver remain separate manual hardware gates.
|