Skip to main content

tauri_cli/interface/
rust.rs

1// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
2// SPDX-License-Identifier: Apache-2.0
3// SPDX-License-Identifier: MIT
4
5use std::{
6  collections::HashMap,
7  ffi::OsStr,
8  fs::FileType,
9  io::{BufRead, Write},
10  iter::once,
11  path::{Path, PathBuf},
12  process::Command,
13  str::FromStr,
14  sync::{mpsc::sync_channel, Arc, Mutex},
15  time::Duration,
16};
17
18use dunce::canonicalize;
19use ignore::gitignore::{Gitignore, GitignoreBuilder};
20use notify::RecursiveMode;
21use notify_debouncer_full::new_debouncer;
22use serde::{Deserialize, Deserializer};
23use tauri_bundler::{
24  AppCategory, AppImageSettings, BundleBinary, BundleSettings, DebianSettings, DmgSettings,
25  IosSettings, MacOsSettings, PackageSettings, Position, RpmSettings, Size, UpdaterSettings,
26  WindowsSettings,
27};
28use tauri_utils::config::{parse::is_configuration_file, DeepLinkProtocol, RunnerConfig, Updater};
29
30use super::{AppSettings, DevProcess, ExitReason};
31use crate::{
32  error::{bail, Context, Error, ErrorExt},
33  helpers::{
34    app_paths::Dirs,
35    config::{nsis_settings, reload_config, wix_settings, BundleResources, Config, ConfigMetadata},
36  },
37  ConfigValue,
38};
39use tauri_utils::{display_path, platform::Target as TargetPlatform};
40
41mod cargo_config;
42mod desktop;
43pub mod installation;
44pub mod manifest;
45use crate::helpers::config::custom_sign_settings;
46use cargo_config::Config as CargoConfig;
47use manifest::{rewrite_manifest, Manifest};
48
49#[derive(Debug, Default, Clone)]
50pub struct Options {
51  pub runner: Option<RunnerConfig>,
52  pub debug: bool,
53  pub target: Option<String>,
54  pub features: Vec<String>,
55  pub args: Vec<String>,
56  pub config: Vec<ConfigValue>,
57  pub no_watch: bool,
58  pub skip_stapling: bool,
59  pub additional_watch_folders: Vec<PathBuf>,
60}
61
62impl From<crate::build::Options> for Options {
63  fn from(options: crate::build::Options) -> Self {
64    Self {
65      runner: options.runner,
66      debug: options.debug,
67      target: options.target,
68      features: options.features,
69      args: options.args,
70      config: options.config,
71      no_watch: true,
72      skip_stapling: options.skip_stapling,
73      additional_watch_folders: Vec::new(),
74    }
75  }
76}
77
78impl From<crate::bundle::Options> for Options {
79  fn from(options: crate::bundle::Options) -> Self {
80    Self {
81      debug: options.debug,
82      config: options.config,
83      target: options.target,
84      features: options.features,
85      no_watch: true,
86      skip_stapling: options.skip_stapling,
87      ..Default::default()
88    }
89  }
90}
91
92impl From<crate::dev::Options> for Options {
93  fn from(options: crate::dev::Options) -> Self {
94    Self {
95      runner: options.runner,
96      debug: !options.release_mode,
97      target: options.target,
98      features: options.features,
99      args: options.args,
100      config: options.config,
101      no_watch: options.no_watch,
102      skip_stapling: false,
103      additional_watch_folders: options.additional_watch_folders,
104    }
105  }
106}
107
108#[derive(Debug, Clone)]
109pub struct MobileOptions {
110  pub debug: bool,
111  pub features: Vec<String>,
112  pub args: Vec<String>,
113  pub config: Vec<ConfigValue>,
114  pub no_watch: bool,
115  pub additional_watch_folders: Vec<PathBuf>,
116}
117
118#[derive(Debug, Clone)]
119pub struct WatcherOptions {
120  pub config: Vec<ConfigValue>,
121  pub additional_watch_folders: Vec<PathBuf>,
122}
123
124#[derive(Debug)]
125pub struct RustupTarget {
126  name: String,
127  installed: bool,
128}
129
130pub struct Rust {
131  app_settings: Arc<RustAppSettings>,
132  config_features: Vec<String>,
133  available_targets: Option<Vec<RustupTarget>>,
134  main_binary_name: Option<String>,
135}
136
137impl Rust {
138  pub fn new(config: &Config, target: Option<String>, tauri_dir: &Path) -> crate::Result<Self> {
139    let manifest = {
140      let (tx, rx) = sync_channel(1);
141      let mut watcher = new_debouncer(Duration::from_secs(1), None, move |r| {
142        if let Ok(_events) = r {
143          let _ = tx.send(());
144        }
145      })
146      .unwrap();
147      let manifest_path = tauri_dir.join("Cargo.toml");
148      watcher
149        .watch(&manifest_path, RecursiveMode::NonRecursive)
150        .with_context(|| format!("failed to watch {}", manifest_path.display()))?;
151      let (manifest, modified) = rewrite_manifest(config, tauri_dir)?;
152      if modified {
153        // Wait for the modified event so we don't trigger a re-build later on
154        let _ = rx.recv_timeout(Duration::from_secs(2));
155      }
156      manifest
157    };
158
159    let target_ios = target
160      .as_ref()
161      .is_some_and(|target| target.ends_with("ios") || target.ends_with("ios-sim"));
162    if target_ios {
163      std::env::set_var(
164        "IPHONEOS_DEPLOYMENT_TARGET",
165        &config.bundle.ios.minimum_system_version,
166      );
167    }
168
169    let app_settings = RustAppSettings::new(config, manifest, target, tauri_dir)?;
170
171    Ok(Self {
172      app_settings: Arc::new(app_settings),
173      config_features: config.build.features.clone().unwrap_or_default(),
174      main_binary_name: config.main_binary_name.clone(),
175      available_targets: None,
176    })
177  }
178
179  pub fn app_settings(&self) -> Arc<RustAppSettings> {
180    self.app_settings.clone()
181  }
182
183  pub fn build(&mut self, options: Options, dirs: &Dirs) -> crate::Result<PathBuf> {
184    desktop::build(
185      options,
186      &self.app_settings,
187      &mut self.available_targets,
188      self.config_features.clone(),
189      self.main_binary_name.as_deref(),
190      dirs.tauri,
191    )
192  }
193
194  pub fn dev<F: Fn(Option<i32>, ExitReason) + Send + Sync + 'static>(
195    &mut self,
196    config: &mut ConfigMetadata,
197    mut options: Options,
198    on_exit: F,
199    dirs: &Dirs,
200  ) -> crate::Result<()> {
201    let on_exit = Arc::new(on_exit);
202
203    let mut run_args = Vec::new();
204    dev_options(
205      false,
206      &mut options.args,
207      &mut run_args,
208      &mut options.features,
209      &self.app_settings,
210    );
211
212    if options.no_watch {
213      let (tx, rx) = sync_channel(1);
214      self.run_dev(options, &run_args, move |status, reason| {
215        on_exit(status, reason);
216        tx.send(()).unwrap();
217      })?;
218
219      rx.recv().unwrap();
220      Ok(())
221    } else {
222      let merge_configs = options.config.iter().map(|c| &c.0).collect::<Vec<_>>();
223      self.run_dev_watcher(
224        config,
225        &options.additional_watch_folders,
226        &merge_configs,
227        |rust: &mut Rust, _config| {
228          let on_exit = on_exit.clone();
229          rust
230            .run_dev(options.clone(), &run_args, move |status, reason| {
231              on_exit(status, reason)
232            })
233            .map(|child| Box::new(child) as Box<dyn DevProcess + Send>)
234        },
235        dirs,
236      )
237    }
238  }
239
240  pub fn mobile_dev<
241    R: Fn(MobileOptions, &ConfigMetadata) -> crate::Result<Box<dyn DevProcess + Send>>,
242  >(
243    &mut self,
244    config: &mut ConfigMetadata,
245    mut options: MobileOptions,
246    runner: R,
247    dirs: &Dirs,
248  ) -> crate::Result<()> {
249    let mut run_args = Vec::new();
250    dev_options(
251      true,
252      &mut options.args,
253      &mut run_args,
254      &mut options.features,
255      &self.app_settings,
256    );
257
258    if options.no_watch {
259      runner(options, config)?;
260      Ok(())
261    } else {
262      self.watch(
263        config,
264        WatcherOptions {
265          config: options.config.clone(),
266          additional_watch_folders: options.additional_watch_folders.clone(),
267        },
268        move |config| runner(options.clone(), config),
269        dirs,
270      )
271    }
272  }
273
274  pub fn watch<R: Fn(&ConfigMetadata) -> crate::Result<Box<dyn DevProcess + Send>>>(
275    &mut self,
276    config: &mut ConfigMetadata,
277    options: WatcherOptions,
278    runner: R,
279    dirs: &Dirs,
280  ) -> crate::Result<()> {
281    let merge_configs = options.config.iter().map(|c| &c.0).collect::<Vec<_>>();
282    self.run_dev_watcher(
283      config,
284      &options.additional_watch_folders,
285      &merge_configs,
286      |_rust: &mut Rust, config| runner(config),
287      dirs,
288    )
289  }
290
291  pub fn env(&self) -> HashMap<&str, String> {
292    let mut env = HashMap::new();
293    env.insert(
294      "TAURI_ENV_TARGET_TRIPLE",
295      self.app_settings.target_triple.clone(),
296    );
297
298    let target_triple = &self.app_settings.target_triple;
299    let target_components: Vec<&str> = target_triple.split('-').collect();
300    let (arch, host, _host_env) = match target_components.as_slice() {
301      // 3 components like aarch64-apple-darwin
302      [arch, _, host] => (*arch, *host, None),
303      // 4 components like x86_64-pc-windows-msvc and aarch64-apple-ios-sim
304      [arch, _, host, host_env] => (*arch, *host, Some(*host_env)),
305      _ => {
306        log::warn!("Invalid target triple: {}", target_triple);
307        return env;
308      }
309    };
310
311    env.insert("TAURI_ENV_ARCH", arch.into());
312    env.insert("TAURI_ENV_PLATFORM", host.into());
313    env.insert(
314      "TAURI_ENV_FAMILY",
315      match host {
316        "windows" => "windows".into(),
317        _ => "unix".into(),
318      },
319    );
320
321    env
322  }
323}
324
325struct IgnoreMatcher(Vec<Gitignore>);
326
327impl IgnoreMatcher {
328  fn is_ignore(&self, path: &Path, is_dir: bool) -> bool {
329    for gitignore in &self.0 {
330      if path.starts_with(gitignore.path())
331        && gitignore
332          .matched_path_or_any_parents(path, is_dir)
333          .is_ignore()
334      {
335        return true;
336      }
337    }
338    false
339  }
340}
341
342fn build_ignore_matcher(dir: &Path) -> IgnoreMatcher {
343  let mut matchers = Vec::new();
344
345  // ignore crate doesn't expose an API to build `ignore::gitignore::GitIgnore`
346  // with custom ignore file names so we have to walk the directory and collect
347  // our custom ignore files and add it using `ignore::gitignore::GitIgnoreBuilder::add`
348  for entry in ignore::WalkBuilder::new(dir)
349    .require_git(false)
350    .ignore(false)
351    .overrides(
352      ignore::overrides::OverrideBuilder::new(dir)
353        .add(".taurignore")
354        .unwrap()
355        .build()
356        .unwrap(),
357    )
358    .build()
359    .flatten()
360  {
361    let path = entry.path();
362    if path.file_name() == Some(OsStr::new(".taurignore")) {
363      let mut ignore_builder = GitignoreBuilder::new(path.parent().unwrap());
364
365      ignore_builder.add(path);
366
367      if let Some(ignore_file) = std::env::var_os("TAURI_CLI_WATCHER_IGNORE_FILENAME") {
368        ignore_builder.add(dir.join(ignore_file));
369      }
370
371      for line in crate::dev::TAURI_CLI_BUILTIN_WATCHER_IGNORE_FILE
372        .lines()
373        .map_while(Result::ok)
374      {
375        let _ = ignore_builder.add_line(None, &line);
376      }
377
378      matchers.push(ignore_builder.build().unwrap());
379    }
380  }
381
382  IgnoreMatcher(matchers)
383}
384
385fn lookup<F: FnMut(FileType, PathBuf)>(dir: &Path, mut f: F) {
386  let mut default_gitignore = std::env::temp_dir();
387  default_gitignore.push(".tauri");
388  let _ = std::fs::create_dir_all(&default_gitignore);
389  default_gitignore.push(".gitignore");
390  if !default_gitignore.exists() {
391    if let Ok(mut file) = std::fs::File::create(default_gitignore.clone()) {
392      let _ = file.write_all(crate::dev::TAURI_CLI_BUILTIN_WATCHER_IGNORE_FILE);
393    }
394  }
395
396  let mut builder = ignore::WalkBuilder::new(dir);
397  builder.add_custom_ignore_filename(".taurignore");
398  let _ = builder.add_ignore(default_gitignore);
399  if let Some(ignore_file) = std::env::var_os("TAURI_CLI_WATCHER_IGNORE_FILENAME") {
400    builder.add_ignore(ignore_file);
401  }
402  builder.require_git(false).ignore(false).max_depth(Some(1));
403
404  for entry in builder.build().flatten() {
405    f(entry.file_type().unwrap(), dir.join(entry.path()));
406  }
407}
408
409fn dev_options(
410  mobile: bool,
411  args: &mut Vec<String>,
412  run_args: &mut Vec<String>,
413  features: &mut Vec<String>,
414  app_settings: &RustAppSettings,
415) {
416  let mut dev_args = Vec::new();
417  let mut reached_run_args = false;
418  for arg in args.clone() {
419    if reached_run_args {
420      run_args.push(arg);
421    } else if arg == "--" {
422      reached_run_args = true;
423    } else {
424      dev_args.push(arg);
425    }
426  }
427  *args = dev_args;
428
429  if mobile && !args.contains(&"--lib".into()) {
430    args.push("--lib".into());
431  }
432
433  if !args.contains(&"--no-default-features".into()) {
434    let manifest_features = app_settings.manifest.lock().unwrap().features();
435    let enable_features: Vec<String> = manifest_features
436      .get("default")
437      .cloned()
438      .unwrap_or_default()
439      .into_iter()
440      .filter(|feature| {
441        if let Some(manifest_feature) = manifest_features.get(feature) {
442          !manifest_feature.contains(&"tauri/custom-protocol".into())
443        } else {
444          feature != "tauri/custom-protocol"
445        }
446      })
447      .collect();
448    args.push("--no-default-features".into());
449    features.extend(enable_features);
450  }
451}
452
453fn get_watch_folders(
454  additional_watch_folders: &[PathBuf],
455  tauri_dir: &Path,
456) -> crate::Result<Vec<PathBuf>> {
457  // We always want to watch the main tauri folder.
458  let mut watch_folders = vec![tauri_dir.to_path_buf()];
459
460  watch_folders.extend(get_in_workspace_dependency_paths(tauri_dir)?);
461
462  // Add the additional watch folders, resolving the path from the tauri path if it is relative
463  watch_folders.extend(additional_watch_folders.iter().filter_map(|dir| {
464    let path = if dir.is_absolute() {
465      dir.to_owned()
466    } else {
467      tauri_dir.join(dir)
468    };
469
470    let canonicalized = canonicalize(&path).ok();
471    if canonicalized.is_none() {
472      log::warn!(
473        "Additional watch folder '{}' not found, ignoring",
474        path.display()
475      );
476    }
477    canonicalized
478  }));
479
480  Ok(watch_folders)
481}
482
483impl Rust {
484  pub fn build_options(&self, args: &mut Vec<String>, features: &mut Vec<String>, mobile: bool) {
485    features.push("tauri/custom-protocol".into());
486    if mobile {
487      if !args.contains(&"--lib".into()) {
488        args.push("--lib".into());
489      }
490    } else {
491      args.push("--bins".into());
492    }
493  }
494
495  fn run_dev<F: Fn(Option<i32>, ExitReason) + Send + Sync + 'static>(
496    &mut self,
497    options: Options,
498    run_args: &[String],
499    on_exit: F,
500  ) -> crate::Result<desktop::DevChild> {
501    desktop::run_dev(
502      options,
503      run_args,
504      &mut self.available_targets,
505      self.config_features.clone(),
506      on_exit,
507    )
508  }
509
510  fn run_dev_watcher<
511    F: Fn(&mut Rust, &ConfigMetadata) -> crate::Result<Box<dyn DevProcess + Send>>,
512  >(
513    &mut self,
514    config: &mut ConfigMetadata,
515    additional_watch_folders: &[PathBuf],
516    merge_configs: &[&serde_json::Value],
517    run: F,
518    dirs: &Dirs,
519  ) -> crate::Result<()> {
520    let mut child = run(self, config)?;
521    let (tx, rx) = sync_channel(1);
522
523    let watch_folders = get_watch_folders(additional_watch_folders, dirs.tauri)?;
524
525    let common_ancestor = common_path::common_path_all(
526      watch_folders
527        .iter()
528        .map(Path::new)
529        .chain(once(self.app_settings.workspace_dir.as_path())),
530    )
531    .expect("watch_folders should not be empty");
532    let ignore_matcher = build_ignore_matcher(&common_ancestor);
533
534    let mut watcher = new_debouncer(Duration::from_secs(1), None, move |r| {
535      if let Ok(events) = r {
536        tx.send(events).unwrap()
537      }
538    })
539    .unwrap();
540    for path in watch_folders {
541      if !ignore_matcher.is_ignore(&path, true) {
542        log::info!("Watching {} for changes...", display_path(&path));
543        lookup(&path, |file_type, p| {
544          if p != path {
545            log::debug!("Watching {} for changes...", display_path(&p));
546            let _ = watcher.watch(
547              &p,
548              if file_type.is_dir() {
549                RecursiveMode::Recursive
550              } else {
551                RecursiveMode::NonRecursive
552              },
553            );
554          }
555        });
556      }
557    }
558
559    while let Ok(events) = rx.recv() {
560      let paths: Vec<PathBuf> = events
561        .into_iter()
562        .filter(|event| !event.kind.is_access())
563        .flat_map(|event| event.event.paths)
564        .filter(|path| !ignore_matcher.is_ignore(path, path.is_dir()))
565        .collect();
566
567      let config_file_changed = paths
568        .iter()
569        .any(|path| is_configuration_file(self.app_settings.target_platform, path));
570      if config_file_changed && reload_config(config, merge_configs, dirs.tauri).is_ok() {
571        let (manifest, modified) = rewrite_manifest(config, dirs.tauri)?;
572        if modified {
573          *self.app_settings.manifest.lock().unwrap() = manifest;
574          // no need to run the watcher logic, the manifest was modified
575          // and it will trigger the watcher again
576          continue;
577        }
578      }
579
580      let Some(first_changed_path) = paths.first() else {
581        continue;
582      };
583
584      log::info!(
585        "File {} changed. Rebuilding application...",
586        display_path(
587          first_changed_path
588            .strip_prefix(dirs.frontend)
589            .unwrap_or(first_changed_path)
590        )
591      );
592
593      child.kill().context("failed to kill app process")?;
594      // wait for the process to exit
595      // note that on mobile, kill() already waits for the process to exit (duct implementation)
596      let _ = child.wait();
597      child = run(self, config)?;
598    }
599    bail!("File watcher exited unexpectedly")
600  }
601}
602
603// Taken from https://github.com/rust-lang/cargo/blob/70898e522116f6c23971e2a554b2dc85fd4c84cd/src/cargo/util/toml/mod.rs#L1008-L1065
604/// Enum that allows for the parsing of `field.workspace = true` in a Cargo.toml
605///
606/// It allows for things to be inherited from a workspace or defined as needed
607#[derive(Clone, Debug)]
608pub enum MaybeWorkspace<T> {
609  Workspace(TomlWorkspaceField),
610  Defined(T),
611}
612
613impl<'de, T: Deserialize<'de>> serde::de::Deserialize<'de> for MaybeWorkspace<T> {
614  fn deserialize<D>(deserializer: D) -> Result<MaybeWorkspace<T>, D::Error>
615  where
616    D: serde::de::Deserializer<'de>,
617  {
618    let value = serde_value::Value::deserialize(deserializer)?;
619    if let Ok(workspace) = TomlWorkspaceField::deserialize(
620      serde_value::ValueDeserializer::<D::Error>::new(value.clone()),
621    ) {
622      return Ok(MaybeWorkspace::Workspace(workspace));
623    }
624    T::deserialize(serde_value::ValueDeserializer::<D::Error>::new(value))
625      .map(MaybeWorkspace::Defined)
626  }
627}
628
629impl<T> MaybeWorkspace<T> {
630  fn resolve(
631    self,
632    label: &str,
633    get_ws_field: impl FnOnce() -> crate::Result<T>,
634  ) -> crate::Result<T> {
635    match self {
636      MaybeWorkspace::Defined(value) => Ok(value),
637      MaybeWorkspace::Workspace(TomlWorkspaceField { workspace: true }) => get_ws_field()
638        .with_context(|| {
639          format!(
640            "error inheriting `{label}` from workspace root manifest's `workspace.package.{label}`"
641          )
642        }),
643      MaybeWorkspace::Workspace(TomlWorkspaceField { workspace: false }) => Err(
644        crate::Error::GenericError("`workspace=false` is unsupported for `package.{label}`".into()),
645      ),
646    }
647  }
648  fn _as_defined(&self) -> Option<&T> {
649    match self {
650      MaybeWorkspace::Workspace(_) => None,
651      MaybeWorkspace::Defined(defined) => Some(defined),
652    }
653  }
654}
655
656#[derive(Deserialize, Clone, Debug)]
657pub struct TomlWorkspaceField {
658  workspace: bool,
659}
660
661/// The `workspace` section of the app configuration (read from Cargo.toml).
662#[derive(Clone, Debug, Deserialize)]
663struct WorkspaceSettings {
664  /// the workspace members.
665  // members: Option<Vec<String>>,
666  package: Option<WorkspacePackageSettings>,
667}
668
669#[derive(Clone, Debug, Deserialize)]
670struct WorkspacePackageSettings {
671  authors: Option<Vec<String>>,
672  description: Option<String>,
673  homepage: Option<String>,
674  version: Option<String>,
675  license: Option<String>,
676}
677
678#[derive(Clone, Debug, Deserialize)]
679#[serde(rename_all = "kebab-case")]
680struct BinarySettings {
681  name: String,
682  /// This is from nightly: https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#different-binary-name
683  filename: Option<String>,
684  path: Option<String>,
685  required_features: Option<Vec<String>>,
686}
687
688impl BinarySettings {
689  /// The file name without the binary extension (e.g. `.exe`)
690  pub fn file_name(&self) -> &str {
691    self.filename.as_ref().unwrap_or(&self.name)
692  }
693}
694
695/// The package settings.
696#[derive(Debug, Clone, Deserialize)]
697#[serde(rename_all = "kebab-case")]
698pub struct CargoPackageSettings {
699  /// the package's name.
700  pub name: String,
701  /// the package's version.
702  pub version: Option<MaybeWorkspace<String>>,
703  /// the package's description.
704  pub description: Option<MaybeWorkspace<String>>,
705  /// the package's homepage.
706  pub homepage: Option<MaybeWorkspace<String>>,
707  /// the package's authors.
708  pub authors: Option<MaybeWorkspace<Vec<String>>>,
709  /// the package's license.
710  pub license: Option<MaybeWorkspace<String>>,
711  /// the default binary to run.
712  pub default_run: Option<String>,
713}
714
715/// The Cargo settings (Cargo.toml root descriptor).
716#[derive(Clone, Debug, Deserialize)]
717struct CargoSettings {
718  /// the package settings.
719  ///
720  /// it's optional because ancestor workspace Cargo.toml files may not have package info.
721  package: Option<CargoPackageSettings>,
722  /// the workspace settings.
723  ///
724  /// it's present if the read Cargo.toml belongs to a workspace root.
725  workspace: Option<WorkspaceSettings>,
726  /// the binary targets configuration.
727  bin: Option<Vec<BinarySettings>>,
728}
729
730impl CargoSettings {
731  /// Try to load a set of CargoSettings from a "Cargo.toml" file in the specified directory.
732  fn load(dir: &Path) -> crate::Result<Self> {
733    let toml_path = dir.join("Cargo.toml");
734    let toml_str = std::fs::read_to_string(&toml_path)
735      .fs_context("Failed to read Cargo manifest", toml_path.clone())?;
736    toml::from_str(&toml_str).context(format!(
737      "failed to parse Cargo manifest at {}",
738      toml_path.display()
739    ))
740  }
741}
742
743pub struct RustAppSettings {
744  manifest: Mutex<Manifest>,
745  cargo_settings: CargoSettings,
746  cargo_package_settings: CargoPackageSettings,
747  cargo_ws_package_settings: Option<WorkspacePackageSettings>,
748  package_settings: PackageSettings,
749  cargo_config: CargoConfig,
750  target_triple: String,
751  target_platform: TargetPlatform,
752  workspace_dir: PathBuf,
753}
754
755#[derive(Deserialize)]
756#[serde(untagged)]
757enum DesktopDeepLinks {
758  One(DeepLinkProtocol),
759  List(Vec<DeepLinkProtocol>),
760}
761
762#[derive(Deserialize)]
763pub struct UpdaterConfig {
764  /// Signature public key.
765  pub pubkey: String,
766  /// The Windows configuration for the updater.
767  #[serde(default)]
768  pub windows: UpdaterWindowsConfig,
769}
770
771/// Install modes for the Windows update.
772#[derive(Default, Debug, PartialEq, Eq, Clone)]
773pub enum WindowsUpdateInstallMode {
774  /// Specifies there's a basic UI during the installation process, including a final dialog box at the end.
775  BasicUi,
776  /// The quiet mode means there's no user interaction required.
777  /// Requires admin privileges if the installer does.
778  Quiet,
779  /// Specifies unattended mode, which means the installation only shows a progress bar.
780  #[default]
781  Passive,
782  // to add more modes, we need to check if the updater relaunch makes sense
783  // i.e. for a full UI mode, the user can also mark the installer to start the app
784}
785
786impl<'de> Deserialize<'de> for WindowsUpdateInstallMode {
787  fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
788  where
789    D: Deserializer<'de>,
790  {
791    let s = String::deserialize(deserializer)?;
792    match s.to_lowercase().as_str() {
793      "basicui" => Ok(Self::BasicUi),
794      "quiet" => Ok(Self::Quiet),
795      "passive" => Ok(Self::Passive),
796      _ => Err(serde::de::Error::custom(format!(
797        "unknown update install mode '{s}'"
798      ))),
799    }
800  }
801}
802
803impl WindowsUpdateInstallMode {
804  /// Returns the associated `msiexec.exe` arguments.
805  pub fn msiexec_args(&self) -> &'static [&'static str] {
806    match self {
807      Self::BasicUi => &["/qb+"],
808      Self::Quiet => &["/quiet"],
809      Self::Passive => &["/passive"],
810    }
811  }
812}
813
814#[derive(Default, Deserialize)]
815#[serde(rename_all = "camelCase")]
816pub struct UpdaterWindowsConfig {
817  #[serde(default, alias = "install-mode")]
818  pub install_mode: WindowsUpdateInstallMode,
819}
820
821impl AppSettings for RustAppSettings {
822  fn get_package_settings(&self) -> PackageSettings {
823    self.package_settings.clone()
824  }
825
826  fn get_bundle_settings(
827    &self,
828    options: &Options,
829    config: &Config,
830    features: &[String],
831    tauri_dir: &Path,
832  ) -> crate::Result<BundleSettings> {
833    let arch64bits = self.target_triple.starts_with("x86_64")
834      || self.target_triple.starts_with("aarch64")
835      || self.target_triple.starts_with("riscv64");
836
837    let updater_enabled = config.bundle.create_updater_artifacts != Updater::Bool(false);
838    let v1_compatible = matches!(config.bundle.create_updater_artifacts, Updater::String(_));
839    let updater_settings = if updater_enabled {
840      let updater: UpdaterConfig = serde_json::from_value(
841        config
842          .plugins
843          .0
844          .get("updater")
845          .context("failed to get updater configuration: plugins > updater doesn't exist")?
846          .clone(),
847      )
848      .context("failed to parse updater plugin configuration")?;
849      Some(UpdaterSettings {
850        v1_compatible,
851        pubkey: updater.pubkey,
852        msiexec_args: updater.windows.install_mode.msiexec_args(),
853      })
854    } else {
855      None
856    };
857
858    let mut settings = tauri_config_to_bundle_settings(
859      self,
860      features,
861      config,
862      tauri_dir,
863      config.bundle.clone(),
864      updater_settings,
865      arch64bits,
866    )?;
867
868    settings.macos.skip_stapling = options.skip_stapling;
869
870    if let Some(plugin_config) = config
871      .plugins
872      .0
873      .get("deep-link")
874      .and_then(|c| c.get("desktop").cloned())
875    {
876      let protocols: DesktopDeepLinks =
877        serde_json::from_value(plugin_config).context("failed to parse desktop deep links from Tauri configuration > plugins > deep-link > desktop")?;
878      settings.deep_link_protocols = Some(match protocols {
879        DesktopDeepLinks::One(p) => vec![p],
880        DesktopDeepLinks::List(p) => p,
881      });
882    }
883
884    if let Some(open) = config.plugins.0.get("shell").and_then(|v| v.get("open")) {
885      if open.as_bool().is_some_and(|x| x) || open.is_string() {
886        settings.appimage.bundle_xdg_open = true;
887      }
888    }
889
890    if let Some(deps) = self
891      .manifest
892      .lock()
893      .unwrap()
894      .inner
895      .as_table()
896      .get("dependencies")
897      .and_then(|f| f.as_table())
898    {
899      if deps.contains_key("tauri-plugin-opener") {
900        settings.appimage.bundle_xdg_open = true;
901      };
902    }
903
904    Ok(settings)
905  }
906
907  fn app_binary_path(&self, options: &Options, tauri_dir: &Path) -> crate::Result<PathBuf> {
908    let binaries = self.get_binaries(options, tauri_dir)?;
909    let bin_name = binaries
910      .iter()
911      .find(|x| x.main())
912      .context("failed to find main binary, make sure you have a `package > default-run` in the Cargo.toml file")?
913      .name();
914
915    let out_dir = self
916      .out_dir(options, tauri_dir)
917      .context("failed to get project out directory")?;
918
919    let mut path = out_dir.join(bin_name);
920    if matches!(self.target_platform, TargetPlatform::Windows) {
921      // Append the `.exe` extension without overriding the existing extensions
922      let extension = if let Some(extension) = path.extension() {
923        let mut extension = extension.to_os_string();
924        extension.push(".exe");
925        extension
926      } else {
927        "exe".into()
928      };
929      path.set_extension(extension);
930    };
931    Ok(path)
932  }
933
934  fn get_binaries(&self, options: &Options, tauri_dir: &Path) -> crate::Result<Vec<BundleBinary>> {
935    let mut binaries = Vec::new();
936
937    if let Some(bins) = &self.cargo_settings.bin {
938      let default_run = self
939        .package_settings
940        .default_run
941        .clone()
942        .unwrap_or_default();
943      for bin in bins {
944        if let Some(req_features) = &bin.required_features {
945          // Check if all required features are enabled.
946          if !req_features
947            .iter()
948            .all(|feat| options.features.contains(feat))
949          {
950            continue;
951          }
952        }
953        let file_name = bin.file_name();
954        let is_main = file_name == self.cargo_package_settings.name || file_name == default_run;
955        binaries.push(BundleBinary::with_path(
956          file_name.to_owned(),
957          is_main,
958          bin.path.clone(),
959        ))
960      }
961    }
962
963    let mut binaries_paths = std::fs::read_dir(tauri_dir.join("src/bin"))
964      .map(|dir| {
965        dir
966          .into_iter()
967          .flatten()
968          .map(|entry| {
969            (
970              entry
971                .path()
972                .file_stem()
973                .unwrap_or_default()
974                .to_string_lossy()
975                .into_owned(),
976              entry.path(),
977            )
978          })
979          .collect::<Vec<_>>()
980      })
981      .unwrap_or_default();
982
983    if !binaries_paths
984      .iter()
985      .any(|(_name, path)| path == Path::new("src/main.rs"))
986      && tauri_dir.join("src/main.rs").exists()
987    {
988      binaries_paths.push((
989        self.cargo_package_settings.name.clone(),
990        tauri_dir.join("src/main.rs"),
991      ));
992    }
993
994    for (name, path) in binaries_paths {
995      // see https://github.com/tauri-apps/tauri/pull/10977#discussion_r1759742414
996      let bin_exists = binaries
997        .iter()
998        .any(|bin| bin.name() == name || path.ends_with(bin.src_path().unwrap_or(&"".to_string())));
999      if !bin_exists {
1000        binaries.push(BundleBinary::new(name, false))
1001      }
1002    }
1003
1004    if let Some(default_run) = self.package_settings.default_run.as_ref() {
1005      if let Some(binary) = binaries.iter_mut().find(|bin| bin.name() == default_run) {
1006        binary.set_main(true);
1007      } else {
1008        binaries.push(BundleBinary::new(default_run.clone(), true));
1009      }
1010    }
1011
1012    match binaries.len() {
1013      0 => binaries.push(BundleBinary::new(
1014        self.cargo_package_settings.name.clone(),
1015        true,
1016      )),
1017      1 => binaries.get_mut(0).unwrap().set_main(true),
1018      _ => {}
1019    }
1020
1021    Ok(binaries)
1022  }
1023
1024  fn app_name(&self) -> Option<String> {
1025    self
1026      .manifest
1027      .lock()
1028      .unwrap()
1029      .inner
1030      .as_table()
1031      .get("package")?
1032      .as_table()?
1033      .get("name")?
1034      .as_str()
1035      .map(|n| n.to_string())
1036  }
1037
1038  fn lib_name(&self) -> Option<String> {
1039    self
1040      .manifest
1041      .lock()
1042      .unwrap()
1043      .inner
1044      .as_table()
1045      .get("lib")?
1046      .as_table()?
1047      .get("name")?
1048      .as_str()
1049      .map(|n| n.to_string())
1050  }
1051}
1052
1053impl RustAppSettings {
1054  pub fn new(
1055    config: &Config,
1056    manifest: Manifest,
1057    target: Option<String>,
1058    tauri_dir: &Path,
1059  ) -> crate::Result<Self> {
1060    let cargo_settings = CargoSettings::load(tauri_dir).context("failed to load Cargo settings")?;
1061    let cargo_package_settings = match &cargo_settings.package {
1062      Some(package_info) => package_info.clone(),
1063      None => {
1064        return Err(crate::Error::GenericError(
1065          "No package info in the config file".to_owned(),
1066        ))
1067      }
1068    };
1069
1070    let workspace_dir = get_workspace_dir(tauri_dir)?;
1071    let ws_package_settings = CargoSettings::load(&workspace_dir)
1072      .context("failed to load Cargo settings from workspace root")?
1073      .workspace
1074      .and_then(|v| v.package);
1075
1076    let version = config.version.clone().unwrap_or_else(|| {
1077      cargo_package_settings
1078        .version
1079        .clone()
1080        .expect("Cargo manifest must have the `package.version` field")
1081        .resolve("version", || {
1082          ws_package_settings
1083            .as_ref()
1084            .and_then(|p| p.version.clone())
1085            .context("Couldn't inherit value for `version` from workspace")
1086        })
1087        .expect("Cargo project does not have a version")
1088    });
1089
1090    let package_settings = PackageSettings {
1091      product_name: config
1092        .product_name
1093        .clone()
1094        .unwrap_or_else(|| cargo_package_settings.name.clone()),
1095      version,
1096      description: cargo_package_settings
1097        .description
1098        .clone()
1099        .map(|description| {
1100          description
1101            .resolve("description", || {
1102              ws_package_settings
1103                .as_ref()
1104                .and_then(|v| v.description.clone())
1105                .context("Couldn't inherit value for `description` from workspace")
1106            })
1107            .unwrap()
1108        })
1109        .unwrap_or_default(),
1110      homepage: cargo_package_settings.homepage.clone().map(|homepage| {
1111        homepage
1112          .resolve("homepage", || {
1113            ws_package_settings
1114              .as_ref()
1115              .and_then(|v| v.homepage.clone())
1116              .context("Couldn't inherit value for `homepage` from workspace")
1117          })
1118          .unwrap()
1119      }),
1120      authors: cargo_package_settings.authors.clone().map(|authors| {
1121        authors
1122          .resolve("authors", || {
1123            ws_package_settings
1124              .as_ref()
1125              .and_then(|v| v.authors.clone())
1126              .context("Couldn't inherit value for `authors` from workspace")
1127          })
1128          .unwrap()
1129      }),
1130      default_run: cargo_package_settings.default_run.clone(),
1131    };
1132
1133    let cargo_config = CargoConfig::load(tauri_dir)?;
1134
1135    let target_triple = target.unwrap_or_else(|| {
1136      cargo_config
1137        .build()
1138        .target()
1139        .map(|t| t.to_string())
1140        .unwrap_or_else(|| {
1141          let output = Command::new("rustc")
1142            .args(["-vV"])
1143            .output()
1144            .expect("\"rustc\" could not be found, did you install Rust?");
1145          let stdout = String::from_utf8_lossy(&output.stdout);
1146          stdout
1147            .split('\n')
1148            .find(|l| l.starts_with("host:"))
1149            .unwrap()
1150            .replace("host:", "")
1151            .trim()
1152            .to_string()
1153        })
1154    });
1155    let target_platform = TargetPlatform::from_triple(&target_triple);
1156
1157    Ok(Self {
1158      manifest: Mutex::new(manifest),
1159      cargo_settings,
1160      cargo_package_settings,
1161      cargo_ws_package_settings: ws_package_settings,
1162      package_settings,
1163      cargo_config,
1164      target_triple,
1165      target_platform,
1166      workspace_dir,
1167    })
1168  }
1169
1170  fn target<'a>(&'a self, options: &'a Options) -> Option<&'a str> {
1171    options
1172      .target
1173      .as_deref()
1174      .or_else(|| self.cargo_config.build().target())
1175  }
1176
1177  pub fn out_dir(&self, options: &Options, tauri_dir: &Path) -> crate::Result<PathBuf> {
1178    get_target_dir(self.target(options), options, tauri_dir)
1179  }
1180}
1181
1182#[derive(Deserialize)]
1183pub(crate) struct CargoMetadata {
1184  pub(crate) target_directory: PathBuf,
1185  pub(crate) workspace_root: PathBuf,
1186  workspace_members: Vec<String>,
1187  packages: Vec<Package>,
1188}
1189
1190#[derive(Deserialize)]
1191struct Package {
1192  name: String,
1193  id: String,
1194  manifest_path: PathBuf,
1195  dependencies: Vec<Dependency>,
1196}
1197
1198#[derive(Deserialize)]
1199struct Dependency {
1200  name: String,
1201  /// Local package
1202  path: Option<PathBuf>,
1203}
1204
1205pub(crate) fn get_cargo_metadata(tauri_dir: &Path) -> crate::Result<CargoMetadata> {
1206  let output = Command::new("cargo")
1207    .args(["metadata", "--no-deps", "--format-version", "1"])
1208    .current_dir(tauri_dir)
1209    .output()
1210    .map_err(|error| Error::CommandFailed {
1211      command: "cargo metadata --no-deps --format-version 1".to_string(),
1212      error,
1213    })?;
1214
1215  if !output.status.success() {
1216    return Err(Error::CommandFailed {
1217      command: "cargo metadata".to_string(),
1218      error: std::io::Error::other(String::from_utf8_lossy(&output.stderr)),
1219    });
1220  }
1221
1222  serde_json::from_slice(&output.stdout).context("failed to parse cargo metadata")
1223}
1224
1225/// Get the tauri project crate's dependencies that are inside the workspace
1226fn get_in_workspace_dependency_paths(tauri_dir: &Path) -> crate::Result<Vec<PathBuf>> {
1227  let metadata = get_cargo_metadata(tauri_dir)?;
1228  let tauri_project_manifest_path = tauri_dir.join("Cargo.toml");
1229  let tauri_project_package = metadata
1230    .packages
1231    .iter()
1232    .find(|package| package.manifest_path == tauri_project_manifest_path)
1233    .context("tauri project package doesn't exist in cargo metadata output `packages`")?;
1234
1235  let workspace_packages = metadata
1236    .workspace_members
1237    .iter()
1238    .map(|member_package_id| {
1239      metadata
1240        .packages
1241        .iter()
1242        .find(|package| package.id == *member_package_id)
1243        .context("workspace member doesn't exist in cargo metadata output `packages`")
1244    })
1245    .collect::<crate::Result<Vec<_>>>()?;
1246
1247  let mut found_dependency_paths = Vec::new();
1248  find_dependencies(
1249    tauri_project_package,
1250    &workspace_packages,
1251    &mut found_dependency_paths,
1252  );
1253  Ok(found_dependency_paths)
1254}
1255
1256fn find_dependencies(
1257  package: &Package,
1258  workspace_packages: &Vec<&Package>,
1259  found_dependency_paths: &mut Vec<PathBuf>,
1260) {
1261  for dependency in &package.dependencies {
1262    if let Some(path) = &dependency.path {
1263      if let Some(package) = workspace_packages.iter().find(|workspace_package| {
1264        workspace_package.name == dependency.name
1265          && path.join("Cargo.toml") == workspace_package.manifest_path
1266          && !found_dependency_paths.contains(path)
1267      }) {
1268        found_dependency_paths.push(path.to_owned());
1269        find_dependencies(package, workspace_packages, found_dependency_paths);
1270      }
1271    }
1272  }
1273}
1274
1275/// Get the cargo target directory based on the provided arguments.
1276/// If "--target-dir" is specified in args, use it as the target directory (relative to current directory).
1277/// Otherwise, use the target directory from cargo metadata.
1278pub(crate) fn get_cargo_target_dir(args: &[String], tauri_dir: &Path) -> crate::Result<PathBuf> {
1279  let path = if let Some(target) = get_cargo_option(args, "--target-dir") {
1280    std::env::current_dir()
1281      .context("failed to get current directory")?
1282      .join(target)
1283  } else {
1284    get_cargo_metadata(tauri_dir)
1285      .context("failed to run 'cargo metadata' command to get target directory")?
1286      .target_directory
1287  };
1288
1289  Ok(path)
1290}
1291
1292/// This function determines the 'target' directory and suffixes it with the profile
1293/// to determine where the compiled binary will be located.
1294fn get_target_dir(
1295  triple: Option<&str>,
1296  options: &Options,
1297  tauri_dir: &Path,
1298) -> crate::Result<PathBuf> {
1299  let mut path = get_cargo_target_dir(&options.args, tauri_dir)?;
1300
1301  if let Some(triple) = triple {
1302    path.push(triple);
1303  }
1304
1305  path.push(get_profile_dir(options));
1306
1307  Ok(path)
1308}
1309
1310#[inline]
1311fn get_cargo_option<'a>(args: &'a [String], option: &'a str) -> Option<&'a str> {
1312  args
1313    .iter()
1314    .position(|a| a.starts_with(option))
1315    .and_then(|i| {
1316      args[i]
1317        .split_once('=')
1318        .map(|(_, p)| Some(p))
1319        .unwrap_or_else(|| args.get(i + 1).map(|s| s.as_str()))
1320    })
1321}
1322
1323/// Executes `cargo metadata` to get the workspace directory.
1324pub fn get_workspace_dir(tauri_dir: &Path) -> crate::Result<PathBuf> {
1325  Ok(
1326    get_cargo_metadata(tauri_dir)
1327      .context("failed to run 'cargo metadata' command to get workspace directory")?
1328      .workspace_root,
1329  )
1330}
1331
1332pub fn get_profile(options: &Options) -> &str {
1333  get_cargo_option(&options.args, "--profile").unwrap_or(if options.debug {
1334    "dev"
1335  } else {
1336    "release"
1337  })
1338}
1339
1340pub fn get_profile_dir(options: &Options) -> &str {
1341  match get_profile(options) {
1342    "dev" => "debug",
1343    profile => profile,
1344  }
1345}
1346
1347#[allow(unused_variables, deprecated)]
1348fn tauri_config_to_bundle_settings(
1349  settings: &RustAppSettings,
1350  features: &[String],
1351  tauri_config: &Config,
1352  tauri_dir: &Path,
1353  config: crate::helpers::config::BundleConfig,
1354  updater_config: Option<UpdaterSettings>,
1355  arch64bits: bool,
1356) -> crate::Result<BundleSettings> {
1357  let enabled_features = settings
1358    .manifest
1359    .lock()
1360    .unwrap()
1361    .all_enabled_features(features);
1362
1363  #[allow(unused_mut)]
1364  let mut resources = config
1365    .resources
1366    .unwrap_or(BundleResources::List(Vec::new()));
1367  #[allow(unused_mut)]
1368  let mut depends_deb = config.linux.deb.depends.unwrap_or_default();
1369
1370  #[allow(unused_mut)]
1371  let mut depends_rpm = config.linux.rpm.depends.unwrap_or_default();
1372
1373  #[allow(unused_mut)]
1374  let mut appimage_files = config.linux.appimage.files;
1375
1376  // set env vars used by the bundler and inject dependencies
1377  #[cfg(target_os = "linux")]
1378  {
1379    let mut libs: Vec<String> = Vec::new();
1380
1381    if enabled_features.contains(&"tray-icon".into())
1382      || enabled_features.contains(&"tauri/tray-icon".into())
1383    {
1384      let (tray_kind, path) = std::env::var_os("TAURI_LINUX_AYATANA_APPINDICATOR")
1385        .map(|ayatana| {
1386          if ayatana == "true" || ayatana == "1" {
1387            (
1388              pkgconfig_utils::TrayKind::Ayatana,
1389              format!(
1390                "{}/libayatana-appindicator3.so.1",
1391                pkgconfig_utils::get_library_path("ayatana-appindicator3-0.1")
1392                  .expect("failed to get ayatana-appindicator library path using pkg-config.")
1393              ),
1394            )
1395          } else {
1396            (
1397              pkgconfig_utils::TrayKind::Libappindicator,
1398              format!(
1399                "{}/libappindicator3.so.1",
1400                pkgconfig_utils::get_library_path("appindicator3-0.1")
1401                  .expect("failed to get libappindicator-gtk library path using pkg-config.")
1402              ),
1403            )
1404          }
1405        })
1406        .unwrap_or_else(pkgconfig_utils::get_appindicator_library_path);
1407      match tray_kind {
1408        pkgconfig_utils::TrayKind::Ayatana => {
1409          depends_deb.push("libayatana-appindicator3-1".into());
1410          libs.push("libayatana-appindicator3.so.1".into());
1411        }
1412        pkgconfig_utils::TrayKind::Libappindicator => {
1413          depends_deb.push("libappindicator3-1".into());
1414          libs.push("libappindicator3.so.1".into());
1415        }
1416      }
1417
1418      // conditionally setting it in case the user provided its own version for some reason
1419      let path = PathBuf::from(path);
1420      if !appimage_files.contains_key(&path) {
1421        // manually construct target path, just in case the source path is something unexpected
1422        appimage_files.insert(Path::new("/usr/lib/").join(path.file_name().unwrap()), path);
1423      }
1424    }
1425
1426    depends_deb.push("libwebkit2gtk-4.1-0".to_string());
1427    depends_deb.push("libgtk-3-0".to_string());
1428
1429    libs.push("libwebkit2gtk-4.1.so.0".into());
1430    libs.push("libgtk-3.so.0".into());
1431
1432    for lib in libs {
1433      let mut requires = lib;
1434      if arch64bits {
1435        requires.push_str("()(64bit)");
1436      }
1437      depends_rpm.push(requires);
1438    }
1439  }
1440
1441  #[cfg(windows)]
1442  {
1443    if let crate::helpers::config::WebviewInstallMode::FixedRuntime { path } =
1444      &config.windows.webview_install_mode
1445    {
1446      resources.push(path.display().to_string());
1447    }
1448  }
1449
1450  let signing_identity = match std::env::var_os("APPLE_SIGNING_IDENTITY") {
1451    Some(signing_identity) => Some(
1452      signing_identity
1453        .to_str()
1454        .expect("failed to convert APPLE_SIGNING_IDENTITY to string")
1455        .to_string(),
1456    ),
1457    None => config.macos.signing_identity,
1458  };
1459
1460  let provider_short_name = match std::env::var_os("APPLE_PROVIDER_SHORT_NAME") {
1461    Some(provider_short_name) => Some(
1462      provider_short_name
1463        .to_str()
1464        .expect("failed to convert APPLE_PROVIDER_SHORT_NAME to string")
1465        .to_string(),
1466    ),
1467    None => config.macos.provider_short_name,
1468  };
1469
1470  let (resources, resources_map) = match resources {
1471    BundleResources::List(paths) => (Some(paths), None),
1472    BundleResources::Map(map) => (None, Some(map)),
1473  };
1474
1475  #[cfg(target_os = "macos")]
1476  let entitlements = if let Some(plugin_config) = tauri_config
1477    .plugins
1478    .0
1479    .get("deep-link")
1480    .and_then(|c| c.get("desktop").cloned())
1481  {
1482    let protocols: DesktopDeepLinks =
1483      serde_json::from_value(plugin_config).context("failed to parse deep link plugin config")?;
1484    let domains = match protocols {
1485      DesktopDeepLinks::One(protocol) => protocol.domains,
1486      DesktopDeepLinks::List(protocols) => protocols.into_iter().flat_map(|p| p.domains).collect(),
1487    };
1488
1489    if domains.is_empty() {
1490      config
1491        .macos
1492        .entitlements
1493        .map(PathBuf::from)
1494        .map(tauri_bundler::bundle::Entitlements::Path)
1495    } else {
1496      let mut app_links_entitlements = plist::Dictionary::new();
1497      if !domains.is_empty() {
1498        app_links_entitlements.insert(
1499          "com.apple.developer.associated-domains".to_string(),
1500          domains
1501            .into_iter()
1502            .map(|domain| format!("applinks:{domain}").into())
1503            .collect::<Vec<_>>()
1504            .into(),
1505        );
1506      }
1507      let entitlements = if let Some(user_provided_entitlements) = config.macos.entitlements {
1508        crate::helpers::plist::merge_plist(vec![
1509          PathBuf::from(user_provided_entitlements).into(),
1510          plist::Value::Dictionary(app_links_entitlements).into(),
1511        ])?
1512      } else {
1513        app_links_entitlements.into()
1514      };
1515
1516      Some(tauri_bundler::bundle::Entitlements::Plist(entitlements))
1517    }
1518  } else {
1519    config
1520      .macos
1521      .entitlements
1522      .map(PathBuf::from)
1523      .map(tauri_bundler::bundle::Entitlements::Path)
1524  };
1525  #[cfg(not(target_os = "macos"))]
1526  let entitlements = None;
1527
1528  Ok(BundleSettings {
1529    identifier: Some(tauri_config.identifier.clone()),
1530    publisher: config.publisher,
1531    homepage: config.homepage,
1532    icon: Some(config.icon),
1533    resources,
1534    resources_map,
1535    copyright: config.copyright,
1536    category: match config.category {
1537      Some(category) => Some(AppCategory::from_str(&category).map_err(|e| match e {
1538        Some(e) => Error::GenericError(format!("invalid category, did you mean `{e}`?")),
1539        None => Error::GenericError("invalid category".to_string()),
1540      })?),
1541      None => None,
1542    },
1543    file_associations: config.file_associations,
1544    short_description: config.short_description,
1545    long_description: config.long_description,
1546    external_bin: config.external_bin,
1547    deb: DebianSettings {
1548      depends: if depends_deb.is_empty() {
1549        None
1550      } else {
1551        Some(depends_deb)
1552      },
1553      recommends: config.linux.deb.recommends,
1554      provides: config.linux.deb.provides,
1555      conflicts: config.linux.deb.conflicts,
1556      replaces: config.linux.deb.replaces,
1557      files: config.linux.deb.files,
1558      desktop_template: config.linux.deb.desktop_template,
1559      section: config.linux.deb.section,
1560      priority: config.linux.deb.priority,
1561      changelog: config.linux.deb.changelog,
1562      pre_install_script: config.linux.deb.pre_install_script,
1563      post_install_script: config.linux.deb.post_install_script,
1564      pre_remove_script: config.linux.deb.pre_remove_script,
1565      post_remove_script: config.linux.deb.post_remove_script,
1566    },
1567    appimage: AppImageSettings {
1568      files: appimage_files,
1569      bundle_media_framework: config.linux.appimage.bundle_media_framework,
1570      bundle_xdg_open: false,
1571    },
1572    rpm: RpmSettings {
1573      depends: if depends_rpm.is_empty() {
1574        None
1575      } else {
1576        Some(depends_rpm)
1577      },
1578      recommends: config.linux.rpm.recommends,
1579      provides: config.linux.rpm.provides,
1580      conflicts: config.linux.rpm.conflicts,
1581      obsoletes: config.linux.rpm.obsoletes,
1582      release: config.linux.rpm.release,
1583      epoch: config.linux.rpm.epoch,
1584      files: config.linux.rpm.files,
1585      desktop_template: config.linux.rpm.desktop_template,
1586      pre_install_script: config.linux.rpm.pre_install_script,
1587      post_install_script: config.linux.rpm.post_install_script,
1588      pre_remove_script: config.linux.rpm.pre_remove_script,
1589      post_remove_script: config.linux.rpm.post_remove_script,
1590      compression: config.linux.rpm.compression,
1591    },
1592    dmg: DmgSettings {
1593      background: config.macos.dmg.background,
1594      window_position: config
1595        .macos
1596        .dmg
1597        .window_position
1598        .map(|window_position| Position {
1599          x: window_position.x,
1600          y: window_position.y,
1601        }),
1602      window_size: Size {
1603        width: config.macos.dmg.window_size.width,
1604        height: config.macos.dmg.window_size.height,
1605      },
1606      app_position: Position {
1607        x: config.macos.dmg.app_position.x,
1608        y: config.macos.dmg.app_position.y,
1609      },
1610      application_folder_position: Position {
1611        x: config.macos.dmg.application_folder_position.x,
1612        y: config.macos.dmg.application_folder_position.y,
1613      },
1614    },
1615    ios: IosSettings {
1616      bundle_version: config.ios.bundle_version,
1617    },
1618    macos: MacOsSettings {
1619      frameworks: config.macos.frameworks,
1620      files: config.macos.files,
1621      bundle_version: config.macos.bundle_version,
1622      bundle_name: config.macos.bundle_name,
1623      minimum_system_version: config.macos.minimum_system_version,
1624      exception_domain: config.macos.exception_domain,
1625      signing_identity,
1626      skip_stapling: false,
1627      hardened_runtime: config.macos.hardened_runtime,
1628      provider_short_name,
1629      entitlements,
1630      #[cfg(not(target_os = "macos"))]
1631      info_plist: None,
1632      #[cfg(target_os = "macos")]
1633      info_plist: {
1634        let mut src_plists = vec![];
1635
1636        let path = tauri_dir.join("Info.plist");
1637        if path.exists() {
1638          src_plists.push(path.into());
1639        }
1640        if let Some(info_plist) = &config.macos.info_plist {
1641          src_plists.push(info_plist.clone().into());
1642        }
1643
1644        Some(tauri_bundler::bundle::PlistKind::Plist(
1645          crate::helpers::plist::merge_plist(src_plists)?,
1646        ))
1647      },
1648    },
1649    windows: WindowsSettings {
1650      timestamp_url: config.windows.timestamp_url,
1651      tsp: config.windows.tsp,
1652      digest_algorithm: config.windows.digest_algorithm,
1653      certificate_thumbprint: config.windows.certificate_thumbprint,
1654      wix: config.windows.wix.map(wix_settings),
1655      nsis: config.windows.nsis.map(nsis_settings),
1656      icon_path: PathBuf::new(),
1657      webview_install_mode: config.windows.webview_install_mode,
1658      allow_downgrades: config.windows.allow_downgrades,
1659      sign_command: config.windows.sign_command.map(custom_sign_settings),
1660    },
1661    license: config.license.or_else(|| {
1662      settings
1663        .cargo_package_settings
1664        .license
1665        .clone()
1666        .map(|license| {
1667          license
1668            .resolve("license", || {
1669              settings
1670                .cargo_ws_package_settings
1671                .as_ref()
1672                .and_then(|v| v.license.clone())
1673                .context("Couldn't inherit value for `license` from workspace")
1674            })
1675            .unwrap()
1676        })
1677    }),
1678    license_file: config.license_file.map(|l| tauri_dir.join(l)),
1679    updater: updater_config,
1680    ..Default::default()
1681  })
1682}
1683
1684#[cfg(target_os = "linux")]
1685mod pkgconfig_utils {
1686  use std::process::Command;
1687
1688  pub enum TrayKind {
1689    Ayatana,
1690    Libappindicator,
1691  }
1692
1693  pub fn get_appindicator_library_path() -> (TrayKind, String) {
1694    match get_library_path("ayatana-appindicator3-0.1") {
1695      Some(p) => (
1696        TrayKind::Ayatana,
1697        format!("{p}/libayatana-appindicator3.so.1"),
1698      ),
1699      None => match get_library_path("appindicator3-0.1") {
1700        Some(p) => (
1701          TrayKind::Libappindicator,
1702          format!("{p}/libappindicator3.so.1"),
1703        ),
1704        None => panic!("Can't detect any appindicator library"),
1705      },
1706    }
1707  }
1708
1709  /// Gets the folder in which a library is located using `pkg-config`.
1710  pub fn get_library_path(name: &str) -> Option<String> {
1711    let mut cmd = Command::new("pkg-config");
1712    cmd.env("PKG_CONFIG_ALLOW_SYSTEM_LIBS", "1");
1713    cmd.arg("--libs-only-L");
1714    cmd.arg(name);
1715    if let Ok(output) = cmd.output() {
1716      if !output.stdout.is_empty() {
1717        // output would be "-L/path/to/library\n"
1718        let word = output.stdout[2..].to_vec();
1719        Some(String::from_utf8_lossy(&word).trim().to_string())
1720      } else {
1721        None
1722      }
1723    } else {
1724      None
1725    }
1726  }
1727}
1728
1729#[cfg(test)]
1730mod tests {
1731  use super::*;
1732
1733  #[test]
1734  fn parse_cargo_option() {
1735    let args = [
1736      "build".into(),
1737      "--".into(),
1738      "--profile".into(),
1739      "holla".into(),
1740      "--features".into(),
1741      "a".into(),
1742      "b".into(),
1743      "--target-dir".into(),
1744      "path/to/dir".into(),
1745    ];
1746
1747    assert_eq!(get_cargo_option(&args, "--profile"), Some("holla"));
1748    assert_eq!(get_cargo_option(&args, "--target-dir"), Some("path/to/dir"));
1749    assert_eq!(get_cargo_option(&args, "--non-existent"), None);
1750  }
1751
1752  #[test]
1753  fn parse_profile_from_opts() {
1754    let options = Options {
1755      args: vec![
1756        "build".into(),
1757        "--".into(),
1758        "--profile".into(),
1759        "testing".into(),
1760        "--features".into(),
1761        "feat1".into(),
1762      ],
1763      ..Default::default()
1764    };
1765    assert_eq!(get_profile(&options), "testing");
1766
1767    let options = Options {
1768      args: vec![
1769        "build".into(),
1770        "--".into(),
1771        "--profile=customprofile".into(),
1772        "testing".into(),
1773        "--features".into(),
1774        "feat1".into(),
1775      ],
1776      ..Default::default()
1777    };
1778    assert_eq!(get_profile(&options), "customprofile");
1779
1780    let options = Options {
1781      debug: true,
1782      args: vec![
1783        "build".into(),
1784        "--".into(),
1785        "testing".into(),
1786        "--features".into(),
1787        "feat1".into(),
1788      ],
1789      ..Default::default()
1790    };
1791    assert_eq!(get_profile(&options), "dev");
1792
1793    let options = Options {
1794      debug: false,
1795      args: vec![
1796        "build".into(),
1797        "--".into(),
1798        "testing".into(),
1799        "--features".into(),
1800        "feat1".into(),
1801      ],
1802      ..Default::default()
1803    };
1804    assert_eq!(get_profile(&options), "release");
1805
1806    let options = Options {
1807      args: vec!["build".into(), "--".into(), "--profile".into()],
1808      ..Default::default()
1809    };
1810    assert_eq!(get_profile(&options), "release");
1811  }
1812
1813  #[test]
1814  fn parse_target_dir_from_opts() {
1815    let dirs = crate::helpers::app_paths::resolve_dirs();
1816    let current_dir = std::env::current_dir().unwrap();
1817
1818    let options = Options {
1819      args: vec![
1820        "build".into(),
1821        "--".into(),
1822        "--target-dir".into(),
1823        "path/to/some/dir".into(),
1824        "--features".into(),
1825        "feat1".into(),
1826      ],
1827      debug: false,
1828      ..Default::default()
1829    };
1830
1831    assert_eq!(
1832      get_target_dir(None, &options, dirs.tauri).unwrap(),
1833      current_dir.join("path/to/some/dir/release")
1834    );
1835    assert_eq!(
1836      get_target_dir(Some("x86_64-pc-windows-msvc"), &options, dirs.tauri).unwrap(),
1837      current_dir
1838        .join("path/to/some/dir")
1839        .join("x86_64-pc-windows-msvc")
1840        .join("release")
1841    );
1842
1843    let options = Options {
1844      args: vec![
1845        "build".into(),
1846        "--".into(),
1847        "--features".into(),
1848        "feat1".into(),
1849      ],
1850      debug: false,
1851      ..Default::default()
1852    };
1853
1854    #[cfg(windows)]
1855    assert!(
1856      get_target_dir(Some("x86_64-pc-windows-msvc"), &options, dirs.tauri)
1857        .unwrap()
1858        .ends_with("x86_64-pc-windows-msvc\\release")
1859    );
1860    #[cfg(not(windows))]
1861    assert!(
1862      get_target_dir(Some("x86_64-pc-windows-msvc"), &options, dirs.tauri)
1863        .unwrap()
1864        .ends_with("x86_64-pc-windows-msvc/release")
1865    );
1866
1867    #[cfg(windows)]
1868    {
1869      std::env::set_var("CARGO_TARGET_DIR", "D:\\path\\to\\env\\dir");
1870      assert_eq!(
1871        get_target_dir(None, &options, dirs.tauri).unwrap(),
1872        PathBuf::from("D:\\path\\to\\env\\dir\\release")
1873      );
1874      assert_eq!(
1875        get_target_dir(Some("x86_64-pc-windows-msvc"), &options, dirs.tauri).unwrap(),
1876        PathBuf::from("D:\\path\\to\\env\\dir\\x86_64-pc-windows-msvc\\release")
1877      );
1878    }
1879
1880    #[cfg(not(windows))]
1881    {
1882      std::env::set_var("CARGO_TARGET_DIR", "/path/to/env/dir");
1883      assert_eq!(
1884        get_target_dir(None, &options, dirs.tauri).unwrap(),
1885        PathBuf::from("/path/to/env/dir/release")
1886      );
1887      assert_eq!(
1888        get_target_dir(Some("x86_64-pc-windows-msvc"), &options, dirs.tauri).unwrap(),
1889        PathBuf::from("/path/to/env/dir/x86_64-pc-windows-msvc/release")
1890      );
1891    }
1892  }
1893}