diff --git a/.changes/ios-macos-bundleversion.md b/.changes/ios-macos-bundleversion.md new file mode 100644 index 000000000000..6e3146ddd85d --- /dev/null +++ b/.changes/ios-macos-bundleversion.md @@ -0,0 +1,8 @@ +--- +"tauri-utils": minor:feat +"@tauri-apps/cli": minor:feat +"tauri-cli": minor:feat +"tauri-bundler": minor:feat +--- + +Added `bundleVersion` to iOS and macOS configuration to support specifying a `CFBundleVersion`. diff --git a/crates/tauri-bundler/src/bundle.rs b/crates/tauri-bundler/src/bundle.rs index 682f2a06c31f..8ec19148be5f 100644 --- a/crates/tauri-bundler/src/bundle.rs +++ b/crates/tauri-bundler/src/bundle.rs @@ -19,8 +19,8 @@ pub use self::{ category::AppCategory, settings::{ AppImageSettings, BundleBinary, BundleSettings, CustomSignCommandSettings, DebianSettings, - DmgSettings, MacOsSettings, PackageSettings, PackageType, Position, RpmSettings, Settings, - SettingsBuilder, Size, UpdaterSettings, + DmgSettings, IosSettings, MacOsSettings, PackageSettings, PackageType, Position, RpmSettings, + Settings, SettingsBuilder, Size, UpdaterSettings, }, }; #[cfg(target_os = "macos")] diff --git a/crates/tauri-bundler/src/bundle/macos/app.rs b/crates/tauri-bundler/src/bundle/macos/app.rs index 20cdd990911e..213ad4f36e35 100644 --- a/crates/tauri-bundler/src/bundle/macos/app.rs +++ b/crates/tauri-bundler/src/bundle/macos/app.rs @@ -191,12 +191,6 @@ fn create_info_plist( bundle_icon_file: Option, settings: &Settings, ) -> crate::Result<()> { - let format = time::format_description::parse("[year][month][day].[hour][minute][second]") - .map_err(time::error::Error::from)?; - let build_number = time::OffsetDateTime::now_utc() - .format(&format) - .map_err(time::error::Error::from)?; - let mut plist = plist::Dictionary::new(); plist.insert("CFBundleDevelopmentRegion".into(), "English".into()); plist.insert("CFBundleDisplayName".into(), settings.product_name().into()); @@ -226,7 +220,15 @@ fn create_info_plist( "CFBundleShortVersionString".into(), settings.version_string().into(), ); - plist.insert("CFBundleVersion".into(), build_number.into()); + plist.insert( + "CFBundleVersion".into(), + settings + .macos() + .bundle_version + .as_deref() + .unwrap_or_else(|| settings.version_string()) + .into(), + ); plist.insert("CSResourcesFileMapped".into(), true.into()); if let Some(category) = settings.app_category() { plist.insert( diff --git a/crates/tauri-bundler/src/bundle/macos/ios.rs b/crates/tauri-bundler/src/bundle/macos/ios.rs index 5f4a87a0e538..421f43c63e79 100644 --- a/crates/tauri-bundler/src/bundle/macos/ios.rs +++ b/crates/tauri-bundler/src/bundle/macos/ios.rs @@ -178,7 +178,11 @@ fn generate_info_plist( writeln!( file, " CFBundleVersion\n {}", - settings.version_string() + settings + .ios() + .bundle_version + .as_deref() + .unwrap_or_else(|| settings.version_string()) )?; writeln!( file, diff --git a/crates/tauri-bundler/src/bundle/settings.rs b/crates/tauri-bundler/src/bundle/settings.rs index 2538f6826802..b1ac81c9e787 100644 --- a/crates/tauri-bundler/src/bundle/settings.rs +++ b/crates/tauri-bundler/src/bundle/settings.rs @@ -306,6 +306,13 @@ pub struct DmgSettings { pub application_folder_position: Position, } +/// The iOS bundle settings. +#[derive(Clone, Debug, Default)] +pub struct IosSettings { + /// The version of the build that identifies an iteration of the bundle. + pub bundle_version: Option, +} + /// The macOS bundle settings. #[derive(Clone, Debug, Default)] pub struct MacOsSettings { @@ -323,6 +330,8 @@ pub struct MacOsSettings { /// List of custom files to add to the application bundle. /// Maps the path in the Contents directory in the app to the path of the file to include (relative to the current working directory). pub files: HashMap, + /// The version of the build that identifies an iteration of the bundle. + pub bundle_version: Option, /// A version string indicating the minimum MacOS version that the bundled app supports (e.g. `"10.11"`). /// If you are using this config field, you may also want have your `build.rs` script emit `cargo:rustc-env=MACOSX_DEPLOYMENT_TARGET=10.11`. pub minimum_system_version: Option, @@ -643,6 +652,8 @@ pub struct BundleSettings { pub rpm: RpmSettings, /// DMG-specific settings. pub dmg: DmgSettings, + /// iOS-specific settings. + pub ios: IosSettings, /// MacOS-specific settings. pub macos: MacOsSettings, /// Updater configuration. @@ -1190,6 +1201,11 @@ impl Settings { &self.bundle_settings.dmg } + /// Returns the iOS settings. + pub fn ios(&self) -> &IosSettings { + &self.bundle_settings.ios + } + /// Returns the MacOS settings. pub fn macos(&self) -> &MacOsSettings { &self.bundle_settings.macos diff --git a/crates/tauri-cli/config.schema.json b/crates/tauri-cli/config.schema.json index 5ca75b858442..d31da69368a7 100644 --- a/crates/tauri-cli/config.schema.json +++ b/crates/tauri-cli/config.schema.json @@ -31,7 +31,7 @@ ] }, "version": { - "description": "App version. It is a semver version number or a path to a `package.json` file containing the `version` field. If removed the version number from `Cargo.toml` is used.\n\n By default version 1.0 is used on Android.", + "description": "App version. It is a semver version number or a path to a `package.json` file containing the `version` field.\n\n If removed the version number from `Cargo.toml` is used.\n It's recommended to manage the app versioning in the Tauri config.\n\n ## Platform-specific\n\n - **macOS**: Translates to the bundle's CFBundleShortVersionString property and is used as the default CFBundleVersion.\n You can set an specific bundle version using [`bundle > macOS > bundleVersion`](MacConfig::bundle_version).\n - **iOS**: Translates to the bundle's CFBundleShortVersionString property and is used as the default CFBundleVersion.\n You can set an specific bundle version using [`bundle > iOS > bundleVersion`](IosConfig::bundle_version).\n The `tauri ios build` CLI command has a `--build-number ` option that lets you append a build number to the app version.\n - **Android**: By default version 1.0 is used. You can set a version code using [`bundle > android > versionCode`](AndroidConfig::version_code).\n\n By default version 1.0 is used on Android.", "type": [ "string", "null" @@ -3297,6 +3297,13 @@ "type": "string" } }, + "bundleVersion": { + "description": "The version of the build that identifies an iteration of the bundle.\n\n Translates to the bundle's CFBundleVersion property.", + "type": [ + "string", + "null" + ] + }, "minimumSystemVersion": { "description": "A version string indicating the minimum macOS X version that the bundled application supports. Defaults to `10.13`.\n\n Setting it to `null` completely removes the `LSMinimumSystemVersion` field on the bundle's `Info.plist`\n and the `MACOSX_DEPLOYMENT_TARGET` environment variable.\n\n An empty string is considered an invalid value so the default value is used.", "default": "10.13", @@ -3498,6 +3505,13 @@ "null" ] }, + "bundleVersion": { + "description": "The version of the build that identifies an iteration of the bundle.\n\n Translates to the bundle's CFBundleVersion property.", + "type": [ + "string", + "null" + ] + }, "minimumSystemVersion": { "description": "A version string indicating the minimum iOS version that the bundled application supports. Defaults to `13.0`.\n\n Maps to the IPHONEOS_DEPLOYMENT_TARGET value.", "default": "13.0", diff --git a/crates/tauri-cli/src/interface/rust.rs b/crates/tauri-cli/src/interface/rust.rs index 896b92983e5c..53ba9350aae3 100644 --- a/crates/tauri-cli/src/interface/rust.rs +++ b/crates/tauri-cli/src/interface/rust.rs @@ -22,7 +22,8 @@ use notify_debouncer_full::new_debouncer; use serde::{Deserialize, Deserializer}; use tauri_bundler::{ AppCategory, AppImageSettings, BundleBinary, BundleSettings, DebianSettings, DmgSettings, - MacOsSettings, PackageSettings, Position, RpmSettings, Size, UpdaterSettings, WindowsSettings, + IosSettings, MacOsSettings, PackageSettings, Position, RpmSettings, Size, UpdaterSettings, + WindowsSettings, }; use tauri_utils::config::{parse::is_configuration_file, DeepLinkProtocol, Updater}; @@ -1016,24 +1017,26 @@ impl RustAppSettings { .workspace .and_then(|v| v.package); + let version = config.version.clone().unwrap_or_else(|| { + cargo_package_settings + .version + .clone() + .expect("Cargo manifest must have the `package.version` field") + .resolve("version", || { + ws_package_settings + .as_ref() + .and_then(|p| p.version.clone()) + .ok_or_else(|| anyhow::anyhow!("Couldn't inherit value for `version` from workspace")) + }) + .expect("Cargo project does not have a version") + }); + let package_settings = PackageSettings { product_name: config .product_name .clone() .unwrap_or_else(|| cargo_package_settings.name.clone()), - version: config.version.clone().unwrap_or_else(|| { - cargo_package_settings - .version - .clone() - .expect("Cargo manifest must have the `package.version` field") - .resolve("version", || { - ws_package_settings - .as_ref() - .and_then(|p| p.version.clone()) - .ok_or_else(|| anyhow::anyhow!("Couldn't inherit value for `version` from workspace")) - }) - .expect("Cargo project does not have a version") - }), + version: version.clone(), description: cargo_package_settings .description .clone() @@ -1418,9 +1421,13 @@ fn tauri_config_to_bundle_settings( y: config.macos.dmg.application_folder_position.y, }, }, + ios: IosSettings { + bundle_version: config.ios.bundle_version, + }, macos: MacOsSettings { frameworks: config.macos.frameworks, files: config.macos.files, + bundle_version: config.macos.bundle_version, minimum_system_version: config.macos.minimum_system_version, exception_domain: config.macos.exception_domain, signing_identity, diff --git a/crates/tauri-cli/src/mobile/init.rs b/crates/tauri-cli/src/mobile/init.rs index eaa5ea991435..8cdcbf19f321 100644 --- a/crates/tauri-cli/src/mobile/init.rs +++ b/crates/tauri-cli/src/mobile/init.rs @@ -142,7 +142,7 @@ pub fn exec( // Generate Xcode project Target::Ios => { let (config, metadata) = - super::ios::get_config(&app, tauri_config_, None, &Default::default()); + super::ios::get_config(&app, tauri_config_, None, &Default::default())?; map.insert("apple", &config); super::ios::project::gen( tauri_config_, diff --git a/crates/tauri-cli/src/mobile/ios/build.rs b/crates/tauri-cli/src/mobile/ios/build.rs index 529d59faf36a..31d9f724415c 100644 --- a/crates/tauri-cli/src/mobile/ios/build.rs +++ b/crates/tauri-cli/src/mobile/ios/build.rs @@ -36,6 +36,7 @@ use std::{ env::{set_current_dir, var, var_os}, fs, path::PathBuf, + str::FromStr, }; #[derive(Debug, Clone, Parser)] @@ -166,7 +167,7 @@ pub fn command(options: Options, noise_level: NoiseLevel) -> Result<()> { tauri_config_, build_options.features.as_ref(), &Default::default(), - ); + )?; (interface, config) }; @@ -182,9 +183,36 @@ pub fn command(options: Options, noise_level: NoiseLevel) -> Result<()> { inject_resources(&config, tauri_config.lock().unwrap().as_ref().unwrap())?; let mut plist = plist::Dictionary::new(); - let version = interface.app_settings().get_package_settings().version; - plist.insert("CFBundleShortVersionString".into(), version.clone().into()); - plist.insert("CFBundleVersion".into(), version.into()); + { + let tauri_config_guard = tauri_config.lock().unwrap(); + let tauri_config_ = tauri_config_guard.as_ref().unwrap(); + let app_version = tauri_config_ + .version + .clone() + .unwrap_or_else(|| interface.app_settings().get_package_settings().version); + + let mut version = semver::Version::from_str(&app_version) + .with_context(|| format!("failed to parse {app_version:?} as a semver string"))?; + if !version.pre.is_empty() { + log::info!( + "CFBundleShortVersionString cannot have prerelease identifier; stripping {}", + version.pre.as_str() + ); + version.pre = semver::Prerelease::EMPTY; + } + if !version.build.is_empty() { + log::info!( + "CFBundleShortVersionString cannot have build number; stripping {}", + version.build.as_str() + ); + version.build = semver::BuildMetadata::EMPTY; + } + + plist.insert( + "CFBundleShortVersionString".into(), + version.to_string().into(), + ); + }; let info_plist_path = config .project_dir() diff --git a/crates/tauri-cli/src/mobile/ios/dev.rs b/crates/tauri-cli/src/mobile/ios/dev.rs index 3e971c3bc4bb..1fe890f6ea0b 100644 --- a/crates/tauri-cli/src/mobile/ios/dev.rs +++ b/crates/tauri-cli/src/mobile/ios/dev.rs @@ -168,7 +168,7 @@ fn run_command(options: Options, noise_level: NoiseLevel) -> Result<()> { tauri_config_, dev_options.features.as_ref(), &Default::default(), - ); + )?; (interface, config) }; diff --git a/crates/tauri-cli/src/mobile/ios/mod.rs b/crates/tauri-cli/src/mobile/ios/mod.rs index dd1fb0f46ccd..23fc24ff5f80 100644 --- a/crates/tauri-cli/src/mobile/ios/mod.rs +++ b/crates/tauri-cli/src/mobile/ios/mod.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT +use anyhow::Context; use cargo_mobile2::{ apple::{ config::{ @@ -39,6 +40,7 @@ use std::{ env::{set_var, var_os}, fs::create_dir_all, path::PathBuf, + str::FromStr, thread::sleep, time::Duration, }; @@ -112,7 +114,7 @@ pub fn get_config( tauri_config: &TauriConfig, features: Option<&Vec>, cli_options: &CliOptions, -) -> (AppleConfig, AppleMetadata) { +) -> Result<(AppleConfig, AppleMetadata)> { let mut ios_options = cli_options.clone(); if let Some(features) = features { ios_options @@ -121,6 +123,41 @@ pub fn get_config( .extend_from_slice(features); } + let bundle_version = if let Some(bundle_version) = tauri_config + .bundle + .ios + .bundle_version + .clone() + .or_else(|| tauri_config.version.clone()) + { + let mut version = semver::Version::from_str(&bundle_version) + .with_context(|| format!("failed to parse {bundle_version:?} as a semver string"))?; + if !version.pre.is_empty() { + if let Some((_prerelease_tag, number)) = version.pre.as_str().to_string().split_once('.') { + version.pre = semver::Prerelease::EMPTY; + if version.build.is_empty() { + version.build = semver::BuildMetadata::new(number) + .with_context(|| format!("bundle version {number:?} prerelease is invalid"))?; + } else { + anyhow::bail!("bundle version {bundle_version:?} is invalid, it cannot have both prerelease and build metadata"); + } + } + } + + let maybe_build_number = if version.build.is_empty() { + "".to_string() + } else { + format!(".{}", version.build.as_str()) + }; + + Some(format!( + "{}.{}.{}{}", + version.major, version.minor, version.patch, maybe_build_number + )) + } else { + None + }; + let raw = RawAppleConfig { development_team: std::env::var(APPLE_DEVELOPMENT_TEAM_ENV_VAR_NAME) .ok() @@ -140,12 +177,11 @@ pub fn get_config( } }), ios_features: ios_options.features.clone(), - bundle_version: tauri_config.version.clone(), - bundle_version_short: tauri_config.version.clone(), + bundle_version, ios_version: Some(tauri_config.bundle.ios.minimum_system_version.clone()), ..Default::default() }; - let config = AppleConfig::from_raw(app.clone(), Some(raw)).unwrap(); + let config = AppleConfig::from_raw(app.clone(), Some(raw))?; let tauri_dir = tauri_dir(); @@ -194,7 +230,7 @@ pub fn get_config( set_var("TAURI_IOS_PROJECT_PATH", config.project_dir()); set_var("TAURI_IOS_APP_NAME", config.app().name()); - (config, metadata) + Ok((config, metadata)) } fn connected_device_prompt<'a>(env: &'_ Env, target: Option<&str>) -> Result> { diff --git a/crates/tauri-cli/src/mobile/ios/xcode_script.rs b/crates/tauri-cli/src/mobile/ios/xcode_script.rs index 7c63a2f6f665..ce0c9cc3462c 100644 --- a/crates/tauri-cli/src/mobile/ios/xcode_script.rs +++ b/crates/tauri-cli/src/mobile/ios/xcode_script.rs @@ -97,7 +97,7 @@ pub fn command(options: Options) -> Result<()> { tauri_config_, None, &cli_options, - ); + )?; (config, metadata, cli_options) }; ensure_init( diff --git a/crates/tauri-codegen/src/context.rs b/crates/tauri-codegen/src/context.rs index c253637fdce4..e2c759f79104 100644 --- a/crates/tauri-codegen/src/context.rs +++ b/crates/tauri-codegen/src/context.rs @@ -313,8 +313,15 @@ pub fn context_codegen(data: ContextData) -> EmbeddedAssetsResult { plist.insert("CFBundleName".into(), product_name.clone().into()); } if let Some(version) = &config.version { + let bundle_version = &config.bundle.macos.bundle_version; plist.insert("CFBundleShortVersionString".into(), version.clone().into()); - plist.insert("CFBundleVersion".into(), version.clone().into()); + plist.insert( + "CFBundleVersion".into(), + bundle_version + .clone() + .unwrap_or_else(|| version.clone()) + .into(), + ); } } diff --git a/crates/tauri-schema-generator/schemas/config.schema.json b/crates/tauri-schema-generator/schemas/config.schema.json index 5ca75b858442..d31da69368a7 100644 --- a/crates/tauri-schema-generator/schemas/config.schema.json +++ b/crates/tauri-schema-generator/schemas/config.schema.json @@ -31,7 +31,7 @@ ] }, "version": { - "description": "App version. It is a semver version number or a path to a `package.json` file containing the `version` field. If removed the version number from `Cargo.toml` is used.\n\n By default version 1.0 is used on Android.", + "description": "App version. It is a semver version number or a path to a `package.json` file containing the `version` field.\n\n If removed the version number from `Cargo.toml` is used.\n It's recommended to manage the app versioning in the Tauri config.\n\n ## Platform-specific\n\n - **macOS**: Translates to the bundle's CFBundleShortVersionString property and is used as the default CFBundleVersion.\n You can set an specific bundle version using [`bundle > macOS > bundleVersion`](MacConfig::bundle_version).\n - **iOS**: Translates to the bundle's CFBundleShortVersionString property and is used as the default CFBundleVersion.\n You can set an specific bundle version using [`bundle > iOS > bundleVersion`](IosConfig::bundle_version).\n The `tauri ios build` CLI command has a `--build-number ` option that lets you append a build number to the app version.\n - **Android**: By default version 1.0 is used. You can set a version code using [`bundle > android > versionCode`](AndroidConfig::version_code).\n\n By default version 1.0 is used on Android.", "type": [ "string", "null" @@ -3297,6 +3297,13 @@ "type": "string" } }, + "bundleVersion": { + "description": "The version of the build that identifies an iteration of the bundle.\n\n Translates to the bundle's CFBundleVersion property.", + "type": [ + "string", + "null" + ] + }, "minimumSystemVersion": { "description": "A version string indicating the minimum macOS X version that the bundled application supports. Defaults to `10.13`.\n\n Setting it to `null` completely removes the `LSMinimumSystemVersion` field on the bundle's `Info.plist`\n and the `MACOSX_DEPLOYMENT_TARGET` environment variable.\n\n An empty string is considered an invalid value so the default value is used.", "default": "10.13", @@ -3498,6 +3505,13 @@ "null" ] }, + "bundleVersion": { + "description": "The version of the build that identifies an iteration of the bundle.\n\n Translates to the bundle's CFBundleVersion property.", + "type": [ + "string", + "null" + ] + }, "minimumSystemVersion": { "description": "A version string indicating the minimum iOS version that the bundled application supports. Defaults to `13.0`.\n\n Maps to the IPHONEOS_DEPLOYMENT_TARGET value.", "default": "13.0", diff --git a/crates/tauri-utils/src/config.rs b/crates/tauri-utils/src/config.rs index fd2e59a4553e..7747918662b1 100644 --- a/crates/tauri-utils/src/config.rs +++ b/crates/tauri-utils/src/config.rs @@ -612,6 +612,11 @@ pub struct MacConfig { /// The files to include in the application relative to the Contents directory. #[serde(default)] pub files: HashMap, + /// The version of the build that identifies an iteration of the bundle. + /// + /// Translates to the bundle's CFBundleVersion property. + #[serde(alias = "bundle-version")] + pub bundle_version: Option, /// A version string indicating the minimum macOS X version that the bundled application supports. Defaults to `10.13`. /// /// Setting it to `null` completely removes the `LSMinimumSystemVersion` field on the bundle's `Info.plist` @@ -651,6 +656,7 @@ impl Default for MacConfig { Self { frameworks: None, files: HashMap::new(), + bundle_version: None, minimum_system_version: macos_minimum_system_version(), exception_domain: None, signing_identity: None, @@ -2527,6 +2533,11 @@ pub struct IosConfig { /// The `APPLE_DEVELOPMENT_TEAM` environment variable can be set to overwrite it. #[serde(alias = "development-team")] pub development_team: Option, + /// The version of the build that identifies an iteration of the bundle. + /// + /// Translates to the bundle's CFBundleVersion property. + #[serde(alias = "bundle-version")] + pub bundle_version: Option, /// A version string indicating the minimum iOS version that the bundled application supports. Defaults to `13.0`. /// /// Maps to the IPHONEOS_DEPLOYMENT_TARGET value. @@ -2543,6 +2554,7 @@ impl Default for IosConfig { template: None, frameworks: None, development_team: None, + bundle_version: None, minimum_system_version: ios_minimum_system_version(), } } @@ -2846,7 +2858,19 @@ pub struct Config { /// App main binary filename. Defaults to the name of your cargo crate. #[serde(alias = "main-binary-name")] pub main_binary_name: Option, - /// App version. It is a semver version number or a path to a `package.json` file containing the `version` field. If removed the version number from `Cargo.toml` is used. + /// App version. It is a semver version number or a path to a `package.json` file containing the `version` field. + /// + /// If removed the version number from `Cargo.toml` is used. + /// It's recommended to manage the app versioning in the Tauri config. + /// + /// ## Platform-specific + /// + /// - **macOS**: Translates to the bundle's CFBundleShortVersionString property and is used as the default CFBundleVersion. + /// You can set an specific bundle version using [`bundle > macOS > bundleVersion`](MacConfig::bundle_version). + /// - **iOS**: Translates to the bundle's CFBundleShortVersionString property and is used as the default CFBundleVersion. + /// You can set an specific bundle version using [`bundle > iOS > bundleVersion`](IosConfig::bundle_version). + /// The `tauri ios build` CLI command has a `--build-number ` option that lets you append a build number to the app version. + /// - **Android**: By default version 1.0 is used. You can set a version code using [`bundle > android > versionCode`](AndroidConfig::version_code). /// /// By default version 1.0 is used on Android. #[serde(deserialize_with = "version_deserializer", default)]