Fix iOS app icon and extension bundle version validation
Declare XSAppIconAssets in the app manifest so actool receives --app-icon and the bundle carries CFBundleIconName, and always pass both Xcode version placeholders to the ReplayKit extension so an empty build number cannot drop CFBundleVersion and fail installd.
This commit is contained in:
@@ -3,6 +3,7 @@
|
|||||||
<plist version="1.0"><dict>
|
<plist version="1.0"><dict>
|
||||||
<key>CFBundleDisplayName</key><string>VoiceCat</string>
|
<key>CFBundleDisplayName</key><string>VoiceCat</string>
|
||||||
<key>CFBundleIdentifier</key><string>me.iamtalon.voicecat</string>
|
<key>CFBundleIdentifier</key><string>me.iamtalon.voicecat</string>
|
||||||
|
<key>XSAppIconAssets</key><string>Assets.xcassets/AppIcon.appiconset</string>
|
||||||
<key>LSRequiresIPhoneOS</key><true/>
|
<key>LSRequiresIPhoneOS</key><true/>
|
||||||
<key>NSMicrophoneUsageDescription</key><string>VoiceCat needs microphone access to transmit your voice in channels.</string>
|
<key>NSMicrophoneUsageDescription</key><string>VoiceCat needs microphone access to transmit your voice in channels.</string>
|
||||||
<key>UIBackgroundModes</key><array><string>audio</string><string>screen-capture</string></array>
|
<key>UIBackgroundModes</key><array><string>audio</string><string>screen-capture</string></array>
|
||||||
|
|||||||
@@ -10,10 +10,14 @@ project="$script_dir/../../../native/apple/broadcast/VoiceCatBroadcast.xcodeproj
|
|||||||
|
|
||||||
mkdir -p "$output"
|
mkdir -p "$output"
|
||||||
signing=()
|
signing=()
|
||||||
versioning=(MARKETING_VERSION="${VOICECAT_DISPLAY_VERSION:-0.0.1}")
|
# Both keys must always be set. The extension manifest expands these placeholders, and an
|
||||||
if [[ -n "${VOICECAT_BUILD_NUMBER:-}" ]]; then
|
# empty CURRENT_PROJECT_VERSION drops CFBundleVersion, which installd rejects. The defaults
|
||||||
versioning+=(CURRENT_PROJECT_VERSION="$VOICECAT_BUILD_NUMBER")
|
# match ApplicationDisplayVersion/ApplicationVersion in VoiceCat.iOS.csproj so that the host
|
||||||
fi
|
# 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
|
if [[ "$sdk" == "iphonesimulator" ]]; then
|
||||||
signing+=(CODE_SIGNING_ALLOWED=NO)
|
signing+=(CODE_SIGNING_ALLOWED=NO)
|
||||||
elif [[ -n "${VOICECAT_BROADCAST_CODESIGN_KEY:-}" && -n "${VOICECAT_BROADCAST_CODESIGN_PROVISION:-}" ]]; then
|
elif [[ -n "${VOICECAT_BROADCAST_CODESIGN_KEY:-}" && -n "${VOICECAT_BROADCAST_CODESIGN_PROVISION:-}" ]]; then
|
||||||
|
|||||||
@@ -162,8 +162,10 @@ Before packaging, confirm:
|
|||||||
- both entitlements contain the shared App Group;
|
- both entitlements contain the shared App Group;
|
||||||
- both have `get-task-allow=false` and `beta-reports-active=true`;
|
- both have `get-task-allow=false` and `beta-reports-active=true`;
|
||||||
- the bundle identifiers are the stable identifiers above;
|
- the bundle identifiers are the stable identifiers above;
|
||||||
- both version pairs match; and
|
- both version pairs match;
|
||||||
- both executables contain `arm64`.
|
- 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
|
Also decode each embedded profile and verify its name, UUID, application identifier, and
|
||||||
`get-task-allow` value:
|
`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
|
account. Manual App Store Connect profiles avoid that dependency and make the two-target signing
|
||||||
inputs explicit.
|
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
|
||||||
|
<key>XSAppIconAssets</key><string>Assets.xcassets/AppIcon.appiconset</string>
|
||||||
|
```
|
||||||
|
|
||||||
|
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/<config>/<tfm>/<rid>/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
|
### App and extension versions differ
|
||||||
|
|
||||||
Apple validates nested bundle metadata. Confirm the host does not hard-code version keys, the
|
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
|
## 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
|
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
|
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
|
`get-task-allow=false`. The host carries `CFBundleIconName` with the 120x120 and 152x152 icons.
|
||||||
validation remains a separate gate.
|
The resulting IPA was packaged locally; Apple server-side upload validation remains a separate
|
||||||
|
gate.
|
||||||
|
|||||||
+27
-5
@@ -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
|
session before changing certificates. A successful build prints the selected Apple Development
|
||||||
identity, provisioning profile, bundle ID, and app ID before linking.
|
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
|
### Developer disk image cannot be mounted
|
||||||
|
|
||||||
CoreDevice errors 10003 or 12040 mean the phone locked. Unlock it, keep the display awake, and
|
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
|
## Verified hardware result
|
||||||
|
|
||||||
On 2026-09-21, the Debug build completed with .NET 10.0.401 and Xcode 27.0. The host and
|
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`, installed wirelessly on Talon's iPhone,
|
ReplayKit extension were signed under team `FJV8L966W4` with matching versions, installed on
|
||||||
and launched as `me.iamtalon.voicecat`; the running UI was confirmed on the device. Audio,
|
Talon's iPhone (iPhone 16 Pro Max), and launched as `me.iamtalon.voicecat`; the process was
|
||||||
background/lock behavior, Bluetooth, ReplayKit, ScreenCaptureKit, and VoiceOver remain separate
|
confirmed running on the device. Audio, background/lock behavior, Bluetooth, ReplayKit,
|
||||||
manual hardware gates.
|
ScreenCaptureKit, and VoiceOver remain separate manual hardware gates.
|
||||||
|
|||||||
@@ -66,8 +66,11 @@ public class PublishServerScriptTests
|
|||||||
Assert.Contains("group.me.iamtalon.voicecat", entitlements);
|
Assert.Contains("group.me.iamtalon.voicecat", entitlements);
|
||||||
Assert.Contains("CODE_SIGN_STYLE=Manual", script);
|
Assert.Contains("CODE_SIGN_STYLE=Manual", script);
|
||||||
Assert.Contains("PROVISIONING_PROFILE_SPECIFIER=\"$VOICECAT_BROADCAST_CODESIGN_PROVISION\"", 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.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(
|
string deviceScript = await File.ReadAllTextAsync(Path.Combine(
|
||||||
root, "clients", "apple", "build-ios-device.sh"));
|
root, "clients", "apple", "build-ios-device.sh"));
|
||||||
@@ -81,6 +84,24 @@ public class PublishServerScriptTests
|
|||||||
Assert.Contains("<string>$(CURRENT_PROJECT_VERSION)</string>", extensionManifest);
|
Assert.Contains("<string>$(CURRENT_PROJECT_VERSION)</string>", 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("<key>XSAppIconAssets</key>", manifest);
|
||||||
|
Assert.Contains("<string>Assets.xcassets/AppIcon.appiconset</string>", manifest);
|
||||||
|
Assert.Contains("AppIcon-1024.png", iconSet);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task IosScreenSharingDeclaresBackgroundCaptureAndKeepsReplayKitFallback()
|
public async Task IosScreenSharingDeclaresBackgroundCaptureAndKeepsReplayKitFallback()
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user