Skip to content

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

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

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

Conversation

@mirkobozzetto

@mirkobozzetto mirkobozzetto commented May 26, 2026 •

Copy link
Copy Markdown

Summary

dx bundle --platform ios --release produces an .ipa that Apple's validator rejects (#3817). This PR fixes the bundle and adds an opt-in --appstore flag that signs for App Store distribution.

Tested on FlowFlow, an app already on TestFlight: xcrun altool --validate-app passes (output below).

What changes for every iOS bundle

The template changes apply to every iOS build, with or without --appstore:

  • Info.plist gets the keys Xcode normally adds: CFBundlePackageType, MinimumOSVersion and the DT* set, probed from the Xcode that xcode-select points at. A probe that fails leaves its key out, so a machine without full Xcode still gets a .app.
  • MinimumOSVersion comes from [ios] deployment_target, default 15.0. Widget extensions keep their own 16.0 default.
  • CFBundleSupportedPlatforms is [iPhoneOS] only. iPadOS is not a valid value there.
  • UILaunchStoryboardName=LaunchScreen pointed to a storyboard the build never produced. It is replaced by an empty UILaunchScreen dict (iOS 14+).

When the app is signed (device builds, --codesign, --appstore), dx also:

  • copies ios/PrivacyInfo.xcprivacy into the app when present,
  • gives each PlugIns/*.appex the app's version strings, the DT* keys, LSRequiresIPhoneOS, CFBundleSupportedPlatforms and UIRequiredDeviceCapabilities, and signs it with its own provisioning profile before the app,
  • passes the certificate's SHA-1 to codesign instead of its name, so two certificates with the same name (a "Developer ID Application" next to an "Apple Distribution" one, or the same identity on two Macs) don't make it ambiguous.

What --appstore changes

  • Signs with an "Apple Distribution" identity instead of "Apple Development".
  • Picks an App Store provisioning profile (no ProvisionedDevices).
  • Writes get-task-allow=false in the generated entitlements.
  • Forces codesigning.

Without the flag, signing works as before.

Not in this PR

  • App icon: Build the iOS app icon from [ios] icon or [bundle] icon #5901 builds it from [ios] icon / [bundle] icon for every iOS build. An App Store upload needs it.
  • ITSAppUsesNonExemptEncryption: an export compliance statement, so it stays with each app ([ios.plist]).
  • CFBundleVersion bump strategy, ad-hoc / TestFlight-only signing.

Proof

FlowFlow (Live Activity widget, background audio, PrivacyInfo.xcprivacy), built with this branch and #5901 merged locally for the icon:

$ dx bundle --platform ios --release --appstore
$ xcrun altool --validate-app -f FlowFlow_2.1.1_aarch64.ipa -t ios -u <apple-id> -p <app-specific-password> --output-format json
==========================================
VERIFY SUCCEEDED with no errors
==========================================
{
  "success-message" : "No errors validating archive at 'target/dx/flowflow/bundle/ios/ipa/FlowFlow_2.1.1_aarch64.ipa'.",
  "os-version" : "Version 27.0.1 (Build 26A434)",
  "tool-version" : "27.0.5 (2.1)"
}

In that IPA: DTXcodeBuild 27A266a (what Xcode itself writes), MinimumOSVersion 16.0 for the app (FlowFlow sets it), the widget's own 16.2, matching versions, signed by "Apple Distribution".

Tests

  • sync_widget_info_plists: an extension takes the app's version and skips empty DT* keys.
  • pick_provisioning_profile: with --appstore, profiles tied to devices are skipped.
  • find_signing_identity: returns the SHA-1 of the certificate with the right label.
  • cargo fmt clean, no new cargo clippy -p dioxus-cli warnings, cargo test -p dioxus-cli passes (165 tests).

@mirkobozzetto
mirkobozzetto requested a review from a team as a code owner May 26, 2026 20:11
@mirkobozzetto
mirkobozzetto force-pushed the fix-ios-appstore-bundle branch from 96952df to 119125b Compare May 26, 2026 22:29
@ealmloff
ealmloff requested a review from jkelleyrtp June 2, 2026 13:31

@ealmloff ealmloff left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems mostly reasonable from afar, but I'm not familiar enough with the current bundling process for ios to approve this

Comment thread packages/cli/assets/ios/ios.plist.hbs
@mirkobozzetto

Copy link
Copy Markdown
Author

Hey @jkelleyrtp - this is ready for a code-owner review whenever you get a chance. You mentioned in #3817 you wanted these changes, so flagging that it's up.

Status, to save you time:

Also - dioxus is genuinely a lot of fun to build with, really enjoy using it. Thanks!

@ealmloff

ealmloff commented Jun 2, 2026

Copy link
Copy Markdown
Member

The min dep check will be fixed by #5609

@mirkobozzetto
mirkobozzetto force-pushed the fix-ios-appstore-bundle branch from 119125b to 8fd6cd6 Compare June 2, 2026 18:40
@mirkobozzetto

Copy link
Copy Markdown
Author

Rebased onto main, so it now includes #5609 - the min-deps check should go green on this run. The only remaining red, Playwright | Linux (beta), is the unrelated beta-toolchain issue.

@LucaCappelletti94 LucaCappelletti94 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a maintainer, so I can only comment in the review. This is meant as a "request chages" review.

In my opinion, several things need to change before this can merge, and I tried to add inline suggestion wherever possible but I do not believe I covered all things.

There is a lot of documentation verbosity that is, in my experience, caused by LLMs not be refined. Please read through that documentation and trim it down as needed. I tried to cover a bit of it myself, but it is a lot.

The AppIcon.xcassets step should come out of this PR. It reads an undocumented catalog at the crate root and ignores [ios] icon, which the manifest documents as the iOS override of [bundle] icon. It also runs inside codesign_apple, so simulator and unsigned builds never get an icon. #5901 builds the icon from that config for every iOS build.

The template should not declare ITSAppUsesNonExemptEncryption=false for every app. That is an export compliance statement made on the developer's behalf, and it is false for apps that ship their own cryptography. Without the key App Store Connect asks at upload, and an app that qualifies can set it in [ios.plist]. Together with the other template changes, this also means a default iOS bundle does change, whatever the description says.

DTXcode and DTXcodeBuild are read from a hard-coded /Applications/Xcode.app, while the SDK keys come from the Xcode that xcode-select points at. A renamed or second Xcode install therefore yields empty or mismatched values. The deployment_target fallback is 15.0 here and 16.0 in compile_widget_extensions, so with the key unset an app and its widget declare different minimum versions.

The PR adds no tests. sync_widget_info_plists runs without Xcode and is easy to cover, and a test is attached as a suggestion. The App Store profile filter is the heart of --appstore, but it reads profiles straight from ~/Library/MobileDevice, so it would need to take the parsed profiles as input before it can be tested. I recommend the maintainers consider my issue regarding adding test coverage in CI, which I can add if they green light it.

Comment thread packages/cli/assets/ios/ios.plist.hbs Outdated
Comment thread packages/cli/src/build/apple.rs Outdated
Comment thread packages/cli/src/build/apple.rs Outdated
Comment thread packages/cli/src/build/apple.rs Outdated
Comment thread packages/cli/src/build/apple.rs Outdated
Comment thread packages/cli/assets/ios/ios.plist.hbs Outdated
Comment thread packages/cli/src/build/apple.rs Outdated
Comment on lines +86 to +97
let xcode = run(
"defaults",
&["read", "/Applications/Xcode.app/Contents/Info", "DTXcode"],
);
let xcode_build = run(
"defaults",
&[
"read",
"/Applications/Xcode.app/Contents/Info",
"DTXcodeBuild",
],
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should read DTXcode and DTXcodeBuild from the Xcode that xcode-select points at, which is the one xcrun used for the SDK keys above. With Xcode installed under another name, such as Xcode-beta.app or the versioned names xcodes creates, the probe returns nothing, and with two installs it records a different Xcode than the one that built the app.

Suggested change
let xcode = run(
"defaults",
&["read", "/Applications/Xcode.app/Contents/Info", "DTXcode"],
);
let xcode_build = run(
"defaults",
&[
"read",
"/Applications/Xcode.app/Contents/Info",
"DTXcodeBuild",
],
);
// `xcode-select -p` prints `<Xcode.app>/Contents/Developer`, the Xcode xcrun uses above
let xcode_info = Path::new(&run("xcode-select", &["-p"]))
.with_file_name("Info")
.to_string_lossy()
.into_owned();
let xcode = run("defaults", &["read", &xcode_info, "DTXcode"]);
let xcode_build = run("defaults", &["read", &xcode_info, "DTXcodeBuild"]);

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 2c83424, with one change. DTXcodeBuild now comes from Contents/version.plist (ProductBuildVersion). The one in Contents/Info is the build that compiled Xcode itself: on my machine it gives 27A266, while Xcode writes 27A266a into the apps it builds (I checked on an extension built with xcodebuild). DTXcode still comes from Contents/Info like you suggested.

Comment thread packages/cli/src/build/apple.rs Outdated
Comment thread packages/cli/src/build/apple.rs Outdated
Comment thread packages/cli/src/build/apple.rs
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.
…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 8fd6cd6 to 8be276e Compare October 11, 2026 17:17
@mirkobozzetto

Copy link
Copy Markdown
Author

Thanks for the careful review @LucaCappelletti94, you were right on every point. I went through them one by one, it's all pushed and I answered in each thread.

Two small differences with your suggestions: DTXcodeBuild comes from Contents/version.plist and not Contents/Info (details in the thread), and the 16.0 default is one constant shared with compile_widget_extensions. I also made the profile choice testable like you said, pick_provisioning_profile now takes parsed profiles, and added a test next to yours.

Re-validating found a real bug. With a "Developer ID Application" certificate in the keychain, --appstore signing failed as ambiguous, because dx passed the name without the "Apple Distribution:" part. Fixed in aa77594.

To check it end to end, I merged #5901 locally on top of this branch (not pushed) and built FlowFlow with it. xcrun altool --validate-app says VERIFY SUCCEEDED, the output is in the description. So for an App Store upload, this PR needs #5901 for the icon.

I rebased on main and rewrote the description, the old one was wrong about default bundles not changing.

About iPad (ealmloff's thread): I only ship on iPhone and I have no iPad use case to test, so I'd rather not touch device targeting in this PR.

@LucaCappelletti94 LucaCappelletti94 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As per the previous review, I can only leave a comment not being a maintainer. This is still a "request changes" note, part of it my own fault because I was wrong on the widget/app version thing.

I could not place this note in the code due to GitHub UI limits, basically you should sign with the certificate's SHA-1 hash instead of its name, which still fails as ambiguous when two certificates share it, for example one person's development certificates from two Macs, and Apple recommends the hash for this. This range reaches past the changed lines, so it cannot be a suggestion.

I have added a red test for this in one of the suggestions.

@@ -773,20 +773,22 @@ impl BuildRequest {
         } else {
             "Apple Development"
         };
-        // Keep the label: "Apple Distribution: Name (TEAM)" and "Developer ID Application: Name (TEAM)"
-        // share the same suffix, so codesign rejects the bare name as ambiguous.
-        let pattern = format!(r#""({label}: [^"]+)""#);
 
-        let app_dev_name = regex::Regex::new(&pattern)
-            .unwrap()
-            .captures(&identities)
-            .and_then(|caps| caps.get(1))
-            .map(|m| m.as_str())
+        Self::find_signing_identity(&identities, label)
+            .map(str::to_string)
             .with_context(|| {
                 format!("Failed to find {label} in `security find-identity -v -p codesigning`")
-            })?;
+            })
+    }
 
-        Ok(app_dev_name.to_string())
+    /// SHA-1 of the first `label` identity, which codesign matches to exactly one certificate.
+    fn find_signing_identity<'a>(identities: &'a str, label: &str) -> Option<&'a str> {
+        let pattern = format!(r#"(?m)^\s*\d+\) ([0-9A-F]{{40}}) "{label}: [^"]+""#);
+        regex::Regex::new(&pattern)
+            .unwrap()
+            .captures(identities)
+            .and_then(|caps| caps.get(1))
+            .map(|m| m.as_str())
     }

Comment thread packages/cli/src/build/apple.rs Outdated
Comment thread packages/cli/src/build/apple.rs Outdated
Comment thread packages/cli/src/build/apple.rs
@mirkobozzetto

Copy link
Copy Markdown
Author

Thanks for the second round @LucaCappelletti94. I applied everything as suggested: signing uses the certificate's SHA-1 (b683790), the app is back to 15.0 and widgets stay at 16.0 (677d5d6).

I rebuilt FlowFlow with #5901 merged locally and xcrun altool --validate-app still says VERIFY SUCCEEDED. It's signed by Apple Distribution, with the Developer ID cert still in the keychain.

FlowFlow sets its own 16.0 minimum, so the new 15.0 default is only covered by the tests. I also updated the description.

Comment thread packages/cli/src/build/apple.rs Outdated
Comment thread packages/cli/src/build/apple.rs Outdated
Comment thread packages/cli/src/build/apple.rs Outdated
Comment thread packages/cli/src/build/apple.rs Outdated

@LucaCappelletti94 LucaCappelletti94 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hopefully this should be the last review round. What remains is how --appstore picks a provisioning profile when several match the app, hopefully with the suggestions ready to apply it should be fairly straightforward.

I am being this thorough because some of my PRs are stacked on top of this one, so I really look forward to the best possible version of it being merged.

Comment thread packages/cli/src/build/apple.rs Outdated
Comment on lines +168 to +221
/// Picks the profile for `bundle_id`: an exact app ID beats a wildcard, then more provisioned
/// devices win. With `appstore`, profiles tied to devices are skipped.
fn pick_provisioning_profile(
profiles: impl IntoIterator<Item = (PathBuf, ProvisioningProfile)>,
bundle_id: &str,
appstore: bool,
) -> Option<(PathBuf, ProvisioningProfile)> {
let mut best_match: Option<(PathBuf, ProvisioningProfile, bool, usize)> = None;

for (path, profile) in profiles {
let app_id = &profile.entitlements.application_identifier;
if !profile_matches_bundle_id(app_id, bundle_id) {
tracing::debug!(
"Skipping profile {} (app ID {app_id} does not match bundle ID {bundle_id})",
path.display()
);
continue;
}

// App Store profiles carry no `ProvisionedDevices`.
if appstore && !profile.provisioned_devices.is_empty() {
tracing::debug!(
"Skipping profile {} ({} provisioned devices — not an App Store profile)",
path.display(),
profile.provisioned_devices.len()
);
continue;
}

let is_exact = !app_id.ends_with(".*") && !app_id.ends_with("*");
let num_devices = profile.provisioned_devices.len();

tracing::debug!(
"Found matching profile {} (app ID: {app_id}, exact: {is_exact}, devices: {num_devices})",
path.display()
);

// Prefer: exact match > more provisioned devices (newer profiles have more devices)
let dominated = match &best_match {
Some((_, _, prev_exact, prev_devices)) => {
if *prev_exact && !is_exact {
true // existing exact match beats wildcard
} else if is_exact && !*prev_exact {
false // new exact match beats existing wildcard
} else {
// same specificity — prefer more provisioned devices
num_devices <= *prev_devices
}
}
None => false,
};

if !dominated {
best_match = Some((path, profile, is_exact, num_devices));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should skip expired profiles and prefer the newer profile on a tie. With --appstore every candidate has zero ProvisionedDevices, so with two App Store profiles for the bundle ID the first one read_dir returns wins, even when it is expired or older than the other.

The profiles folder can hold several valid profiles for one bundle ID, and signing with the stale one only fails at upload. I only happened against this by pure chance, so I guess we are lucky to have caught it.

Suggested change
/// Picks the profile for `bundle_id`: an exact app ID beats a wildcard, then more provisioned
/// devices win. With `appstore`, profiles tied to devices are skipped.
fn pick_provisioning_profile(
profiles: impl IntoIterator<Item = (PathBuf, ProvisioningProfile)>,
bundle_id: &str,
appstore: bool,
) -> Option<(PathBuf, ProvisioningProfile)> {
let mut best_match: Option<(PathBuf, ProvisioningProfile, bool, usize)> = None;
for (path, profile) in profiles {
let app_id = &profile.entitlements.application_identifier;
if !profile_matches_bundle_id(app_id, bundle_id) {
tracing::debug!(
"Skipping profile {} (app ID {app_id} does not match bundle ID {bundle_id})",
path.display()
);
continue;
}
// App Store profiles carry no `ProvisionedDevices`.
if appstore && !profile.provisioned_devices.is_empty() {
tracing::debug!(
"Skipping profile {} ({} provisioned devices — not an App Store profile)",
path.display(),
profile.provisioned_devices.len()
);
continue;
}
let is_exact = !app_id.ends_with(".*") && !app_id.ends_with("*");
let num_devices = profile.provisioned_devices.len();
tracing::debug!(
"Found matching profile {} (app ID: {app_id}, exact: {is_exact}, devices: {num_devices})",
path.display()
);
// Prefer: exact match > more provisioned devices (newer profiles have more devices)
let dominated = match &best_match {
Some((_, _, prev_exact, prev_devices)) => {
if *prev_exact && !is_exact {
true // existing exact match beats wildcard
} else if is_exact && !*prev_exact {
false // new exact match beats existing wildcard
} else {
// same specificity — prefer more provisioned devices
num_devices <= *prev_devices
}
}
None => false,
};
if !dominated {
best_match = Some((path, profile, is_exact, num_devices));
/// Picks the unexpired profile for `bundle_id`: an exact app ID beats a wildcard, then more
/// provisioned devices win, then the newer profile. With `appstore`, profiles tied to devices
/// are skipped.
fn pick_provisioning_profile(
profiles: impl IntoIterator<Item = (PathBuf, ProvisioningProfile)>,
bundle_id: &str,
appstore: bool,
) -> Option<(PathBuf, ProvisioningProfile)> {
let now = SystemTime::now();
let mut best_match: Option<(PathBuf, ProvisioningProfile, bool, (usize, SystemTime))> = None;
for (path, profile) in profiles {
let app_id = &profile.entitlements.application_identifier;
if !profile_matches_bundle_id(app_id, bundle_id) {
tracing::debug!(
"Skipping profile {} (app ID {app_id} does not match bundle ID {bundle_id})",
path.display()
);
continue;
}
// App Store profiles carry no `ProvisionedDevices`.
if appstore && !profile.provisioned_devices.is_empty() {
tracing::debug!(
"Skipping profile {} ({} provisioned devices — not an App Store profile)",
path.display(),
profile.provisioned_devices.len()
);
continue;
}
if SystemTime::from(profile.expiration_date) <= now {
tracing::debug!("Skipping profile {} (expired)", path.display());
continue;
}
let is_exact = !app_id.ends_with(".*") && !app_id.ends_with("*");
let num_devices = profile.provisioned_devices.len();
let rank = (num_devices, SystemTime::from(profile.creation_date));
tracing::debug!(
"Found matching profile {} (app ID: {app_id}, exact: {is_exact}, devices: {num_devices})",
path.display()
);
// Prefer: exact match > more provisioned devices > newer profile
let dominated = match &best_match {
Some((_, _, prev_exact, prev_rank)) => {
if *prev_exact && !is_exact {
true // existing exact match beats wildcard
} else if is_exact && !*prev_exact {
false // new exact match beats existing wildcard
} else {
// same specificity — prefer more provisioned devices, then the newer profile
rank <= *prev_rank
}
}
None => false,
};
if !dominated {
best_match = Some((path, profile, is_exact, rank));

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Applied in f2c12f3.

It opened one gap. When the only matching profile has expired, the error said no profile matches and told you to change the bundle ID or create a wildcard profile. Profiles last a year, so anyone coming back to an app after that gets sent the wrong way. 18aac35 says the profile expired and needs renewing, with a test.

Comment thread packages/cli/src/build/apple.rs
Comment thread packages/cli/src/build/apple.rs
Comment thread packages/cli/src/build/apple.rs
Comment thread packages/cli/src/build/apple.rs
@grosjeanbaptiste

Copy link
Copy Markdown

One more data point for this PR, from a second app.

A Dioxus 0.7.9 mobile app, bundled with dx bundle --platform ios --release, was refused by App Store Connect on its first upload. It was accepted on TestFlight after a post-processing step added what this PR adds to the template, plus the icon from #5901:

  • CFBundlePackageType = APPL;
  • MinimumOSVersion = 15.0. Without it, App Store Connect assumed an old iOS and asked for armv7 and the legacy CodeResources symlink;
  • the DT* keys, read from the installed Xcode (xcodebuild -version, xcrun --sdk iphoneos --show-sdk-version / --show-sdk-build-version);
  • an empty UILaunchScreen dict in place of UILaunchStoryboardName = LaunchScreen, since no storyboard is bundled;
  • a compiled Assets.car from a single 1024×1024 opaque icon (actool --app-icon AppIcon --output-partial-info-plist …), with the icon keys actool reports merged into Info.plist. Flat PNGs plus CFBundleIcons install fine on a device, but the upload refuses them.

So the same set of keys was needed on a different app. The set was enough once the app was signed with the App Store profile's entitlements.

A side note for 0.7.x users: on 0.7.9, CFBundleName and CFBundleDisplayName come from the crate name and ignore [application] name. main already uses application.name (bundled_app_name in build/request.rs), so nothing is needed here.

@mirkobozzetto

Copy link
Copy Markdown
Author

Same keys on FlowFlow, so two apps needed them.

FlowFlow is live on the App Store: https://apps.apple.com/us/app/flowflow/id6773033233
To get there, its release target patches Info.plist by hand after dx build: https://github.com/mirkobozzetto/flowflow/blob/24aa3ce75433bbc7dba9fa4790ead4f9af9bd806/Makefile#L193-L323
It sets the same keys as yours: CFBundlePackageType, MinimumOSVersion, the DT* set, and the icon through actool.
The repo is public if you want to see the whole workaround: https://github.com/mirkobozzetto/flowflow
This PR moves the plist keys and the App Store signing into dx, so that patching isn't needed anymore.

I validated it on my side with this branch, on top of main.
Built with #5901 for the icon and no patching, FlowFlow passes xcrun altool --validate-app.
The output is in the Proof section of the PR description.
Still waiting for it to be validated here ;)

The last step you did by hand, the App Store signing, is what --appstore does.
It signs with "Apple Distribution".
It picks the newest unexpired App Store profile for the bundle ID, the one with no device list.
It writes the entitlements from that profile with get-task-allow=false, plus anything under [ios.entitlements] in Dioxus.toml.

To try it on your app without the post-processing:

git clone -b fix-ios-appstore-bundle https://github.com/mirkobozzetto/dioxus
cd dioxus
git fetch https://github.com/DioxusLabs/dioxus pull/5901/head
git merge --no-edit FETCH_HEAD
cargo install --path packages/cli

Then in your app, with icon = ["path/to/icon-1024.png"] under [ios] in Dioxus.toml:

dx bundle --platform ios --release --appstore
xcrun altool --validate-app -f target/dx/<app>/bundle/ios/ipa/<App>_<version>_aarch64.ipa -t ios -u <apple-id> -p <app-specific-password>

You need an "Apple Distribution" certificate in your keychain.
You also need an App Store profile for the bundle ID in ~/Library/Developer/Xcode/UserData/Provisioning Profiles.

One catch: this branch's dx is 0.8.0-alpha.1.
On a 0.7.9 app it prints a version mismatch error, and I haven't tried that combination.
FlowFlow runs 0.8.0-alpha.1.

This branch has not been deployed

No deployments
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.

4 participants