mirror of
https://github.com/tauri-apps/tauri.git
synced 2024-09-11 16:08:21 +03:00
feat(cli): add tauri bundle
subcommand (#9734)
* feat(cli): add `tauri bundle` subcommand closes #8734 * license header * log application path after building * fix no-bundle check * typo * enhance error with deep causes --------- Co-authored-by: Lucas Nogueira <lucas@tauri.studio>
This commit is contained in:
parent
5462e5cadc
commit
9e4b2253f6
7
.changes/tauri-bundle-command.md
Normal file
7
.changes/tauri-bundle-command.md
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
"tauri-cli": "patch:feat"
|
||||||
|
"@tauri-apps/cli": "patch:feat"
|
||||||
|
---
|
||||||
|
|
||||||
|
Add `tauri bundle` subcommand which runs the bundle phase only, best paired with `tauri build --no-bundle`
|
||||||
|
|
@ -14,7 +14,7 @@ pub enum Error {
|
|||||||
#[error("{0}")]
|
#[error("{0}")]
|
||||||
Resource(#[from] tauri_utils::Error),
|
Resource(#[from] tauri_utils::Error),
|
||||||
/// Bundler error.
|
/// Bundler error.
|
||||||
#[error("{0}")]
|
#[error("{0:#}")]
|
||||||
BundlerError(#[from] anyhow::Error),
|
BundlerError(#[from] anyhow::Error),
|
||||||
/// I/O error.
|
/// I/O error.
|
||||||
#[error("`{0}`")]
|
#[error("`{0}`")]
|
||||||
|
@ -3,54 +3,20 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
bundle::BundleFormat,
|
||||||
helpers::{
|
helpers::{
|
||||||
app_paths::{app_dir, tauri_dir},
|
self,
|
||||||
command_env,
|
app_paths::tauri_dir,
|
||||||
config::{get as get_config, ConfigHandle, ConfigMetadata, FrontendDist, HookCommand},
|
config::{get as get_config, ConfigHandle, FrontendDist},
|
||||||
updater_signature::{secret_key as updater_secret_key, sign_file},
|
|
||||||
},
|
},
|
||||||
interface::{AppInterface, AppSettings, Interface},
|
interface::{AppInterface, AppSettings, Interface},
|
||||||
CommandExt, ConfigValue, Result,
|
ConfigValue, Result,
|
||||||
};
|
|
||||||
use anyhow::{bail, Context};
|
|
||||||
use base64::Engine;
|
|
||||||
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, PackageType},
|
|
||||||
Bundle,
|
|
||||||
};
|
};
|
||||||
|
use anyhow::Context;
|
||||||
|
use clap::{ArgAction, Parser};
|
||||||
|
use std::env::set_current_dir;
|
||||||
use tauri_utils::platform::Target;
|
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<Self> {
|
|
||||||
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<Vec<BundleFormat>> = OnceLock::new();
|
|
||||||
VARIANTS.get_or_init(|| PackageType::all().iter().map(|t| Self(*t)).collect())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn to_possible_value(&self) -> Option<PossibleValue> {
|
|
||||||
Some(PossibleValue::new(self.0.short_name()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Parser)]
|
#[derive(Debug, Clone, Parser)]
|
||||||
#[clap(
|
#[clap(
|
||||||
about = "Build your app in release mode and generate bundles and installers",
|
about = "Build your app in release mode and generate bundles and installers",
|
||||||
@ -120,10 +86,13 @@ pub fn command(mut options: Options, verbosity: u8) -> Result<()> {
|
|||||||
|
|
||||||
interface.build(interface_options)?;
|
interface.build(interface_options)?;
|
||||||
|
|
||||||
|
log::info!(action ="Built"; "application at: {}", tauri_utils::display_path(&bin_path));
|
||||||
|
|
||||||
let app_settings = interface.app_settings();
|
let app_settings = interface.app_settings();
|
||||||
|
|
||||||
bundle(
|
if !options.no_bundle && (config_.bundle.active || options.bundles.is_some()) {
|
||||||
&options,
|
crate::bundle::bundle(
|
||||||
|
&options.into(),
|
||||||
verbosity,
|
verbosity,
|
||||||
ci,
|
ci,
|
||||||
&interface,
|
&interface,
|
||||||
@ -131,6 +100,7 @@ pub fn command(mut options: Options, verbosity: u8) -> Result<()> {
|
|||||||
config_,
|
config_,
|
||||||
out_dir,
|
out_dir,
|
||||||
)?;
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -173,7 +143,7 @@ pub fn setup(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some(before_build) = config_.build.before_build_command.clone() {
|
if let Some(before_build) = config_.build.before_build_command.clone() {
|
||||||
run_hook("beforeBuildCommand", before_build, interface, options.debug)?;
|
helpers::run_hook("beforeBuildCommand", before_build, interface, options.debug)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(FrontendDist::Directory(web_asset_path)) = &config_.build.frontend_dist {
|
if let Some(FrontendDist::Directory(web_asset_path)) = &config_.build.frontend_dist {
|
||||||
@ -222,223 +192,3 @@ pub fn setup(
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn bundle<A: AppSettings>(
|
|
||||||
options: &Options,
|
|
||||||
verbosity: u8,
|
|
||||||
ci: bool,
|
|
||||||
interface: &AppInterface,
|
|
||||||
app_settings: &std::sync::Arc<A>,
|
|
||||||
config: &ConfigMetadata,
|
|
||||||
out_dir: &Path,
|
|
||||||
) -> crate::Result<()> {
|
|
||||||
if options.no_bundle || (options.bundles.is_none() && !config.bundle.active) {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
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() {
|
|
||||||
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 update_enabled_bundles: Vec<&Bundle> = bundles
|
|
||||||
.iter()
|
|
||||||
.filter(|bundle| {
|
|
||||||
matches!(
|
|
||||||
bundle.package_type,
|
|
||||||
PackageType::Updater | PackageType::Nsis | PackageType::WindowsMsi | PackageType::AppImage
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
// Skip if no updater is active
|
|
||||||
if !update_enabled_bundles.is_empty() {
|
|
||||||
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(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 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.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),
|
|
||||||
HookCommand::Script(s) => (Some(s), None),
|
|
||||||
HookCommand::ScriptWithOptions { script, cwd } => (Some(script), cwd.map(Into::into)),
|
|
||||||
};
|
|
||||||
let cwd = script_cwd.unwrap_or_else(|| app_dir().clone());
|
|
||||||
if let Some(script) = script {
|
|
||||||
log::info!(action = "Running"; "{} `{}`", name, script);
|
|
||||||
|
|
||||||
let mut env = command_env(debug);
|
|
||||||
env.extend(interface.env());
|
|
||||||
|
|
||||||
log::debug!("Setting environment for hook {:?}", env);
|
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
let status = Command::new("cmd")
|
|
||||||
.arg("/S")
|
|
||||||
.arg("/C")
|
|
||||||
.arg(&script)
|
|
||||||
.current_dir(cwd)
|
|
||||||
.envs(env)
|
|
||||||
.piped()
|
|
||||||
.with_context(|| format!("failed to run `{}` with `cmd /C`", script))?;
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
|
||||||
let status = Command::new("sh")
|
|
||||||
.arg("-c")
|
|
||||||
.arg(&script)
|
|
||||||
.current_dir(cwd)
|
|
||||||
.envs(env)
|
|
||||||
.piped()
|
|
||||||
.with_context(|| format!("failed to run `{script}` with `sh -c`"))?;
|
|
||||||
|
|
||||||
if !status.success() {
|
|
||||||
bail!(
|
|
||||||
"{} `{}` failed with exit code {}",
|
|
||||||
name,
|
|
||||||
script,
|
|
||||||
status.code().unwrap_or_default()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn print_signed_updater_archive(output_paths: &[PathBuf]) -> crate::Result<()> {
|
|
||||||
use std::fmt::Write;
|
|
||||||
if !output_paths.is_empty() {
|
|
||||||
let pluralised = if output_paths.len() == 1 {
|
|
||||||
"updater signature"
|
|
||||||
} else {
|
|
||||||
"updater signatures"
|
|
||||||
};
|
|
||||||
let mut printable_paths = String::new();
|
|
||||||
for path in output_paths {
|
|
||||||
writeln!(
|
|
||||||
printable_paths,
|
|
||||||
" {}",
|
|
||||||
tauri_utils::display_path(path)
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
log::info!( action = "Finished"; "{} {} at:\n{}", output_paths.len(), pluralised, printable_paths);
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
303
tooling/cli/src/bundle.rs
Normal file
303
tooling/cli/src/bundle.rs
Normal file
@ -0,0 +1,303 @@
|
|||||||
|
// 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 anyhow::Context;
|
||||||
|
use base64::Engine;
|
||||||
|
use clap::{builder::PossibleValue, ArgAction, Parser, ValueEnum};
|
||||||
|
use tauri_bundler::PackageType;
|
||||||
|
use tauri_utils::platform::Target;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
helpers::{
|
||||||
|
self,
|
||||||
|
app_paths::tauri_dir,
|
||||||
|
config::{get as get_config, ConfigMetadata},
|
||||||
|
updater_signature,
|
||||||
|
},
|
||||||
|
interface::{AppInterface, AppSettings, Interface},
|
||||||
|
ConfigValue,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct BundleFormat(PackageType);
|
||||||
|
|
||||||
|
impl FromStr for BundleFormat {
|
||||||
|
type Err = anyhow::Error;
|
||||||
|
fn from_str(s: &str) -> crate::Result<Self> {
|
||||||
|
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<Vec<BundleFormat>> = OnceLock::new();
|
||||||
|
VARIANTS.get_or_init(|| PackageType::all().iter().map(|t| Self(*t)).collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_possible_value(&self) -> Option<PossibleValue> {
|
||||||
|
Some(PossibleValue::new(self.0.short_name()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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.
|
||||||
|
///
|
||||||
|
/// 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<Vec<BundleFormat>>,
|
||||||
|
/// JSON string or path to JSON file to merge with tauri.conf.json
|
||||||
|
#[clap(short, long)]
|
||||||
|
pub config: Option<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: Option<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,
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn command(options: Options, verbosity: u8) -> crate::Result<()> {
|
||||||
|
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.as_ref().map(|c| &c.0))?;
|
||||||
|
|
||||||
|
let interface = AppInterface::new(
|
||||||
|
config.lock().unwrap().as_ref().unwrap(),
|
||||||
|
options.target.clone(),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let tauri_path = tauri_dir();
|
||||||
|
std::env::set_current_dir(tauri_path)
|
||||||
|
.with_context(|| "failed to change current working directory")?;
|
||||||
|
|
||||||
|
let config_guard = config.lock().unwrap();
|
||||||
|
let config_ = config_guard.as_ref().unwrap();
|
||||||
|
|
||||||
|
let app_settings = interface.app_settings();
|
||||||
|
let interface_options = options.clone().into();
|
||||||
|
|
||||||
|
let bin_path = app_settings.app_binary_path(&interface_options)?;
|
||||||
|
let out_dir = bin_path.parent().unwrap();
|
||||||
|
|
||||||
|
bundle(
|
||||||
|
&options,
|
||||||
|
verbosity,
|
||||||
|
ci,
|
||||||
|
&interface,
|
||||||
|
&app_settings,
|
||||||
|
config_,
|
||||||
|
out_dir,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn bundle<A: AppSettings>(
|
||||||
|
options: &Options,
|
||||||
|
verbosity: u8,
|
||||||
|
ci: bool,
|
||||||
|
interface: &AppInterface,
|
||||||
|
app_settings: &std::sync::Arc<A>,
|
||||||
|
config: &ConfigMetadata,
|
||||||
|
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,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = tauri_bundler::bundle_project(settings)
|
||||||
|
.map_err(|e| match e {
|
||||||
|
tauri_bundler::Error::BundlerError(e) => e,
|
||||||
|
e => anyhow::anyhow!("{e:#}"),
|
||||||
|
})
|
||||||
|
.with_context(|| "failed to bundle project")?;
|
||||||
|
|
||||||
|
let update_enabled_bundles: Vec<&tauri_bundler::Bundle> = bundles
|
||||||
|
.iter()
|
||||||
|
.filter(|bundle| {
|
||||||
|
matches!(
|
||||||
|
bundle.package_type,
|
||||||
|
PackageType::Updater | PackageType::Nsis | PackageType::WindowsMsi | PackageType::AppImage
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Skip if no updater is active
|
||||||
|
if !update_enabled_bundles.is_empty() {
|
||||||
|
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(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 = std::env::var("TAURI_SIGNING_PRIVATE_KEY_PASSWORD")
|
||||||
|
.ok()
|
||||||
|
.or_else(|| if ci { Some("".into()) } else { None });
|
||||||
|
|
||||||
|
// get the private key
|
||||||
|
let secret_key = match std::env::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_signature::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 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.iter() {
|
||||||
|
// 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_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 pluralised = if output_paths.len() == 1 {
|
||||||
|
"updater signature"
|
||||||
|
} else {
|
||||||
|
"updater signatures"
|
||||||
|
};
|
||||||
|
let mut printable_paths = String::new();
|
||||||
|
for path in output_paths {
|
||||||
|
writeln!(
|
||||||
|
printable_paths,
|
||||||
|
" {}",
|
||||||
|
tauri_utils::display_path(path)
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
log::info!( action = "Finished"; "{} {} at:\n{}", output_paths.len(), pluralised, printable_paths);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
@ -18,6 +18,16 @@ use std::{
|
|||||||
process::Command,
|
process::Command,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use anyhow::Context;
|
||||||
|
use tauri_utils::config::HookCommand;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
interface::{AppInterface, Interface},
|
||||||
|
CommandExt,
|
||||||
|
};
|
||||||
|
|
||||||
|
use self::app_paths::app_dir;
|
||||||
|
|
||||||
pub fn command_env(debug: bool) -> HashMap<&'static str, String> {
|
pub fn command_env(debug: bool) -> HashMap<&'static str, String> {
|
||||||
let mut map = HashMap::new();
|
let mut map = HashMap::new();
|
||||||
|
|
||||||
@ -53,3 +63,54 @@ pub fn cross_command(bin: &str) -> Command {
|
|||||||
let cmd = Command::new(bin);
|
let cmd = Command::new(bin);
|
||||||
cmd
|
cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn run_hook(
|
||||||
|
name: &str,
|
||||||
|
hook: HookCommand,
|
||||||
|
interface: &AppInterface,
|
||||||
|
debug: bool,
|
||||||
|
) -> crate::Result<()> {
|
||||||
|
let (script, script_cwd) = match hook {
|
||||||
|
HookCommand::Script(s) if s.is_empty() => (None, None),
|
||||||
|
HookCommand::Script(s) => (Some(s), None),
|
||||||
|
HookCommand::ScriptWithOptions { script, cwd } => (Some(script), cwd.map(Into::into)),
|
||||||
|
};
|
||||||
|
let cwd = script_cwd.unwrap_or_else(|| app_dir().clone());
|
||||||
|
if let Some(script) = script {
|
||||||
|
log::info!(action = "Running"; "{} `{}`", name, script);
|
||||||
|
|
||||||
|
let mut env = command_env(debug);
|
||||||
|
env.extend(interface.env());
|
||||||
|
|
||||||
|
log::debug!("Setting environment for hook {:?}", env);
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
let status = Command::new("cmd")
|
||||||
|
.arg("/S")
|
||||||
|
.arg("/C")
|
||||||
|
.arg(&script)
|
||||||
|
.current_dir(cwd)
|
||||||
|
.envs(env)
|
||||||
|
.piped()
|
||||||
|
.with_context(|| format!("failed to run `{}` with `cmd /C`", script))?;
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
let status = Command::new("sh")
|
||||||
|
.arg("-c")
|
||||||
|
.arg(&script)
|
||||||
|
.current_dir(cwd)
|
||||||
|
.envs(env)
|
||||||
|
.piped()
|
||||||
|
.with_context(|| format!("failed to run `{script}` with `sh -c`"))?;
|
||||||
|
|
||||||
|
if !status.success() {
|
||||||
|
anyhow::bail!(
|
||||||
|
"{} `{}` failed with exit code {}",
|
||||||
|
name,
|
||||||
|
script,
|
||||||
|
status.code().unwrap_or_default()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
@ -69,6 +69,19 @@ impl From<crate::build::Options> for Options {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<crate::bundle::Options> for Options {
|
||||||
|
fn from(options: crate::bundle::Options) -> Self {
|
||||||
|
Self {
|
||||||
|
debug: options.debug,
|
||||||
|
config: options.config,
|
||||||
|
target: options.target,
|
||||||
|
features: options.features,
|
||||||
|
no_watch: true,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<crate::dev::Options> for Options {
|
impl From<crate::dev::Options> for Options {
|
||||||
fn from(options: crate::dev::Options) -> Self {
|
fn from(options: crate::dev::Options) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
@ -17,6 +17,7 @@ pub use anyhow::Result;
|
|||||||
mod acl;
|
mod acl;
|
||||||
mod add;
|
mod add;
|
||||||
mod build;
|
mod build;
|
||||||
|
mod bundle;
|
||||||
mod completions;
|
mod completions;
|
||||||
mod dev;
|
mod dev;
|
||||||
mod helpers;
|
mod helpers;
|
||||||
@ -135,6 +136,7 @@ enum Commands {
|
|||||||
Init(init::Options),
|
Init(init::Options),
|
||||||
Dev(dev::Options),
|
Dev(dev::Options),
|
||||||
Build(build::Options),
|
Build(build::Options),
|
||||||
|
Bundle(bundle::Options),
|
||||||
Android(mobile::android::Cli),
|
Android(mobile::android::Cli),
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
Ios(mobile::ios::Cli),
|
Ios(mobile::ios::Cli),
|
||||||
@ -173,7 +175,19 @@ where
|
|||||||
A: Into<OsString> + Clone,
|
A: Into<OsString> + Clone,
|
||||||
{
|
{
|
||||||
if let Err(e) = try_run(args, bin_name) {
|
if let Err(e) = try_run(args, bin_name) {
|
||||||
log::error!("{:#}", e);
|
let mut message = e.to_string();
|
||||||
|
if e.chain().count() > 1 {
|
||||||
|
message.push(':');
|
||||||
|
}
|
||||||
|
e.chain().skip(1).for_each(|cause| {
|
||||||
|
let m = cause.to_string();
|
||||||
|
if !message.contains(&m) {
|
||||||
|
message.push('\n');
|
||||||
|
message.push_str(" - ");
|
||||||
|
message.push_str(&m);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
log::error!("{message}");
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -238,6 +252,7 @@ where
|
|||||||
|
|
||||||
match cli.command {
|
match cli.command {
|
||||||
Commands::Build(options) => build::command(options, cli.verbose)?,
|
Commands::Build(options) => build::command(options, cli.verbose)?,
|
||||||
|
Commands::Bundle(options) => bundle::command(options, cli.verbose)?,
|
||||||
Commands::Dev(options) => dev::command(options)?,
|
Commands::Dev(options) => dev::command(options)?,
|
||||||
Commands::Add(options) => add::command(options)?,
|
Commands::Add(options) => add::command(options)?,
|
||||||
Commands::Icon(options) => icon::command(options)?,
|
Commands::Icon(options) => icon::command(options)?,
|
||||||
|
Loading…
Reference in New Issue
Block a user