use crate::{
helpers::{
app_paths::{app_dir, tauri_dir},
command_env,
config::{get as get_config, AppUrl, HookCommand, WindowUrl, MERGE_CONFIG_EXTENSION_NAME},
updater_signature::{read_key_from_file, secret_key as updater_secret_key, sign_file},
},
interface::{AppInterface, AppSettings, Interface},
CommandExt, Result,
};
use anyhow::{bail, Context};
use base64::Engine;
use clap::{ArgAction, Parser};
use log::{debug, error, info, warn};
use std::{
env::{set_current_dir, var_os},
path::{Path, PathBuf},
process::Command,
};
use tauri_bundler::bundle::{bundle_project, Bundle, PackageType};
#[derive(Debug, Clone, Parser)]
#[clap(about = "Tauri build")]
pub struct Options {
#[clap(short, long)]
pub runner: Option<String>,
#[clap(short, long)]
pub debug: bool,
#[clap(short, long)]
pub target: Option<String>,
#[clap(short, long, action = ArgAction::Append, num_args(0..))]
pub features: Option<Vec<String>>,
#[clap(short, long, action = ArgAction::Append, num_args(0..))]
pub bundles: Option<Vec<String>>,
#[clap(short, long)]
pub config: Option<String>,
pub args: Vec<String>,
#[clap(long)]
ci: bool,
}
pub fn command(mut options: Options, verbosity: u8) -> Result<()> {
options.ci = options.ci || std::env::var("CI").is_ok();
let ci = options.ci;
let (merge_config, merge_config_path) = if let Some(config) = &options.config {
if config.starts_with('{') {
(Some(config.to_string()), None)
} else {
(
Some(
std::fs::read_to_string(config).with_context(|| "failed to read custom configuration")?,
),
Some(config.clone()),
)
}
} else {
(None, None)
};
options.config = merge_config;
let tauri_path = tauri_dir();
set_current_dir(tauri_path).with_context(|| "failed to change current working directory")?;
let config = get_config(options.config.as_deref())?;
let config_guard = config.lock().unwrap();
let config_ = config_guard.as_ref().unwrap();
let bundle_identifier_source = match config_.find_bundle_identifier_overwriter() {
Some(source) if source == MERGE_CONFIG_EXTENSION_NAME => merge_config_path.unwrap_or(source),
Some(source) => source,
None => "tauri.conf.json".into(),
};
if config_.tauri.bundle.identifier == "com.tauri.dev" {
error!(
"You must change the bundle identifier in `{} > tauri > bundle > identifier`. The default value `com.tauri.dev` is not allowed as it must be unique across applications.",
bundle_identifier_source
);
std::process::exit(1);
}
if config_
.tauri
.bundle
.identifier
.chars()
.any(|ch| !(ch.is_alphanumeric() || ch == '-' || ch == '.'))
{
error!(
"The bundle identifier \"{}\" set in `{} > tauri > bundle > identifier`. The bundle identifier string must contain only alphanumeric characters (A–Z, a–z, and 0–9), hyphens (-), and periods (.).",
config_.tauri.bundle.identifier,
bundle_identifier_source
);
std::process::exit(1);
}
let mut interface = AppInterface::new(config_, options.target.clone())?;
let app_settings = interface.app_settings();
let interface_options = options.clone().into();
if let Some(before_build) = config_.build.before_build_command.clone() {
run_hook(
"beforeBuildCommand",
before_build,
&interface,
options.debug,
)?;
}
if let AppUrl::Url(WindowUrl::App(web_asset_path)) = &config_.build.dist_dir {
if !web_asset_path.exists() {
return Err(anyhow::anyhow!(
"Unable to find your web assets, did you forget to build your web app? Your distDir is set to \"{:?}\".",
web_asset_path
));
}
if web_asset_path.canonicalize()?.file_name() == Some(std::ffi::OsStr::new("src-tauri")) {
return Err(anyhow::anyhow!(
"The configured distDir is the `src-tauri` folder.
Please isolate your web assets on a separate folder and update `tauri.conf.json > build > distDir`.",
));
}
let mut out_folders = Vec::new();
for folder in &["node_modules", "src-tauri", "target"] {
if web_asset_path.join(folder).is_dir() {
out_folders.push(folder.to_string());
}
}
if !out_folders.is_empty() {
return Err(anyhow::anyhow!(
"The configured distDir includes the `{:?}` {}. Please isolate your web assets on a separate folder and update `tauri.conf.json > build > distDir`.",
out_folders,
if out_folders.len() == 1 { "folder" }else { "folders" }
)
);
}
}
if options.runner.is_none() {
options.runner = config_.build.runner.clone();
}
options
.features
.get_or_insert(Vec::new())
.extend(config_.build.features.clone().unwrap_or_default());
let bin_path = app_settings.app_binary_path(&interface_options)?;
let out_dir = bin_path.parent().unwrap();
interface.build(interface_options)?;
let app_settings = interface.app_settings();
if config_.tauri.bundle.active {
let package_types = if let Some(names) = &options.bundles {
let mut types = vec![];
for name in names
.iter()
.flat_map(|n| n.split(',').map(|s| s.to_string()).collect::<Vec<String>>())
{
if name == "none" {
break;
}
match PackageType::from_short_name(&name) {
Some(package_type) => {
types.push(package_type);
}
None => {
return Err(anyhow::anyhow!(format!(
"Unsupported bundle format: {name}"
)));
}
}
}
Some(types)
} else {
let targets = config_.tauri.bundle.targets.to_vec();
if targets.is_empty() {
None
} else {
Some(targets.into_iter().map(Into::into).collect())
}
};
if let Some(types) = &package_types {
if config_.tauri.updater.active && !types.contains(&PackageType::Updater) {
warn!("The updater is enabled but the bundle target list does not contain `updater`, so the updater artifacts won't be generated.");
}
}
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,
});
#[cfg(target_os = "linux")]
{
use crate::helpers::config::ShellAllowlistOpen;
if matches!(
config_.tauri.allowlist.shell.open,
ShellAllowlistOpen::Flag(true) | ShellAllowlistOpen::Validate(_)
) {
std::env::set_var("APPIMAGE_BUNDLE_XDG_OPEN", "1");
}
if config_.tauri.system_tray.is_some() {
if let Ok(tray) = std::env::var("TAURI_TRAY") {
std::env::set_var(
"TRAY_LIBRARY_PATH",
if tray == "ayatana" {
format!(
"{}/libayatana-appindicator3.so.1",
pkgconfig_utils::get_library_path("ayatana-appindicator3-0.1")
.expect("failed to get ayatana-appindicator library path using pkg-config.")
)
} else {
format!(
"{}/libappindicator3.so.1",
pkgconfig_utils::get_library_path("appindicator3-0.1")
.expect("failed to get libappindicator-gtk library path using pkg-config.")
)
},
);
} else {
std::env::set_var(
"TRAY_LIBRARY_PATH",
pkgconfig_utils::get_appindicator_library_path(),
);
}
}
if config_.tauri.bundle.appimage.bundle_media_framework {
std::env::set_var("APPIMAGE_BUNDLE_GSTREAMER", "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 config_.tauri.updater.active && !updater_bundles.is_empty() {
let password = var_os("TAURI_KEY_PASSWORD")
.map(|v| v.to_str().unwrap().to_string())
.or_else(|| if ci { Some("".into()) } else { None });
let secret_key = if let Some(mut private_key) =
var_os("TAURI_PRIVATE_KEY").map(|v| v.to_str().unwrap().to_string())
{
let pk_dir = Path::new(&private_key);
if pk_dir.exists() {
private_key = read_key_from_file(pk_dir)?;
}
updater_secret_key(private_key, password)
} else {
Err(anyhow::anyhow!("A public key has been found, but no private key. Make sure to set `TAURI_PRIVATE_KEY` environment variable."))
}?;
let pubkey =
base64::engine::general_purpose::STANDARD.decode(&config_.tauri.updater.pubkey)?;
let pub_key_decoded = String::from_utf8_lossy(&pubkey);
let public_key = minisign::PublicKeyBox::from_string(&pub_key_decoded)?.into_public_key()?;
let mut signed_paths = Vec::new();
for elem in updater_bundles {
for path in elem.bundle_paths.iter() {
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 defined in `tauri.conf.json > tauri > updater > pubkey`. If you are not rotating keys, this means your configuration is wrong and won't be accepted at runtime."
);
}
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 {
info!(action = "Running"; "{} `{}`", name, script);
let mut env = command_env(debug);
env.extend(interface.env());
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)
)?;
}
info!( action = "Finished"; "{} {} at:\n{}", output_paths.len(), pluralised, printable_paths);
}
Ok(())
}
#[cfg(target_os = "linux")]
mod pkgconfig_utils {
use std::{path::PathBuf, process::Command};
pub fn get_appindicator_library_path() -> PathBuf {
match get_library_path("ayatana-appindicator3-0.1") {
Some(p) => format!("{p}/libayatana-appindicator3.so.1").into(),
None => match get_library_path("appindicator3-0.1") {
Some(p) => format!("{p}/libappindicator3.so.1").into(),
None => panic!("Can't detect any appindicator library"),
},
}
}
pub fn get_library_path(name: &str) -> Option<String> {
let mut cmd = Command::new("pkg-config");
cmd.env("PKG_CONFIG_ALLOW_SYSTEM_LIBS", "1");
cmd.arg("--libs-only-L");
cmd.arg(name);
if let Ok(output) = cmd.output() {
if !output.stdout.is_empty() {
let word = output.stdout[2..].to_vec();
return Some(String::from_utf8_lossy(&word).trim().to_string());
} else {
None
}
} else {
None
}
}
}