Skip to content

fix(cli/ios): produce App Store-ready IPA from dx bundle (closes #3817)#5591

Open
mirkobozzetto wants to merge 3 commits into
DioxusLabs:mainfrom
mirkobozzetto:fix-ios-appstore-bundle
Open

fix(cli/ios): produce App Store-ready IPA from dx bundle (closes #3817)#5591
mirkobozzetto wants to merge 3 commits into
DioxusLabs:mainfrom
mirkobozzetto:fix-ios-appstore-bundle

Conversation

@mirkobozzetto
Copy link
Copy Markdown

@mirkobozzetto mirkobozzetto commented May 26, 2026

Summary

Adds end-to-end App Store support to dx bundle --platform=ios --release. Closes #3817 (open since 2025-03-01, 6 reactions). A new opt-in --appstore flag switches signing to Apple Distribution; existing dev signing behavior is unchanged when the flag is absent.

Verified against a real app (FlowFlow, already shipping on TestFlight): xcrun altool --validate-app returns VERIFY SUCCEEDED with no errors.

Problem

dx bundle --platform=ios --release produces an .ipa Apple Transporter / altool rejects. The original report (#3817) shows Validation failed (409) Incorrect Platform, but full validation surfaces a chain of related gaps:

  • Info.plist is missing the DT* / MinimumOSVersion / CFBundlePackageType keys Apple injects from the Xcode toolchain
  • CFBundleSupportedPlatforms ships as [iPhoneOS, iPadOS] (Apple requires a single value)
  • UILaunchStoryboardName=LaunchScreen references a storyboard the build never produces (Apple rejects iPad-supporting bundles for this)
  • Codesigning auto-detects "Apple Development" only — App Store needs "Apple Distribution"
  • Provisioning profile auto-discovery picks any matching profile, including dev profiles tied to specific devices
  • Generated entitlements always include get-task-allow=true (Apple ITMS rejects on App Store upload)
  • Nested .appex extensions under PlugIns/ ship unsigned (or with stale dev certs)
  • AppIcon.xcassets is never compiled — Apple rejects bundles missing the 120x120 iPhone icon
  • PrivacyInfo.xcprivacy is never copied into the bundle (required since 2024)
  • Widget Info.plists are missing version parity, DT*, supported platforms, and required device capabilities

The bash workaround most users land on (e.g. FlowFlow's Makefile) reproduces this exact chain — this PR upstreams it.

Changes

Three commits, ordered so each compiles standalone:

  1. fix(cli/ios): modernize Info.plist defaults for App Store compliance — template-only changes. ITSAppUsesNonExemptEncryption=false, modern UILaunchScreen dict (iOS 14+), single iPhoneOS in CFBundleSupportedPlatforms, handlebars {{#if}} slots for the DT* values the next commit populates.

  2. feat(cli/ios): probe Xcode toolchain to populate Info.plist DT* metadata — adds IosDtMetadata + collect_ios_dt_metadata (xcrun / defaults / sw_vers probes). Best-effort: empty fields are dropped by the template guards, so machines without a full Xcode install still produce a .app. MinimumOSVersion reads from Dioxus.toml's [ios].deployment_target (default 15.0).

  3. feat(cli/ios): produce App Store-ready IPA via --appstore flag (closes #3817) — adds the --appstore clap flag (packages/cli/src/cli/target.rs) and wires it through BuildRequest. When set: cert pattern switches to Apple Distribution, profile auto-discovery filters out profiles with ProvisionedDevices, generated entitlements use get-task-allow=false, and should_codesign is forced on. Independently of the flag (every iOS bundle benefits), iOS pre-sign now:

    • signs every PlugIns/*.appex inside-out with its own provisioning profile,
    • compiles AppIcon.xcassets at the crate root via xcrun actool and merges the partial Info.plist back into the main one,
    • copies ios/PrivacyInfo.xcprivacy into the .app if present,
    • patches each .appex Info.plist with the DT*, version strings (CFBundleVersion + CFBundleShortVersionString parity), LSRequiresIPhoneOS, CFBundleSupportedPlatforms=[iPhoneOS], and UIRequiredDeviceCapabilities=[arm64] the main bundle carries.

Default behavior (no flag, no widget, no xcassets, no PrivacyInfo) is identical to today's CLI — every new code path is gated behind feature detection or the explicit flag.

Proof

End-to-end validation against FlowFlow, using xcrun altool --validate-app against Apple's server-side validator:

$ xcrun altool --validate-app \
  -f target/dx/flowflow/bundle/ios/ipa/Flowflow_0.1.0_aarch64.ipa \
  -t ios -u <apple-id> -p <app-spec-password> --output-format json
...
==========================================
VERIFY SUCCEEDED with no errors
==========================================
{
  "success-message": "No errors validating archive at '.../Flowflow_0.1.0_aarch64.ipa'",
  "os-version": "Version 26.5 (Build 25F71)",
  "tool-version": "26.30.4 (173004)"
}

The bundle Apple validated contains a Live Activity widget extension (recording_widget.appex), background audio mode, and PrivacyInfo.xcprivacy — i.e. all the non-trivial App Store features the PR touches.

Out of scope

These keep the PR focused; happy to follow up:

  • CFBundleVersion auto-bump strategy (compile-time / Cargo.toml / external counter)
  • Macros / config for AppIcon catalog generation from a single PNG
  • TestFlight-only signing variant (e.g. Ad-Hoc profiles)

Test plan

  • cargo build -p dioxus-cli clean
  • cargo fmt -p dioxus-cli --check clean
  • cargo clippy -p dioxus-cli --no-deps no findings
  • dx bundle --platform=ios --release without --appstore: behavior unchanged (dev signing, no Apple Distribution lookup, get-task-allow stays true)
  • dx bundle --platform=ios --release --appstore: produces IPA Apple altool accepts (see Proof)
  • App with no widget / no xcassets / no PrivacyInfo: all new code paths no-op cleanly

Adds the keys Apple Transporter / altool validation has required since
iOS 14 / 2024 App Store rules:

- ITSAppUsesNonExemptEncryption=false default (skips per-upload export
  compliance prompt; apps using non-exempt crypto can override via
  Dioxus.toml `[ios.plist]`).
- Modern `UILaunchScreen` dict replaces `UILaunchStoryboardName`. Apple
  rejects iPad-supporting bundles that reference a missing storyboard;
  the dict requires no storyboard file (iOS 14+).
- `CFBundleSupportedPlatforms` reduced to a single `iPhoneOS` value.
  Apple rejects `[iPhoneOS, iPadOS]` — iPadOS is not a valid platform
  identifier here, the binary is iPhoneOS regardless of device family.
- Placeholders for `DT*` / `MinimumOSVersion` / `CFBundlePackageType`
  metadata wired into the template via handlebars guards; the Rust side
  populates them in the next commit.

Part of the fix for DioxusLabs#3817.
Adds a small `IosDtMetadata` struct and `collect_ios_dt_metadata` helper
that probes the local SDK / Xcode install (`xcrun --sdk iphoneos
--show-sdk-version`, `defaults read .../DTXcode`, `sw_vers
-buildVersion`) and feeds the values into the handlebars iOS Info.plist
template.

Apple Transporter / altool reject App Store IPAs that omit
`DTPlatformName`, `DTPlatformVersion`, `DTSDKName`, `DTSDKBuild`,
`DTXcode`, `DTXcodeBuild`, `DTCompiler`, `DTPlatformBuild`,
`BuildMachineOSBuild`, `MinimumOSVersion`, or `CFBundlePackageType` —
the same set Xcode injects when it builds an iOS app.

Each probe is best-effort: a failure leaves the field empty and the
template's `{{#if}}` guards drop the key, so non-developer machines
still produce a valid `.app` (just not one App Store will accept).
`MinimumOSVersion` is sourced from `Dioxus.toml`'s
`[ios].deployment_target` (defaults to `15.0`).

Part of the fix for DioxusLabs#3817.
@mirkobozzetto mirkobozzetto requested a review from a team as a code owner May 26, 2026 20:11
…DioxusLabs#3817)

Adds an opt-in `--appstore` flag to `dx bundle --platform=ios` that
completes the chain Apple App Store / Transporter / altool require for
upload. Default behavior (dev signing) is unchanged.

When `--appstore` is set:

- `auto_provision_signing_name` looks for an "Apple Distribution"
  identity instead of "Apple Development".
- `auto_provision_entitlements` filters provisioning profile candidates
  to those without a `ProvisionedDevices` key (= App Store distribution
  profiles, not dev / ad-hoc).
- Generated entitlements use `get-task-allow=false` (Apple ITMS rejects
  App Store uploads where the key is `true`).
- `should_codesign` is forced on so `dx bundle` produces a signed `.app`
  even without an explicit `--codesign` flag.

Regardless of `--appstore`, iOS bundles are now App Store-shaped:

- Nested `.appex` extensions under `PlugIns/` are signed inside-out with
  their own provisioning profiles (matched by each extension's
  CFBundleIdentifier). Apple rejects bundles whose nested extensions
  ship unsigned or with stale dev certs.
- `AppIcon.xcassets` at the crate root is compiled via `xcrun actool`
  and the resulting CFBundleIcons keys are merged into the main
  Info.plist. Apple rejects iOS bundles missing the 120x120 icon.
- `ios/PrivacyInfo.xcprivacy`, if present at the crate root, is copied
  into the bundle. Apple has required PrivacyInfo on App Store
  submissions since 2024.
- Widget / extension Info.plists are patched with the same DT*
  metadata, version strings (CFBundleVersion + CFBundleShortVersionString
  parity), `LSRequiresIPhoneOS`, `CFBundleSupportedPlatforms`, and
  `UIRequiredDeviceCapabilities` the main bundle carries.

End-to-end verified against a real iOS app (FlowFlow) using
`xcrun altool --validate-app -f <ipa> -t ios`: Apple responds
`VERIFY SUCCEEDED with no errors`.

All bundle-content modifications happen before any `codesign` call so
the signature covers the final bundle contents.

Closes DioxusLabs#3817.
@mirkobozzetto mirkobozzetto force-pushed the fix-ios-appstore-bundle branch from 96952df to 119125b Compare May 26, 2026 22:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Deploying to the iOS app store

1 participant