diff --git a/packages/cli/Cargo.toml b/packages/cli/Cargo.toml index 230fb89c7..766ce53e0 100644 --- a/packages/cli/Cargo.toml +++ b/packages/cli/Cargo.toml @@ -27,7 +27,7 @@ dioxus-cli-telemetry = { workspace = true } dioxus-component-manifest = { workspace = true } bytes = { workspace = true } -clap = { workspace = true, features = ["derive", "cargo"] } +clap = { workspace = true, features = ["derive", "cargo", "env"] } convert_case = { workspace = true } thiserror = { workspace = true } uuid = { workspace = true, features = ["v4", "v5", "v7"] } diff --git a/packages/cli/assets/android/gen/app/build.gradle.kts.hbs b/packages/cli/assets/android/gen/app/build.gradle.kts.hbs index 232c18dc7..31f44a952 100644 --- a/packages/cli/assets/android/gen/app/build.gradle.kts.hbs +++ b/packages/cli/assets/android/gen/app/build.gradle.kts.hbs @@ -13,7 +13,7 @@ android { applicationId = "{{ application_id }}" minSdk = {{ min_sdk }} targetSdk = {{ target_sdk }} - versionCode = 1 + versionCode = {{ version_code }} versionName = "{{ version }}" } {{#if android_bundle}} diff --git a/packages/cli/assets/ios/ios.plist.hbs b/packages/cli/assets/ios/ios.plist.hbs index 16b7cec8b..a3f7a8fcd 100644 --- a/packages/cli/assets/ios/ios.plist.hbs +++ b/packages/cli/assets/ios/ios.plist.hbs @@ -15,7 +15,7 @@ {{ bundle_name }} CFBundleVersion - {{ version }} + {{ bundle_version }} CFBundleShortVersionString {{ version }} CFBundleDevelopmentRegion diff --git a/packages/cli/assets/macos/mac.plist.hbs b/packages/cli/assets/macos/mac.plist.hbs index 999ffd84c..096fd687e 100644 --- a/packages/cli/assets/macos/mac.plist.hbs +++ b/packages/cli/assets/macos/mac.plist.hbs @@ -27,7 +27,7 @@ {{ version }} CFBundleVersion - {{ version }} + {{ bundle_version }} LSMinimumSystemVersion {{ minimum_system_version }} diff --git a/packages/cli/schema.json b/packages/cli/schema.json index aa147decf..1355d7e0e 100644 --- a/packages/cli/schema.json +++ b/packages/cli/schema.json @@ -267,6 +267,15 @@ "items": { "type": "string" } + }, + "version_code": { + "description": "The version code used for `versionCode` in the generated Gradle project.\n\nMust be between 1 and 2100000000 (the Google Play Store maximum).\n\nCan be overridden per-build with `--version-code` or the `DX_ANDROID_VERSION_CODE`\nenvironment variable. If unset, it is derived from the crate version as\n`major * 1000000 + minor * 1000 + patch`.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0 } } }, @@ -964,6 +973,13 @@ "type": "string" } }, + "bundle_version": { + "description": "The bundle version string (CFBundleVersion).\n\nThis is the machine-readable build number that must increase with every upload\nto App Store Connect. Can be overridden per-build with `--bundle-version` or the\n`DX_BUNDLE_VERSION` environment variable. Defaults to the crate version.", + "type": [ + "string", + "null" + ] + }, "category": { "description": "App category. Overrides `bundle.category` for iOS builds.", "type": [ diff --git a/packages/cli/src/build/android.rs b/packages/cli/src/build/android.rs index 556ebb23e..b02120b1d 100644 --- a/packages/cli/src/build/android.rs +++ b/packages/cli/src/build/android.rs @@ -120,6 +120,7 @@ impl BuildRequest { application_id: String, app_name: String, version: String, + version_code: u32, android_bundle: Option, /// Android SDK version settings min_sdk: u32, @@ -180,6 +181,7 @@ impl BuildRequest { application_id: self.bundle_identifier(), app_name: self.bundled_app_name(), version: self.crate_version(), + version_code: self.android_version_code()?, android_bundle: self.config.bundle.android.clone(), min_sdk: self.config.android.min_sdk.unwrap_or(24), target_sdk: self.config.android.target_sdk.unwrap_or(34), diff --git a/packages/cli/src/build/apple.rs b/packages/cli/src/build/apple.rs index d7bc277b4..96a7183b2 100644 --- a/packages/cli/src/build/apple.rs +++ b/packages/cli/src/build/apple.rs @@ -160,6 +160,8 @@ impl BuildRequest { pub executable_name: String, /// App version string (from Cargo.toml) pub version: String, + /// Bundle version string (CFBundleVersion) + pub bundle_version: String, /// Permission usage descriptions pub permissions: Vec, /// Additional plist entries as raw XML @@ -231,6 +233,7 @@ impl BuildRequest { executable_name: self.platform_exe_name(), bundle_identifier: self.bundle_identifier(), version: self.crate_version(), + bundle_version: self.apple_bundle_version(), permissions, plist_entries, raw_plist, @@ -265,6 +268,7 @@ impl BuildRequest { executable_name: self.platform_exe_name(), bundle_identifier: self.bundle_identifier(), version: self.crate_version(), + bundle_version: self.apple_bundle_version(), permissions, plist_entries, raw_plist, diff --git a/packages/cli/src/build/request.rs b/packages/cli/src/build/request.rs index 2a4154594..d0b809dc7 100644 --- a/packages/cli/src/build/request.rs +++ b/packages/cli/src/build/request.rs @@ -269,6 +269,8 @@ pub(crate) struct BuildRequest { pub(crate) using_dioxus_explicitly: bool, pub(crate) apple_entitlements: Option, pub(crate) apple_team_id: Option, + pub(crate) version_code: Option, + pub(crate) bundle_version: Option, pub(crate) session_cache_dir: PathBuf, pub(crate) raw_json_diagnostics: bool, pub(crate) windows_subsystem: Option, @@ -905,6 +907,8 @@ impl BuildRequest { inject_loading_scripts: args.inject_loading_scripts, apple_entitlements: args.apple_entitlements.clone(), apple_team_id: args.apple_team_id.clone(), + version_code: args.version_code, + bundle_version: args.bundle_version.clone(), raw_json_diagnostics: args.raw_json_diagnostics, windows_subsystem: args.windows_subsystem.clone(), }) @@ -2685,6 +2689,64 @@ impl BuildRequest { .to_string() } + /// Get the Android `versionCode` for the generated Gradle project. + /// + /// Resolution order: `--version-code` / `DX_ANDROID_VERSION_CODE`, then + /// `[android] version_code` in Dioxus.toml, then a value derived from the crate + /// version as `major * 1000000 + minor * 1000 + patch`. + pub(crate) fn android_version_code(&self) -> Result { + /// The maximum value accepted by the Google Play Store. + const MAX_VERSION_CODE: u32 = 2_100_000_000; + + let code = match self.version_code.or(self.config.android.version_code) { + Some(code) => code, + None => { + let version = &self.workspace.krates[self.crate_package].version; + if version.major > 2099 || version.minor > 999 || version.patch > 999 { + bail!( + "Cannot derive an Android versionCode from crate version `{version}`: each component must fit `major <= 2099`, `minor <= 999`, `patch <= 999`. Set an explicit version code via `[android] version_code` in Dioxus.toml or the `--version-code` flag." + ); + } + if !version.pre.is_empty() { + tracing::warn!( + "Pre-release identifier of crate version `{version}` is ignored when deriving the Android versionCode" + ); + } + let code = + (version.major * 1_000_000 + version.minor * 1_000 + version.patch) as u32; + + // Google Play requires versionCode >= 1, so bump 0.0.0 up to 1. + code.max(1) + } + }; + + if !(1..=MAX_VERSION_CODE).contains(&code) { + bail!( + "Android versionCode must be between 1 and {MAX_VERSION_CODE}, got {code}. See https://developer.android.com/studio/publish/versioning" + ); + } + + Ok(code) + } + + /// The explicitly-configured `CFBundleVersion` for iOS/macOS bundles, if any. + /// + /// Resolution order: `--bundle-version` / `DX_BUNDLE_VERSION`, then the platform's + /// `bundle_version` in Dioxus.toml (`[ios]` or `[macos]`). + pub(crate) fn apple_bundle_version_override(&self) -> Option { + self.bundle_version.clone().or_else(|| match self.bundle { + BundleFormat::Ios => self.config.ios.bundle_version.clone(), + BundleFormat::MacOS => self.config.macos.bundle_version.clone(), + _ => None, + }) + } + + /// The `CFBundleVersion` for iOS/macOS bundles, falling back to the crate version. + pub(crate) fn apple_bundle_version(&self) -> String { + self.apple_bundle_version_override() + .unwrap_or_else(|| self.crate_version()) + } + pub(crate) fn bundle_identifier(&self) -> String { use crate::config::BundlePlatform; diff --git a/packages/cli/src/bundler/macos.rs b/packages/cli/src/bundler/macos.rs index cb4691cb3..49887be13 100644 --- a/packages/cli/src/bundler/macos.rs +++ b/packages/cli/src/bundler/macos.rs @@ -482,7 +482,11 @@ impl BundleContext<'_> { ); let version = self.version_string(); - let bundle_version = macos_settings.bundle_version.as_deref().unwrap_or(&version); + let bundle_version = self + .build + .apple_bundle_version_override() + .or_else(|| macos_settings.bundle_version.clone()) + .unwrap_or_else(|| version.clone()); dict.insert( "CFBundleShortVersionString".into(), diff --git a/packages/cli/src/cli/target.rs b/packages/cli/src/cli/target.rs index ea77d895c..4ca7d66cf 100644 --- a/packages/cli/src/cli/target.rs +++ b/packages/cli/src/cli/target.rs @@ -162,6 +162,22 @@ pub(crate) struct TargetArgs { #[clap(long, default_value_t = true, help_heading = HELP_HEADING, num_args = 0..=1)] pub(crate) wasm_js_cfg: bool, + /// The Android `versionCode` to stamp into the generated Gradle project. + /// + /// Must be between 1 and 2100000000 (the Google Play Store maximum). Overrides + /// `[android] version_code` in Dioxus.toml. If neither is set, the version code is + /// derived from the crate version as `major * 1000000 + minor * 1000 + patch`. + #[clap(long, env = "DX_ANDROID_VERSION_CODE", help_heading = HELP_HEADING)] + pub(crate) version_code: Option, + + /// The bundle version (CFBundleVersion) to stamp into the Info.plist of iOS and macOS bundles. + /// + /// This is the machine-readable build number that must increase with every upload to + /// App Store Connect. Overrides `[ios] bundle_version` / `[macos] bundle_version` in + /// Dioxus.toml. Defaults to the crate version. + #[clap(long, env = "DX_BUNDLE_VERSION", help_heading = HELP_HEADING)] + pub(crate) bundle_version: Option, + /// The Windows subsystem to use when building for Windows targets. This can be either `CONSOLE` or `WINDOWS`. /// /// By default, DX uses `WINDOWS` since it assumes a GUI application, but you can override this behavior with this flag. @@ -200,6 +216,8 @@ impl Anonymized for TargetArgs { "debug_symbols": self.debug_symbols, "keep_names": self.keep_names, "device": self.device, + "version_code": self.version_code, + "bundle_version": self.bundle_version.is_some(), "base_path": self.base_path.is_some(), "cargo_args": self.cargo_args.is_some(), "rustc_args": self.rustc_args.is_some(), diff --git a/packages/cli/src/config/manifest.rs b/packages/cli/src/config/manifest.rs index 2d133fd84..6fdb43151 100644 --- a/packages/cli/src/config/manifest.rs +++ b/packages/cli/src/config/manifest.rs @@ -326,6 +326,14 @@ pub struct IosConfig { pub long_description: Option, // === iOS-specific settings === + /// The bundle version string (CFBundleVersion). + /// + /// This is the machine-readable build number that must increase with every upload + /// to App Store Connect. Can be overridden per-build with `--bundle-version` or the + /// `DX_BUNDLE_VERSION` environment variable. Defaults to the crate version. + #[serde(default)] + pub bundle_version: Option, + /// Minimum iOS deployment target (e.g., "15.0"). #[serde(default)] pub deployment_target: Option, @@ -590,6 +598,16 @@ pub struct AndroidConfig { pub signing: Option, // === Android-specific settings === + /// The version code used for `versionCode` in the generated Gradle project. + /// + /// Must be between 1 and 2100000000 (the Google Play Store maximum). + /// + /// Can be overridden per-build with `--version-code` or the `DX_ANDROID_VERSION_CODE` + /// environment variable. If unset, it is derived from the crate version as + /// `major * 1000000 + minor * 1000 + patch`. + #[serde(default)] + pub version_code: Option, + /// Minimum SDK version. #[serde(default)] pub min_sdk: Option,