diff --git a/.changes/support-raw-entitlements.md b/.changes/support-raw-entitlements.md new file mode 100644 index 000000000..a23732405 --- /dev/null +++ b/.changes/support-raw-entitlements.md @@ -0,0 +1,5 @@ +--- +"tauri-bundler": minor:feat +--- + +Support providing `plist::Value` as macOS entitlements. diff --git a/.changes/universal-app-links-macos.md b/.changes/universal-app-links-macos.md new file mode 100644 index 000000000..8aef107f7 --- /dev/null +++ b/.changes/universal-app-links-macos.md @@ -0,0 +1,7 @@ +--- +"tauri-cli": minor:feat +"@tauri-apps/cli": minor:feat +"tauri-utils": minor:feat +--- + +Added support to universal app links on macOS with the `plugins > deep-link > desktop > domains` configuration. diff --git a/Cargo.lock b/Cargo.lock index 1725eaf66..56ec5ecad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8411,11 +8411,12 @@ dependencies = [ [[package]] name = "tao" -version = "0.34.1" +version = "0.34.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b65dc99ae111a3255027d1eca24a3833bb3267d4556a6defddb455f3ca4f5b6c" +checksum = "4daa814018fecdfb977b59a094df4bd43b42e8e21f88fddfc05807e6f46efaaf" dependencies = [ "bitflags 2.7.0", + "block2 0.6.0", "core-foundation 0.10.0", "core-graphics", "crossbeam-channel", diff --git a/Cargo.toml b/Cargo.toml index 7e5af58c1..670a64767 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -71,3 +71,4 @@ opt-level = "s" schemars_derive = { git = 'https://github.com/tauri-apps/schemars.git', branch = 'feat/preserve-description-newlines' } tauri = { path = "./crates/tauri" } tauri-plugin = { path = "./crates/tauri-plugin" } +tauri-utils = { path = "./crates/tauri-utils" } diff --git a/crates/tauri-bundler/Cargo.toml b/crates/tauri-bundler/Cargo.toml index 707775d2a..37e19f4f7 100644 --- a/crates/tauri-bundler/Cargo.toml +++ b/crates/tauri-bundler/Cargo.toml @@ -46,6 +46,7 @@ regex = "1" goblin = "0.9" plist = "1" + [target."cfg(target_os = \"windows\")".dependencies] bitness = "0.4" windows-registry = "0.5" diff --git a/crates/tauri-bundler/src/bundle.rs b/crates/tauri-bundler/src/bundle.rs index dae3ba71d..c5a3acc81 100644 --- a/crates/tauri-bundler/src/bundle.rs +++ b/crates/tauri-bundler/src/bundle.rs @@ -45,8 +45,8 @@ pub use self::{ category::AppCategory, settings::{ AppImageSettings, BundleBinary, BundleSettings, CustomSignCommandSettings, DebianSettings, - DmgSettings, IosSettings, MacOsSettings, PackageSettings, PackageType, PlistKind, Position, - RpmSettings, Settings, SettingsBuilder, Size, UpdaterSettings, + DmgSettings, Entitlements, IosSettings, MacOsSettings, PackageSettings, PackageType, PlistKind, + Position, RpmSettings, Settings, SettingsBuilder, Size, UpdaterSettings, }, }; pub use settings::{NsisSettings, WindowsSettings, WixLanguage, WixLanguageConfig, WixSettings}; diff --git a/crates/tauri-bundler/src/bundle/macos/app.rs b/crates/tauri-bundler/src/bundle/macos/app.rs index 7e6e6e564..25ae06a01 100644 --- a/crates/tauri-bundler/src/bundle/macos/app.rs +++ b/crates/tauri-bundler/src/bundle/macos/app.rs @@ -312,6 +312,7 @@ fn create_info_plist( plist::Value::Array( protocols .iter() + .filter(|p| !p.schemes.is_empty()) .map(|protocol| { let mut dict = plist::Dictionary::new(); dict.insert( diff --git a/crates/tauri-bundler/src/bundle/macos/sign.rs b/crates/tauri-bundler/src/bundle/macos/sign.rs index 195908ff9..8d8bf6c2b 100644 --- a/crates/tauri-bundler/src/bundle/macos/sign.rs +++ b/crates/tauri-bundler/src/bundle/macos/sign.rs @@ -6,10 +6,10 @@ use std::{ env::{var, var_os}, ffi::OsString, - path::{Path, PathBuf}, + path::PathBuf, }; -use crate::{error::NotarizeAuthError, Settings}; +use crate::{error::NotarizeAuthError, Entitlements, Settings}; pub struct SignTarget { pub path: PathBuf, @@ -51,15 +51,20 @@ pub fn sign( log::info!(action = "Signing"; "with identity \"{}\"", keychain.signing_identity()); for target in targets { - let entitlements_path = if target.is_an_executable { - settings.macos().entitlements.as_ref().map(Path::new) - } else { - None + let (entitlements_path, _temp_file) = match settings.macos().entitlements.as_ref() { + Some(Entitlements::Path(path)) => (Some(path.to_owned()), None), + Some(Entitlements::Plist(plist)) => { + let mut temp_file = tempfile::NamedTempFile::new()?; + plist::to_writer_xml(temp_file.as_file_mut(), &plist)?; + (Some(temp_file.path().to_path_buf()), Some(temp_file)) + } + None => (None, None), }; + keychain .sign( &target.path, - entitlements_path, + entitlements_path.as_deref(), target.is_an_executable && settings.macos().hardened_runtime, ) .map_err(Box::new)?; diff --git a/crates/tauri-bundler/src/bundle/settings.rs b/crates/tauri-bundler/src/bundle/settings.rs index 4b3165cc5..62f7813d2 100644 --- a/crates/tauri-bundler/src/bundle/settings.rs +++ b/crates/tauri-bundler/src/bundle/settings.rs @@ -360,12 +360,21 @@ pub struct MacOsSettings { pub hardened_runtime: bool, /// Provider short name for notarization. pub provider_short_name: Option, - /// Path to the entitlements.plist file. - pub entitlements: Option, + /// Path or contents of the entitlements.plist file. + pub entitlements: Option, /// Path to the Info.plist file or raw plist value to merge with the bundle Info.plist. pub info_plist: Option, } +/// Entitlements for macOS code signing. +#[derive(Debug, Clone)] +pub enum Entitlements { + /// Path to the entitlements.plist file. + Path(PathBuf), + /// Raw plist::Value. + Plist(plist::Value), +} + /// Plist format. #[derive(Debug, Clone)] pub enum PlistKind { diff --git a/crates/tauri-bundler/src/bundle/windows/msi/mod.rs b/crates/tauri-bundler/src/bundle/windows/msi/mod.rs index be0624f16..cc5f83883 100644 --- a/crates/tauri-bundler/src/bundle/windows/msi/mod.rs +++ b/crates/tauri-bundler/src/bundle/windows/msi/mod.rs @@ -697,7 +697,9 @@ pub fn build_wix_app_installer( .iter() .flat_map(|p| &p.schemes) .collect::>(); - data.insert("deep_link_protocols", to_json(schemes)); + if !schemes.is_empty() { + data.insert("deep_link_protocols", to_json(schemes)); + } } if let Some(path) = custom_template_path { diff --git a/crates/tauri-bundler/src/bundle/windows/nsis/mod.rs b/crates/tauri-bundler/src/bundle/windows/nsis/mod.rs index d63a93241..d1729c82f 100644 --- a/crates/tauri-bundler/src/bundle/windows/nsis/mod.rs +++ b/crates/tauri-bundler/src/bundle/windows/nsis/mod.rs @@ -495,7 +495,9 @@ fn build_nsis_app_installer( .iter() .flat_map(|p| &p.schemes) .collect::>(); - data.insert("deep_link_protocols", to_json(schemes)); + if !schemes.is_empty() { + data.insert("deep_link_protocols", to_json(schemes)); + } } let silent_webview2_install = if let WebviewInstallMode::DownloadBootstrapper { silent } diff --git a/crates/tauri-cli/src/interface/rust.rs b/crates/tauri-cli/src/interface/rust.rs index b6e176e85..710bc7e96 100644 --- a/crates/tauri-cli/src/interface/rust.rs +++ b/crates/tauri-cli/src/interface/rust.rs @@ -858,7 +858,7 @@ impl AppSettings for RustAppSettings { let mut settings = tauri_config_to_bundle_settings( self, features, - config.identifier.clone(), + config, config.bundle.clone(), updater_settings, arch64bits, @@ -1263,7 +1263,7 @@ pub fn get_profile_dir(options: &Options) -> &str { fn tauri_config_to_bundle_settings( settings: &RustAppSettings, features: &[String], - identifier: String, + tauri_config: &Config, config: crate::helpers::config::BundleConfig, updater_config: Option, arch64bits: bool, @@ -1386,8 +1386,59 @@ fn tauri_config_to_bundle_settings( BundleResources::Map(map) => (None, Some(map)), }; + #[cfg(target_os = "macos")] + let entitlements = if let Some(plugin_config) = tauri_config + .plugins + .0 + .get("deep-link") + .and_then(|c| c.get("desktop").cloned()) + { + let protocols: DesktopDeepLinks = + serde_json::from_value(plugin_config).context("failed to parse deep link plugin config")?; + let domains = match protocols { + DesktopDeepLinks::One(protocol) => protocol.domains, + DesktopDeepLinks::List(protocols) => protocols.into_iter().flat_map(|p| p.domains).collect(), + }; + + if domains.is_empty() { + config + .macos + .entitlements + .map(PathBuf::from) + .map(tauri_bundler::bundle::Entitlements::Path) + } else { + let mut app_links_entitlements = plist::Dictionary::new(); + app_links_entitlements.insert( + "com.apple.developer.associated-domains".to_string(), + domains + .into_iter() + .map(|domain| format!("applinks:{domain}").into()) + .collect::>() + .into(), + ); + let entitlements = if let Some(user_provided_entitlements) = config.macos.entitlements { + crate::helpers::plist::merge_plist(vec![ + PathBuf::from(user_provided_entitlements).into(), + plist::Value::Dictionary(app_links_entitlements).into(), + ])? + } else { + app_links_entitlements.into() + }; + + Some(tauri_bundler::bundle::Entitlements::Plist(entitlements)) + } + } else { + config + .macos + .entitlements + .map(PathBuf::from) + .map(tauri_bundler::bundle::Entitlements::Path) + }; + #[cfg(not(target_os = "macos"))] + let entitlements = None; + Ok(BundleSettings { - identifier: Some(identifier), + identifier: Some(tauri_config.identifier.clone()), publisher: config.publisher, homepage: config.homepage, icon: Some(config.icon), @@ -1487,7 +1538,7 @@ fn tauri_config_to_bundle_settings( skip_stapling: false, hardened_runtime: config.macos.hardened_runtime, provider_short_name, - entitlements: config.macos.entitlements, + entitlements, #[cfg(not(target_os = "macos"))] info_plist: None, #[cfg(target_os = "macos")] diff --git a/crates/tauri-runtime-wry/Cargo.toml b/crates/tauri-runtime-wry/Cargo.toml index 6271f64dd..d2d7a34ff 100644 --- a/crates/tauri-runtime-wry/Cargo.toml +++ b/crates/tauri-runtime-wry/Cargo.toml @@ -23,7 +23,7 @@ wry = { version = "0.53.2", default-features = false, features = [ "os-webview", "linux-body", ] } -tao = { version = "0.34.1", default-features = false, features = ["rwh_06"] } +tao = { version = "0.34.2", default-features = false, features = ["rwh_06"] } tauri-runtime = { version = "2.8.0", path = "../tauri-runtime" } tauri-utils = { version = "2.7.0", path = "../tauri-utils" } raw-window-handle = "0.6" diff --git a/crates/tauri-utils/src/config.rs b/crates/tauri-utils/src/config.rs index a42ae99b8..a967815e7 100644 --- a/crates/tauri-utils/src/config.rs +++ b/crates/tauri-utils/src/config.rs @@ -1198,7 +1198,17 @@ pub struct FileAssociation { #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct DeepLinkProtocol { /// URL schemes to associate with this app without `://`. For example `my-app` + #[serde(default)] pub schemes: Vec, + /// Domains to associate with this app. For example `example.com`. + /// Currently only supported on macOS, translating to an [universal app link]. + /// + /// Note that universal app links require signed apps with a provisioning profile to work. + /// You can accomplish that by including the `embedded.provisionprofile` file in the `macOS > files` option. + /// + /// [universal app link]: https://developer.apple.com/documentation/xcode/supporting-universal-links-in-your-app + #[serde(default)] + pub domains: Vec, /// The protocol name. **macOS-only** and maps to `CFBundleTypeName`. Defaults to `.` pub name: Option, /// The app's role for these schemes. **macOS-only** and maps to `CFBundleTypeRole`.