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 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#[derive(Debug, Clone, Deserialize)]
715#[serde(rename_all = "kebab-case")]
716pub struct CargoPackageSettings {
717 pub name: String,
719 pub version: Option<MaybeWorkspace<String>>,
721 pub description: Option<MaybeWorkspace<String>>,
723 pub homepage: Option<MaybeWorkspace<String>>,
725 pub authors: Option<MaybeWorkspace<Vec<String>>>,
727 pub license: Option<MaybeWorkspace<String>>,
729 pub default_run: Option<String>,
731}
732
733#[derive(Clone, Debug, Deserialize)]
735struct CargoSettings {
736 package: Option<CargoPackageSettings>,
740 workspace: Option<WorkspaceSettings>,
744 bin: Option<Vec<BinarySettings>>,
746}
747
748impl CargoSettings {
749 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 pub pubkey: String,
784 #[serde(default)]
786 pub windows: UpdaterWindowsConfig,
787}
788
789#[derive(Default, Debug, PartialEq, Eq, Clone)]
791pub enum WindowsUpdateInstallMode {
792 BasicUi,
794 Quiet,
797 #[default]
799 Passive,
800 }
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 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 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 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 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
1242fn 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
1292pub(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
1309fn 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
1340pub 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 #[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 let path = PathBuf::from(path);
1437 if !appimage_files.contains_key(&path) {
1438 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 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 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}