diff --git a/.changes/cli-build-no-bundle.md b/.changes/cli-build-no-bundle.md new file mode 100644 index 000000000..bbcdd1437 --- /dev/null +++ b/.changes/cli-build-no-bundle.md @@ -0,0 +1,6 @@ +--- +'tauri-cli': 'patch:enhance' +'@tauri-apps/cli': 'patch:enhance' +--- + +Add `--no-bundle` flag for `tauri build` command to skip bundling. Previously `none` was used to skip bundling, it will now be treated as invalid format and a warning will be emitted instead. diff --git a/.changes/utils-bundle-target-all.md b/.changes/utils-bundle-target-all.md new file mode 100644 index 000000000..3a3e98a4e --- /dev/null +++ b/.changes/utils-bundle-target-all.md @@ -0,0 +1,5 @@ +--- +'tauri-utils': 'patch:bug' +--- + +Fix `BundleTarget::to_vec` returning an empty vec for `BundleTarget::All` variant. diff --git a/.changes/utils-bundle-type-all.md b/.changes/utils-bundle-type-all.md new file mode 100644 index 000000000..07906a628 --- /dev/null +++ b/.changes/utils-bundle-type-all.md @@ -0,0 +1,5 @@ +--- +'tauri-utils': 'patch:bug' +--- + +Add `BundleType::all` method to return all possible `BundleType` variants. diff --git a/core/tauri-utils/src/config.rs b/core/tauri-utils/src/config.rs index 43dc980ef..194e3c742 100644 --- a/core/tauri-utils/src/config.rs +++ b/core/tauri-utils/src/config.rs @@ -121,6 +121,22 @@ pub enum BundleType { Updater, } +impl BundleType { + /// All bundle types. + fn all() -> &'static [Self] { + &[ + BundleType::Deb, + BundleType::Rpm, + BundleType::AppImage, + BundleType::Msi, + BundleType::Nsis, + BundleType::App, + BundleType::Dmg, + BundleType::Updater, + ] + } +} + impl Display for BundleType { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( @@ -274,7 +290,7 @@ impl BundleTarget { #[allow(dead_code)] pub fn to_vec(&self) -> Vec { match self { - Self::All => vec![], + Self::All => BundleType::all().to_vec(), Self::List(list) => list.clone(), Self::One(i) => vec![i.clone()], } diff --git a/examples/api/src-tauri/Cargo.lock b/examples/api/src-tauri/Cargo.lock index 8046980b6..051e55d2e 100644 --- a/examples/api/src-tauri/Cargo.lock +++ b/examples/api/src-tauri/Cargo.lock @@ -3151,7 +3151,7 @@ checksum = "e1fc403891a21bcfb7c37834ba66a547a8f402146eba7265b5a6d88059c9ff2f" [[package]] name = "tauri" -version = "2.0.0-beta.7" +version = "2.0.0-beta.8" dependencies = [ "anyhow", "bytes", @@ -3202,7 +3202,7 @@ dependencies = [ [[package]] name = "tauri-build" -version = "2.0.0-beta.5" +version = "2.0.0-beta.6" dependencies = [ "anyhow", "cargo_toml", @@ -3224,7 +3224,7 @@ dependencies = [ [[package]] name = "tauri-codegen" -version = "2.0.0-beta.5" +version = "2.0.0-beta.6" dependencies = [ "base64", "brotli", @@ -3249,7 +3249,7 @@ dependencies = [ [[package]] name = "tauri-macros" -version = "2.0.0-beta.5" +version = "2.0.0-beta.6" dependencies = [ "heck", "proc-macro2", @@ -3261,7 +3261,7 @@ dependencies = [ [[package]] name = "tauri-plugin" -version = "2.0.0-beta.5" +version = "2.0.0-beta.6" dependencies = [ "anyhow", "glob", @@ -3287,7 +3287,7 @@ dependencies = [ [[package]] name = "tauri-runtime" -version = "2.0.0-beta.5" +version = "2.0.0-beta.6" dependencies = [ "gtk", "http", @@ -3303,7 +3303,7 @@ dependencies = [ [[package]] name = "tauri-runtime-wry" -version = "2.0.0-beta.5" +version = "2.0.0-beta.6" dependencies = [ "cocoa", "gtk", @@ -3324,7 +3324,7 @@ dependencies = [ [[package]] name = "tauri-utils" -version = "2.0.0-beta.5" +version = "2.0.0-beta.6" dependencies = [ "aes-gcm", "brotli", diff --git a/tooling/cli/src/build.rs b/tooling/cli/src/build.rs index 664313e43..862d348d4 100644 --- a/tooling/cli/src/build.rs +++ b/tooling/cli/src/build.rs @@ -6,7 +6,7 @@ use crate::{ helpers::{ app_paths::{app_dir, tauri_dir}, command_env, - config::{get as get_config, ConfigHandle, FrontendDist, HookCommand}, + config::{get as get_config, ConfigHandle, ConfigMetadata, FrontendDist, HookCommand}, updater_signature::{secret_key as updater_secret_key, sign_file}, }, interface::{AppInterface, AppSettings, Interface}, @@ -14,16 +14,40 @@ use crate::{ }; use anyhow::{bail, Context}; use base64::Engine; -use clap::{ArgAction, Parser}; -use log::{debug, error, info, warn}; +use clap::{builder::PossibleValue, ArgAction, Parser, ValueEnum}; use std::{ env::{set_current_dir, var}, path::{Path, PathBuf}, process::Command, + str::FromStr, + sync::OnceLock, }; use tauri_bundler::bundle::{bundle_project, Bundle, PackageType}; use tauri_utils::platform::Target; +#[derive(Debug, Clone)] +pub struct BundleFormat(PackageType); + +impl FromStr for BundleFormat { + type Err = anyhow::Error; + fn from_str(s: &str) -> Result { + PackageType::from_short_name(s) + .map(Self) + .ok_or_else(|| anyhow::anyhow!("unknown bundle format {s}")) + } +} + +impl ValueEnum for BundleFormat { + fn value_variants<'a>() -> &'a [Self] { + static VARIANTS: OnceLock> = OnceLock::new(); + VARIANTS.get_or_init(|| PackageType::all().iter().map(|t| Self(*t)).collect()) + } + + fn to_possible_value(&self) -> Option { + Some(PossibleValue::new(self.0.short_name())) + } +} + #[derive(Debug, Clone, Parser)] #[clap( about = "Build your app in release mode and generate bundles and installers", @@ -48,12 +72,12 @@ pub struct Options { pub features: Option>, /// Space or comma separated list of bundles to package. /// - /// Each bundle must be one of `deb`, `rpm`, `appimage`, `msi`, `app` or `dmg` on MacOS and `updater` on all platforms. - /// If `none` is specified, the bundler will be skipped. - /// /// Note that the `updater` bundle is not automatically added so you must specify it if the updater is enabled. #[clap(short, long, action = ArgAction::Append, num_args(0..), value_delimiter = ',')] - pub bundles: Option>, + pub bundles: Option>, + /// Skip the bundling step even if `bundle > active` is `true` in tauri config. + #[clap(long)] + pub no_bundle: bool, /// JSON string or path to JSON file to merge with tauri.conf.json #[clap(short, long)] pub config: Option, @@ -95,171 +119,15 @@ pub fn command(mut options: Options, verbosity: u8) -> Result<()> { let app_settings = interface.app_settings(); - if config_.bundle.active { - let package_types = if let Some(names) = &options.bundles { - let mut types = vec![]; - let mut skip = false; - for name in names { - if name == "none" { - skip = true; - break; - } - - match PackageType::from_short_name(name) { - Some(package_type) => { - types.push(package_type); - } - None => { - return Err(anyhow::anyhow!(format!( - "Unsupported bundle format: {name}" - ))); - } - } - } - - if skip { - None - } else { - Some(types) - } - } else { - let targets = config_.bundle.targets.to_vec(); - if targets.is_empty() { - None - } else { - Some(targets.into_iter().map(Into::into).collect()) - } - }; - - let updater_pub_key = config_ - .plugins - .0 - .get("updater") - .and_then(|k| k.get("pubkey")) - .and_then(|v| v.as_str()) - .map(|v| v.to_string()); - if let Some(types) = &package_types { - if updater_pub_key - .as_ref() - .map(|v| !v.is_empty()) - .unwrap_or(false) - && !types.contains(&PackageType::Updater) - { - warn!("`plugins > updater > pubkey` is set, but the bundle target list does not contain `updater`, so the updater artifacts won't be generated."); - } - } - - // if we have a package to bundle, let's run the `before_bundle_command`. - if package_types.as_ref().map_or(true, |p| !p.is_empty()) { - if let Some(before_bundle) = config_.build.before_bundle_command.clone() { - run_hook( - "beforeBundleCommand", - before_bundle, - &interface, - options.debug, - )?; - } - } - - let mut settings = app_settings - .get_bundler_settings(&options.into(), config_, out_dir, package_types) - .with_context(|| "failed to build bundler settings")?; - - settings.set_log_level(match verbosity { - 0 => log::Level::Error, - 1 => log::Level::Info, - _ => log::Level::Trace, - }); - - // set env vars used by the bundler - #[cfg(target_os = "linux")] - { - if config_.bundle.linux.appimage.bundle_media_framework { - std::env::set_var("APPIMAGE_BUNDLE_GSTREAMER", "1"); - } - - if let Some(open) = config_.plugins.0.get("shell").and_then(|v| v.get("open")) { - if open.as_bool().is_some_and(|x| x) || open.is_string() { - std::env::set_var("APPIMAGE_BUNDLE_XDG_OPEN", "1"); - } - } - - if settings.deep_link_protocols().is_some() { - std::env::set_var("APPIMAGE_BUNDLE_XDG_MIME", "1"); - } - } - - let bundles = bundle_project(settings) - .map_err(|e| anyhow::anyhow!("{:#}", e)) - .with_context(|| "failed to bundle project")?; - - let updater_bundles: Vec<&Bundle> = bundles - .iter() - .filter(|bundle| bundle.package_type == PackageType::Updater) - .collect(); - - // If updater is active and we bundled it - if !updater_bundles.is_empty() { - if let Some(pubkey) = updater_pub_key { - // get the public key - // check if pubkey points to a file... - let maybe_path = Path::new(&pubkey); - let pubkey = if maybe_path.exists() { - std::fs::read_to_string(maybe_path)? - } else { - pubkey - }; - - // if no password provided we use an empty string - let password = var("TAURI_SIGNING_PRIVATE_KEY_PASSWORD").ok().or_else(|| { - if ci { - Some("".into()) - } else { - None - } - }); - - // get the private key - let secret_key = match var("TAURI_SIGNING_PRIVATE_KEY") { - Ok(private_key) => { - // check if private_key points to a file... - let maybe_path = Path::new(&private_key); - let private_key = if maybe_path.exists() { - std::fs::read_to_string(maybe_path)? - } else { - private_key - }; - updater_secret_key(private_key, password) - } - _ => Err(anyhow::anyhow!("A public key has been found, but no private key. Make sure to set `TAURI_SIGNING_PRIVATE_KEY` environment variable.")), - }?; - - let pubkey = base64::engine::general_purpose::STANDARD.decode(pubkey)?; - let pub_key_decoded = String::from_utf8_lossy(&pubkey); - let public_key = - minisign::PublicKeyBox::from_string(&pub_key_decoded)?.into_public_key()?; - - // make sure we have our package built - let mut signed_paths = Vec::new(); - for elem in updater_bundles { - // we expect to have only one path in the vec but we iter if we add - // another type of updater package who require multiple file signature - for path in elem.bundle_paths.iter() { - // sign our path from environment variables - let (signature_path, signature) = sign_file(&secret_key, path)?; - if signature.keynum() != public_key.keynum() { - log::warn!( - "The updater secret key from `TAURI_PRIVATE_KEY` does not match the public key from `plugins > updater > pubkey`. If you are not rotating keys, this means your configuration is wrong and won't be accepted at runtime when performing update." - ); - } - signed_paths.push(signature_path); - } - } - - print_signed_updater_archive(&signed_paths)?; - } - } - } + bundle( + &options, + verbosity, + ci, + &interface, + &app_settings, + config_, + out_dir, + )?; Ok(()) } @@ -281,7 +149,7 @@ pub fn setup( .unwrap_or_else(|| "tauri.conf.json".into()); if config_.identifier == "com.tauri.dev" { - error!( + log::error!( "You must change the bundle identifier in `{} identifier`. The default value `com.tauri.dev` is not allowed as it must be unique across applications.", bundle_identifier_source ); @@ -293,7 +161,7 @@ pub fn setup( .chars() .any(|ch| !(ch.is_alphanumeric() || ch == '-' || ch == '.')) { - error!( + log::error!( "The bundle identifier \"{}\" set in `{} identifier`. The bundle identifier string must contain only alphanumeric characters (A-Z, a-z, and 0-9), hyphens (-), and periods (.).", config_.identifier, bundle_identifier_source @@ -347,6 +215,165 @@ pub fn setup( Ok(()) } +fn bundle( + options: &Options, + verbosity: u8, + ci: bool, + interface: &AppInterface, + app_settings: &std::sync::Arc, + config: &ConfigMetadata, + out_dir: &Path, +) -> crate::Result<()> { + if options.no_bundle || (options.bundles.is_none() && !config.bundle.active) { + return Ok(()); + } + + let package_types: Vec = if let Some(bundles) = &options.bundles { + bundles.iter().map(|bundle| bundle.0).collect::>() + } else { + config + .bundle + .targets + .to_vec() + .into_iter() + .map(Into::into) + .collect() + }; + + if package_types.is_empty() { + return Ok(()); + } + + let updater_pub_key = config + .plugins + .0 + .get("updater") + .and_then(|k| k.get("pubkey")) + .and_then(|v| v.as_str()) + .map(|v| v.to_string()); + + if updater_pub_key + .as_ref() + .map(|v| !v.is_empty()) + .unwrap_or(false) + && !package_types.contains(&PackageType::Updater) + { + log::warn!("`plugins > updater > pubkey` is set, but the bundle target list does not contain `updater`, so the updater artifacts won't be generated."); + } + + // if we have a package to bundle, let's run the `before_bundle_command`. + if !package_types.is_empty() { + if let Some(before_bundle) = config.build.before_bundle_command.clone() { + run_hook( + "beforeBundleCommand", + before_bundle, + interface, + options.debug, + )?; + } + } + + let mut settings = app_settings + .get_bundler_settings(options.clone().into(), config, out_dir, package_types) + .with_context(|| "failed to build bundler settings")?; + + settings.set_log_level(match verbosity { + 0 => log::Level::Error, + 1 => log::Level::Info, + _ => log::Level::Trace, + }); + + // set env vars used by the bundler + #[cfg(target_os = "linux")] + { + if config.bundle.linux.appimage.bundle_media_framework { + std::env::set_var("APPIMAGE_BUNDLE_GSTREAMER", "1"); + } + + if let Some(open) = config.plugins.0.get("shell").and_then(|v| v.get("open")) { + if open.as_bool().is_some_and(|x| x) || open.is_string() { + std::env::set_var("APPIMAGE_BUNDLE_XDG_OPEN", "1"); + } + } + + if settings.deep_link_protocols().is_some() { + std::env::set_var("APPIMAGE_BUNDLE_XDG_MIME", "1"); + } + } + + let bundles = bundle_project(settings) + .map_err(|e| anyhow::anyhow!("{:#}", e)) + .with_context(|| "failed to bundle project")?; + + let updater_bundles: Vec<&Bundle> = bundles + .iter() + .filter(|bundle| bundle.package_type == PackageType::Updater) + .collect(); + + // If updater is active and we bundled it + if !updater_bundles.is_empty() { + if let Some(pubkey) = updater_pub_key { + // get the public key + // check if pubkey points to a file... + let maybe_path = Path::new(&pubkey); + let pubkey = if maybe_path.exists() { + std::fs::read_to_string(maybe_path)? + } else { + pubkey + }; + + // if no password provided we use an empty string + let password = var("TAURI_SIGNING_PRIVATE_KEY_PASSWORD").ok().or_else(|| { + if ci { + Some("".into()) + } else { + None + } + }); + + // get the private key + let secret_key = match var("TAURI_SIGNING_PRIVATE_KEY") { + Ok(private_key) => { + // check if private_key points to a file... + let maybe_path = Path::new(&private_key); + let private_key = if maybe_path.exists() { + std::fs::read_to_string(maybe_path)? + } else { + private_key + }; + updater_secret_key(private_key, password) + } + _ => Err(anyhow::anyhow!("A public key has been found, but no private key. Make sure to set `TAURI_SIGNING_PRIVATE_KEY` environment variable.")), + }?; + + let pubkey = base64::engine::general_purpose::STANDARD.decode(pubkey)?; + let pub_key_decoded = String::from_utf8_lossy(&pubkey); + let public_key = minisign::PublicKeyBox::from_string(&pub_key_decoded)?.into_public_key()?; + + // make sure we have our package built + let mut signed_paths = Vec::new(); + for elem in updater_bundles { + // we expect to have only one path in the vec but we iter if we add + // another type of updater package who require multiple file signature + for path in elem.bundle_paths.iter() { + // sign our path from environment variables + let (signature_path, signature) = sign_file(&secret_key, path)?; + if signature.keynum() != public_key.keynum() { + log::warn!( + "The updater secret key from `TAURI_PRIVATE_KEY` does not match the public key from `plugins > updater > pubkey`. If you are not rotating keys, this means your configuration is wrong and won't be accepted at runtime when performing update." + ); + } + signed_paths.push(signature_path); + } + } + + print_signed_updater_archive(&signed_paths)?; + } + } + + Ok(()) +} + fn run_hook(name: &str, hook: HookCommand, interface: &AppInterface, debug: bool) -> Result<()> { let (script, script_cwd) = match hook { HookCommand::Script(s) if s.is_empty() => (None, None), @@ -355,12 +382,12 @@ fn run_hook(name: &str, hook: HookCommand, interface: &AppInterface, debug: bool }; let cwd = script_cwd.unwrap_or_else(|| app_dir().clone()); if let Some(script) = script { - info!(action = "Running"; "{} `{}`", name, script); + log::info!(action = "Running"; "{} `{}`", name, script); let mut env = command_env(debug); env.extend(interface.env()); - debug!("Setting environment for hook {:?}", env); + log::debug!("Setting environment for hook {:?}", env); #[cfg(target_os = "windows")] let status = Command::new("cmd") @@ -409,7 +436,7 @@ fn print_signed_updater_archive(output_paths: &[PathBuf]) -> crate::Result<()> { tauri_utils::display_path(path) )?; } - info!( action = "Finished"; "{} {} at:\n{}", output_paths.len(), pluralised, printable_paths); + log::info!( action = "Finished"; "{} {} at:\n{}", output_paths.len(), pluralised, printable_paths); } Ok(()) } diff --git a/tooling/cli/src/interface/mod.rs b/tooling/cli/src/interface/mod.rs index 6b7292da1..ed38e19bb 100644 --- a/tooling/cli/src/interface/mod.rs +++ b/tooling/cli/src/interface/mod.rs @@ -41,10 +41,10 @@ pub trait AppSettings { fn get_bundler_settings( &self, - options: &Options, + options: Options, config: &Config, out_dir: &Path, - package_types: Option>, + package_types: Vec, ) -> crate::Result { let no_default_features = options.args.contains(&"--no-default-features".into()); let mut enabled_features = options.features.clone().unwrap_or_default(); @@ -58,18 +58,15 @@ pub trait AppSettings { tauri_utils::platform::target_triple()? }; - let mut settings_builder = SettingsBuilder::new() + SettingsBuilder::new() .package_settings(self.get_package_settings()) .bundle_settings(self.get_bundle_settings(config, &enabled_features)?) .binaries(self.get_binaries(config, &target)?) .project_out_directory(out_dir) - .target(target); - - if let Some(types) = package_types { - settings_builder = settings_builder.package_types(types); - } - - settings_builder.build().map_err(Into::into) + .target(target) + .package_types(package_types) + .build() + .map_err(Into::into) } } diff --git a/tooling/cli/src/mobile/android/build.rs b/tooling/cli/src/mobile/android/build.rs index 5886d3d72..24eca6545 100644 --- a/tooling/cli/src/mobile/android/build.rs +++ b/tooling/cli/src/mobile/android/build.rs @@ -77,6 +77,7 @@ impl From for BuildOptions { target: None, features: options.features, bundles: None, + no_bundle: false, config: options.config, args: Vec::new(), ci: options.ci, diff --git a/tooling/cli/src/mobile/ios/build.rs b/tooling/cli/src/mobile/ios/build.rs index 3a922e5ed..1d01b51dc 100644 --- a/tooling/cli/src/mobile/ios/build.rs +++ b/tooling/cli/src/mobile/ios/build.rs @@ -73,6 +73,7 @@ impl From for BuildOptions { target: None, features: options.features, bundles: None, + no_bundle: false, config: options.config, args: Vec::new(), ci: options.ci,