use crate::{
bundle::BundleFormat,
error::{Context, ErrorExt},
helpers::{
self,
app_paths::Dirs,
config::{get_config, ConfigMetadata, FrontendDist},
},
info::plugins::check_mismatched_packages,
interface::{rust::get_cargo_target_dir, AppInterface},
ConfigValue, Result,
};
use clap::{ArgAction, Parser};
use std::env::set_current_dir;
use tauri_utils::config::RunnerConfig;
use tauri_utils::platform::Target;
#[derive(Debug, Clone, Parser)]
#[clap(
about = "Build your app in release mode and generate bundles and installers",
long_about = "Build your app in release mode and generate bundles and installers. It makes use of the `build.frontendDist` property from your `tauri.conf.json` file. It also runs your `build.beforeBuildCommand` which usually builds your frontend into `build.frontendDist`. This will also run `build.beforeBundleCommand` before generating the bundles and installers of your app."
)]
pub struct Options {
#[clap(short, long)]
pub runner: Option<RunnerConfig>,
#[clap(short, long)]
pub debug: bool,
#[clap(short, long)]
pub target: Option<String>,
#[clap(short, long, action = ArgAction::Append, num_args(0..), value_delimiter = ',')]
pub features: Vec<String>,
#[clap(short, long, action = ArgAction::Append, num_args(0..), value_delimiter = ',')]
pub bundles: Option<Vec<BundleFormat>>,
#[clap(long)]
pub no_bundle: bool,
#[clap(short, long)]
pub config: Vec<ConfigValue>,
pub args: Vec<String>,
#[clap(long, env = "CI")]
pub ci: bool,
#[clap(long)]
pub skip_stapling: bool,
#[clap(long)]
pub ignore_version_mismatches: bool,
#[clap(long)]
pub no_sign: bool,
}
pub fn command(mut options: Options, verbosity: u8) -> Result<()> {
let dirs = crate::helpers::app_paths::resolve_dirs();
if options.no_sign {
log::warn!("--no-sign flag detected: Signing will be skipped.");
}
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 mut interface = AppInterface::new(&config, options.target.clone(), dirs.tauri)?;
setup(&interface, &mut options, &config, &dirs, false)?;
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)?;
let bin_path = interface.build(interface_options, &dirs)?;
log::info!(action = "Built"; "application at: {}", tauri_utils::display_path(bin_path));
let app_settings = interface.app_settings();
if !options.no_bundle && (config.bundle.active || options.bundles.is_some()) {
crate::bundle::bundle(
&options.into(),
verbosity,
ci,
&interface,
&*app_settings,
&config,
&dirs,
&out_dir,
)?;
}
Ok(())
}
pub fn setup(
interface: &AppInterface,
options: &mut Options,
config: &ConfigMetadata,
dirs: &Dirs,
mobile: bool,
) -> Result<()> {
log::info!("Looking up installed tauri packages to check mismatched versions...");
if let Err(error) = check_mismatched_packages(dirs.frontend, dirs.tauri) {
if options.ignore_version_mismatches {
log::error!("{error}");
} else {
return Err(error);
}
}
set_current_dir(dirs.tauri).context("failed to set current directory")?;
let bundle_identifier_source = config
.find_bundle_identifier_overwriter()
.unwrap_or_else(|| "tauri.conf.json".into());
if config.identifier == "com.tauri.dev" {
crate::error::bail!(
"You must change the bundle identifier in `{bundle_identifier_source} identifier`. The default value `com.tauri.dev` is not allowed as it must be unique across applications.",
);
}
if config
.identifier
.chars()
.any(|ch| !(ch.is_alphanumeric() || ch == '-' || ch == '.'))
{
crate::error::bail!(
"The bundle identifier \"{}\" set in `{bundle_identifier_source:?} identifier`. The bundle identifier string must contain only alphanumeric characters (A-Z, a-z, and 0-9), hyphens (-), and periods (.).",
config.identifier,
);
}
if config.identifier.ends_with(".app") {
log::warn!(
"The bundle identifier \"{}\" set in `{bundle_identifier_source:?} identifier` ends with `.app`. This is not recommended because it conflicts with the application bundle extension on macOS.",
config.identifier,
);
}
if let Some(before_build) = config.build.before_build_command.clone() {
helpers::run_hook(
"beforeBuildCommand",
before_build,
interface,
options.debug,
dirs.frontend,
)?;
}
if let Some(FrontendDist::Directory(web_asset_path)) = &config.build.frontend_dist {
if !web_asset_path.exists() {
let absolute_path = web_asset_path
.parent()
.and_then(|p| p.canonicalize().ok())
.map(|p| p.join(web_asset_path.file_name().unwrap()))
.unwrap_or_else(|| std::env::current_dir().unwrap().join(web_asset_path));
crate::error::bail!(
"Unable to find your web assets, did you forget to build your web app? Your frontendDist is set to \"{}\" (which is `{}`).",
web_asset_path.display(), absolute_path.display(),
);
}
if web_asset_path
.canonicalize()
.fs_context("failed to canonicalize path", web_asset_path.to_path_buf())?
.file_name()
== Some(std::ffi::OsStr::new("src-tauri"))
{
crate::error::bail!(
"The configured frontendDist is the `src-tauri` folder. Please isolate your web assets on a separate folder and update `tauri.conf.json > build > frontendDist`.",
);
}
let target_path = get_cargo_target_dir(&options.args, dirs.tauri)?;
let mut out_folders = Vec::new();
if let Ok(web_asset_canonical) = dunce::canonicalize(web_asset_path) {
if let Ok(relative_path) = target_path.strip_prefix(&web_asset_canonical) {
let relative_str = relative_path.to_string_lossy();
if !relative_str.is_empty() {
out_folders.push(relative_str.to_string());
}
}
for folder in &["node_modules", "src-tauri"] {
let sub_path = web_asset_canonical.join(folder);
if sub_path.is_dir() {
out_folders.push(folder.to_string());
}
}
}
if !out_folders.is_empty() {
crate::error::bail!(
"The configured frontendDist includes the `{:?}` {}. Please isolate your web assets on a separate folder and update `tauri.conf.json > build > frontendDist`.",
out_folders,
if out_folders.len() == 1 { "folder" } else { "folders" }
);
}
}
if options.runner.is_none() {
options.runner = config.build.runner.clone();
}
options
.features
.extend_from_slice(config.build.features.as_deref().unwrap_or_default());
interface.build_options(&mut options.args, &mut options.features, mobile);
Ok(())
}