Skip to content

feat: Support custom CFBundleVersion for iOS and macOS #13030

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Apr 12, 2025
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