diff --git a/clients/apple/VoiceCat.iOS/Info.plist b/clients/apple/VoiceCat.iOS/Info.plist index 52017fd..6979a59 100644 --- a/clients/apple/VoiceCat.iOS/Info.plist +++ b/clients/apple/VoiceCat.iOS/Info.plist @@ -3,6 +3,7 @@ CFBundleDisplayNameVoiceCat CFBundleIdentifierme.iamtalon.voicecat + XSAppIconAssetsAssets.xcassets/AppIcon.appiconset LSRequiresIPhoneOS NSMicrophoneUsageDescriptionVoiceCat needs microphone access to transmit your voice in channels. UIBackgroundModesaudioscreen-capture diff --git a/clients/apple/VoiceCat.iOS/build-broadcast-extension.sh b/clients/apple/VoiceCat.iOS/build-broadcast-extension.sh index b75f564..0e48805 100755 --- a/clients/apple/VoiceCat.iOS/build-broadcast-extension.sh +++ b/clients/apple/VoiceCat.iOS/build-broadcast-extension.sh @@ -10,10 +10,14 @@ project="$script_dir/../../../native/apple/broadcast/VoiceCatBroadcast.xcodeproj mkdir -p "$output" signing=() -versioning=(MARKETING_VERSION="${VOICECAT_DISPLAY_VERSION:-0.0.1}") -if [[ -n "${VOICECAT_BUILD_NUMBER:-}" ]]; then - versioning+=(CURRENT_PROJECT_VERSION="$VOICECAT_BUILD_NUMBER") -fi +# Both keys must always be set. The extension manifest expands these placeholders, and an +# empty CURRENT_PROJECT_VERSION drops CFBundleVersion, which installd rejects. The defaults +# match ApplicationDisplayVersion/ApplicationVersion in VoiceCat.iOS.csproj so that the host +# and extension versions agree when no release build number is supplied. +versioning=( + MARKETING_VERSION="${VOICECAT_DISPLAY_VERSION:-0.0.1}" + CURRENT_PROJECT_VERSION="${VOICECAT_BUILD_NUMBER:-1}" +) if [[ "$sdk" == "iphonesimulator" ]]; then signing+=(CODE_SIGNING_ALLOWED=NO) elif [[ -n "${VOICECAT_BROADCAST_CODESIGN_KEY:-}" && -n "${VOICECAT_BROADCAST_CODESIGN_PROVISION:-}" ]]; then diff --git a/docs/apple-ios-release.md b/docs/apple-ios-release.md index ce25c4a..698d962 100644 --- a/docs/apple-ios-release.md +++ b/docs/apple-ios-release.md @@ -162,8 +162,10 @@ Before packaging, confirm: - both entitlements contain the shared App Group; - both have `get-task-allow=false` and `beta-reports-active=true`; - the bundle identifiers are the stable identifiers above; -- both version pairs match; and -- both executables contain `arm64`. +- both version pairs match; +- both executables contain `arm64`; and +- the host `Info.plist` contains `CFBundleIconName` and the bundle contains both + `AppIcon60x60@2x.png` (120x120) and `AppIcon76x76@2x~ipad.png` (152x152). Also decode each embedded profile and verify its name, UUID, application identifier, and `get-task-allow` value: @@ -232,6 +234,34 @@ Xcode command-line builds can report a missing `Xcode-Token` even when the GUI d account. Manual App Store Connect profiles avoid that dependency and make the two-target signing inputs explicit. +### Missing app icon or `CFBundleIconName` + +App Store Connect rejects uploads with errors 90022, 90023, and 90713 when the host bundle has +no `CFBundleIconName`. Local `codesign` cannot catch this; only upload validation does. + +The asset catalog alone is not enough. `Info.plist` must name it: + +```xml +XSAppIconAssetsAssets.xcassets/AppIcon.appiconset +``` + +This is an **app-manifest key**, not an MSBuild property. The `ReadAppManifest` task reads +`XSAppIconAssets` out of `Info.plist` and passes `--app-icon` to `actool`; setting a csproj +property of the same name has no effect. The build strips the key from the shipped manifest. + +Without it, `actool` still compiles the catalog but emits an empty +`obj////actool/partial-info.plist`, so no icon keys reach the app. Verify the +build inputs rather than the bundle, since stale intermediates can leave icon PNGs from an +earlier build in place while the manifest has no icon keys: + +```bash +cat clients/apple/VoiceCat.iOS/obj/Release/net10.0-ios27.0/ios-arm64/actool/partial-info.plist +``` + +A correct build writes `CFBundleIcons` and `CFBundleIcons~ipad` there. A single 1024x1024 +universal entry in `AppIcon.appiconset` is sufficient; `actool` derives the 120x120 and 152x152 +variants. + ### App and extension versions differ Apple validates nested bundle metadata. Confirm the host does not hard-code version keys, the @@ -240,8 +270,9 @@ clean build regenerated the app manifests. ## Last verified release build -On 2026-09-22, version `0.0.1`, build `2026092202` was built with .NET 10.0.401 and Xcode 27.0. +On 2026-09-22, version `0.0.1`, build `2026092203` was built with .NET 10.0.401 and Xcode 27.0. The host and ReplayKit extension passed strict nested-signature validation with App Store Connect profiles, matching distribution identities, matching versions, the shared App Group, and -`get-task-allow=false`. The resulting IPA was packaged locally; Apple server-side upload -validation remains a separate gate. +`get-task-allow=false`. The host carries `CFBundleIconName` with the 120x120 and 152x152 icons. +The resulting IPA was packaged locally; Apple server-side upload validation remains a separate +gate. diff --git a/docs/ios-deploy.md b/docs/ios-deploy.md index 28c951f..31e42f7 100644 --- a/docs/ios-deploy.md +++ b/docs/ios-deploy.md @@ -101,6 +101,28 @@ be a shell sandbox or keychain-access artifact. Run the identity check from an o 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 @@ -113,8 +135,8 @@ listing devices or processes do not imply that an already-confirmed install or l ## Verified hardware result -On 2026-09-21, the Debug build completed with .NET 10.0.401 and Xcode 27.0. The host and -ReplayKit extension were signed under team `FJV8L966W4`, installed wirelessly on Talon's iPhone, -and launched as `me.iamtalon.voicecat`; the running UI was confirmed on the device. Audio, -background/lock behavior, Bluetooth, ReplayKit, ScreenCaptureKit, and VoiceOver remain separate -manual hardware gates. +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. diff --git a/tests/VoiceCat.Tests/PublishServerScriptTests.cs b/tests/VoiceCat.Tests/PublishServerScriptTests.cs index 5746e97..ad28248 100644 --- a/tests/VoiceCat.Tests/PublishServerScriptTests.cs +++ b/tests/VoiceCat.Tests/PublishServerScriptTests.cs @@ -66,8 +66,11 @@ public class PublishServerScriptTests Assert.Contains("group.me.iamtalon.voicecat", entitlements); Assert.Contains("CODE_SIGN_STYLE=Manual", script); Assert.Contains("PROVISIONING_PROFILE_SPECIFIER=\"$VOICECAT_BROADCAST_CODESIGN_PROVISION\"", script); - Assert.Contains("CURRENT_PROJECT_VERSION=\"$VOICECAT_BUILD_NUMBER\"", script); + // Both version placeholders must always be passed to xcodebuild. A conditional + // CURRENT_PROJECT_VERSION expands to an empty CFBundleVersion, which installd rejects. + Assert.Contains("CURRENT_PROJECT_VERSION=\"${VOICECAT_BUILD_NUMBER:-1}\"", script); Assert.Contains("MARKETING_VERSION=\"${VOICECAT_DISPLAY_VERSION:-0.0.1}\"", script); + Assert.DoesNotContain("if [[ -n \"${VOICECAT_BUILD_NUMBER:-}\" ]]; then", script); string deviceScript = await File.ReadAllTextAsync(Path.Combine( root, "clients", "apple", "build-ios-device.sh")); @@ -81,6 +84,24 @@ public class PublishServerScriptTests Assert.Contains("$(CURRENT_PROJECT_VERSION)", extensionManifest); } + [Fact] + public async Task IosHostDeclaresItsAppIconAssetForAppStoreValidation() + { + string root = FindRoot(); + string manifest = await File.ReadAllTextAsync(Path.Combine( + root, "clients", "apple", "VoiceCat.iOS", "Info.plist")); + string iconSet = await File.ReadAllTextAsync(Path.Combine( + root, "clients", "apple", "VoiceCat.iOS", + "Assets.xcassets", "AppIcon.appiconset", "Contents.json")); + + // XSAppIconAssets is an app-manifest key, not an MSBuild property. Without it actool + // never receives --app-icon, so the bundle ships without CFBundleIconName and App Store + // Connect rejects the upload with errors 90022, 90023, and 90713. + Assert.Contains("XSAppIconAssets", manifest); + Assert.Contains("Assets.xcassets/AppIcon.appiconset", manifest); + Assert.Contains("AppIcon-1024.png", iconSet); + } + [Fact] public async Task IosScreenSharingDeclaresBackgroundCaptureAndKeepsReplayKitFallback() {