diff --git a/packages/cli/assets/ios/ios.plist.hbs b/packages/cli/assets/ios/ios.plist.hbs
index 16b7cec8be..2ad938ab85 100644
--- a/packages/cli/assets/ios/ios.plist.hbs
+++ b/packages/cli/assets/ios/ios.plist.hbs
@@ -20,10 +20,19 @@
{{ version }}
CFBundleDevelopmentRegion
en_US
- UILaunchStoryboardName
- LaunchScreen
+
+ UILaunchScreen
+
LSRequiresIPhoneOS
+
+ ITSAppUsesNonExemptEncryption
+
UISupportsTrueScreenSizeOnMac
UIRequiredDeviceCapabilities
@@ -39,8 +48,53 @@
CFBundleSupportedPlatforms
iPhoneOS
- iPadOS
+
+
+ {{#if cf_bundle_package_type}}
+ CFBundlePackageType
+ {{ cf_bundle_package_type }}
+ {{/if}}
+ {{#if minimum_os_version}}
+ MinimumOSVersion
+ {{ minimum_os_version }}
+ {{/if}}
+ {{#if dt_platform_name}}
+ DTPlatformName
+ {{ dt_platform_name }}
+ {{/if}}
+ {{#if dt_platform_version}}
+ DTPlatformVersion
+ {{ dt_platform_version }}
+ {{/if}}
+ {{#if dt_platform_build}}
+ DTPlatformBuild
+ {{ dt_platform_build }}
+ {{/if}}
+ {{#if dt_sdk_name}}
+ DTSDKName
+ {{ dt_sdk_name }}
+ {{/if}}
+ {{#if dt_sdk_build}}
+ DTSDKBuild
+ {{ dt_sdk_build }}
+ {{/if}}
+ {{#if dt_xcode}}
+ DTXcode
+ {{ dt_xcode }}
+ {{/if}}
+ {{#if dt_xcode_build}}
+ DTXcodeBuild
+ {{ dt_xcode_build }}
+ {{/if}}
+ {{#if dt_compiler}}
+ DTCompiler
+ {{ dt_compiler }}
+ {{/if}}
+ {{#if build_machine_os_build}}
+ BuildMachineOSBuild
+ {{ build_machine_os_build }}
+ {{/if}}
UISupportedInterfaceOrientations
UIInterfaceOrientationPortrait
diff --git a/packages/cli/src/build/apple.rs b/packages/cli/src/build/apple.rs
index 36953743d3..e1cb077d5c 100644
--- a/packages/cli/src/build/apple.rs
+++ b/packages/cli/src/build/apple.rs
@@ -43,6 +43,81 @@ use std::{
use target_lexicon::{OperatingSystem, Triple};
use tokio::process::Command;
+/// iOS-only Info.plist metadata probed from the local Xcode/SDK install.
+///
+/// Apple Transporter rejects App Store IPAs that don't carry these keys
+/// (DTPlatformName, DTSDKName, etc.). Xcode injects them automatically;
+/// `dx` reproduces the same set so device + App Store bundles match what
+/// the App Store expects.
+#[derive(Serialize, Default, Clone)]
+pub struct IosDtMetadata {
+ pub dt_platform_name: String,
+ pub dt_platform_version: String,
+ pub dt_platform_build: String,
+ pub dt_sdk_name: String,
+ pub dt_sdk_build: String,
+ pub dt_xcode: String,
+ pub dt_xcode_build: String,
+ pub dt_compiler: String,
+ pub build_machine_os_build: String,
+ pub minimum_os_version: String,
+ pub cf_bundle_package_type: String,
+}
+
+/// Probe the local Xcode toolchain for iOS bundle metadata.
+///
+/// Each probe is best-effort: a failure leaves the corresponding field empty
+/// (which keeps the build working on machines without a full Xcode install,
+/// just without App Store-grade Info.plist output).
+fn collect_ios_dt_metadata(deployment_target: &str) -> IosDtMetadata {
+ fn run(cmd: &str, args: &[&str]) -> String {
+ std::process::Command::new(cmd)
+ .args(args)
+ .output()
+ .ok()
+ .filter(|o| o.status.success())
+ .and_then(|o| String::from_utf8(o.stdout).ok())
+ .map(|s| s.trim().to_string())
+ .unwrap_or_default()
+ }
+
+ let sdk_version = run("xcrun", &["--sdk", "iphoneos", "--show-sdk-version"]);
+ let sdk_build = run("xcrun", &["--sdk", "iphoneos", "--show-sdk-build-version"]);
+ let xcode = run(
+ "defaults",
+ &["read", "/Applications/Xcode.app/Contents/Info", "DTXcode"],
+ );
+ let xcode_build = run(
+ "defaults",
+ &[
+ "read",
+ "/Applications/Xcode.app/Contents/Info",
+ "DTXcodeBuild",
+ ],
+ );
+ let os_build = run("sw_vers", &["-buildVersion"]);
+
+ let dt_sdk_name = if sdk_version.is_empty() {
+ String::new()
+ } else {
+ format!("iphoneos{sdk_version}")
+ };
+
+ IosDtMetadata {
+ dt_platform_name: "iphoneos".to_string(),
+ dt_platform_version: sdk_version.clone(),
+ dt_platform_build: sdk_build.clone(),
+ dt_sdk_name,
+ dt_sdk_build: sdk_build,
+ dt_xcode: xcode,
+ dt_xcode_build: xcode_build,
+ dt_compiler: "com.apple.compilers.llvm.clang.1_0".to_string(),
+ build_machine_os_build: os_build,
+ minimum_os_version: deployment_target.to_string(),
+ cf_bundle_package_type: "APPL".to_string(),
+ }
+}
+
impl BuildRequest {
/// Currently does nothing, but eventually we need to check that the mobile tooling is installed.
///
@@ -172,6 +247,9 @@ impl BuildRequest {
pub url_schemes: Vec,
/// iOS UIBackgroundModes
pub background_modes: Vec,
+ /// iOS-only DT metadata (xcrun / Xcode probe). Empty struct on macOS.
+ #[serde(flatten)]
+ pub ios_dt: IosDtMetadata,
}
// Attempt to use the user's manually specified
@@ -237,6 +315,7 @@ impl BuildRequest {
minimum_system_version,
url_schemes: mapper.macos_url_schemes.clone(),
background_modes: Vec::new(), // macOS doesn't use UIBackgroundModes
+ ios_dt: IosDtMetadata::default(),
},
)
.map_err(|e| e.into())
@@ -256,6 +335,18 @@ impl BuildRequest {
let plist_entries = generate_plist_entries(&self.config.ios.plist);
let raw_plist = self.config.ios.raw.info_plist.clone().unwrap_or_default();
+ // Probe Xcode toolchain for the DT* / SDK metadata Apple Transporter
+ // requires (DTPlatformName, DTPlatformVersion, DTSDKName, DTXcode, etc.).
+ // Falls back to empty strings if probes fail — the build still produces
+ // a valid .app, just one that won't pass App Store validation.
+ let deployment_target = self
+ .config
+ .ios
+ .deployment_target
+ .clone()
+ .unwrap_or_else(|| "15.0".to_string());
+ let ios_dt = collect_ios_dt_metadata(&deployment_target);
+
handlebars::Handlebars::new()
.render_template(
include_str!("../../assets/ios/ios.plist.hbs"),
@@ -271,6 +362,7 @@ impl BuildRequest {
minimum_system_version: String::new(), // Not used for iOS
url_schemes: mapper.ios_url_schemes.clone(),
background_modes: mapper.ios_background_modes.clone(),
+ ios_dt,
},
)
.map_err(|e| e.into())
@@ -287,18 +379,23 @@ impl BuildRequest {
let mut app_dev_name = self.apple_team_id.clone();
if app_dev_name.is_none() {
- app_dev_name = Some(Self::auto_provision_signing_name().await.context(
- "Failed to automatically provision signing name for Apple codesigning.",
- )?);
+ app_dev_name = Some(
+ Self::auto_provision_signing_name(self.appstore)
+ .await
+ .context(
+ "Failed to automatically provision signing name for Apple codesigning.",
+ )?,
+ );
}
let mut entitlements_file = self.apple_entitlements.clone();
let mut provisioning_profile_path = None;
if entitlements_file.is_none() {
let bundle_id = self.bundle_identifier();
- let (entitlements_xml, profile_path) = Self::auto_provision_entitlements(&bundle_id)
- .await
- .context("Failed to auto-provision entitlements for Apple codesigning.")?;
+ let (entitlements_xml, profile_path) =
+ Self::auto_provision_entitlements(&bundle_id, self.appstore)
+ .await
+ .context("Failed to auto-provision entitlements for Apple codesigning.")?;
// Enrich with entitlements from Dioxus.toml config
let entitlements_xml = self.enrich_entitlements_from_config(entitlements_xml)?;
@@ -331,13 +428,51 @@ impl BuildRequest {
_ => bail!("Codesigning is only supported for MacOS and iOS bundles"),
};
- // iOS devices require the provisioning profile to be embedded in the .app bundle
+ // iOS bundles need several pre-codesign passes so Apple App Store
+ // validation accepts the output. All bundle-content modifications
+ // (icon catalog, PrivacyInfo, widget Info.plist patching) must happen
+ // BEFORE any codesign call — codesign hashes the bundle and any later
+ // change invalidates the signature.
if self.bundle == BundleFormat::Ios {
+ let deployment_target = self
+ .config
+ .ios
+ .deployment_target
+ .clone()
+ .unwrap_or_else(|| "15.0".to_string());
+ let ios_dt = collect_ios_dt_metadata(&deployment_target);
+ let crate_dir = self.crate_dir();
+
+ // Compile AppIcon.xcassets via actool and merge the resulting
+ // CFBundleIcons keys into Info.plist. Apple rejects App Store
+ // uploads without a 120x120 iPhone icon present in the bundle.
+ Self::inject_ios_app_icon(&crate_dir, &target_exe, &deployment_target).await?;
+
+ // PrivacyInfo.xcprivacy is mandatory for App Store submissions
+ // since 2024. Auto-copy from `ios/PrivacyInfo.xcprivacy` if the
+ // user provides one at the conventional location.
+ Self::copy_ios_privacy_info(&crate_dir, &target_exe)?;
+
+ // App extensions get their own Info.plist with the same DT*,
+ // LSRequiresIPhoneOS, CFBundleSupportedPlatforms,
+ // UIRequiredDeviceCapabilities, and version strings the main
+ // bundle carries. Apple App Store requires version parity between
+ // an app and its extensions (CFBundleVersion +
+ // CFBundleShortVersionString) and rejects uploads when they
+ // diverge.
+ Self::sync_widget_info_plists(&target_exe, &ios_dt, &self.crate_version())?;
+
if let Some(profile_path) = &provisioning_profile_path {
let dest = target_exe.join("embedded.mobileprovision");
std::fs::copy(profile_path, &dest)
.context("Failed to embed provisioning profile into .app bundle")?;
}
+
+ // iOS app extensions live under .app/PlugIns/*.appex and must be
+ // signed BEFORE the parent .app (codesign computes the parent
+ // signature over the nested bundle hashes — sign children last
+ // and the parent signature is invalidated).
+ Self::sign_ios_app_extensions(&target_exe, app_dev_name, self.appstore).await?;
}
// codesign the app
@@ -364,7 +499,280 @@ impl BuildRequest {
Ok(())
}
- async fn auto_provision_signing_name() -> Result {
+ /// Compile `AppIcon.xcassets` (at the crate root) into the .app bundle
+ /// and merge the resulting CFBundleIcons keys into Info.plist.
+ ///
+ /// No-op when no `AppIcon.xcassets` is found — the build still produces a
+ /// `.app`, just one Apple App Store will reject for missing icons.
+ async fn inject_ios_app_icon(
+ crate_dir: &Path,
+ app_root: &Path,
+ deployment_target: &str,
+ ) -> Result<()> {
+ let xcassets = crate_dir.join("AppIcon.xcassets");
+ if !xcassets.exists() {
+ return Ok(());
+ }
+
+ let tmpdir = tempfile::tempdir()?;
+ let partial_plist = tmpdir.path().join("appicon-partial.plist");
+
+ let output = Command::new("xcrun")
+ .args(["actool", "--compile"])
+ .arg(app_root)
+ .args([
+ "--platform",
+ "iphoneos",
+ "--minimum-deployment-target",
+ deployment_target,
+ "--app-icon",
+ "AppIcon",
+ "--output-partial-info-plist",
+ ])
+ .arg(&partial_plist)
+ .arg(&xcassets)
+ .output()
+ .await
+ .context("Failed to run `xcrun actool` — install Xcode command line tools")?;
+
+ if !output.status.success() {
+ bail!(
+ "actool failed to compile AppIcon.xcassets: {}",
+ String::from_utf8_lossy(&output.stderr)
+ );
+ }
+
+ if !partial_plist.exists() {
+ // actool exited successfully but produced no partial plist
+ // (xcassets had no AppIcon entry). Nothing to merge.
+ return Ok(());
+ }
+
+ let main_plist_path = app_root.join("Info.plist");
+ let mut main = plist::Value::from_file(&main_plist_path)
+ .with_context(|| format!("Failed to read {}", main_plist_path.display()))?;
+ let partial = plist::Value::from_file(&partial_plist)
+ .with_context(|| format!("Failed to read {}", partial_plist.display()))?;
+
+ let main_dict = main
+ .as_dictionary_mut()
+ .context("Main Info.plist root is not a dictionary")?;
+
+ // actool may emit CFBundleIcons~ipad variants the bundle doesn't
+ // actually ship. Drop them first so the merge stays consistent.
+ main_dict.remove("CFBundleIcons");
+ main_dict.remove("CFBundleIcons~ipad");
+
+ if let Some(partial_dict) = partial.as_dictionary() {
+ for (k, v) in partial_dict {
+ main_dict.insert(k.clone(), v.clone());
+ }
+ }
+
+ plist::to_file_xml(&main_plist_path, &main).with_context(|| {
+ format!(
+ "Failed to write merged Info.plist at {}",
+ main_plist_path.display()
+ )
+ })?;
+
+ Ok(())
+ }
+
+ /// Copy `ios/PrivacyInfo.xcprivacy` (at the crate root) into the .app
+ /// bundle. Apple has required a PrivacyInfo manifest in App Store
+ /// submissions since 2024. No-op if the file isn't present.
+ fn copy_ios_privacy_info(crate_dir: &Path, app_root: &Path) -> Result<()> {
+ let src = crate_dir.join("ios").join("PrivacyInfo.xcprivacy");
+ if !src.exists() {
+ return Ok(());
+ }
+ let dest = app_root.join("PrivacyInfo.xcprivacy");
+ std::fs::copy(&src, &dest).with_context(|| {
+ format!(
+ "Failed to copy PrivacyInfo.xcprivacy from {} to {}",
+ src.display(),
+ dest.display()
+ )
+ })?;
+ Ok(())
+ }
+
+ /// Patch each `PlugIns/*.appex` Info.plist with the same iOS bundle
+ /// metadata the main app carries (DT* keys, LSRequiresIPhoneOS,
+ /// CFBundleSupportedPlatforms, UIRequiredDeviceCapabilities). Apple
+ /// validates app extension Info.plists independently and rejects
+ /// uploads where these keys are missing or mismatched.
+ fn sync_widget_info_plists(
+ app_root: &Path,
+ ios_dt: &IosDtMetadata,
+ version: &str,
+ ) -> Result<()> {
+ let plugins_dir = app_root.join("PlugIns");
+ if !plugins_dir.exists() {
+ return Ok(());
+ }
+
+ for entry in std::fs::read_dir(&plugins_dir)?.flatten() {
+ let appex_path = entry.path();
+ let is_appex = appex_path
+ .extension()
+ .map(|e| e == "appex")
+ .unwrap_or(false);
+ if !is_appex {
+ continue;
+ }
+
+ let plist_path = appex_path.join("Info.plist");
+ let mut value = plist::Value::from_file(&plist_path)
+ .with_context(|| format!("Failed to read {}", plist_path.display()))?;
+
+ let dict = value
+ .as_dictionary_mut()
+ .with_context(|| format!("{} root is not a dictionary", plist_path.display()))?;
+
+ // Required device family / OS markers
+ dict.insert(
+ "LSRequiresIPhoneOS".to_string(),
+ plist::Value::Boolean(true),
+ );
+ dict.insert(
+ "CFBundleSupportedPlatforms".to_string(),
+ plist::Value::Array(vec![plist::Value::String("iPhoneOS".to_string())]),
+ );
+ dict.insert(
+ "UIRequiredDeviceCapabilities".to_string(),
+ plist::Value::Array(vec![plist::Value::String("arm64".to_string())]),
+ );
+
+ // Version parity with main bundle (App Store requirement)
+ dict.insert(
+ "CFBundleVersion".to_string(),
+ plist::Value::String(version.to_string()),
+ );
+ dict.insert(
+ "CFBundleShortVersionString".to_string(),
+ plist::Value::String(version.to_string()),
+ );
+
+ // DT* / SDK metadata mirroring the main bundle. Skip empties so
+ // we don't pollute the plist with blank entries on machines
+ // missing parts of the Xcode toolchain.
+ let dt_entries: [(&str, &str); 9] = [
+ ("DTPlatformName", &ios_dt.dt_platform_name),
+ ("DTPlatformVersion", &ios_dt.dt_platform_version),
+ ("DTPlatformBuild", &ios_dt.dt_platform_build),
+ ("DTSDKName", &ios_dt.dt_sdk_name),
+ ("DTSDKBuild", &ios_dt.dt_sdk_build),
+ ("DTXcode", &ios_dt.dt_xcode),
+ ("DTXcodeBuild", &ios_dt.dt_xcode_build),
+ ("DTCompiler", &ios_dt.dt_compiler),
+ ("BuildMachineOSBuild", &ios_dt.build_machine_os_build),
+ ];
+ for (k, v) in dt_entries {
+ if !v.is_empty() {
+ dict.insert(k.to_string(), plist::Value::String(v.to_string()));
+ }
+ }
+
+ plist::to_file_xml(&plist_path, &value)
+ .with_context(|| format!("Failed to write patched {}", plist_path.display()))?;
+ }
+
+ Ok(())
+ }
+
+ /// Sign every `.appex` bundle under `/PlugIns/` so the parent .app
+ /// signature is valid and App Store upload accepts the nested extensions.
+ ///
+ /// Each extension gets its own provisioning profile matched by its
+ /// CFBundleIdentifier and is signed with the same signing identity as the
+ /// parent app.
+ async fn sign_ios_app_extensions(
+ app_root: &Path,
+ dev_name: &str,
+ appstore: bool,
+ ) -> Result<()> {
+ let plugins_dir = app_root.join("PlugIns");
+ if !plugins_dir.exists() {
+ return Ok(());
+ }
+
+ for entry in std::fs::read_dir(&plugins_dir)?.flatten() {
+ let appex_path = entry.path();
+ let is_appex = appex_path
+ .extension()
+ .map(|e| e == "appex")
+ .unwrap_or(false);
+ if !is_appex {
+ continue;
+ }
+
+ let info_plist = appex_path.join("Info.plist");
+ let appex_bundle_id = plist::Value::from_file(&info_plist)
+ .with_context(|| format!("Failed to read {}", info_plist.display()))?
+ .as_dictionary()
+ .and_then(|d| d.get("CFBundleIdentifier"))
+ .and_then(|v| v.as_string())
+ .map(|s| s.to_string())
+ .with_context(|| {
+ format!(
+ "Missing CFBundleIdentifier in app extension {}",
+ info_plist.display()
+ )
+ })?;
+
+ tracing::debug!(
+ "Signing app extension {} (bundle id: {appex_bundle_id})",
+ appex_path.display()
+ );
+
+ let (entitlements_xml, profile_path) = Self::auto_provision_entitlements(
+ &appex_bundle_id,
+ appstore,
+ )
+ .await
+ .with_context(|| {
+ format!("Failed to auto-provision entitlements for app extension {appex_bundle_id}")
+ })?;
+
+ let dest_profile = appex_path.join("embedded.mobileprovision");
+ std::fs::copy(&profile_path, &dest_profile).with_context(|| {
+ format!(
+ "Failed to embed provisioning profile into {}",
+ appex_path.display()
+ )
+ })?;
+
+ let entitlements_tmp = tempfile::NamedTempFile::new()?;
+ std::fs::write(entitlements_tmp.path(), entitlements_xml)?;
+
+ let output = Command::new("codesign")
+ .args([
+ "--force",
+ "--entitlements",
+ entitlements_tmp.path().to_str().unwrap(),
+ "--sign",
+ dev_name,
+ ])
+ .arg(&appex_path)
+ .output()
+ .await
+ .context("Failed to codesign app extension - is `codesign` in your path?")?;
+
+ if !output.status.success() {
+ bail!(
+ "Failed to codesign app extension {}: {}",
+ appex_path.display(),
+ String::from_utf8(output.stderr).unwrap_or_default()
+ );
+ }
+ }
+
+ Ok(())
+ }
+
+ async fn auto_provision_signing_name(appstore: bool) -> Result {
let identities = Command::new("security")
.args(["find-identity", "-v", "-p", "codesigning"])
.output()
@@ -375,16 +783,23 @@ impl BuildRequest {
.context("Failed to parse `security find-identity -v -p codesigning`")
})??;
- // Parsing this:
- // 1231231231231asdasdads123123 "Apple Development: foo@gmail.com (XYZYZY)"
- let app_dev_name = regex::Regex::new(r#""Apple Development: (.+)""#)
+ // App Store distribution requires an "Apple Distribution" certificate
+ // (issued by the paid Apple Developer Program). Otherwise we look for
+ // the standard "Apple Development" identity used for device builds.
+ let (label, pattern) = if appstore {
+ ("Apple Distribution", r#""Apple Distribution: (.+)""#)
+ } else {
+ ("Apple Development", r#""Apple Development: (.+)""#)
+ };
+
+ let app_dev_name = regex::Regex::new(pattern)
.unwrap()
.captures(&identities)
.and_then(|caps| caps.get(1))
.map(|m| m.as_str())
- .context(
- "Failed to find Apple Development in `security find-identity -v -p codesigning`",
- )?;
+ .with_context(|| {
+ format!("Failed to find {label} in `security find-identity -v -p codesigning`")
+ })?;
Ok(app_dev_name.to_string())
}
@@ -615,7 +1030,10 @@ impl BuildRequest {
}
}
- async fn auto_provision_entitlements(bundle_id: &str) -> Result<(String, PathBuf)> {
+ async fn auto_provision_entitlements(
+ bundle_id: &str,
+ appstore: bool,
+ ) -> Result<(String, PathBuf)> {
const CODESIGN_ERROR: &str = r#"This is likely because you haven't
- Created a provisioning profile before
- Accepted the Apple Developer Program License Agreement
@@ -751,6 +1169,19 @@ We checked the folders:
continue;
}
+ // App Store distribution profiles have no ProvisionedDevices entry
+ // (they're not tied to a specific device list). Filter out dev /
+ // ad-hoc profiles here so the auto-discovery picks a profile that
+ // Apple will accept for App Store upload.
+ 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();
@@ -805,6 +1236,11 @@ We checked the folders:
}
};
+ // App Store binaries must ship with `get-task-allow=false` (or the key
+ // omitted). Apple rejects the upload otherwise. Dev / device builds
+ // keep `get-task-allow=true` so the debugger can attach.
+ let get_task_allow = if appstore { "false" } else { "true" };
+
let entitlements_xml = format!(
r#"
@@ -816,13 +1252,14 @@ We checked the folders:
{APP_ID_ACCESS_GROUP}.*
get-task-allow
-
+ <{GET_TASK_ALLOW}/>
com.apple.developer.team-identifier
{TEAM_IDENTIFIER}
"#,
APPLICATION_IDENTIFIER = mbfile.entitlements.application_identifier,
APP_ID_ACCESS_GROUP = mbfile.entitlements.keychain_access_groups[0],
+ GET_TASK_ALLOW = get_task_allow,
TEAM_IDENTIFIER = mbfile.team_identifier[0],
);
diff --git a/packages/cli/src/build/request.rs b/packages/cli/src/build/request.rs
index 8d93217328..3ab0e76d10 100644
--- a/packages/cli/src/build/request.rs
+++ b/packages/cli/src/build/request.rs
@@ -269,6 +269,7 @@ pub(crate) struct BuildRequest {
pub(crate) using_dioxus_explicitly: bool,
pub(crate) apple_entitlements: Option,
pub(crate) apple_team_id: Option,
+ pub(crate) appstore: bool,
pub(crate) session_cache_dir: PathBuf,
pub(crate) raw_json_diagnostics: bool,
pub(crate) windows_subsystem: Option,
@@ -719,7 +720,7 @@ impl BuildRequest {
// Determine if we should codesign
let should_codesign =
- args.codesign || device.is_some() || args.apple_entitlements.is_some();
+ args.codesign || args.appstore || device.is_some() || args.apple_entitlements.is_some();
// Determining release mode is based on the profile, actually, so we need to check that
let release = workspace.is_release_profile(&profile);
@@ -919,6 +920,7 @@ impl BuildRequest {
inject_loading_scripts: args.inject_loading_scripts,
apple_entitlements: args.apple_entitlements.clone(),
apple_team_id: args.apple_team_id.clone(),
+ appstore: args.appstore,
raw_json_diagnostics: args.raw_json_diagnostics,
windows_subsystem: args.windows_subsystem.clone(),
})
diff --git a/packages/cli/src/cli/target.rs b/packages/cli/src/cli/target.rs
index deccd0d815..37b6a9992c 100644
--- a/packages/cli/src/cli/target.rs
+++ b/packages/cli/src/cli/target.rs
@@ -147,6 +147,19 @@ pub(crate) struct TargetArgs {
#[clap(long, help_heading = HELP_HEADING)]
pub(crate) apple_team_id: Option,
+ /// Bundle the iOS app for App Store distribution.
+ ///
+ /// When set, codesigning auto-detect looks for an "Apple Distribution" identity
+ /// instead of "Apple Development", and provisioning profile auto-discovery
+ /// prefers App Store profiles (those without a `ProvisionedDevices` key) over
+ /// development / ad-hoc profiles.
+ ///
+ /// Combine with `--release` to produce an `.ipa` Apple Transporter will accept.
+ /// Requires a paid Apple Developer Program account with a valid Distribution
+ /// certificate and an App Store provisioning profile installed locally.
+ #[clap(long, default_value_t = false, help_heading = HELP_HEADING, num_args = 0..=1)]
+ pub(crate) appstore: bool,
+
/// The folder where DX stores its temporary artifacts for things like hotpatching, build caches,
/// window position, etc. This is meant to be stable within an invocation of the CLI, but you can
/// persist it by setting this flag.