Skip to content

Commit 9e4b225

Browse files
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>
1 parent 5462e5c commit 9e4b225

7 files changed

Lines changed: 423 additions & 274 deletions

File tree

.changes/tauri-bundle-command.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"tauri-cli": "patch:feat"
3+
"@tauri-apps/cli": "patch:feat"
4+
---
5+
6+
Add `tauri bundle` subcommand which runs the bundle phase only, best paired with `tauri build --no-bundle`
7+

tooling/bundler/src/error.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ pub enum Error {
1414
#[error("{0}")]
1515
Resource(#[from] tauri_utils::Error),
1616
/// Bundler error.
17-
#[error("{0}")]
17+
#[error("{0:#}")]
1818
BundlerError(#[from] anyhow::Error),
1919
/// I/O error.
2020
#[error("`{0}`")]

tooling/cli/src/build.rs

Lines changed: 22 additions & 272 deletions
Original file line numberDiff line numberDiff line change
@@ -3,54 +3,20 @@
33
// SPDX-License-Identifier: MIT
44

55
use crate::{
6+
bundle::BundleFormat,
67
helpers::{
7-
app_paths::{app_dir, tauri_dir},
8-
command_env,
9-
config::{get as get_config, ConfigHandle, ConfigMetadata, FrontendDist, HookCommand},
10-
updater_signature::{secret_key as updater_secret_key, sign_file},
8+
self,
9+
app_paths::tauri_dir,
10+
config::{get as get_config, ConfigHandle, FrontendDist},
1111
},
1212
interface::{AppInterface, AppSettings, Interface},
13-
CommandExt, ConfigValue, Result,
14-
};
15-
use anyhow::{bail, Context};
16-
use base64::Engine;
17-
use clap::{builder::PossibleValue, ArgAction, Parser, ValueEnum};
18-
use std::{
19-
env::{set_current_dir, var},
20-
path::{Path, PathBuf},
21-
process::Command,
22-
str::FromStr,
23-
sync::OnceLock,
24-
};
25-
use tauri_bundler::{
26-
bundle::{bundle_project, PackageType},
27-
Bundle,
13+
ConfigValue, Result,
2814
};
15+
use anyhow::Context;
16+
use clap::{ArgAction, Parser};
17+
use std::env::set_current_dir;
2918
use tauri_utils::platform::Target;
3019

31-
#[derive(Debug, Clone)]
32-
pub struct BundleFormat(PackageType);
33-
34-
impl FromStr for BundleFormat {
35-
type Err = anyhow::Error;
36-
fn from_str(s: &str) -> Result<Self> {
37-
PackageType::from_short_name(s)
38-
.map(Self)
39-
.ok_or_else(|| anyhow::anyhow!("unknown bundle format {s}"))
40-
}
41-
}
42-
43-
impl ValueEnum for BundleFormat {
44-
fn value_variants<'a>() -> &'a [Self] {
45-
static VARIANTS: OnceLock<Vec<BundleFormat>> = OnceLock::new();
46-
VARIANTS.get_or_init(|| PackageType::all().iter().map(|t| Self(*t)).collect())
47-
}
48-
49-
fn to_possible_value(&self) -> Option<PossibleValue> {
50-
Some(PossibleValue::new(self.0.short_name()))
51-
}
52-
}
53-
5420
#[derive(Debug, Clone, Parser)]
5521
#[clap(
5622
about = "Build your app in release mode and generate bundles and installers",
@@ -120,17 +86,21 @@ pub fn command(mut options: Options, verbosity: u8) -> Result<()> {
12086

12187
interface.build(interface_options)?;
12288

89+
log::info!(action ="Built"; "application at: {}", tauri_utils::display_path(&bin_path));
90+
12391
let app_settings = interface.app_settings();
12492

125-
bundle(
126-
&options,
127-
verbosity,
128-
ci,
129-
&interface,
130-
&app_settings,
131-
config_,
132-
out_dir,
133-
)?;
93+
if !options.no_bundle && (config_.bundle.active || options.bundles.is_some()) {
94+
crate::bundle::bundle(
95+
&options.into(),
96+
verbosity,
97+
ci,
98+
&interface,
99+
&app_settings,
100+
config_,
101+
out_dir,
102+
)?;
103+
}
134104

135105
Ok(())
136106
}
@@ -173,7 +143,7 @@ pub fn setup(
173143
}
174144

175145
if let Some(before_build) = config_.build.before_build_command.clone() {
176-
run_hook("beforeBuildCommand", before_build, interface, options.debug)?;
146+
helpers::run_hook("beforeBuildCommand", before_build, interface, options.debug)?;
177147
}
178148

179149
if let Some(FrontendDist::Directory(web_asset_path)) = &config_.build.frontend_dist {
@@ -222,223 +192,3 @@ pub fn setup(
222192

223193
Ok(())
224194
}
225-
226-
fn bundle<A: AppSettings>(
227-
options: &Options,
228-
verbosity: u8,
229-
ci: bool,
230-
interface: &AppInterface,
231-
app_settings: &std::sync::Arc<A>,
232-
config: &ConfigMetadata,
233-
out_dir: &Path,
234-
) -> crate::Result<()> {
235-
if options.no_bundle || (options.bundles.is_none() && !config.bundle.active) {
236-
return Ok(());
237-
}
238-
239-
let package_types: Vec<PackageType> = if let Some(bundles) = &options.bundles {
240-
bundles.iter().map(|bundle| bundle.0).collect::<Vec<_>>()
241-
} else {
242-
config
243-
.bundle
244-
.targets
245-
.to_vec()
246-
.into_iter()
247-
.map(Into::into)
248-
.collect()
249-
};
250-
251-
if package_types.is_empty() {
252-
return Ok(());
253-
}
254-
255-
// if we have a package to bundle, let's run the `before_bundle_command`.
256-
if !package_types.is_empty() {
257-
if let Some(before_bundle) = config.build.before_bundle_command.clone() {
258-
run_hook(
259-
"beforeBundleCommand",
260-
before_bundle,
261-
interface,
262-
options.debug,
263-
)?;
264-
}
265-
}
266-
267-
let mut settings = app_settings
268-
.get_bundler_settings(options.clone().into(), config, out_dir, package_types)
269-
.with_context(|| "failed to build bundler settings")?;
270-
271-
settings.set_log_level(match verbosity {
272-
0 => log::Level::Error,
273-
1 => log::Level::Info,
274-
_ => log::Level::Trace,
275-
});
276-
277-
// set env vars used by the bundler
278-
#[cfg(target_os = "linux")]
279-
{
280-
if config.bundle.linux.appimage.bundle_media_framework {
281-
std::env::set_var("APPIMAGE_BUNDLE_GSTREAMER", "1");
282-
}
283-
284-
if let Some(open) = config.plugins.0.get("shell").and_then(|v| v.get("open")) {
285-
if open.as_bool().is_some_and(|x| x) || open.is_string() {
286-
std::env::set_var("APPIMAGE_BUNDLE_XDG_OPEN", "1");
287-
}
288-
}
289-
290-
if settings.deep_link_protocols().is_some() {
291-
std::env::set_var("APPIMAGE_BUNDLE_XDG_MIME", "1");
292-
}
293-
}
294-
295-
let bundles = bundle_project(settings)
296-
.map_err(|e| anyhow::anyhow!("{:#}", e))
297-
.with_context(|| "failed to bundle project")?;
298-
299-
let update_enabled_bundles: Vec<&Bundle> = bundles
300-
.iter()
301-
.filter(|bundle| {
302-
matches!(
303-
bundle.package_type,
304-
PackageType::Updater | PackageType::Nsis | PackageType::WindowsMsi | PackageType::AppImage
305-
)
306-
})
307-
.collect();
308-
309-
// Skip if no updater is active
310-
if !update_enabled_bundles.is_empty() {
311-
let updater_pub_key = config
312-
.plugins
313-
.0
314-
.get("updater")
315-
.and_then(|k| k.get("pubkey"))
316-
.and_then(|v| v.as_str())
317-
.map(|v| v.to_string());
318-
319-
if let Some(pubkey) = updater_pub_key {
320-
// get the public key
321-
// check if pubkey points to a file...
322-
let maybe_path = Path::new(&pubkey);
323-
let pubkey = if maybe_path.exists() {
324-
std::fs::read_to_string(maybe_path)?
325-
} else {
326-
pubkey
327-
};
328-
329-
// if no password provided we use an empty string
330-
let password = var("TAURI_SIGNING_PRIVATE_KEY_PASSWORD").ok().or_else(|| {
331-
if ci {
332-
Some("".into())
333-
} else {
334-
None
335-
}
336-
});
337-
338-
// get the private key
339-
let secret_key = match var("TAURI_SIGNING_PRIVATE_KEY") {
340-
Ok(private_key) => {
341-
// check if private_key points to a file...
342-
let maybe_path = Path::new(&private_key);
343-
let private_key = if maybe_path.exists() {
344-
std::fs::read_to_string(maybe_path)?
345-
} else {
346-
private_key
347-
};
348-
updater_secret_key(private_key, password)
349-
}
350-
_ => Err(anyhow::anyhow!("A public key has been found, but no private key. Make sure to set `TAURI_SIGNING_PRIVATE_KEY` environment variable.")),
351-
}?;
352-
353-
let pubkey = base64::engine::general_purpose::STANDARD.decode(pubkey)?;
354-
let pub_key_decoded = String::from_utf8_lossy(&pubkey);
355-
let public_key = minisign::PublicKeyBox::from_string(&pub_key_decoded)?.into_public_key()?;
356-
357-
// make sure we have our package built
358-
let mut signed_paths = Vec::new();
359-
for bundle in update_enabled_bundles {
360-
// we expect to have only one path in the vec but we iter if we add
361-
// another type of updater package who require multiple file signature
362-
for path in bundle.bundle_paths.iter() {
363-
// sign our path from environment variables
364-
let (signature_path, signature) = sign_file(&secret_key, path)?;
365-
if signature.keynum() != public_key.keynum() {
366-
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.");
367-
}
368-
signed_paths.push(signature_path);
369-
}
370-
}
371-
372-
print_signed_updater_archive(&signed_paths)?;
373-
}
374-
}
375-
376-
Ok(())
377-
}
378-
379-
fn run_hook(name: &str, hook: HookCommand, interface: &AppInterface, debug: bool) -> Result<()> {
380-
let (script, script_cwd) = match hook {
381-
HookCommand::Script(s) if s.is_empty() => (None, None),
382-
HookCommand::Script(s) => (Some(s), None),
383-
HookCommand::ScriptWithOptions { script, cwd } => (Some(script), cwd.map(Into::into)),
384-
};
385-
let cwd = script_cwd.unwrap_or_else(|| app_dir().clone());
386-
if let Some(script) = script {
387-
log::info!(action = "Running"; "{} `{}`", name, script);
388-
389-
let mut env = command_env(debug);
390-
env.extend(interface.env());
391-
392-
log::debug!("Setting environment for hook {:?}", env);
393-
394-
#[cfg(target_os = "windows")]
395-
let status = Command::new("cmd")
396-
.arg("/S")
397-
.arg("/C")
398-
.arg(&script)
399-
.current_dir(cwd)
400-
.envs(env)
401-
.piped()
402-
.with_context(|| format!("failed to run `{}` with `cmd /C`", script))?;
403-
#[cfg(not(target_os = "windows"))]
404-
let status = Command::new("sh")
405-
.arg("-c")
406-
.arg(&script)
407-
.current_dir(cwd)
408-
.envs(env)
409-
.piped()
410-
.with_context(|| format!("failed to run `{script}` with `sh -c`"))?;
411-
412-
if !status.success() {
413-
bail!(
414-
"{} `{}` failed with exit code {}",
415-
name,
416-
script,
417-
status.code().unwrap_or_default()
418-
);
419-
}
420-
}
421-
422-
Ok(())
423-
}
424-
425-
fn print_signed_updater_archive(output_paths: &[PathBuf]) -> crate::Result<()> {
426-
use std::fmt::Write;
427-
if !output_paths.is_empty() {
428-
let pluralised = if output_paths.len() == 1 {
429-
"updater signature"
430-
} else {
431-
"updater signatures"
432-
};
433-
let mut printable_paths = String::new();
434-
for path in output_paths {
435-
writeln!(
436-
printable_paths,
437-
" {}",
438-
tauri_utils::display_path(path)
439-
)?;
440-
}
441-
log::info!( action = "Finished"; "{} {} at:\n{}", output_paths.len(), pluralised, printable_paths);
442-
}
443-
Ok(())
444-
}

0 commit comments

Comments
 (0)