// Copyright 2019-2024 Tauri Programme within The Commons Conservancy // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT use std::{ collections::HashMap, ffi::OsStr, fs::{File, FileType}, io::{BufRead, Read, Write}, path::{Path, PathBuf}, process::Command, str::FromStr, sync::{mpsc::sync_channel, Arc, Mutex}, time::{Duration, Instant}, }; use anyhow::Context; use glob::glob; use ignore::gitignore::{Gitignore, GitignoreBuilder}; use notify::RecursiveMode; use notify_debouncer_mini::new_debouncer; use serde::{Deserialize, Deserializer}; use tauri_bundler::{ AppCategory, AppImageSettings, BundleBinary, BundleSettings, DebianSettings, DmgSettings, MacOsSettings, PackageSettings, Position, RpmSettings, Size, UpdaterSettings, WindowsSettings, }; use tauri_utils::config::{parse::is_configuration_file, DeepLinkProtocol, Updater}; use super::{AppSettings, DevProcess, ExitReason, Interface}; use crate::{ helpers::{ app_paths::{app_dir, tauri_dir}, config::{nsis_settings, reload as reload_config, wix_settings, BundleResources, Config}, }, ConfigValue, }; use tauri_utils::{display_path, platform::Target}; mod cargo_config; mod desktop; pub mod installation; pub mod manifest; use crate::helpers::config::custom_sign_settings; use cargo_config::Config as CargoConfig; use manifest::{rewrite_manifest, Manifest}; #[derive(Debug, Default, Clone)] pub struct Options { pub runner: Option, pub debug: bool, pub target: Option, pub features: Option>, pub args: Vec, pub config: Option, pub no_watch: bool, } impl From for Options { fn from(options: crate::build::Options) -> Self { Self { runner: options.runner, debug: options.debug, target: options.target, features: options.features, args: options.args, config: options.config, no_watch: true, } } } impl From 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 for Options { fn from(options: crate::dev::Options) -> Self { Self { runner: options.runner, debug: !options.release_mode, target: options.target, features: options.features, args: options.args, config: options.config, no_watch: options.no_watch, } } } #[derive(Debug, Clone)] pub struct MobileOptions { pub debug: bool, pub features: Option>, pub args: Vec, pub config: Option, pub no_watch: bool, } #[derive(Debug)] pub struct RustupTarget { name: String, installed: bool, } pub struct Rust { app_settings: Arc, config_features: Vec, available_targets: Option>, main_binary_name: Option, } impl Interface for Rust { type AppSettings = RustAppSettings; fn new(config: &Config, target: Option) -> crate::Result { let manifest = { let (tx, rx) = sync_channel(1); let mut watcher = new_debouncer(Duration::from_secs(1), move |r| { if let Ok(events) = r { let _ = tx.send(events); } }) .unwrap(); watcher .watcher() .watch(&tauri_dir().join("Cargo.toml"), RecursiveMode::Recursive)?; let (manifest, _modified) = rewrite_manifest(config)?; let now = Instant::now(); let timeout = Duration::from_secs(2); loop { if now.elapsed() >= timeout { break; } if rx.try_recv().is_ok() { break; } } manifest }; let target_ios = target.as_ref().map_or(false, |target| { target.ends_with("ios") || target.ends_with("ios-sim") }); if target_ios { std::env::set_var( "IPHONEOS_DEPLOYMENT_TARGET", &config.bundle.ios.minimum_system_version, ); } else 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 = RustAppSettings::new(config, manifest, target)?; Ok(Self { app_settings: Arc::new(app_settings), config_features: config.build.features.clone().unwrap_or_default(), main_binary_name: config.main_binary_name.clone(), available_targets: None, }) } fn app_settings(&self) -> Arc { self.app_settings.clone() } fn build(&mut self, options: Options) -> crate::Result { desktop::build( options, &self.app_settings, &mut self.available_targets, self.config_features.clone(), self.main_binary_name.as_deref(), ) } fn dev, ExitReason) + Send + Sync + 'static>( &mut self, mut options: Options, on_exit: F, ) -> crate::Result<()> { let on_exit = Arc::new(on_exit); let mut run_args = Vec::new(); dev_options( false, &mut options.args, &mut run_args, &mut options.features, &self.app_settings, ); if options.no_watch { let (tx, rx) = sync_channel(1); self.run_dev(options, run_args, move |status, reason| { tx.send(()).unwrap(); on_exit(status, reason) })?; rx.recv().unwrap(); Ok(()) } else { let config = options.config.clone().map(|c| c.0); let run = Arc::new(|rust: &mut Rust| { let on_exit = on_exit.clone(); rust.run_dev(options.clone(), run_args.clone(), move |status, reason| { on_exit(status, reason) }) }); self.run_dev_watcher(config, run) } } fn mobile_dev crate::Result>>( &mut self, mut options: MobileOptions, runner: R, ) -> crate::Result<()> { let mut run_args = Vec::new(); dev_options( true, &mut options.args, &mut run_args, &mut options.features, &self.app_settings, ); if options.no_watch { runner(options)?; Ok(()) } else { let config = options.config.clone().map(|c| c.0); let run = Arc::new(|_rust: &mut Rust| runner(options.clone())); self.run_dev_watcher(config, run) } } fn env(&self) -> HashMap<&str, String> { let mut env = HashMap::new(); env.insert( "TAURI_ENV_TARGET_TRIPLE", self.app_settings.target_triple.clone(), ); let target_triple = &self.app_settings.target_triple; let target_components: Vec<&str> = target_triple.split('-').collect(); let (arch, host, _host_env) = match target_components.as_slice() { // 3 components like aarch64-apple-darwin [arch, _, host] => (*arch, *host, None), // 4 components like x86_64-pc-windows-msvc and aarch64-apple-ios-sim [arch, _, host, host_env] => (*arch, *host, Some(*host_env)), _ => { log::warn!("Invalid target triple: {}", target_triple); return env; } }; env.insert("TAURI_ENV_ARCH", arch.into()); env.insert("TAURI_ENV_PLATFORM", host.into()); env.insert( "TAURI_ENV_FAMILY", match host { "windows" => "windows".into(), _ => "unix".into(), }, ); env } } struct IgnoreMatcher(Vec); impl IgnoreMatcher { fn is_ignore(&self, path: &Path, is_dir: bool) -> bool { for gitignore in &self.0 { if path.starts_with(gitignore.path()) && gitignore .matched_path_or_any_parents(path, is_dir) .is_ignore() { return true; } } false } } fn build_ignore_matcher(dir: &Path) -> IgnoreMatcher { let mut matchers = Vec::new(); // ignore crate doesn't expose an API to build `ignore::gitignore::GitIgnore` // with custom ignore file names so we have to walk the directory and collect // our custom ignore files and add it using `ignore::gitignore::GitIgnoreBuilder::add` for entry in ignore::WalkBuilder::new(dir) .require_git(false) .ignore(false) .overrides( ignore::overrides::OverrideBuilder::new(dir) .add(".taurignore") .unwrap() .build() .unwrap(), ) .build() .flatten() { let path = entry.path(); if path.file_name() == Some(OsStr::new(".taurignore")) { let mut ignore_builder = GitignoreBuilder::new(path.parent().unwrap()); ignore_builder.add(path); if let Ok(ignore_file) = std::env::var("TAURI_CLI_WATCHER_IGNORE_FILENAME") { ignore_builder.add(dir.join(ignore_file)); } for line in crate::dev::TAURI_CLI_BUILTIN_WATCHER_IGNORE_FILE .lines() .map_while(Result::ok) { let _ = ignore_builder.add_line(None, &line); } matchers.push(ignore_builder.build().unwrap()); } } IgnoreMatcher(matchers) } fn lookup(dir: &Path, mut f: F) { let mut default_gitignore = std::env::temp_dir(); default_gitignore.push(".tauri"); let _ = std::fs::create_dir_all(&default_gitignore); default_gitignore.push(".gitignore"); if !default_gitignore.exists() { if let Ok(mut file) = std::fs::File::create(default_gitignore.clone()) { let _ = file.write_all(crate::dev::TAURI_CLI_BUILTIN_WATCHER_IGNORE_FILE); } } let mut builder = ignore::WalkBuilder::new(dir); builder.add_custom_ignore_filename(".taurignore"); let _ = builder.add_ignore(default_gitignore); if let Ok(ignore_file) = std::env::var("TAURI_CLI_WATCHER_IGNORE_FILENAME") { builder.add_ignore(ignore_file); } builder.require_git(false).ignore(false).max_depth(Some(1)); for entry in builder.build().flatten() { f(entry.file_type().unwrap(), dir.join(entry.path())); } } fn shared_options( mobile: bool, args: &mut Vec, features: &mut Option>, app_settings: &RustAppSettings, ) { if mobile { args.push("--lib".into()); features .get_or_insert(Vec::new()) .push("tauri/rustls-tls".into()); } else { args.push("--bins".into()); let all_features = app_settings .manifest .lock() .unwrap() .all_enabled_features(if let Some(f) = features { f } else { &[] }); if !all_features.contains(&"tauri/rustls-tls".into()) { features .get_or_insert(Vec::new()) .push("tauri/native-tls".into()); } } } fn dev_options( mobile: bool, args: &mut Vec, run_args: &mut Vec, features: &mut Option>, app_settings: &RustAppSettings, ) { let mut dev_args = Vec::new(); let mut reached_run_args = false; for arg in args.clone() { if reached_run_args { run_args.push(arg); } else if arg == "--" { reached_run_args = true; } else { dev_args.push(arg); } } *args = dev_args; shared_options(mobile, args, features, app_settings); if !args.contains(&"--no-default-features".into()) { let manifest_features = app_settings.manifest.lock().unwrap().features(); let enable_features: Vec = manifest_features .get("default") .cloned() .unwrap_or_default() .into_iter() .filter(|feature| { if let Some(manifest_feature) = manifest_features.get(feature) { !manifest_feature.contains(&"tauri/custom-protocol".into()) } else { feature != "tauri/custom-protocol" } }) .collect(); args.push("--no-default-features".into()); if !enable_features.is_empty() { features.get_or_insert(Vec::new()).extend(enable_features); } } } // Copied from https://github.com/rust-lang/cargo/blob/69255bb10de7f74511b5cef900a9d102247b6029/src/cargo/core/workspace.rs#L665 fn expand_member_path(path: &Path) -> crate::Result> { let Some(path) = path.to_str() else { return Err(anyhow::anyhow!("path is not UTF-8 compatible")); }; let res = glob(path).with_context(|| format!("could not parse pattern `{}`", &path))?; let res = res .map(|p| p.with_context(|| format!("unable to match path to pattern `{}`", &path))) .collect::, _>>()?; Ok(res) } fn get_watch_folders() -> crate::Result> { let tauri_path = tauri_dir(); let workspace_path = get_workspace_dir()?; // We always want to watch the main tauri folder. let mut watch_folders = vec![tauri_path.to_path_buf()]; // We also try to watch workspace members, no matter if the tauri cargo project is the workspace root or a workspace member let cargo_settings = CargoSettings::load(&workspace_path)?; if let Some(members) = cargo_settings.workspace.and_then(|w| w.members) { for p in members { let p = workspace_path.join(p); match expand_member_path(&p) { // Sometimes expand_member_path returns an empty vec, for example if the path contains `[]` as in `C:/[abc]/project/`. // Cargo won't complain unless theres a workspace.members config with glob patterns so we should support it too. Ok(expanded_paths) => { if expanded_paths.is_empty() { watch_folders.push(p); } else { watch_folders.extend(expanded_paths); } } Err(err) => { // If this fails cargo itself should fail too. But we still try to keep going with the unexpanded path. log::error!("Error watching {}: {}", p.display(), err.to_string()); watch_folders.push(p); } }; } } Ok(watch_folders) } impl Rust { pub fn build_options( &self, args: &mut Vec, features: &mut Option>, mobile: bool, ) { features .get_or_insert(Vec::new()) .push("tauri/custom-protocol".into()); shared_options(mobile, args, features, &self.app_settings); } fn run_dev, ExitReason) + Send + Sync + 'static>( &mut self, options: Options, run_args: Vec, on_exit: F, ) -> crate::Result> { desktop::run_dev( options, run_args, &mut self.available_targets, self.config_features.clone(), &self.app_settings, self.main_binary_name.clone(), on_exit, ) .map(|c| Box::new(c) as Box) } fn run_dev_watcher crate::Result>>( &mut self, config: Option, run: Arc, ) -> crate::Result<()> { let child = run(self)?; let process = Arc::new(Mutex::new(child)); let (tx, rx) = sync_channel(1); let app_path = app_dir(); let watch_folders = get_watch_folders()?; let common_ancestor = common_path::common_path_all(watch_folders.iter().map(Path::new)) .expect("watch_folders should not be empty"); let ignore_matcher = build_ignore_matcher(&common_ancestor); let mut watcher = new_debouncer(Duration::from_secs(1), move |r| { if let Ok(events) = r { tx.send(events).unwrap() } }) .unwrap(); for path in watch_folders { if !ignore_matcher.is_ignore(&path, true) { log::info!("Watching {} for changes...", display_path(&path)); lookup(&path, |file_type, p| { if p != path { log::debug!("Watching {} for changes...", display_path(&p)); let _ = watcher.watcher().watch( &p, if file_type.is_dir() { RecursiveMode::Recursive } else { RecursiveMode::NonRecursive }, ); } }); } } loop { if let Ok(events) = rx.recv() { for event in events { let event_path = event.path; if !ignore_matcher.is_ignore(&event_path, event_path.is_dir()) { if is_configuration_file(self.app_settings.target, &event_path) { if let Ok(config) = reload_config(config.as_ref()) { let (manifest, modified) = rewrite_manifest(config.lock().unwrap().as_ref().unwrap())?; if modified { *self.app_settings.manifest.lock().unwrap() = manifest; // no need to run the watcher logic, the manifest was modified // and it will trigger the watcher again continue; } } } log::info!( "File {} changed. Rebuilding application...", display_path(event_path.strip_prefix(app_path).unwrap_or(&event_path)) ); let mut p = process.lock().unwrap(); p.kill().with_context(|| "failed to kill app process")?; // wait for the process to exit // note that on mobile, kill() already waits for the process to exit (duct implementation) loop { if !matches!(p.try_wait(), Ok(None)) { break; } } *p = run(self)?; } } } } } } // Taken from https://github.com/rust-lang/cargo/blob/70898e522116f6c23971e2a554b2dc85fd4c84cd/src/cargo/util/toml/mod.rs#L1008-L1065 /// Enum that allows for the parsing of `field.workspace = true` in a Cargo.toml /// /// It allows for things to be inherited from a workspace or defined as needed #[derive(Clone, Debug)] pub enum MaybeWorkspace { Workspace(TomlWorkspaceField), Defined(T), } impl<'de, T: Deserialize<'de>> serde::de::Deserialize<'de> for MaybeWorkspace { fn deserialize(deserializer: D) -> Result, D::Error> where D: serde::de::Deserializer<'de>, { let value = serde_value::Value::deserialize(deserializer)?; if let Ok(workspace) = TomlWorkspaceField::deserialize( serde_value::ValueDeserializer::::new(value.clone()), ) { return Ok(MaybeWorkspace::Workspace(workspace)); } T::deserialize(serde_value::ValueDeserializer::::new(value)) .map(MaybeWorkspace::Defined) } } impl MaybeWorkspace { fn resolve( self, label: &str, get_ws_field: impl FnOnce() -> anyhow::Result, ) -> anyhow::Result { match self { MaybeWorkspace::Defined(value) => Ok(value), MaybeWorkspace::Workspace(TomlWorkspaceField { workspace: true }) => { get_ws_field().context(format!( "error inheriting `{label}` from workspace root manifest's `workspace.package.{label}`" )) } MaybeWorkspace::Workspace(TomlWorkspaceField { workspace: false }) => Err(anyhow::anyhow!( "`workspace=false` is unsupported for `package.{}`", label, )), } } fn _as_defined(&self) -> Option<&T> { match self { MaybeWorkspace::Workspace(_) => None, MaybeWorkspace::Defined(defined) => Some(defined), } } } #[derive(Deserialize, Clone, Debug)] pub struct TomlWorkspaceField { workspace: bool, } /// The `workspace` section of the app configuration (read from Cargo.toml). #[derive(Clone, Debug, Deserialize)] struct WorkspaceSettings { /// the workspace members. members: Option>, package: Option, } #[derive(Clone, Debug, Deserialize)] struct WorkspacePackageSettings { authors: Option>, description: Option, homepage: Option, version: Option, license: Option, } #[derive(Clone, Debug, Deserialize)] struct BinarySettings { name: String, path: Option, } /// The package settings. #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "kebab-case")] pub struct CargoPackageSettings { /// the package's name. pub name: String, /// the package's version. pub version: Option>, /// the package's description. pub description: Option>, /// the package's homepage. pub homepage: Option>, /// the package's authors. pub authors: Option>>, /// the package's license. pub license: Option>, /// the default binary to run. pub default_run: Option, } /// The Cargo settings (Cargo.toml root descriptor). #[derive(Clone, Debug, Deserialize)] struct CargoSettings { /// the package settings. /// /// it's optional because ancestor workspace Cargo.toml files may not have package info. package: Option, /// the workspace settings. /// /// it's present if the read Cargo.toml belongs to a workspace root. workspace: Option, /// the binary targets configuration. bin: Option>, } impl CargoSettings { /// Try to load a set of CargoSettings from a "Cargo.toml" file in the specified directory. fn load(dir: &Path) -> crate::Result { let toml_path = dir.join("Cargo.toml"); let mut toml_str = String::new(); let mut toml_file = File::open(toml_path).with_context(|| "failed to open Cargo.toml")?; toml_file .read_to_string(&mut toml_str) .with_context(|| "failed to read Cargo.toml")?; toml::from_str(&toml_str) .with_context(|| "failed to parse Cargo.toml") .map_err(Into::into) } } pub struct RustAppSettings { manifest: Mutex, cargo_settings: CargoSettings, cargo_package_settings: CargoPackageSettings, cargo_ws_package_settings: Option, package_settings: PackageSettings, cargo_config: CargoConfig, target_triple: String, target: Target, } #[derive(Deserialize)] #[serde(untagged)] enum DesktopDeepLinks { One(DeepLinkProtocol), List(Vec), } #[derive(Deserialize)] pub struct UpdaterConfig { /// Signature public key. pub pubkey: String, /// The Windows configuration for the updater. #[serde(default)] pub windows: UpdaterWindowsConfig, } /// Install modes for the Windows update. #[derive(Debug, PartialEq, Eq, Clone)] pub enum WindowsUpdateInstallMode { /// Specifies there's a basic UI during the installation process, including a final dialog box at the end. BasicUi, /// The quiet mode means there's no user interaction required. /// Requires admin privileges if the installer does. Quiet, /// Specifies unattended mode, which means the installation only shows a progress bar. Passive, // to add more modes, we need to check if the updater relaunch makes sense // i.e. for a full UI mode, the user can also mark the installer to start the app } impl Default for WindowsUpdateInstallMode { fn default() -> Self { Self::Passive } } impl<'de> Deserialize<'de> for WindowsUpdateInstallMode { fn deserialize(deserializer: D) -> std::result::Result where D: Deserializer<'de>, { let s = String::deserialize(deserializer)?; match s.to_lowercase().as_str() { "basicui" => Ok(Self::BasicUi), "quiet" => Ok(Self::Quiet), "passive" => Ok(Self::Passive), _ => Err(serde::de::Error::custom(format!( "unknown update install mode '{s}'" ))), } } } impl WindowsUpdateInstallMode { /// Returns the associated `msiexec.exe` arguments. pub fn msiexec_args(&self) -> &'static [&'static str] { match self { Self::BasicUi => &["/qb+"], Self::Quiet => &["/quiet"], Self::Passive => &["/passive"], } } } #[derive(Default, Deserialize)] #[serde(rename_all = "camelCase")] pub struct UpdaterWindowsConfig { #[serde(default, alias = "install-mode")] pub install_mode: WindowsUpdateInstallMode, } impl AppSettings for RustAppSettings { fn get_package_settings(&self) -> PackageSettings { self.package_settings.clone() } fn get_bundle_settings( &self, config: &Config, features: &[String], ) -> crate::Result { let arch64bits = self.target_triple.starts_with("x86_64") || self.target_triple.starts_with("aarch64"); let updater_enabled = config.bundle.create_updater_artifacts != Updater::Bool(false); let v1_compatible = matches!(config.bundle.create_updater_artifacts, Updater::String(_)); let updater_settings = if updater_enabled { let updater: UpdaterConfig = serde_json::from_value( config .plugins .0 .get("updater") .ok_or_else(|| { anyhow::anyhow!("failed to get updater configuration: plugins > updater doesn't exist") })? .clone(), )?; Some(UpdaterSettings { v1_compatible, pubkey: updater.pubkey, msiexec_args: updater.windows.install_mode.msiexec_args(), }) } else { None }; let mut settings = tauri_config_to_bundle_settings( self, features, config.identifier.clone(), config.bundle.clone(), updater_settings, arch64bits, )?; if let Some(plugin_config) = config .plugins .0 .get("deep-link") .and_then(|c| c.get("desktop").cloned()) { let protocols: DesktopDeepLinks = serde_json::from_value(plugin_config.clone())?; settings.deep_link_protocols = Some(match protocols { DesktopDeepLinks::One(p) => vec![p], DesktopDeepLinks::List(p) => p, }); } Ok(settings) } fn app_binary_path(&self, options: &Options) -> crate::Result { let binaries = self.get_binaries()?; let bin_name = binaries .iter() .find(|x| x.main()) .context("failed to find main binary, make sure you have a `package > default-run` in the Cargo.toml file")? .name(); let out_dir = self .out_dir(options) .context("failed to get project out directory")?; let ext = if self.target_triple.contains("windows") { "exe" } else { "" }; Ok(out_dir.join(bin_name).with_extension(ext)) } fn get_binaries(&self) -> crate::Result> { let mut binaries: Vec = vec![]; if let Some(bins) = &self.cargo_settings.bin { let default_run = self .package_settings .default_run .clone() .unwrap_or_default(); for bin in bins { let is_main = bin.name == self.cargo_package_settings.name || bin.name == default_run; binaries.push(BundleBinary::with_path( bin.name.clone(), is_main, bin.path.clone(), )) } } let mut binaries_paths = std::fs::read_dir(tauri_dir().join("src/bin")) .map(|dir| { dir .into_iter() .flatten() .map(|entry| { ( entry .path() .file_stem() .unwrap_or_default() .to_string_lossy() .into_owned(), entry.path(), ) }) .collect::>() }) .unwrap_or_default(); if !binaries_paths .iter() .any(|(_name, path)| path == Path::new("src/main.rs")) && tauri_dir().join("src/main.rs").exists() { binaries_paths.push(( self.cargo_package_settings.name.clone(), tauri_dir().join("src/main.rs"), )); } for (name, path) in binaries_paths { // see https://github.com/tauri-apps/tauri/pull/10977#discussion_r1759742414 let bin_exists = binaries .iter() .any(|bin| bin.name() == name || path.ends_with(bin.src_path().unwrap_or(&"".to_string()))); if !bin_exists { binaries.push(BundleBinary::new(name, false)) } } if let Some(default_run) = self.package_settings.default_run.as_ref() { if let Some(binary) = binaries.iter_mut().find(|bin| bin.name() == default_run) { binary.set_main(true); } else { binaries.push(BundleBinary::new(default_run.clone(), true)); } } match binaries.len() { 0 => binaries.push(BundleBinary::new( self.cargo_package_settings.name.clone(), true, )), 1 => binaries.get_mut(0).unwrap().set_main(true), _ => {} } Ok(binaries) } fn app_name(&self) -> Option { self .manifest .lock() .unwrap() .inner .as_table() .get("package") .and_then(|p| p.as_table()) .and_then(|p| p.get("name")) .and_then(|n| n.as_str()) .map(|n| n.to_string()) } fn lib_name(&self) -> Option { self .manifest .lock() .unwrap() .inner .as_table() .get("lib") .and_then(|p| p.as_table()) .and_then(|p| p.get("name")) .and_then(|n| n.as_str()) .map(|n| n.to_string()) } } impl RustAppSettings { pub fn new(config: &Config, manifest: Manifest, target: Option) -> crate::Result { let cargo_settings = CargoSettings::load(tauri_dir()).with_context(|| "failed to load cargo settings")?; let cargo_package_settings = match &cargo_settings.package { Some(package_info) => package_info.clone(), None => { return Err(anyhow::anyhow!( "No package info in the config file".to_owned(), )) } }; let ws_package_settings = CargoSettings::load(&get_workspace_dir()?) .with_context(|| "failed to load cargo settings from workspace root")? .workspace .and_then(|v| v.package); let package_settings = PackageSettings { product_name: config .product_name .clone() .unwrap_or_else(|| cargo_package_settings.name.clone()), version: config.version.clone().unwrap_or_else(|| { cargo_package_settings .version .clone() .expect("Cargo manifest must have the `package.version` field") .resolve("version", || { ws_package_settings .as_ref() .and_then(|p| p.version.clone()) .ok_or_else(|| anyhow::anyhow!("Couldn't inherit value for `version` from workspace")) }) .expect("Cargo project does not have a version") }), description: cargo_package_settings .description .clone() .map(|description| { description .resolve("description", || { ws_package_settings .as_ref() .and_then(|v| v.description.clone()) .ok_or_else(|| { anyhow::anyhow!("Couldn't inherit value for `description` from workspace") }) }) .unwrap() }) .unwrap_or_default(), homepage: cargo_package_settings.homepage.clone().map(|homepage| { homepage .resolve("homepage", || { ws_package_settings .as_ref() .and_then(|v| v.homepage.clone()) .ok_or_else(|| { anyhow::anyhow!("Couldn't inherit value for `homepage` from workspace") }) }) .unwrap() }), authors: cargo_package_settings.authors.clone().map(|authors| { authors .resolve("authors", || { ws_package_settings .as_ref() .and_then(|v| v.authors.clone()) .ok_or_else(|| anyhow::anyhow!("Couldn't inherit value for `authors` from workspace")) }) .unwrap() }), default_run: cargo_package_settings.default_run.clone(), }; let cargo_config = CargoConfig::load(tauri_dir())?; let target_triple = target.unwrap_or_else(|| { cargo_config .build() .target() .map(|t| t.to_string()) .unwrap_or_else(|| { let output = Command::new("rustc") .args(["-vV"]) .output() .expect("\"rustc\" could not be found, did you install Rust?"); let stdout = String::from_utf8_lossy(&output.stdout); stdout .split('\n') .find(|l| l.starts_with("host:")) .unwrap() .replace("host:", "") .trim() .to_string() }) }); let target = Target::from_triple(&target_triple); Ok(Self { manifest: Mutex::new(manifest), cargo_settings, cargo_package_settings, cargo_ws_package_settings: ws_package_settings, package_settings, cargo_config, target_triple, target, }) } fn target<'a>(&'a self, options: &'a Options) -> Option<&'a str> { options .target .as_deref() .or_else(|| self.cargo_config.build().target()) } pub fn out_dir(&self, options: &Options) -> crate::Result { get_target_dir(self.target(options), options) } } #[derive(Deserialize)] pub(crate) struct CargoMetadata { pub(crate) target_directory: PathBuf, pub(crate) workspace_root: PathBuf, } pub(crate) fn get_cargo_metadata() -> crate::Result { let output = Command::new("cargo") .args(["metadata", "--no-deps", "--format-version", "1"]) .current_dir(tauri_dir()) .output()?; if !output.status.success() { return Err(anyhow::anyhow!( "cargo metadata command exited with a non zero exit code: {}", String::from_utf8_lossy(&output.stderr) )); } Ok(serde_json::from_slice(&output.stdout)?) } /// This function determines the 'target' directory and suffixes it with the profile /// to determine where the compiled binary will be located. fn get_target_dir(triple: Option<&str>, options: &Options) -> crate::Result { let mut path = if let Some(target) = get_cargo_option(&options.args, "--target-dir") { std::env::current_dir()?.join(target) } else { let mut path = get_cargo_metadata() .with_context(|| "failed to get cargo metadata")? .target_directory; if let Some(triple) = triple { path.push(triple); } path }; path.push(get_profile_dir(options)); Ok(path) } #[inline] fn get_cargo_option<'a>(args: &'a [String], option: &'a str) -> Option<&'a str> { args .iter() .position(|a| a.starts_with(option)) .and_then(|i| { args[i] .split_once('=') .map(|(_, p)| Some(p)) .unwrap_or_else(|| args.get(i + 1).map(|s| s.as_str())) }) } /// Executes `cargo metadata` to get the workspace directory. pub fn get_workspace_dir() -> crate::Result { Ok( get_cargo_metadata() .with_context(|| "failed to get cargo metadata")? .workspace_root, ) } pub fn get_profile(options: &Options) -> &str { get_cargo_option(&options.args, "--profile").unwrap_or(if options.debug { "dev" } else { "release" }) } pub fn get_profile_dir(options: &Options) -> &str { match get_profile(options) { "dev" => "debug", profile => profile, } } #[allow(unused_variables)] fn tauri_config_to_bundle_settings( settings: &RustAppSettings, features: &[String], identifier: String, config: crate::helpers::config::BundleConfig, updater_config: Option, arch64bits: bool, ) -> crate::Result { let enabled_features = settings .manifest .lock() .unwrap() .all_enabled_features(features); #[cfg(windows)] let windows_icon_path = PathBuf::from( config .icon .iter() .find(|i| i.ends_with(".ico")) .cloned() .expect("the bundle config must have a `.ico` icon"), ); #[cfg(not(windows))] let windows_icon_path = PathBuf::from(""); #[allow(unused_mut)] let mut resources = config .resources .unwrap_or(BundleResources::List(Vec::new())); #[allow(unused_mut)] let mut depends_deb = config.linux.deb.depends.unwrap_or_default(); #[allow(unused_mut)] let mut depends_rpm = config.linux.rpm.depends.unwrap_or_default(); // set env vars used by the bundler and inject dependencies #[cfg(target_os = "linux")] { let mut libs: Vec = Vec::new(); if enabled_features.contains(&"tray-icon".into()) || enabled_features.contains(&"tauri/tray-icon".into()) { let (tray_kind, path) = std::env::var("TAURI_LINUX_AYATANA_APPINDICATOR") .map(|ayatana| { if ayatana == "true" || ayatana == "1" { ( pkgconfig_utils::TrayKind::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 { ( pkgconfig_utils::TrayKind::Libappindicator, format!( "{}/libappindicator3.so.1", pkgconfig_utils::get_library_path("appindicator3-0.1") .expect("failed to get libappindicator-gtk library path using pkg-config.") ), ) } }) .unwrap_or_else(|_| pkgconfig_utils::get_appindicator_library_path()); match tray_kind { pkgconfig_utils::TrayKind::Ayatana => { depends_deb.push("libayatana-appindicator3-1".into()); libs.push("libayatana-appindicator3.so.1".into()); } pkgconfig_utils::TrayKind::Libappindicator => { depends_deb.push("libappindicator3-1".into()); libs.push("libappindicator3.so.1".into()); } } std::env::set_var("TAURI_TRAY_LIBRARY_PATH", path); } depends_deb.push("libwebkit2gtk-4.1-0".to_string()); depends_deb.push("libgtk-3-0".to_string()); libs.push("libwebkit2gtk-4.1.so.0".into()); libs.push("libgtk-3.so.0".into()); for lib in libs { let mut requires = lib; if arch64bits { requires.push_str("()(64bit)"); } depends_rpm.push(requires); } } #[cfg(windows)] { if let crate::helpers::config::WebviewInstallMode::FixedRuntime { path } = &config.windows.webview_install_mode { resources.push(path.display().to_string()); } } let signing_identity = match std::env::var_os("APPLE_SIGNING_IDENTITY") { Some(signing_identity) => Some( signing_identity .to_str() .expect("failed to convert APPLE_SIGNING_IDENTITY to string") .to_string(), ), None => config.macos.signing_identity, }; let provider_short_name = match std::env::var_os("APPLE_PROVIDER_SHORT_NAME") { Some(provider_short_name) => Some( provider_short_name .to_str() .expect("failed to convert APPLE_PROVIDER_SHORT_NAME to string") .to_string(), ), None => config.macos.provider_short_name, }; let (resources, resources_map) = match resources { BundleResources::List(paths) => (Some(paths), None), BundleResources::Map(map) => (None, Some(map)), }; Ok(BundleSettings { identifier: Some(identifier), publisher: config.publisher, homepage: config.homepage, icon: Some(config.icon), resources, resources_map, copyright: config.copyright, category: match config.category { Some(category) => Some(AppCategory::from_str(&category).map_err(|e| match e { Some(e) => anyhow::anyhow!("invalid category, did you mean `{}`?", e), None => anyhow::anyhow!("invalid category"), })?), None => None, }, file_associations: config.file_associations, short_description: config.short_description, long_description: config.long_description, external_bin: config.external_bin, deb: DebianSettings { depends: if depends_deb.is_empty() { None } else { Some(depends_deb) }, provides: config.linux.deb.provides, conflicts: config.linux.deb.conflicts, replaces: config.linux.deb.replaces, files: config.linux.deb.files, desktop_template: config.linux.deb.desktop_template, section: config.linux.deb.section, priority: config.linux.deb.priority, changelog: config.linux.deb.changelog, pre_install_script: config.linux.deb.pre_install_script, post_install_script: config.linux.deb.post_install_script, pre_remove_script: config.linux.deb.pre_remove_script, post_remove_script: config.linux.deb.post_remove_script, }, appimage: AppImageSettings { files: config.linux.appimage.files, }, rpm: RpmSettings { depends: if depends_rpm.is_empty() { None } else { Some(depends_rpm) }, provides: config.linux.rpm.provides, conflicts: config.linux.rpm.conflicts, obsoletes: config.linux.rpm.obsoletes, release: config.linux.rpm.release, epoch: config.linux.rpm.epoch, files: config.linux.rpm.files, desktop_template: config.linux.rpm.desktop_template, pre_install_script: config.linux.rpm.pre_install_script, post_install_script: config.linux.rpm.post_install_script, pre_remove_script: config.linux.rpm.pre_remove_script, post_remove_script: config.linux.rpm.post_remove_script, }, dmg: DmgSettings { background: config.macos.dmg.background, window_position: config .macos .dmg .window_position .map(|window_position| Position { x: window_position.x, y: window_position.y, }), window_size: Size { width: config.macos.dmg.window_size.width, height: config.macos.dmg.window_size.height, }, app_position: Position { x: config.macos.dmg.app_position.x, y: config.macos.dmg.app_position.y, }, application_folder_position: Position { x: config.macos.dmg.application_folder_position.x, y: config.macos.dmg.application_folder_position.y, }, }, macos: MacOsSettings { frameworks: config.macos.frameworks, files: config.macos.files, minimum_system_version: config.macos.minimum_system_version, exception_domain: config.macos.exception_domain, signing_identity, hardened_runtime: config.macos.hardened_runtime, provider_short_name, entitlements: config.macos.entitlements, info_plist_path: { let path = tauri_dir().join("Info.plist"); if path.exists() { Some(path) } else { None } }, }, windows: WindowsSettings { timestamp_url: config.windows.timestamp_url, tsp: config.windows.tsp, digest_algorithm: config.windows.digest_algorithm, certificate_thumbprint: config.windows.certificate_thumbprint, wix: config.windows.wix.map(wix_settings), nsis: config.windows.nsis.map(nsis_settings), icon_path: windows_icon_path, webview_install_mode: config.windows.webview_install_mode, allow_downgrades: config.windows.allow_downgrades, sign_command: config.windows.sign_command.map(custom_sign_settings), }, license: config.license.or_else(|| { settings .cargo_package_settings .license .clone() .map(|license| { license .resolve("license", || { settings .cargo_ws_package_settings .as_ref() .and_then(|v| v.license.clone()) .ok_or_else(|| { anyhow::anyhow!("Couldn't inherit value for `license` from workspace") }) }) .unwrap() }) }), license_file: config.license_file.map(|l| tauri_dir().join(l)), updater: updater_config, ..Default::default() }) } #[cfg(target_os = "linux")] mod pkgconfig_utils { use std::process::Command; pub enum TrayKind { Ayatana, Libappindicator, } pub fn get_appindicator_library_path() -> (TrayKind, String) { match get_library_path("ayatana-appindicator3-0.1") { Some(p) => ( TrayKind::Ayatana, format!("{p}/libayatana-appindicator3.so.1"), ), None => match get_library_path("appindicator3-0.1") { Some(p) => ( TrayKind::Libappindicator, format!("{p}/libappindicator3.so.1"), ), None => panic!("Can't detect any appindicator library"), }, } } /// Gets the folder in which a library is located using `pkg-config`. pub fn get_library_path(name: &str) -> Option { 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() { // output would be "-L/path/to/library\n" let word = output.stdout[2..].to_vec(); return Some(String::from_utf8_lossy(&word).trim().to_string()); } else { None } } else { None } } } #[cfg(test)] mod tests { use super::*; #[test] fn parse_cargo_option() { let args = vec![ "build".into(), "--".into(), "--profile".into(), "holla".into(), "--features".into(), "a".into(), "b".into(), "--target-dir".into(), "path/to/dir".into(), ]; assert_eq!(get_cargo_option(&args, "--profile"), Some("holla")); assert_eq!(get_cargo_option(&args, "--target-dir"), Some("path/to/dir")); assert_eq!(get_cargo_option(&args, "--non-existent"), None); } #[test] fn parse_profile_from_opts() { let options = Options { args: vec![ "build".into(), "--".into(), "--profile".into(), "testing".into(), "--features".into(), "feat1".into(), ], ..Default::default() }; assert_eq!(get_profile(&options), "testing"); let options = Options { args: vec![ "build".into(), "--".into(), "--profile=customprofile".into(), "testing".into(), "--features".into(), "feat1".into(), ], ..Default::default() }; assert_eq!(get_profile(&options), "customprofile"); let options = Options { debug: true, args: vec![ "build".into(), "--".into(), "testing".into(), "--features".into(), "feat1".into(), ], ..Default::default() }; assert_eq!(get_profile(&options), "dev"); let options = Options { debug: false, args: vec![ "build".into(), "--".into(), "testing".into(), "--features".into(), "feat1".into(), ], ..Default::default() }; assert_eq!(get_profile(&options), "release"); let options = Options { args: vec!["build".into(), "--".into(), "--profile".into()], ..Default::default() }; assert_eq!(get_profile(&options), "release"); } #[test] fn parse_target_dir_from_opts() { crate::helpers::app_paths::resolve(); let current_dir = std::env::current_dir().unwrap(); let options = Options { args: vec![ "build".into(), "--".into(), "--target-dir".into(), "path/to/some/dir".into(), "--features".into(), "feat1".into(), ], debug: false, ..Default::default() }; assert_eq!( get_target_dir(None, &options).unwrap(), current_dir.join("path/to/some/dir/release") ); assert_eq!( get_target_dir(Some("x86_64-pc-windows-msvc"), &options).unwrap(), current_dir.join("path/to/some/dir/release") ); let options = Options { args: vec![ "build".into(), "--".into(), "--features".into(), "feat1".into(), ], debug: false, ..Default::default() }; #[cfg(windows)] assert!(get_target_dir(Some("x86_64-pc-windows-msvc"), &options) .unwrap() .ends_with("x86_64-pc-windows-msvc\\release")); #[cfg(not(windows))] assert!(get_target_dir(Some("x86_64-pc-windows-msvc"), &options) .unwrap() .ends_with("x86_64-pc-windows-msvc/release")); #[cfg(windows)] { std::env::set_var("CARGO_TARGET_DIR", "D:\\path\\to\\env\\dir"); assert_eq!( get_target_dir(None, &options).unwrap(), PathBuf::from("D:\\path\\to\\env\\dir\\release") ); assert_eq!( get_target_dir(Some("x86_64-pc-windows-msvc"), &options).unwrap(), PathBuf::from("D:\\path\\to\\env\\dir\\x86_64-pc-windows-msvc\\release") ); } #[cfg(not(windows))] { std::env::set_var("CARGO_TARGET_DIR", "/path/to/env/dir"); assert_eq!( get_target_dir(None, &options).unwrap(), PathBuf::from("/path/to/env/dir/release") ); assert_eq!( get_target_dir(Some("x86_64-pc-windows-msvc"), &options).unwrap(), PathBuf::from("/path/to/env/dir/x86_64-pc-windows-msvc/release") ); } } }