feat: add option to not wait on notarization to finish (#13521)

* feat: add option to not wait on notarization to finish

* cli arg istead of config

* changefile

* fix serde

---------

Co-authored-by: Lucas Nogueira <lucas@tauri.app>
This commit is contained in:
Fabian-Lars 2025-08-11 19:29:41 +02:00 committed by GitHub
parent f0dcf9637c
commit a9ec12843a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 109 additions and 11 deletions

View File

@ -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.

View File

@ -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<Vec<PathBuf>> {
// 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) {

View File

@ -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(

View File

@ -346,6 +346,15 @@ pub struct MacOsSettings {
pub exception_domain: Option<String>,
/// Code signing identity.
pub signing_identity: Option<String>,
/// 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 <https://developer.apple.com/documentation/security/hardened_runtime>
///
/// Settings this to `false` is useful when using an ad-hoc signature, making it less strict.

View File

@ -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<()> {

View File

@ -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<crate::build::Options> for Options {
@ -93,6 +103,7 @@ impl From<crate::build::Options> for Options {
debug: value.debug,
ci: value.ci,
config: value.config,
skip_stapling: value.skip_stapling,
}
}
}

View File

@ -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<tauri_bundler::BundleSettings>;
@ -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)

View File

@ -55,6 +55,7 @@ pub struct Options {
pub args: Vec<String>,
pub config: Vec<ConfigValue>,
pub no_watch: bool,
pub skip_stapling: bool,
pub additional_watch_folders: Vec<PathBuf>,
}
@ -68,6 +69,7 @@ impl From<crate::build::Options> 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<crate::bundle::Options> for Options {
target: options.target,
features: options.features,
no_watch: true,
skip_stapling: options.skip_stapling,
..Default::default()
}
}
@ -96,6 +99,7 @@ impl From<crate::dev::Options> 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<BundleSettings> {
@ -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,

View File

@ -92,6 +92,7 @@ impl From<Options> for BuildOptions {
config: options.config,
args: options.args,
ci: options.ci,
skip_stapling: false,
}
}
}

View File

@ -132,6 +132,7 @@ impl From<Options> for BuildOptions {
config: options.config,
args: options.args,
ci: options.ci,
skip_stapling: false,
}
}
}

View File

@ -57,7 +57,8 @@ pub enum AppleNotarizationCredentials {
#[derive(Deserialize)]
struct NotarytoolSubmitOutput {
id: String,
status: String,
#[serde(default)]
status: Option<String>,
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::<NotarytoolSubmitOutput>(&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"])