1use 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 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 [arch, _, host] => (*arch, *host, None),
303 [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 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 let mut watch_folders = vec![tauri_dir.to_path_buf()];
459
460 watch_folders.extend(get_in_workspace_dependency_paths(tauri_dir)?);
461
462 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 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 let _ = child.wait();
597 child = run(self, config)?;
598 }
599 bail!("File watcher exited unexpectedly")
600 }
601}
602
603#[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#[derive(Clone, Debug, Deserialize)]
663struct WorkspaceSettings {
664 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 filename: Option<String>,
684 path: Option<String>,
685 required_features: Option<Vec<String>>,
686}
687
688impl BinarySettings {
689 pub fn file_name(&self) -> &str {
691 self.filename.as_ref().unwrap_or(&self.name)
692 }
693}
694
695#[derive(Debug, Clone, Deserialize)]
697#[serde(rename_all = "kebab-case")]
698pub struct CargoPackageSettings {
699 pub name: String,
701 pub version: Option<MaybeWorkspace<String>>,
703 pub description: Option<MaybeWorkspace<String>>,
705 pub homepage: Option<MaybeWorkspace<String>>,
707 pub authors: Option<MaybeWorkspace<Vec<String>>>,
709 pub license: Option<MaybeWorkspace<String>>,
711 pub default_run: Option<String>,
713}
714
715#[derive(Clone, Debug, Deserialize)]
717struct CargoSettings {
718 package: Option<CargoPackageSettings>,
722 workspace: Option<WorkspaceSettings>,
726 bin: Option<Vec<BinarySettings>>,
728}
729
730impl CargoSettings {
731 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 pub pubkey: String,
766 #[serde(default)]
768 pub windows: UpdaterWindowsConfig,
769}
770
771#[derive(Default, Debug, PartialEq, Eq, Clone)]
773pub enum WindowsUpdateInstallMode {
774 BasicUi,
776 Quiet,
779 #[default]
781 Passive,
782 }
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 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 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 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 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 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
1225fn 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
1275pub(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
1292fn 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
1323pub 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 #[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 let path = PathBuf::from(path);
1420 if !appimage_files.contains_key(&path) {
1421 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 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 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}