tauri/crates/tauri-cli/src/bundle.rs

334 lines
10 KiB
Rust

// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use std::{
path::{Path, PathBuf},
str::FromStr,
sync::OnceLock,
};
use clap::{builder::PossibleValue, ArgAction, Parser, ValueEnum};
use tauri_bundler::PackageType;
use tauri_utils::platform::Target;
use crate::{
error::{Context, ErrorExt},
helpers::{
self,
app_paths::Dirs,
config::{get_config, ConfigMetadata},
updater_signature,
},
interface::{AppInterface, AppSettings, Interface},
ConfigValue,
};
#[derive(Debug, Clone)]
pub struct BundleFormat(PackageType);
impl FromStr for BundleFormat {
type Err = crate::Error;
fn from_str(s: &str) -> crate::Result<Self> {
PackageType::from_short_name(s)
.map(Self)
.with_context(|| format!("unknown bundle format {s}"))
}
}
impl ValueEnum for BundleFormat {
fn value_variants<'a>() -> &'a [Self] {
static VARIANTS: OnceLock<Vec<BundleFormat>> = OnceLock::new();
VARIANTS.get_or_init(|| PackageType::all().iter().map(|t| Self(*t)).collect())
}
fn to_possible_value(&self) -> Option<PossibleValue> {
let hide = self.0 == PackageType::Updater;
Some(PossibleValue::new(self.0.short_name()).hide(hide))
}
}
#[derive(Debug, Parser, Clone)]
#[clap(
about = "Generate bundles and installers for your app (already built by `tauri build`)",
long_about = "Generate bundles and installers for your app (already built by `tauri build`). This run `build.beforeBundleCommand` before generating the bundles and installers of your app."
)]
pub struct Options {
/// Builds with the debug flag
#[clap(short, long)]
pub debug: bool,
/// Space or comma separated list of bundles to package.
#[clap(short, long, action = ArgAction::Append, num_args(0..), value_delimiter = ',')]
pub bundles: Option<Vec<BundleFormat>>,
/// JSON strings or paths to JSON, JSON5 or TOML files to merge with the default configuration file
///
/// Configurations are merged in the order they are provided, which means a particular value overwrites previous values when a config key-value pair conflicts.
///
/// Note that a platform-specific file is looked up and merged with the default file by default
/// (tauri.macos.conf.json, tauri.linux.conf.json, tauri.windows.conf.json, tauri.android.conf.json and tauri.ios.conf.json)
/// but you can use this for more specific use cases such as different build flavors.
#[clap(short, long)]
pub config: Vec<ConfigValue>,
/// Space or comma separated list of features, should be the same features passed to `tauri build` if any.
#[clap(short, long, action = ArgAction::Append, num_args(0..))]
pub features: Vec<String>,
/// Target triple to build against.
///
/// It must be one of the values outputted by `$rustc --print target-list` or `universal-apple-darwin` for an universal macOS application.
///
/// Note that compiling an universal macOS application requires both `aarch64-apple-darwin` and `x86_64-apple-darwin` targets to be installed.
#[clap(short, long)]
pub target: Option<String>,
/// 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,
/// Skip code signing during the build or bundling process.
///
/// Useful for local development and CI environments
/// where signing certificates or environment variables
/// are not available or not needed.
#[clap(long)]
pub no_sign: bool,
}
impl From<crate::build::Options> for Options {
fn from(value: crate::build::Options) -> Self {
Self {
bundles: value.bundles,
target: value.target,
features: value.features,
debug: value.debug,
ci: value.ci,
config: value.config,
skip_stapling: value.skip_stapling,
no_sign: value.no_sign,
}
}
}
pub fn command(options: Options, verbosity: u8) -> crate::Result<()> {
let dirs = crate::helpers::app_paths::resolve_dirs();
let ci = options.ci;
let target = options
.target
.as_deref()
.map(Target::from_triple)
.unwrap_or_else(Target::current);
let config = get_config(
target,
&options.config.iter().map(|c| &c.0).collect::<Vec<_>>(),
dirs.tauri,
)?;
let interface = AppInterface::new(&config, options.target.clone(), dirs.tauri)?;
std::env::set_current_dir(dirs.tauri).context("failed to set current directory")?;
if let Some(minimum_system_version) = &config.bundle.macos.minimum_system_version {
std::env::set_var("MACOSX_DEPLOYMENT_TARGET", minimum_system_version);
}
let app_settings = interface.app_settings();
let interface_options = options.clone().into();
let out_dir = app_settings.out_dir(&interface_options, dirs.tauri)?;
bundle(
&options,
verbosity,
ci,
&interface,
&*app_settings,
&config,
&dirs,
&out_dir,
)
}
#[allow(clippy::too_many_arguments)]
pub fn bundle<A: AppSettings>(
options: &Options,
verbosity: u8,
ci: bool,
interface: &AppInterface,
app_settings: &A,
config: &ConfigMetadata,
dirs: &Dirs,
out_dir: &Path,
) -> crate::Result<()> {
let package_types: Vec<PackageType> = if let Some(bundles) = &options.bundles {
bundles.iter().map(|bundle| bundle.0).collect::<Vec<_>>()
} else {
config
.bundle
.targets
.to_vec()
.into_iter()
.map(Into::into)
.collect()
};
if package_types.is_empty() {
return Ok(());
}
// 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() {
helpers::run_hook(
"beforeBundleCommand",
before_bundle,
interface,
options.debug,
dirs.frontend,
)?;
}
}
let mut settings = app_settings
.get_bundler_settings(
options.clone().into(),
config,
out_dir,
package_types,
dirs.tauri,
)
.with_context(|| "failed to build bundler settings")?;
settings.set_no_sign(options.no_sign);
settings.set_log_level(match verbosity {
0 => log::Level::Error,
1 => log::Level::Info,
_ => log::Level::Trace,
});
let bundles = tauri_bundler::bundle_project(&settings).map_err(Box::new)?;
sign_updaters(settings, bundles, ci)?;
Ok(())
}
fn sign_updaters(
settings: tauri_bundler::Settings,
bundles: Vec<tauri_bundler::Bundle>,
ci: bool,
) -> crate::Result<()> {
let Some(update_settings) = settings.updater() else {
// Updater not enabled
return Ok(());
};
let update_enabled_bundles: Vec<&tauri_bundler::Bundle> = bundles
.iter()
.filter(|bundle| {
matches!(
bundle.package_type,
PackageType::Updater
| PackageType::Nsis
| PackageType::WindowsMsi
| PackageType::AppImage
| PackageType::Deb
| PackageType::Rpm
)
})
.collect();
if update_enabled_bundles.is_empty() {
return Ok(());
}
if settings.no_sign() {
log::warn!("Updater signing is skipped due to --no-sign flag.");
return Ok(());
}
// get the public key
let pubkey = &update_settings.pubkey;
// 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)
.fs_context("failed to read pubkey from file", maybe_path.to_path_buf())?
} else {
pubkey.to_string()
};
// if no password provided we use an empty string
let password = std::env::var("TAURI_SIGNING_PRIVATE_KEY_PASSWORD")
.ok()
.or_else(|| if ci { Some("".into()) } else { None });
// get the private key
let private_key = std::env::var("TAURI_SIGNING_PRIVATE_KEY")
.ok()
.context("A public key has been found, but no private key. Make sure to set `TAURI_SIGNING_PRIVATE_KEY` environment variable.")?;
// 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).fs_context(
"failed to read private key from file",
maybe_path.to_path_buf(),
)?
} else {
private_key
};
let secret_key =
updater_signature::secret_key(private_key, password).context("failed to decode secret key")?;
let public_key = updater_signature::pub_key(pubkey).context("failed to decode pubkey")?;
let mut signed_paths = Vec::new();
for bundle in update_enabled_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 &bundle.bundle_paths {
// sign our path from environment variables
let (signature_path, signature) = updater_signature::sign_file(&secret_key, path)?;
if signature.keynum() != public_key.keynum() {
log::warn!("The updater secret key from `TAURI_SIGNING_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 print_signed_updater_archive(output_paths: &[PathBuf]) -> crate::Result<()> {
use std::fmt::Write;
if !output_paths.is_empty() {
let finished_bundles = output_paths.len();
let pluralised = if finished_bundles == 1 {
"updater signature"
} else {
"updater signatures"
};
let mut printable_paths = String::new();
for path in output_paths {
let _ = writeln!(
printable_paths,
" {}",
tauri_utils::display_path(path)
);
}
log::info!( action = "Finished"; "{finished_bundles} {pluralised} at:\n{printable_paths}");
}
Ok(())
}