Skip to content
Merged
8 changes: 8 additions & 0 deletions .changes/ios-macos-bundleversion.md
Original file line number Diff line number Diff line change
@@ -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`.
4 changes: 2 additions & 2 deletions crates/tauri-bundler/src/bundle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down
16 changes: 9 additions & 7 deletions crates/tauri-bundler/src/bundle/macos/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -191,12 +191,6 @@ fn create_info_plist(
bundle_icon_file: Option<PathBuf>,
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());
Expand Down Expand Up @@ -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(
Expand Down
6 changes: 5 additions & 1 deletion crates/tauri-bundler/src/bundle/macos/ios.rs
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,11 @@ fn generate_info_plist(
writeln!(
file,
" <key>CFBundleVersion</key>\n <string>{}</string>",
settings.version_string()
settings
.ios()
.bundle_version
.as_deref()
.unwrap_or_else(|| settings.version_string())
)?;
writeln!(
file,
Expand Down
16 changes: 16 additions & 0 deletions crates/tauri-bundler/src/bundle/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,
}

/// The macOS bundle settings.
#[derive(Clone, Debug, Default)]
pub struct MacOsSettings {
Expand All @@ -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<PathBuf, PathBuf>,
/// The version of the build that identifies an iteration of the bundle.
pub bundle_version: Option<String>,
/// 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<String>,
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
16 changes: 15 additions & 1 deletion crates/tauri-cli/config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 <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"
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
35 changes: 21 additions & 14 deletions crates/tauri-cli/src/interface/rust.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion crates/tauri-cli/src/mobile/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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_,
Expand Down
36 changes: 32 additions & 4 deletions crates/tauri-cli/src/mobile/ios/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ use std::{
env::{set_current_dir, var, var_os},
fs,
path::PathBuf,
str::FromStr,
};

#[derive(Debug, Clone, Parser)]
Expand Down Expand Up @@ -166,7 +167,7 @@ pub fn command(options: Options, noise_level: NoiseLevel) -> Result<()> {
tauri_config_,
build_options.features.as_ref(),
&Default::default(),
);
)?;
(interface, config)
};

Expand All @@ -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()
Expand Down
2 changes: 1 addition & 1 deletion crates/tauri-cli/src/mobile/ios/dev.rs
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ fn run_command(options: Options, noise_level: NoiseLevel) -> Result<()> {
tauri_config_,
dev_options.features.as_ref(),
&Default::default(),
);
)?;

(interface, config)
};
Expand Down
46 changes: 41 additions & 5 deletions crates/tauri-cli/src/mobile/ios/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT

use anyhow::Context;
use cargo_mobile2::{
apple::{
config::{
Expand Down Expand Up @@ -39,6 +40,7 @@ use std::{
env::{set_var, var_os},
fs::create_dir_all,
path::PathBuf,
str::FromStr,
thread::sleep,
time::Duration,
};
Expand Down Expand Up @@ -112,7 +114,7 @@ pub fn get_config(
tauri_config: &TauriConfig,
features: Option<&Vec<String>>,
cli_options: &CliOptions,
) -> (AppleConfig, AppleMetadata) {
) -> Result<(AppleConfig, AppleMetadata)> {
let mut ios_options = cli_options.clone();
if let Some(features) = features {
ios_options
Expand All @@ -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()
Expand All @@ -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();

Expand Down Expand Up @@ -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<Device<'a>> {
Expand Down
2 changes: 1 addition & 1 deletion crates/tauri-cli/src/mobile/ios/xcode_script.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ pub fn command(options: Options) -> Result<()> {
tauri_config_,
None,
&cli_options,
);
)?;
(config, metadata, cli_options)
};
ensure_init(
Expand Down
9 changes: 8 additions & 1 deletion crates/tauri-codegen/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -313,8 +313,15 @@ pub fn context_codegen(data: ContextData) -> EmbeddedAssetsResult<TokenStream> {
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(),
);
}
}

Expand Down
Loading
Loading