Files
voice-cat/docs/ios-deploy.md
T
Talon c1763c9a5d
Build and test / test (macos-latest) (push) Canceled after 0s
Build and test / test (ubuntu-24.04) (push) Canceled after 0s
Build and test / test (windows-latest) (push) Canceled after 0s
Build and test / apple-client (push) Canceled after 0s
Activate iOS audio session before stereo input selection
2026-09-26 21:02:54 +02:00

181 lines
8.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.