diff --git a/.changes/feat-skip-stapling.md b/.changes/feat-skip-stapling.md new file mode 100644 index 000000000..e9aa61a30 --- /dev/null +++ b/.changes/feat-skip-stapling.md @@ -0,0 +1,7 @@ +--- +tauri-macos-sign: 'minor:feat' +tauri-bundler: 'minor:feat' +tauri-cli: 'minor:feat' +--- + +Added a `--skip-stapling` option to make `tauri build|bundle` _not_ wait for notarization to finish on macOS. diff --git a/crates/tauri-bundler/src/bundle/macos/app.rs b/crates/tauri-bundler/src/bundle/macos/app.rs index 00a8596bf..b5ef1f816 100644 --- a/crates/tauri-bundler/src/bundle/macos/app.rs +++ b/crates/tauri-bundler/src/bundle/macos/app.rs @@ -24,7 +24,7 @@ use super::{ icon::create_icns_file, - sign::{notarize, notarize_auth, sign, NotarizeAuthError, SignTarget}, + sign::{notarize, notarize_auth, notarize_without_stapling, sign, NotarizeAuthError, SignTarget}, }; use crate::{ utils::{fs_utils, CommandExt}, @@ -121,7 +121,11 @@ pub fn bundle_project(settings: &Settings) -> crate::Result> { // notarization is required for distribution match notarize_auth() { Ok(auth) => { - notarize(&keychain, app_bundle_path.clone(), &auth)?; + if settings.macos().skip_stapling { + notarize_without_stapling(&keychain, app_bundle_path.clone(), &auth)?; + } else { + notarize(&keychain, app_bundle_path.clone(), &auth)?; + } } Err(e) => { if matches!(e, NotarizeAuthError::MissingTeamId) { diff --git a/crates/tauri-bundler/src/bundle/macos/sign.rs b/crates/tauri-bundler/src/bundle/macos/sign.rs index e64125025..052779c0c 100644 --- a/crates/tauri-bundler/src/bundle/macos/sign.rs +++ b/crates/tauri-bundler/src/bundle/macos/sign.rs @@ -71,6 +71,15 @@ pub fn notarize( tauri_macos_sign::notarize(keychain, &app_bundle_path, credentials).map_err(Into::into) } +pub fn notarize_without_stapling( + keychain: &tauri_macos_sign::Keychain, + app_bundle_path: PathBuf, + credentials: &tauri_macos_sign::AppleNotarizationCredentials, +) -> crate::Result<()> { + tauri_macos_sign::notarize_without_stapling(keychain, &app_bundle_path, credentials) + .map_err(Into::into) +} + #[derive(Debug, thiserror::Error)] pub enum NotarizeAuthError { #[error( diff --git a/crates/tauri-bundler/src/bundle/settings.rs b/crates/tauri-bundler/src/bundle/settings.rs index 43ddb8e4c..85642e2e6 100644 --- a/crates/tauri-bundler/src/bundle/settings.rs +++ b/crates/tauri-bundler/src/bundle/settings.rs @@ -346,6 +346,15 @@ pub struct MacOsSettings { pub exception_domain: Option, /// Code signing identity. pub signing_identity: Option, + /// Whether to wait for notarization to finish and `staple` the ticket onto the app. + /// + /// Gatekeeper will look for stapled tickets to tell whether your app was notarized without + /// reaching out to Apple's servers which is helpful in offline environments. + /// + /// Enabling this option will also result in `tauri build` not waiting for notarization to finish + /// which is helpful for the very first time your app is notarized as this can take multiple hours. + /// On subsequent runs, it's recommended to disable this setting again. + pub skip_stapling: bool, /// Preserve the hardened runtime version flag, see /// /// Settings this to `false` is useful when using an ad-hoc signature, making it less strict. diff --git a/crates/tauri-cli/src/build.rs b/crates/tauri-cli/src/build.rs index f31ab3b94..77675e8ae 100644 --- a/crates/tauri-cli/src/build.rs +++ b/crates/tauri-cli/src/build.rs @@ -60,6 +60,16 @@ pub struct Options { /// Skip prompting for values #[clap(long, env = "CI")] pub ci: bool, + /// Whether to wait for notarization to finish and `staple` the ticket onto the app. + /// + /// Gatekeeper will look for stapled tickets to tell whether your app was notarized without + /// reaching out to Apple's servers which is helpful in offline environments. + /// + /// Enabling this option will also result in `tauri build` not waiting for notarization to finish + /// which is helpful for the very first time your app is notarized as this can take multiple hours. + /// On subsequent runs, it's recommended to disable this setting again. + #[clap(long)] + pub skip_stapling: bool, } pub fn command(mut options: Options, verbosity: u8) -> Result<()> { diff --git a/crates/tauri-cli/src/bundle.rs b/crates/tauri-cli/src/bundle.rs index 507dbe006..3df28685c 100644 --- a/crates/tauri-cli/src/bundle.rs +++ b/crates/tauri-cli/src/bundle.rs @@ -82,6 +82,16 @@ pub struct Options { /// Skip prompting for values #[clap(long, env = "CI")] pub ci: bool, + /// Whether to wait for notarization to finish and `staple` the ticket onto the app. + /// + /// Gatekeeper will look for stapled tickets to tell whether your app was notarized without + /// reaching out to Apple's servers which is helpful in offline environments. + /// + /// Enabling this option will also result in `tauri build` not waiting for notarization to finish + /// which is helpful for the very first time your app is notarized as this can take multiple hours. + /// On subsequent runs, it's recommended to disable this setting again. + #[clap(long)] + pub skip_stapling: bool, } impl From for Options { @@ -93,6 +103,7 @@ impl From for Options { debug: value.debug, ci: value.ci, config: value.config, + skip_stapling: value.skip_stapling, } } } diff --git a/crates/tauri-cli/src/interface/mod.rs b/crates/tauri-cli/src/interface/mod.rs index f7fd9f141..16877b41f 100644 --- a/crates/tauri-cli/src/interface/mod.rs +++ b/crates/tauri-cli/src/interface/mod.rs @@ -31,6 +31,7 @@ pub trait AppSettings { fn get_package_settings(&self) -> tauri_bundler::PackageSettings; fn get_bundle_settings( &self, + options: &Options, config: &Config, features: &[String], ) -> crate::Result; @@ -52,7 +53,7 @@ pub trait AppSettings { enabled_features.push("default".into()); } - let target: String = if let Some(target) = options.target { + let target: String = if let Some(target) = options.target.clone() { target } else { tauri_utils::platform::target_triple()? @@ -66,7 +67,7 @@ pub trait AppSettings { let mut settings_builder = SettingsBuilder::new() .package_settings(self.get_package_settings()) - .bundle_settings(self.get_bundle_settings(config, &enabled_features)?) + .bundle_settings(self.get_bundle_settings(&options, config, &enabled_features)?) .binaries(bins) .project_out_directory(out_dir) .target(target) diff --git a/crates/tauri-cli/src/interface/rust.rs b/crates/tauri-cli/src/interface/rust.rs index 98d32ddfb..7ef796eb5 100644 --- a/crates/tauri-cli/src/interface/rust.rs +++ b/crates/tauri-cli/src/interface/rust.rs @@ -55,6 +55,7 @@ pub struct Options { pub args: Vec, pub config: Vec, pub no_watch: bool, + pub skip_stapling: bool, pub additional_watch_folders: Vec, } @@ -68,6 +69,7 @@ impl From for Options { args: options.args, config: options.config, no_watch: true, + skip_stapling: options.skip_stapling, additional_watch_folders: Vec::new(), } } @@ -81,6 +83,7 @@ impl From for Options { target: options.target, features: options.features, no_watch: true, + skip_stapling: options.skip_stapling, ..Default::default() } } @@ -96,6 +99,7 @@ impl From for Options { args: options.args, config: options.config, no_watch: options.no_watch, + skip_stapling: false, additional_watch_folders: options.additional_watch_folders, } } @@ -813,6 +817,7 @@ impl AppSettings for RustAppSettings { fn get_bundle_settings( &self, + options: &Options, config: &Config, features: &[String], ) -> crate::Result { @@ -851,6 +856,8 @@ impl AppSettings for RustAppSettings { arch64bits, )?; + settings.macos.skip_stapling = options.skip_stapling; + if let Some(plugin_config) = config .plugins .0 @@ -1466,6 +1473,7 @@ fn tauri_config_to_bundle_settings( minimum_system_version: config.macos.minimum_system_version, exception_domain: config.macos.exception_domain, signing_identity, + skip_stapling: false, hardened_runtime: config.macos.hardened_runtime, provider_short_name, entitlements: config.macos.entitlements, diff --git a/crates/tauri-cli/src/mobile/android/build.rs b/crates/tauri-cli/src/mobile/android/build.rs index 731df7e4d..4218a76a1 100644 --- a/crates/tauri-cli/src/mobile/android/build.rs +++ b/crates/tauri-cli/src/mobile/android/build.rs @@ -92,6 +92,7 @@ impl From for BuildOptions { config: options.config, args: options.args, ci: options.ci, + skip_stapling: false, } } } diff --git a/crates/tauri-cli/src/mobile/ios/build.rs b/crates/tauri-cli/src/mobile/ios/build.rs index 2d1b51961..c5a1abd8e 100644 --- a/crates/tauri-cli/src/mobile/ios/build.rs +++ b/crates/tauri-cli/src/mobile/ios/build.rs @@ -132,6 +132,7 @@ impl From for BuildOptions { config: options.config, args: options.args, ci: options.ci, + skip_stapling: false, } } } diff --git a/crates/tauri-macos-sign/src/lib.rs b/crates/tauri-macos-sign/src/lib.rs index 8a7bee343..183cb16cc 100644 --- a/crates/tauri-macos-sign/src/lib.rs +++ b/crates/tauri-macos-sign/src/lib.rs @@ -57,7 +57,8 @@ pub enum AppleNotarizationCredentials { #[derive(Deserialize)] struct NotarytoolSubmitOutput { id: String, - status: String, + #[serde(default)] + status: Option, message: String, } @@ -65,6 +66,23 @@ pub fn notarize( keychain: &Keychain, app_bundle_path: &Path, auth: &AppleNotarizationCredentials, +) -> Result<()> { + notarize_inner(keychain, app_bundle_path, auth, true) +} + +pub fn notarize_without_stapling( + keychain: &Keychain, + app_bundle_path: &Path, + auth: &AppleNotarizationCredentials, +) -> Result<()> { + notarize_inner(keychain, app_bundle_path, auth, false) +} + +fn notarize_inner( + keychain: &Keychain, + app_bundle_path: &Path, + auth: &AppleNotarizationCredentials, + wait: bool, ) -> Result<()> { let bundle_stem = app_bundle_path .file_stem() @@ -97,16 +115,19 @@ pub fn notarize( // sign the zip file keychain.sign(&zip_path, None, false)?; - let notarize_args = vec![ + let mut notarize_args = vec![ "notarytool", "submit", zip_path .to_str() .expect("failed to convert zip_path to string"), - "--wait", "--output-format", "json", ]; + if wait { + notarize_args.push("--wait"); + } + let notarize_args = notarize_args; println!("Notarizing {}", app_bundle_path.display()); @@ -126,12 +147,28 @@ pub fn notarize( let output_str = String::from_utf8_lossy(&output.stdout); if let Ok(submit_output) = serde_json::from_str::(&output_str) { let log_message = format!( - "Finished with status {} for id {} ({})", - submit_output.status, submit_output.id, submit_output.message + "{} with status {} for id {} ({})", + if wait { "Finished" } else { "Submitted" }, + submit_output.status.as_deref().unwrap_or("Pending"), + submit_output.id, + submit_output.message ); - if submit_output.status == "Accepted" { + // status is empty when not waiting for the notarization to finish + if submit_output.status.map_or(!wait, |s| s == "Accepted") { println!("Notarizing {log_message}"); - staple_app(app_bundle_path.to_path_buf())?; + + if wait { + println!("Stapling app..."); + staple_app(app_bundle_path.to_path_buf())?; + } else { + println!("Not waiting for notarization to finish."); + println!("You can use `xcrun notarytool log` to check the notarization progress."); + println!( + "When it's done you can optionally staple your app via `xcrun stapler staple {}`", + app_bundle_path.display() + ); + } + Ok(()) } else if let Ok(output) = Command::new("xcrun") .args(["notarytool", "log"])