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  fn required_features_enabled(&self, enabled_features: &[String]) -> bool {
695    match &self.required_features {
696      Some(req_features) => req_features
697        .iter()
698        .all(|feat| enabled_features.contains(feat)),
699      None => true,
700    }
701  }
702
703  fn matches_src_bin(&self, name: &str, path: &Path) -> bool {
704    self.name == name
705      || self.file_name() == name
706      || self
707        .path
708        .as_ref()
709        .is_some_and(|src_path| path.ends_with(src_path))
710  }
711}
712
713/// The package settings.
714#[derive(Debug, Clone, Deserialize)]
715#[serde(rename_all = "kebab-case")]
716pub struct CargoPackageSettings {
717  /// the package's name.
718  pub name: String,
719  /// the package's version.
720  pub version: Option<MaybeWorkspace<String>>,
721  /// the package's description.
722  pub description: Option<MaybeWorkspace<String>>,
723  /// the package's homepage.
724  pub homepage: Option<MaybeWorkspace<String>>,
725  /// the package's authors.
726  pub authors: Option<MaybeWorkspace<Vec<String>>>,
727  /// the package's license.
728  pub license: Option<MaybeWorkspace<String>>,
729  /// the default binary to run.
730  pub default_run: Option<String>,
731}
732
733/// The Cargo settings (Cargo.toml root descriptor).
734#[derive(Clone, Debug, Deserialize)]
735struct CargoSettings {
736  /// the package settings.
737  ///
738  /// it's optional because ancestor workspace Cargo.toml files may not have package info.
739  package: Option<CargoPackageSettings>,
740  /// the workspace settings.
741  ///
742  /// it's present if the read Cargo.toml belongs to a workspace root.
743  workspace: Option<WorkspaceSettings>,
744  /// the binary targets configuration.
745  bin: Option<Vec<BinarySettings>>,
746}
747
748impl CargoSettings {
749  /// Try to load a set of CargoSettings from a "Cargo.toml" file in the specified directory.
750  fn load(dir: &Path) -> crate::Result<Self> {
751    let toml_path = dir.join("Cargo.toml");
752    let toml_str = std::fs::read_to_string(&toml_path)
753      .fs_context("Failed to read Cargo manifest", toml_path.clone())?;
754    toml::from_str(&toml_str).context(format!(
755      "failed to parse Cargo manifest at {}",
756      toml_path.display()
757    ))
758  }
759}
760
761pub struct RustAppSettings {
762  manifest: Mutex<Manifest>,
763  cargo_settings: CargoSettings,
764  cargo_package_settings: CargoPackageSettings,
765  cargo_ws_package_settings: Option<WorkspacePackageSettings>,
766  package_settings: PackageSettings,
767  cargo_config: CargoConfig,
768  target_triple: String,
769  target_platform: TargetPlatform,
770  workspace_dir: PathBuf,
771}
772
773#[derive(Deserialize)]
774#[serde(untagged)]
775enum DesktopDeepLinks {
776  One(DeepLinkProtocol),
777  List(Vec<DeepLinkProtocol>),
778}
779
780#[derive(Deserialize)]
781pub struct UpdaterConfig {
782  /// Signature public key.
783  pub pubkey: String,
784  /// The Windows configuration for the updater.
785  #[serde(default)]
786  pub windows: UpdaterWindowsConfig,
787}
788
789/// Install modes for the Windows update.
790#[derive(Default, Debug, PartialEq, Eq, Clone)]
791pub enum WindowsUpdateInstallMode {
792  /// Specifies there's a basic UI during the installation process, including a final dialog box at the end.
793  BasicUi,
794  /// The quiet mode means there's no user interaction required.
795  /// Requires admin privileges if the installer does.
796  Quiet,
797  /// Specifies unattended mode, which means the installation only shows a progress bar.
798  #[default]
799  Passive,
800  // to add more modes, we need to check if the updater relaunch makes sense
801  // i.e. for a full UI mode, the user can also mark the installer to start the app
802}
803
804impl<'de> Deserialize<'de> for WindowsUpdateInstallMode {
805  fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
806  where
807    D: Deserializer<'de>,
808  {
809    let s = String::deserialize(deserializer)?;
810    match s.to_lowercase().as_str() {
811      "basicui" => Ok(Self::BasicUi),
812      "quiet" => Ok(Self::Quiet),
813      "passive" => Ok(Self::Passive),
814      _ => Err(serde::de::Error::custom(format!(
815        "unknown update install mode '{s}'"
816      ))),
817    }
818  }
819}
820
821impl WindowsUpdateInstallMode {
822  /// Returns the associated `msiexec.exe` arguments.
823  pub fn msiexec_args(&self) -> &'static [&'static str] {
824    match self {
825      Self::BasicUi => &["/qb+"],
826      Self::Quiet => &["/quiet"],
827      Self::Passive => &["/passive"],
828    }
829  }
830}
831
832#[derive(Default, Deserialize)]
833#[serde(rename_all = "camelCase")]
834pub struct UpdaterWindowsConfig {
835  #[serde(default, alias = "install-mode")]
836  pub install_mode: WindowsUpdateInstallMode,
837}
838
839impl AppSettings for RustAppSettings {
840  fn get_package_settings(&self) -> PackageSettings {
841    self.package_settings.clone()
842  }
843
844  fn get_bundle_settings(
845    &self,
846    options: &Options,
847    config: &Config,
848    features: &[String],
849    tauri_dir: &Path,
850  ) -> crate::Result<BundleSettings> {
851    let arch64bits = self.target_triple.starts_with("x86_64")
852      || self.target_triple.starts_with("aarch64")
853      || self.target_triple.starts_with("riscv64");
854
855    let updater_enabled = config.bundle.create_updater_artifacts != Updater::Bool(false);
856    let v1_compatible = matches!(config.bundle.create_updater_artifacts, Updater::String(_));
857    let updater_settings = if updater_enabled {
858      let updater: UpdaterConfig = serde_json::from_value(
859        config
860          .plugins
861          .0
862          .get("updater")
863          .context("failed to get updater configuration: plugins > updater doesn't exist")?
864          .clone(),
865      )
866      .context("failed to parse updater plugin configuration")?;
867      Some(UpdaterSettings {
868        v1_compatible,
869        pubkey: updater.pubkey,
870        msiexec_args: updater.windows.install_mode.msiexec_args(),
871      })
872    } else {
873      None
874    };
875
876    let mut settings = tauri_config_to_bundle_settings(
877      self,
878      features,
879      config,
880      tauri_dir,
881      config.bundle.clone(),
882      updater_settings,
883      arch64bits,
884    )?;
885
886    settings.macos.skip_stapling = options.skip_stapling;
887
888    if let Some(plugin_config) = config
889      .plugins
890      .0
891      .get("deep-link")
892      .and_then(|c| c.get("desktop").cloned())
893    {
894      let protocols: DesktopDeepLinks =
895        serde_json::from_value(plugin_config).context("failed to parse desktop deep links from Tauri configuration > plugins > deep-link > desktop")?;
896      settings.deep_link_protocols = Some(match protocols {
897        DesktopDeepLinks::One(p) => vec![p],
898        DesktopDeepLinks::List(p) => p,
899      });
900    }
901
902    if let Some(open) = config.plugins.0.get("shell").and_then(|v| v.get("open")) {
903      if open.as_bool().is_some_and(|x| x) || open.is_string() {
904        settings.appimage.bundle_xdg_open = true;
905      }
906    }
907
908    if let Some(deps) = self
909      .manifest
910      .lock()
911      .unwrap()
912      .inner
913      .as_table()
914      .get("dependencies")
915      .and_then(|f| f.as_table())
916    {
917      if deps.contains_key("tauri-plugin-opener") {
918        settings.appimage.bundle_xdg_open = true;
919      };
920    }
921
922    Ok(settings)
923  }
924
925  fn app_binary_path(&self, options: &Options, tauri_dir: &Path) -> crate::Result<PathBuf> {
926    let binaries = self.get_binaries(options, tauri_dir)?;
927    let bin_name = binaries
928      .iter()
929      .find(|x| x.main())
930      .context("failed to find main binary, make sure you have a `package > default-run` in the Cargo.toml file")?
931      .name();
932
933    let out_dir = self
934      .out_dir(options, tauri_dir)
935      .context("failed to get project out directory")?;
936
937    let mut path = out_dir.join(bin_name);
938    if matches!(self.target_platform, TargetPlatform::Windows) {
939      // Append the `.exe` extension without overriding the existing extensions
940      let extension = if let Some(extension) = path.extension() {
941        let mut extension = extension.to_os_string();
942        extension.push(".exe");
943        extension
944      } else {
945        "exe".into()
946      };
947      path.set_extension(extension);
948    };
949    Ok(path)
950  }
951
952  fn get_binaries(&self, options: &Options, tauri_dir: &Path) -> crate::Result<Vec<BundleBinary>> {
953    let mut binaries = Vec::new();
954    let mut disabled_bins = Vec::new();
955
956    if let Some(bins) = &self.cargo_settings.bin {
957      let default_run = self
958        .package_settings
959        .default_run
960        .clone()
961        .unwrap_or_default();
962      for bin in bins {
963        if !bin.required_features_enabled(&options.features) {
964          disabled_bins.push(bin);
965          continue;
966        }
967        let file_name = bin.file_name();
968        let is_main = file_name == self.cargo_package_settings.name || file_name == default_run;
969        binaries.push(BundleBinary::with_path(
970          file_name.to_owned(),
971          is_main,
972          bin.path.clone(),
973        ))
974      }
975    }
976
977    let mut binaries_paths = std::fs::read_dir(tauri_dir.join("src/bin"))
978      .map(|dir| {
979        dir
980          .into_iter()
981          .flatten()
982          .map(|entry| {
983            (
984              entry
985                .path()
986                .file_stem()
987                .unwrap_or_default()
988                .to_string_lossy()
989                .into_owned(),
990              entry.path(),
991            )
992          })
993          .collect::<Vec<_>>()
994      })
995      .unwrap_or_default();
996
997    if !binaries_paths
998      .iter()
999      .any(|(_name, path)| path == Path::new("src/main.rs"))
1000      && tauri_dir.join("src/main.rs").exists()
1001    {
1002      binaries_paths.push((
1003        self.cargo_package_settings.name.clone(),
1004        tauri_dir.join("src/main.rs"),
1005      ));
1006    }
1007
1008    for (name, path) in binaries_paths {
1009      // see https://github.com/tauri-apps/tauri/pull/10977#discussion_r1759742414
1010      let bin_exists = binaries
1011        .iter()
1012        .any(|bin| bin.name() == name || path.ends_with(bin.src_path().unwrap_or(&"".to_string())));
1013      let bin_disabled = disabled_bins
1014        .iter()
1015        .any(|bin| bin.matches_src_bin(&name, &path));
1016      if !bin_exists && !bin_disabled {
1017        binaries.push(BundleBinary::new(name, false))
1018      }
1019    }
1020
1021    if let Some(default_run) = self.package_settings.default_run.as_ref() {
1022      if let Some(binary) = binaries.iter_mut().find(|bin| bin.name() == default_run) {
1023        binary.set_main(true);
1024      } else {
1025        binaries.push(BundleBinary::new(default_run.clone(), true));
1026      }
1027    }
1028
1029    match binaries.len() {
1030      0 => binaries.push(BundleBinary::new(
1031        self.cargo_package_settings.name.clone(),
1032        true,
1033      )),
1034      1 => binaries.get_mut(0).unwrap().set_main(true),
1035      _ => {}
1036    }
1037
1038    Ok(binaries)
1039  }
1040
1041  fn app_name(&self) -> Option<String> {
1042    self
1043      .manifest
1044      .lock()
1045      .unwrap()
1046      .inner
1047      .as_table()
1048      .get("package")?
1049      .as_table()?
1050      .get("name")?
1051      .as_str()
1052      .map(|n| n.to_string())
1053  }
1054
1055  fn lib_name(&self) -> Option<String> {
1056    self
1057      .manifest
1058      .lock()
1059      .unwrap()
1060      .inner
1061      .as_table()
1062      .get("lib")?
1063      .as_table()?
1064      .get("name")?
1065      .as_str()
1066      .map(|n| n.to_string())
1067  }
1068}
1069
1070impl RustAppSettings {
1071  pub fn new(
1072    config: &Config,
1073    manifest: Manifest,
1074    target: Option<String>,
1075    tauri_dir: &Path,
1076  ) -> crate::Result<Self> {
1077    let cargo_settings = CargoSettings::load(tauri_dir).context("failed to load Cargo settings")?;
1078    let cargo_package_settings = match &cargo_settings.package {
1079      Some(package_info) => package_info.clone(),
1080      None => {
1081        return Err(crate::Error::GenericError(
1082          "No package info in the config file".to_owned(),
1083        ))
1084      }
1085    };
1086
1087    let workspace_dir = get_workspace_dir(tauri_dir)?;
1088    let ws_package_settings = CargoSettings::load(&workspace_dir)
1089      .context("failed to load Cargo settings from workspace root")?
1090      .workspace
1091      .and_then(|v| v.package);
1092
1093    let version = config.version.clone().unwrap_or_else(|| {
1094      cargo_package_settings
1095        .version
1096        .clone()
1097        .expect("Cargo manifest must have the `package.version` field")
1098        .resolve("version", || {
1099          ws_package_settings
1100            .as_ref()
1101            .and_then(|p| p.version.clone())
1102            .context("Couldn't inherit value for `version` from workspace")
1103        })
1104        .expect("Cargo project does not have a version")
1105    });
1106
1107    let package_settings = PackageSettings {
1108      product_name: config
1109        .product_name
1110        .clone()
1111        .unwrap_or_else(|| cargo_package_settings.name.clone()),
1112      version,
1113      description: cargo_package_settings
1114        .description
1115        .clone()
1116        .map(|description| {
1117          description
1118            .resolve("description", || {
1119              ws_package_settings
1120                .as_ref()
1121                .and_then(|v| v.description.clone())
1122                .context("Couldn't inherit value for `description` from workspace")
1123            })
1124            .unwrap()
1125        })
1126        .unwrap_or_default(),
1127      homepage: cargo_package_settings.homepage.clone().map(|homepage| {
1128        homepage
1129          .resolve("homepage", || {
1130            ws_package_settings
1131              .as_ref()
1132              .and_then(|v| v.homepage.clone())
1133              .context("Couldn't inherit value for `homepage` from workspace")
1134          })
1135          .unwrap()
1136      }),
1137      authors: cargo_package_settings.authors.clone().map(|authors| {
1138        authors
1139          .resolve("authors", || {
1140            ws_package_settings
1141              .as_ref()
1142              .and_then(|v| v.authors.clone())
1143              .context("Couldn't inherit value for `authors` from workspace")
1144          })
1145          .unwrap()
1146      }),
1147      default_run: cargo_package_settings.default_run.clone(),
1148    };
1149
1150    let cargo_config = CargoConfig::load(tauri_dir)?;
1151
1152    let target_triple = target.unwrap_or_else(|| {
1153      cargo_config
1154        .build()
1155        .target()
1156        .map(|t| t.to_string())
1157        .unwrap_or_else(|| {
1158          let output = Command::new("rustc")
1159            .args(["-vV"])
1160            .output()
1161            .expect("\"rustc\" could not be found, did you install Rust?");
1162          let stdout = String::from_utf8_lossy(&output.stdout);
1163          stdout
1164            .split('\n')
1165            .find(|l| l.starts_with("host:"))
1166            .unwrap()
1167            .replace("host:", "")
1168            .trim()
1169            .to_string()
1170        })
1171    });
1172    let target_platform = TargetPlatform::from_triple(&target_triple);
1173
1174    Ok(Self {
1175      manifest: Mutex::new(manifest),
1176      cargo_settings,
1177      cargo_package_settings,
1178      cargo_ws_package_settings: ws_package_settings,
1179      package_settings,
1180      cargo_config,
1181      target_triple,
1182      target_platform,
1183      workspace_dir,
1184    })
1185  }
1186
1187  fn target<'a>(&'a self, options: &'a Options) -> Option<&'a str> {
1188    options
1189      .target
1190      .as_deref()
1191      .or_else(|| self.cargo_config.build().target())
1192  }
1193
1194  pub fn out_dir(&self, options: &Options, tauri_dir: &Path) -> crate::Result<PathBuf> {
1195    get_target_dir(self.target(options), options, tauri_dir)
1196  }
1197}
1198
1199#[derive(Deserialize)]
1200pub(crate) struct CargoMetadata {
1201  pub(crate) target_directory: PathBuf,
1202  pub(crate) workspace_root: PathBuf,
1203  workspace_members: Vec<String>,
1204  packages: Vec<Package>,
1205}
1206
1207#[derive(Deserialize)]
1208struct Package {
1209  name: String,
1210  id: String,
1211  manifest_path: PathBuf,
1212  dependencies: Vec<Dependency>,
1213}
1214
1215#[derive(Deserialize)]
1216struct Dependency {
1217  name: String,
1218  /// Local package
1219  path: Option<PathBuf>,
1220}
1221
1222pub(crate) fn get_cargo_metadata(tauri_dir: &Path) -> crate::Result<CargoMetadata> {
1223  let output = Command::new("cargo")
1224    .args(["metadata", "--no-deps", "--format-version", "1"])
1225    .current_dir(tauri_dir)
1226    .output()
1227    .map_err(|error| Error::CommandFailed {
1228      command: "cargo metadata --no-deps --format-version 1".to_string(),
1229      error,
1230    })?;
1231
1232  if !output.status.success() {
1233    return Err(Error::CommandFailed {
1234      command: "cargo metadata".to_string(),
1235      error: std::io::Error::other(String::from_utf8_lossy(&output.stderr)),
1236    });
1237  }
1238
1239  serde_json::from_slice(&output.stdout).context("failed to parse cargo metadata")
1240}
1241
1242/// Get the tauri project crate's dependencies that are inside the workspace
1243fn get_in_workspace_dependency_paths(tauri_dir: &Path) -> crate::Result<Vec<PathBuf>> {
1244  let metadata = get_cargo_metadata(tauri_dir)?;
1245  let tauri_project_manifest_path = tauri_dir.join("Cargo.toml");
1246  let tauri_project_package = metadata
1247    .packages
1248    .iter()
1249    .find(|package| package.manifest_path == tauri_project_manifest_path)
1250    .context("tauri project package doesn't exist in cargo metadata output `packages`")?;
1251
1252  let workspace_packages = metadata
1253    .workspace_members
1254    .iter()
1255    .map(|member_package_id| {
1256      metadata
1257        .packages
1258        .iter()
1259        .find(|package| package.id == *member_package_id)
1260        .context("workspace member doesn't exist in cargo metadata output `packages`")
1261    })
1262    .collect::<crate::Result<Vec<_>>>()?;
1263
1264  let mut found_dependency_paths = Vec::new();
1265  find_dependencies(
1266    tauri_project_package,
1267    &workspace_packages,
1268    &mut found_dependency_paths,
1269  );
1270  Ok(found_dependency_paths)
1271}
1272
1273fn find_dependencies(
1274  package: &Package,
1275  workspace_packages: &Vec<&Package>,
1276  found_dependency_paths: &mut Vec<PathBuf>,
1277) {
1278  for dependency in &package.dependencies {
1279    if let Some(path) = &dependency.path {
1280      if let Some(package) = workspace_packages.iter().find(|workspace_package| {
1281        workspace_package.name == dependency.name
1282          && path.join("Cargo.toml") == workspace_package.manifest_path
1283          && !found_dependency_paths.contains(path)
1284      }) {
1285        found_dependency_paths.push(path.to_owned());
1286        find_dependencies(package, workspace_packages, found_dependency_paths);
1287      }
1288    }
1289  }
1290}
1291
1292/// Get the cargo target directory based on the provided arguments.
1293/// If "--target-dir" is specified in args, use it as the target directory (relative to current directory).
1294/// Otherwise, use the target directory from cargo metadata.
1295pub(crate) fn get_cargo_target_dir(args: &[String], tauri_dir: &Path) -> crate::Result<PathBuf> {
1296  let path = if let Some(target) = get_cargo_option(args, "--target-dir") {
1297    std::env::current_dir()
1298      .context("failed to get current directory")?
1299      .join(target)
1300  } else {
1301    get_cargo_metadata(tauri_dir)
1302      .context("failed to run 'cargo metadata' command to get target directory")?
1303      .target_directory
1304  };
1305
1306  Ok(path)
1307}
1308
1309/// This function determines the 'target' directory and suffixes it with the profile
1310/// to determine where the compiled binary will be located.
1311fn get_target_dir(
1312  triple: Option<&str>,
1313  options: &Options,
1314  tauri_dir: &Path,
1315) -> crate::Result<PathBuf> {
1316  let mut path = get_cargo_target_dir(&options.args, tauri_dir)?;
1317
1318  if let Some(triple) = triple {
1319    path.push(triple);
1320  }
1321
1322  path.push(get_profile_dir(options));
1323
1324  Ok(path)
1325}
1326
1327#[inline]
1328fn get_cargo_option<'a>(args: &'a [String], option: &'a str) -> Option<&'a str> {
1329  args
1330    .iter()
1331    .position(|a| a.starts_with(option))
1332    .and_then(|i| {
1333      args[i]
1334        .split_once('=')
1335        .map(|(_, p)| Some(p))
1336        .unwrap_or_else(|| args.get(i + 1).map(|s| s.as_str()))
1337    })
1338}
1339
1340/// Executes `cargo metadata` to get the workspace directory.
1341pub fn get_workspace_dir(tauri_dir: &Path) -> crate::Result<PathBuf> {
1342  Ok(
1343    get_cargo_metadata(tauri_dir)
1344      .context("failed to run 'cargo metadata' command to get workspace directory")?
1345      .workspace_root,
1346  )
1347}
1348
1349pub fn get_profile(options: &Options) -> &str {
1350  get_cargo_option(&options.args, "--profile").unwrap_or(if options.debug {
1351    "dev"
1352  } else {
1353    "release"
1354  })
1355}
1356
1357pub fn get_profile_dir(options: &Options) -> &str {
1358  match get_profile(options) {
1359    "dev" => "debug",
1360    profile => profile,
1361  }
1362}
1363
1364#[allow(unused_variables, deprecated)]
1365fn tauri_config_to_bundle_settings(
1366  settings: &RustAppSettings,
1367  features: &[String],
1368  tauri_config: &Config,
1369  tauri_dir: &Path,
1370  config: crate::helpers::config::BundleConfig,
1371  updater_config: Option<UpdaterSettings>,
1372  arch64bits: bool,
1373) -> crate::Result<BundleSettings> {
1374  let enabled_features = settings
1375    .manifest
1376    .lock()
1377    .unwrap()
1378    .all_enabled_features(features);
1379
1380  #[allow(unused_mut)]
1381  let mut resources = config
1382    .resources
1383    .unwrap_or(BundleResources::List(Vec::new()));
1384  #[allow(unused_mut)]
1385  let mut depends_deb = config.linux.deb.depends.unwrap_or_default();
1386
1387  #[allow(unused_mut)]
1388  let mut depends_rpm = config.linux.rpm.depends.unwrap_or_default();
1389
1390  #[allow(unused_mut)]
1391  let mut appimage_files = config.linux.appimage.files;
1392
1393  // set env vars used by the bundler and inject dependencies
1394  #[cfg(target_os = "linux")]
1395  {
1396    let mut libs: Vec<String> = Vec::new();
1397
1398    if enabled_features.contains(&"tray-icon".into())
1399      || enabled_features.contains(&"tauri/tray-icon".into())
1400    {
1401      let (tray_kind, path) = std::env::var_os("TAURI_LINUX_AYATANA_APPINDICATOR")
1402        .map(|ayatana| {
1403          if ayatana == "true" || ayatana == "1" {
1404            (
1405              pkgconfig_utils::TrayKind::Ayatana,
1406              format!(
1407                "{}/libayatana-appindicator3.so.1",
1408                pkgconfig_utils::get_library_path("ayatana-appindicator3-0.1")
1409                  .expect("failed to get ayatana-appindicator library path using pkg-config.")
1410              ),
1411            )
1412          } else {
1413            (
1414              pkgconfig_utils::TrayKind::Libappindicator,
1415              format!(
1416                "{}/libappindicator3.so.1",
1417                pkgconfig_utils::get_library_path("appindicator3-0.1")
1418                  .expect("failed to get libappindicator-gtk library path using pkg-config.")
1419              ),
1420            )
1421          }
1422        })
1423        .unwrap_or_else(pkgconfig_utils::get_appindicator_library_path);
1424      match tray_kind {
1425        pkgconfig_utils::TrayKind::Ayatana => {
1426          depends_deb.push("libayatana-appindicator3-1".into());
1427          libs.push("libayatana-appindicator3.so.1".into());
1428        }
1429        pkgconfig_utils::TrayKind::Libappindicator => {
1430          depends_deb.push("libappindicator3-1".into());
1431          libs.push("libappindicator3.so.1".into());
1432        }
1433      }
1434
1435      // conditionally setting it in case the user provided its own version for some reason
1436      let path = PathBuf::from(path);
1437      if !appimage_files.contains_key(&path) {
1438        // manually construct target path, just in case the source path is something unexpected
1439        appimage_files.insert(Path::new("/usr/lib/").join(path.file_name().unwrap()), path);
1440      }
1441    }
1442
1443    depends_deb.push("libwebkit2gtk-4.1-0".to_string());
1444    depends_deb.push("libgtk-3-0".to_string());
1445
1446    libs.push("libwebkit2gtk-4.1.so.0".into());
1447    libs.push("libgtk-3.so.0".into());
1448
1449    for lib in libs {
1450      let mut requires = lib;
1451      if arch64bits {
1452        requires.push_str("()(64bit)");
1453      }
1454      depends_rpm.push(requires);
1455    }
1456  }
1457
1458  #[cfg(windows)]
1459  {
1460    if let crate::helpers::config::WebviewInstallMode::FixedRuntime { path } =
1461      &config.windows.webview_install_mode
1462    {
1463      resources.push(path.display().to_string());
1464    }
1465  }
1466
1467  let signing_identity = match std::env::var_os("APPLE_SIGNING_IDENTITY") {
1468    Some(signing_identity) => Some(
1469      signing_identity
1470        .to_str()
1471        .expect("failed to convert APPLE_SIGNING_IDENTITY to string")
1472        .to_string(),
1473    ),
1474    None => config.macos.signing_identity,
1475  };
1476
1477  let provider_short_name = match std::env::var_os("APPLE_PROVIDER_SHORT_NAME") {
1478    Some(provider_short_name) => Some(
1479      provider_short_name
1480        .to_str()
1481        .expect("failed to convert APPLE_PROVIDER_SHORT_NAME to string")
1482        .to_string(),
1483    ),
1484    None => config.macos.provider_short_name,
1485  };
1486
1487  let (resources, resources_map) = match resources {
1488    BundleResources::List(paths) => (Some(paths), None),
1489    BundleResources::Map(map) => (None, Some(map)),
1490  };
1491
1492  #[cfg(target_os = "macos")]
1493  let entitlements = if let Some(plugin_config) = tauri_config
1494    .plugins
1495    .0
1496    .get("deep-link")
1497    .and_then(|c| c.get("desktop").cloned())
1498  {
1499    let protocols: DesktopDeepLinks =
1500      serde_json::from_value(plugin_config).context("failed to parse deep link plugin config")?;
1501    let domains = match protocols {
1502      DesktopDeepLinks::One(protocol) => protocol.domains,
1503      DesktopDeepLinks::List(protocols) => protocols.into_iter().flat_map(|p| p.domains).collect(),
1504    };
1505
1506    if domains.is_empty() {
1507      config
1508        .macos
1509        .entitlements
1510        .map(PathBuf::from)
1511        .map(tauri_bundler::bundle::Entitlements::Path)
1512    } else {
1513      let mut app_links_entitlements = plist::Dictionary::new();
1514      if !domains.is_empty() {
1515        app_links_entitlements.insert(
1516          "com.apple.developer.associated-domains".to_string(),
1517          domains
1518            .into_iter()
1519            .map(|domain| format!("applinks:{domain}").into())
1520            .collect::<Vec<_>>()
1521            .into(),
1522        );
1523      }
1524      let entitlements = if let Some(user_provided_entitlements) = config.macos.entitlements {
1525        crate::helpers::plist::merge_plist(vec![
1526          PathBuf::from(user_provided_entitlements).into(),
1527          plist::Value::Dictionary(app_links_entitlements).into(),
1528        ])?
1529      } else {
1530        app_links_entitlements.into()
1531      };
1532
1533      Some(tauri_bundler::bundle::Entitlements::Plist(entitlements))
1534    }
1535  } else {
1536    config
1537      .macos
1538      .entitlements
1539      .map(PathBuf::from)
1540      .map(tauri_bundler::bundle::Entitlements::Path)
1541  };
1542  #[cfg(not(target_os = "macos"))]
1543  let entitlements = None;
1544
1545  Ok(BundleSettings {
1546    identifier: Some(tauri_config.identifier.clone()),
1547    publisher: config.publisher,
1548    homepage: config.homepage,
1549    icon: Some(config.icon),
1550    resources,
1551    resources_map,
1552    copyright: config.copyright,
1553    category: match config.category {
1554      Some(category) => Some(AppCategory::from_str(&category).map_err(|e| match e {
1555        Some(e) => Error::GenericError(format!("invalid category, did you mean `{e}`?")),
1556        None => Error::GenericError("invalid category".to_string()),
1557      })?),
1558      None => None,
1559    },
1560    file_associations: config.file_associations,
1561    short_description: config.short_description,
1562    long_description: config.long_description,
1563    external_bin: config.external_bin,
1564    deb: DebianSettings {
1565      depends: if depends_deb.is_empty() {
1566        None
1567      } else {
1568        Some(depends_deb)
1569      },
1570      recommends: config.linux.deb.recommends,
1571      provides: config.linux.deb.provides,
1572      conflicts: config.linux.deb.conflicts,
1573      replaces: config.linux.deb.replaces,
1574      files: config.linux.deb.files,
1575      desktop_template: config.linux.deb.desktop_template,
1576      section: config.linux.deb.section,
1577      priority: config.linux.deb.priority,
1578      changelog: config.linux.deb.changelog,
1579      pre_install_script: config.linux.deb.pre_install_script,
1580      post_install_script: config.linux.deb.post_install_script,
1581      pre_remove_script: config.linux.deb.pre_remove_script,
1582      post_remove_script: config.linux.deb.post_remove_script,
1583    },
1584    appimage: AppImageSettings {
1585      files: appimage_files,
1586      bundle_media_framework: config.linux.appimage.bundle_media_framework,
1587      bundle_xdg_open: false,
1588    },
1589    rpm: RpmSettings {
1590      depends: if depends_rpm.is_empty() {
1591        None
1592      } else {
1593        Some(depends_rpm)
1594      },
1595      recommends: config.linux.rpm.recommends,
1596      provides: config.linux.rpm.provides,
1597      conflicts: config.linux.rpm.conflicts,
1598      obsoletes: config.linux.rpm.obsoletes,
1599      release: config.linux.rpm.release,
1600      epoch: config.linux.rpm.epoch,
1601      files: config.linux.rpm.files,
1602      desktop_template: config.linux.rpm.desktop_template,
1603      pre_install_script: config.linux.rpm.pre_install_script,
1604      post_install_script: config.linux.rpm.post_install_script,
1605      pre_remove_script: config.linux.rpm.pre_remove_script,
1606      post_remove_script: config.linux.rpm.post_remove_script,
1607      compression: config.linux.rpm.compression,
1608    },
1609    dmg: DmgSettings {
1610      background: config.macos.dmg.background,
1611      window_position: config
1612        .macos
1613        .dmg
1614        .window_position
1615        .map(|window_position| Position {
1616          x: window_position.x,
1617          y: window_position.y,
1618        }),
1619      window_size: Size {
1620        width: config.macos.dmg.window_size.width,
1621        height: config.macos.dmg.window_size.height,
1622      },
1623      app_position: Position {
1624        x: config.macos.dmg.app_position.x,
1625        y: config.macos.dmg.app_position.y,
1626      },
1627      application_folder_position: Position {
1628        x: config.macos.dmg.application_folder_position.x,
1629        y: config.macos.dmg.application_folder_position.y,
1630      },
1631    },
1632    ios: IosSettings {
1633      bundle_version: config.ios.bundle_version,
1634    },
1635    macos: MacOsSettings {
1636      frameworks: config.macos.frameworks,
1637      files: config.macos.files,
1638      bundle_version: config.macos.bundle_version,
1639      bundle_name: config.macos.bundle_name,
1640      minimum_system_version: config.macos.minimum_system_version,
1641      exception_domain: config.macos.exception_domain,
1642      signing_identity,
1643      skip_stapling: false,
1644      hardened_runtime: config.macos.hardened_runtime,
1645      provider_short_name,
1646      entitlements,
1647      #[cfg(not(target_os = "macos"))]
1648      info_plist: None,
1649      #[cfg(target_os = "macos")]
1650      info_plist: {
1651        let mut src_plists = vec![];
1652
1653        let path = tauri_dir.join("Info.plist");
1654        if path.exists() {
1655          src_plists.push(path.into());
1656        }
1657        if let Some(info_plist) = &config.macos.info_plist {
1658          src_plists.push(info_plist.clone().into());
1659        }
1660
1661        Some(tauri_bundler::bundle::PlistKind::Plist(
1662          crate::helpers::plist::merge_plist(src_plists)?,
1663        ))
1664      },
1665    },
1666    windows: WindowsSettings {
1667      timestamp_url: config.windows.timestamp_url,
1668      tsp: config.windows.tsp,
1669      digest_algorithm: config.windows.digest_algorithm,
1670      certificate_thumbprint: config.windows.certificate_thumbprint,
1671      wix: config.windows.wix.map(wix_settings),
1672      nsis: config.windows.nsis.map(nsis_settings),
1673      icon_path: PathBuf::new(),
1674      webview_install_mode: config.windows.webview_install_mode,
1675      allow_downgrades: config.windows.allow_downgrades,
1676      sign_command: config.windows.sign_command.map(custom_sign_settings),
1677      minimum_webview2_version: config.windows.minimum_webview2_version,
1678    },
1679    license: config.license.or_else(|| {
1680      settings
1681        .cargo_package_settings
1682        .license
1683        .clone()
1684        .map(|license| {
1685          license
1686            .resolve("license", || {
1687              settings
1688                .cargo_ws_package_settings
1689                .as_ref()
1690                .and_then(|v| v.license.clone())
1691                .context("Couldn't inherit value for `license` from workspace")
1692            })
1693            .unwrap()
1694        })
1695    }),
1696    license_file: config.license_file.map(|l| tauri_dir.join(l)),
1697    updater: updater_config,
1698    ..Default::default()
1699  })
1700}
1701
1702#[cfg(target_os = "linux")]
1703mod pkgconfig_utils {
1704  use std::process::Command;
1705
1706  pub enum TrayKind {
1707    Ayatana,
1708    Libappindicator,
1709  }
1710
1711  pub fn get_appindicator_library_path() -> (TrayKind, String) {
1712    match get_library_path("ayatana-appindicator3-0.1") {
1713      Some(p) => (
1714        TrayKind::Ayatana,
1715        format!("{p}/libayatana-appindicator3.so.1"),
1716      ),
1717      None => match get_library_path("appindicator3-0.1") {
1718        Some(p) => (
1719          TrayKind::Libappindicator,
1720          format!("{p}/libappindicator3.so.1"),
1721        ),
1722        None => panic!("Can't detect any appindicator library"),
1723      },
1724    }
1725  }
1726
1727  /// Gets the folder in which a library is located using `pkg-config`.
1728  pub fn get_library_path(name: &str) -> Option<String> {
1729    let mut cmd = Command::new("pkg-config");
1730    cmd.env("PKG_CONFIG_ALLOW_SYSTEM_LIBS", "1");
1731    cmd.arg("--libs-only-L");
1732    cmd.arg(name);
1733    if let Ok(output) = cmd.output() {
1734      if !output.stdout.is_empty() {
1735        // output would be "-L/path/to/library\n"
1736        let word = output.stdout[2..].to_vec();
1737        Some(String::from_utf8_lossy(&word).trim().to_string())
1738      } else {
1739        None
1740      }
1741    } else {
1742      None
1743    }
1744  }
1745}
1746
1747#[cfg(test)]
1748mod tests {
1749  use super::*;
1750  use std::fs;
1751
1752  fn app_settings_with_manifest(cargo_toml: &str) -> (tempfile::TempDir, RustAppSettings) {
1753    let temp_dir = tempfile::tempdir().unwrap();
1754    let tauri_dir = temp_dir.path().to_path_buf();
1755    fs::create_dir_all(tauri_dir.join("src/bin")).unwrap();
1756    fs::write(tauri_dir.join("Cargo.toml"), cargo_toml).unwrap();
1757    fs::write(tauri_dir.join("src/main.rs"), "").unwrap();
1758    fs::write(tauri_dir.join("src/bin/generate-bindings.rs"), "").unwrap();
1759
1760    let cargo_settings = CargoSettings::load(&tauri_dir).unwrap();
1761    let cargo_package_settings = cargo_settings.package.clone().unwrap();
1762    let package_settings = PackageSettings {
1763      product_name: cargo_package_settings.name.clone(),
1764      version: "0.1.0".into(),
1765      description: String::new(),
1766      homepage: None,
1767      authors: None,
1768      default_run: cargo_package_settings.default_run.clone(),
1769    };
1770
1771    let target_triple = "x86_64-unknown-linux-gnu".to_string();
1772
1773    (
1774      temp_dir,
1775      RustAppSettings {
1776        manifest: Mutex::new(Manifest::default()),
1777        cargo_settings,
1778        cargo_package_settings,
1779        cargo_ws_package_settings: None,
1780        package_settings,
1781        cargo_config: CargoConfig::default(),
1782        target_triple: target_triple.clone(),
1783        target_platform: TargetPlatform::from_triple(&target_triple),
1784        workspace_dir: tauri_dir,
1785      },
1786    )
1787  }
1788
1789  #[test]
1790  fn parse_cargo_option() {
1791    let args = [
1792      "build".into(),
1793      "--".into(),
1794      "--profile".into(),
1795      "holla".into(),
1796      "--features".into(),
1797      "a".into(),
1798      "b".into(),
1799      "--target-dir".into(),
1800      "path/to/dir".into(),
1801    ];
1802
1803    assert_eq!(get_cargo_option(&args, "--profile"), Some("holla"));
1804    assert_eq!(get_cargo_option(&args, "--target-dir"), Some("path/to/dir"));
1805    assert_eq!(get_cargo_option(&args, "--non-existent"), None);
1806  }
1807
1808  #[test]
1809  fn get_binaries_ignores_src_bin_with_disabled_required_features() {
1810    let cargo_toml = r#"
1811      [package]
1812      name = "app"
1813      version = "0.1.0"
1814      default-run = "app"
1815
1816      [[bin]]
1817      name = "generate-bindings"
1818      path = "src/bin/generate-bindings.rs"
1819      required-features = ["bindings"]
1820    "#;
1821
1822    let (temp_dir, app_settings) = app_settings_with_manifest(cargo_toml);
1823    let tauri_dir = temp_dir.path();
1824
1825    let binaries = app_settings
1826      .get_binaries(&Options::default(), tauri_dir)
1827      .unwrap();
1828    assert!(binaries.iter().any(|bin| bin.name() == "app" && bin.main()));
1829    assert!(!binaries.iter().any(|bin| bin.name() == "generate-bindings"));
1830
1831    let binaries = app_settings
1832      .get_binaries(
1833        &Options {
1834          features: vec!["bindings".into()],
1835          ..Default::default()
1836        },
1837        tauri_dir,
1838      )
1839      .unwrap();
1840    assert!(binaries.iter().any(|bin| bin.name() == "generate-bindings"));
1841  }
1842
1843  #[test]
1844  fn parse_profile_from_opts() {
1845    let options = Options {
1846      args: vec![
1847        "build".into(),
1848        "--".into(),
1849        "--profile".into(),
1850        "testing".into(),
1851        "--features".into(),
1852        "feat1".into(),
1853      ],
1854      ..Default::default()
1855    };
1856    assert_eq!(get_profile(&options), "testing");
1857
1858    let options = Options {
1859      args: vec![
1860        "build".into(),
1861        "--".into(),
1862        "--profile=customprofile".into(),
1863        "testing".into(),
1864        "--features".into(),
1865        "feat1".into(),
1866      ],
1867      ..Default::default()
1868    };
1869    assert_eq!(get_profile(&options), "customprofile");
1870
1871    let options = Options {
1872      debug: true,
1873      args: vec![
1874        "build".into(),
1875        "--".into(),
1876        "testing".into(),
1877        "--features".into(),
1878        "feat1".into(),
1879      ],
1880      ..Default::default()
1881    };
1882    assert_eq!(get_profile(&options), "dev");
1883
1884    let options = Options {
1885      debug: false,
1886      args: vec![
1887        "build".into(),
1888        "--".into(),
1889        "testing".into(),
1890        "--features".into(),
1891        "feat1".into(),
1892      ],
1893      ..Default::default()
1894    };
1895    assert_eq!(get_profile(&options), "release");
1896
1897    let options = Options {
1898      args: vec!["build".into(), "--".into(), "--profile".into()],
1899      ..Default::default()
1900    };
1901    assert_eq!(get_profile(&options), "release");
1902  }
1903
1904  #[test]
1905  fn parse_target_dir_from_opts() {
1906    let dirs = crate::helpers::app_paths::resolve_dirs();
1907    let current_dir = std::env::current_dir().unwrap();
1908
1909    let options = Options {
1910      args: vec![
1911        "build".into(),
1912        "--".into(),
1913        "--target-dir".into(),
1914        "path/to/some/dir".into(),
1915        "--features".into(),
1916        "feat1".into(),
1917      ],
1918      debug: false,
1919      ..Default::default()
1920    };
1921
1922    assert_eq!(
1923      get_target_dir(None, &options, dirs.tauri).unwrap(),
1924      current_dir.join("path/to/some/dir/release")
1925    );
1926    assert_eq!(
1927      get_target_dir(Some("x86_64-pc-windows-msvc"), &options, dirs.tauri).unwrap(),
1928      current_dir
1929        .join("path/to/some/dir")
1930        .join("x86_64-pc-windows-msvc")
1931        .join("release")
1932    );
1933
1934    let options = Options {
1935      args: vec![
1936        "build".into(),
1937        "--".into(),
1938        "--features".into(),
1939        "feat1".into(),
1940      ],
1941      debug: false,
1942      ..Default::default()
1943    };
1944
1945    #[cfg(windows)]
1946    assert!(
1947      get_target_dir(Some("x86_64-pc-windows-msvc"), &options, dirs.tauri)
1948        .unwrap()
1949        .ends_with("x86_64-pc-windows-msvc\\release")
1950    );
1951    #[cfg(not(windows))]
1952    assert!(
1953      get_target_dir(Some("x86_64-pc-windows-msvc"), &options, dirs.tauri)
1954        .unwrap()
1955        .ends_with("x86_64-pc-windows-msvc/release")
1956    );
1957
1958    #[cfg(windows)]
1959    {
1960      std::env::set_var("CARGO_TARGET_DIR", "D:\\path\\to\\env\\dir");
1961      assert_eq!(
1962        get_target_dir(None, &options, dirs.tauri).unwrap(),
1963        PathBuf::from("D:\\path\\to\\env\\dir\\release")
1964      );
1965      assert_eq!(
1966        get_target_dir(Some("x86_64-pc-windows-msvc"), &options, dirs.tauri).unwrap(),
1967        PathBuf::from("D:\\path\\to\\env\\dir\\x86_64-pc-windows-msvc\\release")
1968      );
1969    }
1970
1971    #[cfg(not(windows))]
1972    {
1973      std::env::set_var("CARGO_TARGET_DIR", "/path/to/env/dir");
1974      assert_eq!(
1975        get_target_dir(None, &options, dirs.tauri).unwrap(),
1976        PathBuf::from("/path/to/env/dir/release")
1977      );
1978      assert_eq!(
1979        get_target_dir(Some("x86_64-pc-windows-msvc"), &options, dirs.tauri).unwrap(),
1980        PathBuf::from("/path/to/env/dir/x86_64-pc-windows-msvc/release")
1981      );
1982    }
1983  }
1984}