1#![doc(
8 html_logo_url = "https://github.com/tauri-apps/tauri/raw/dev/.github/icon.png",
9 html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/.github/icon.png"
10)]
11#![cfg_attr(docsrs, feature(doc_cfg))]
12
13use anyhow::Context;
14pub use anyhow::Result;
15use cargo_toml::Manifest;
16
17use tauri_utils::{
18 config::{BundleResources, Config, WebviewInstallMode},
19 resources::{external_binaries, ResourcePaths},
20};
21
22use std::{
23 collections::HashMap,
24 env, fs,
25 path::{Path, PathBuf},
26};
27
28mod acl;
29#[cfg(feature = "codegen")]
30mod codegen;
31mod manifest;
32mod mobile;
33mod static_vcruntime;
34
35#[cfg(feature = "codegen")]
36#[cfg_attr(docsrs, doc(cfg(feature = "codegen")))]
37pub use codegen::context::CodegenContext;
38
39pub use acl::{AppManifest, DefaultPermissionRule, InlinedPlugin};
40
41fn copy_file(from: impl AsRef<Path>, to: impl AsRef<Path>) -> Result<()> {
42 let from = from.as_ref();
43 let to = to.as_ref();
44 if !from.exists() {
45 return Err(anyhow::anyhow!("{:?} does not exist", from));
46 }
47 if !from.is_file() {
48 return Err(anyhow::anyhow!("{:?} is not a file", from));
49 }
50 let dest_dir = to.parent().expect("No data in parent");
51 fs::create_dir_all(dest_dir)?;
52 fs::copy(from, to)?;
53 Ok(())
54}
55
56fn copy_binaries(
57 binaries: ResourcePaths,
58 target_triple: &str,
59 path: &Path,
60 package_name: Option<&str>,
61) -> Result<()> {
62 for src in binaries {
63 let src = src?;
64 println!("cargo:rerun-if-changed={}", src.display());
65 let file_name = src
66 .file_name()
67 .expect("failed to extract external binary filename")
68 .to_string_lossy()
69 .replace(&format!("-{target_triple}"), "");
70
71 if package_name == Some(&file_name) {
72 return Err(anyhow::anyhow!(
73 "Cannot define a sidecar with the same name as the Cargo package name `{}`. Please change the sidecar name in the filesystem and the Tauri configuration.",
74 file_name
75 ));
76 }
77
78 let dest = path.join(file_name);
79 if dest.exists() {
80 fs::remove_file(&dest).unwrap();
81 }
82 copy_file(&src, &dest)?;
83 }
84 Ok(())
85}
86
87fn copy_resources(resources: ResourcePaths<'_>, path: &Path) -> Result<()> {
89 let path = path.canonicalize()?;
90 for resource in resources.iter() {
91 let resource = resource?;
92
93 println!("cargo:rerun-if-changed={}", resource.path().display());
94
95 let src = resource.path().canonicalize()?;
97 let target = path.join(resource.target());
98 if src != target {
99 copy_file(src, target)?;
100 }
101 }
102 Ok(())
103}
104
105#[cfg(unix)]
106fn symlink_dir(src: &Path, dst: &Path) -> std::io::Result<()> {
107 std::os::unix::fs::symlink(src, dst)
108}
109
110#[cfg(windows)]
112fn symlink_dir(src: &Path, dst: &Path) -> std::io::Result<()> {
113 std::os::windows::fs::symlink_dir(src, dst)
114}
115
116#[cfg(unix)]
118fn symlink_file(src: &Path, dst: &Path) -> std::io::Result<()> {
119 std::os::unix::fs::symlink(src, dst)
120}
121
122#[cfg(windows)]
124fn symlink_file(src: &Path, dst: &Path) -> std::io::Result<()> {
125 std::os::windows::fs::symlink_file(src, dst)
126}
127
128fn copy_dir(from: &Path, to: &Path) -> Result<()> {
129 for entry in walkdir::WalkDir::new(from) {
130 let entry = entry?;
131 debug_assert!(entry.path().starts_with(from));
132 let rel_path = entry.path().strip_prefix(from)?;
133 let dest_path = to.join(rel_path);
134 if entry.file_type().is_symlink() {
135 let target = fs::read_link(entry.path())?;
136 if entry.path().is_dir() {
137 symlink_dir(&target, &dest_path)?;
138 } else {
139 symlink_file(&target, &dest_path)?;
140 }
141 } else if entry.file_type().is_dir() {
142 fs::create_dir(dest_path)?;
143 } else {
144 fs::copy(entry.path(), dest_path)?;
145 }
146 }
147 Ok(())
148}
149
150fn copy_framework_from(src_dir: &Path, framework: &str, dest_dir: &Path) -> Result<bool> {
152 let src_name = format!("{framework}.framework");
153 let src_path = src_dir.join(&src_name);
154 if src_path.exists() {
155 copy_dir(&src_path, &dest_dir.join(&src_name))?;
156 Ok(true)
157 } else {
158 Ok(false)
159 }
160}
161
162fn copy_frameworks(dest_dir: &Path, frameworks: &[String]) -> Result<()> {
164 fs::create_dir_all(dest_dir)
165 .with_context(|| format!("Failed to create frameworks output directory at {dest_dir:?}"))?;
166 for framework in frameworks.iter() {
167 if framework.ends_with(".framework") {
168 let src_path = Path::new(framework);
169 let src_name = src_path
170 .file_name()
171 .expect("Couldn't get framework filename");
172 let dest_path = dest_dir.join(src_name);
173 copy_dir(src_path, &dest_path)?;
174 continue;
175 } else if framework.ends_with(".dylib") {
176 let src_path = Path::new(framework);
177 if !src_path.exists() {
178 return Err(anyhow::anyhow!("Library not found: {}", framework));
179 }
180 let src_name = src_path.file_name().expect("Couldn't get library filename");
181 let dest_path = dest_dir.join(src_name);
182 copy_file(src_path, &dest_path)?;
183 continue;
184 } else if framework.contains('/') {
185 return Err(anyhow::anyhow!(
186 "Framework path should have .framework extension: {}",
187 framework
188 ));
189 }
190 if let Some(home_dir) = dirs::home_dir() {
191 if copy_framework_from(&home_dir.join("Library/Frameworks/"), framework, dest_dir)? {
192 continue;
193 }
194 }
195 if copy_framework_from("/Library/Frameworks/".as_ref(), framework, dest_dir)?
196 || copy_framework_from("/Network/Library/Frameworks/".as_ref(), framework, dest_dir)?
197 {
198 continue;
199 }
200 }
201 Ok(())
202}
203
204fn cfg_alias(alias: &str, has_feature: bool) {
207 println!("cargo:rustc-check-cfg=cfg({alias})");
208 if has_feature {
209 println!("cargo:rustc-cfg={alias}");
210 }
211}
212
213#[allow(dead_code)]
215#[derive(Debug)]
216pub struct WindowsAttributes {
217 window_icon_path: Option<PathBuf>,
218 #[doc = include_str!("windows-app-manifest.xml")]
223 app_manifest: Option<String>,
245 append_rc_content: Vec<String>,
247}
248
249impl Default for WindowsAttributes {
250 fn default() -> Self {
251 Self::new()
252 }
253}
254
255impl WindowsAttributes {
256 pub fn new() -> Self {
258 Self {
259 window_icon_path: Default::default(),
260 app_manifest: Some(include_str!("windows-app-manifest.xml").into()),
261 append_rc_content: Vec::new(),
262 }
263 }
264
265 #[must_use]
267 pub fn new_without_app_manifest() -> Self {
268 Self {
269 app_manifest: None,
270 window_icon_path: Default::default(),
271 append_rc_content: Vec::new(),
272 }
273 }
274
275 #[must_use]
278 pub fn window_icon_path<P: AsRef<Path>>(mut self, window_icon_path: P) -> Self {
279 self
280 .window_icon_path
281 .replace(window_icon_path.as_ref().into());
282 self
283 }
284
285 #[doc = include_str!("windows-app-manifest.xml")]
290 #[must_use]
337 pub fn app_manifest<S: AsRef<str>>(mut self, manifest: S) -> Self {
338 self.app_manifest = Some(manifest.as_ref().to_string());
339 self
340 }
341
342 #[must_use]
345 pub fn append_rc_content<S: Into<String>>(mut self, content: S) -> Self {
346 self.append_rc_content.push(content.into());
347 self
348 }
349}
350
351#[derive(Debug, Default)]
353pub struct Attributes {
354 #[allow(dead_code)]
355 windows_attributes: WindowsAttributes,
356 capabilities_path_pattern: Option<&'static str>,
357 #[cfg(feature = "codegen")]
358 codegen: Option<codegen::context::CodegenContext>,
359 inlined_plugins: HashMap<&'static str, InlinedPlugin>,
360 app_manifest: AppManifest,
361}
362
363impl Attributes {
364 pub fn new() -> Self {
366 Self::default()
367 }
368
369 #[must_use]
371 pub fn windows_attributes(mut self, windows_attributes: WindowsAttributes) -> Self {
372 self.windows_attributes = windows_attributes;
373 self
374 }
375
376 #[must_use]
384 pub fn capabilities_path_pattern(mut self, pattern: &'static str) -> Self {
385 self.capabilities_path_pattern.replace(pattern);
386 self
387 }
388
389 pub fn plugin(mut self, name: &'static str, plugin: InlinedPlugin) -> Self {
393 self.inlined_plugins.insert(name, plugin);
394 self
395 }
396
397 pub fn plugins<I>(mut self, plugins: I) -> Self
401 where
402 I: IntoIterator<Item = (&'static str, InlinedPlugin)>,
403 {
404 self.inlined_plugins.extend(plugins);
405 self
406 }
407
408 pub fn app_manifest(mut self, manifest: AppManifest) -> Self {
412 self.app_manifest = manifest;
413 self
414 }
415
416 #[cfg(feature = "codegen")]
417 #[cfg_attr(docsrs, doc(cfg(feature = "codegen")))]
418 #[must_use]
419 pub fn codegen(mut self, codegen: codegen::context::CodegenContext) -> Self {
420 self.codegen.replace(codegen);
421 self
422 }
423}
424
425pub fn is_dev() -> bool {
426 env::var_os("DEP_TAURI_DEV")
427 .expect("missing `cargo:dev` instruction, please update tauri to latest")
428 == "true"
429}
430
431pub fn build() {
454 if let Err(error) = try_build(Attributes::default()) {
455 let error = format!("{error:#}");
456 println!("{error}");
457 if error.starts_with("unknown field") {
458 print!("found an unknown configuration field. This usually means that you are using a CLI version that is newer than `tauri-build` and is incompatible. ");
459 println!(
460 "Please try updating the Rust crates by running `cargo update` in the Tauri app folder."
461 );
462 }
463 std::process::exit(1);
464 }
465}
466
467#[allow(unused_variables)]
469pub fn try_build(attributes: Attributes) -> Result<()> {
470 use anyhow::anyhow;
471
472 println!("cargo:rerun-if-env-changed=TAURI_CONFIG");
473
474 let target_os = env::var_os("CARGO_CFG_TARGET_OS").unwrap();
475 let mobile = target_os == "ios" || target_os == "android";
476 cfg_alias("desktop", !mobile);
477 cfg_alias("mobile", mobile);
478
479 let target_triple = env::var("TARGET").unwrap();
480 let target = tauri_utils::platform::Target::from_triple(&target_triple);
481
482 let (mut config, config_paths) =
483 tauri_utils::config::parse::read_from(target, &env::current_dir().unwrap())?;
484 for config_file_path in config_paths {
485 println!("cargo:rerun-if-changed={}", config_file_path.display());
486 }
487 if let Ok(env) = env::var("TAURI_CONFIG") {
488 let merge_config: serde_json::Value = serde_json::from_str(&env)?;
489 json_patch::merge(&mut config, &merge_config);
490 }
491 let config: Config = serde_json::from_value(config)?;
492
493 let s = config.identifier.split('.');
494 let last = s.clone().count() - 1;
495 let mut android_package_prefix = String::new();
496 for (i, w) in s.enumerate() {
497 if i == last {
498 println!(
499 "cargo:rustc-env=TAURI_ANDROID_PACKAGE_NAME_APP_NAME={}",
500 w.replace('-', "_")
501 );
502 } else {
503 android_package_prefix.push_str(&w.replace(['_', '-'], "_1"));
504 android_package_prefix.push('_');
505 }
506 }
507 android_package_prefix.pop();
508 println!("cargo:rustc-env=TAURI_ANDROID_PACKAGE_NAME_PREFIX={android_package_prefix}");
509
510 if let Some(project_dir) = env::var_os("TAURI_ANDROID_PROJECT_PATH").map(PathBuf::from) {
511 mobile::generate_gradle_files(project_dir)?;
512
513 if let Some(associations) = config.bundle.file_associations.as_ref() {
515 mobile::update_android_manifest_file_associations(associations)?;
516 }
517 }
518
519 cfg_alias("dev", is_dev());
520
521 let cargo_toml_path = Path::new("Cargo.toml").canonicalize()?;
522 let mut manifest = Manifest::<cargo_toml::Value>::from_path_with_metadata(cargo_toml_path)?;
523
524 let out_dir = PathBuf::from(env::var_os("OUT_DIR").unwrap());
525
526 manifest::check(&config, &mut manifest)?;
527
528 acl::build(&out_dir, target, &attributes)?;
529
530 tauri_utils::plugin::save_global_api_scripts_paths(&out_dir, None);
531
532 println!("cargo:rustc-env=TAURI_ENV_TARGET_TRIPLE={target_triple}");
533 env::set_var("TAURI_ENV_TARGET_TRIPLE", &target_triple);
535
536 let target_dir = out_dir
538 .parent()
539 .unwrap()
540 .parent()
541 .unwrap()
542 .parent()
543 .unwrap();
544
545 if let Some(paths) = &config.bundle.external_bin {
546 copy_binaries(
547 ResourcePaths::new(&external_binaries(paths, &target_triple, &target), true),
548 &target_triple,
549 target_dir,
550 manifest.package.as_ref().map(|p| p.name.as_ref()),
551 )?;
552 }
553
554 #[allow(unused_mut, clippy::redundant_clone)]
555 let mut resources = config
556 .bundle
557 .resources
558 .clone()
559 .unwrap_or(BundleResources::List(Vec::new()));
560 if target_triple.contains("windows") {
561 if let Some(fixed_webview2_runtime_path) = match &config.bundle.windows.webview_install_mode {
562 WebviewInstallMode::FixedRuntime { path } => Some(path),
563 _ => None,
564 } {
565 resources.push(fixed_webview2_runtime_path.display().to_string());
566 }
567 }
568 match resources {
569 BundleResources::List(res) => {
570 copy_resources(ResourcePaths::new(res.as_slice(), true), target_dir)?
571 }
572 BundleResources::Map(map) => copy_resources(ResourcePaths::from_map(&map, true), target_dir)?,
573 }
574
575 if target_triple.contains("darwin") {
576 if let Some(frameworks) = &config.bundle.macos.frameworks {
577 if !frameworks.is_empty() {
578 let frameworks_dir = target_dir.parent().unwrap().join("Frameworks");
579 let _ = fs::remove_dir_all(&frameworks_dir);
580 copy_frameworks(&frameworks_dir, frameworks)?;
583
584 println!("cargo:rustc-link-arg=-Wl,-rpath,@executable_path/../Frameworks");
587 }
588 }
589
590 if !is_dev() {
591 if let Some(version) = &config.bundle.macos.minimum_system_version {
592 println!("cargo:rustc-env=MACOSX_DEPLOYMENT_TARGET={version}");
593 }
594 }
595 }
596
597 if target_triple.contains("ios") {
598 println!(
599 "cargo:rustc-env=IPHONEOS_DEPLOYMENT_TARGET={}",
600 config.bundle.ios.minimum_system_version
601 );
602 }
603
604 if target_triple.contains("windows") {
605 use semver::Version;
606 use tauri_winres::{VersionInfo, WindowsResource};
607
608 let window_icon_path = attributes
609 .windows_attributes
610 .window_icon_path
611 .unwrap_or_else(|| {
612 config
613 .bundle
614 .icon
615 .iter()
616 .find(|i| i.ends_with(".ico"))
617 .map(AsRef::as_ref)
618 .unwrap_or("icons/icon.ico")
619 .into()
620 });
621
622 let mut res = WindowsResource::new();
623
624 if let Some(manifest) = attributes.windows_attributes.app_manifest {
625 res.set_manifest(&manifest);
626 }
627
628 for content in attributes.windows_attributes.append_rc_content {
629 res.append_rc_content(&content);
630 }
631
632 if let Some(version_str) = &config.version {
633 if let Ok(v) = Version::parse(version_str) {
634 let version = to_winres_version(&v);
635 res.set_version_info(VersionInfo::FILEVERSION, version);
636 res.set_version_info(VersionInfo::PRODUCTVERSION, version);
637 res.set("FileVersion", version_str);
638 res.set("ProductVersion", version_str);
639 }
640 }
641
642 if let Some(product_name) = &config.product_name {
643 res.set("ProductName", product_name);
644 }
645
646 let company_name = config.bundle.publisher.unwrap_or_else(|| {
647 config
648 .identifier
649 .split('.')
650 .nth(1)
651 .unwrap_or(&config.identifier)
652 .to_string()
653 });
654
655 res.set("CompanyName", &company_name);
656
657 let file_description = config
658 .product_name
659 .or_else(|| manifest.package.as_ref().map(|p| p.name.clone()))
660 .or_else(|| std::env::var("CARGO_PKG_NAME").ok());
661
662 res.set("FileDescription", &file_description.unwrap());
663
664 if let Some(copyright) = &config.bundle.copyright {
665 res.set("LegalCopyright", copyright);
666 }
667
668 if window_icon_path.exists() {
669 res.set_icon_with_id(&window_icon_path.display().to_string(), "32512");
670 } else {
671 return Err(anyhow!(format!(
672 "`{}` not found; required for generating a Windows Resource file during tauri-build",
673 window_icon_path.display()
674 )));
675 }
676
677 res.compile().with_context(|| {
678 format!(
679 "failed to compile `{}` into a Windows Resource file during tauri-build",
680 window_icon_path.display()
681 )
682 })?;
683
684 let target_env = env::var("CARGO_CFG_TARGET_ENV").unwrap();
685 match target_env.as_str() {
686 "gnu" => {
687 let target_arch = match env::var("CARGO_CFG_TARGET_ARCH").unwrap().as_str() {
688 "x86_64" => Some("x64"),
689 "x86" => Some("x86"),
690 "aarch64" => Some("arm64"),
691 arch => None,
692 };
693 if let Some(target_arch) = target_arch {
694 for entry in fs::read_dir(target_dir.join("build"))? {
695 let path = entry?.path();
696 let webview2_loader_path = path
697 .join("out")
698 .join(target_arch)
699 .join("WebView2Loader.dll");
700 if path.to_string_lossy().contains("webview2-com-sys") && webview2_loader_path.exists()
701 {
702 fs::copy(webview2_loader_path, target_dir.join("WebView2Loader.dll"))?;
703 break;
704 }
705 }
706 }
707 }
708 "msvc" if env::var_os("STATIC_VCRUNTIME").is_some_and(|v| v == "true") => {
709 static_vcruntime::build();
710 }
711 _ => (),
712 }
713 }
714
715 #[cfg(feature = "codegen")]
716 if let Some(codegen) = attributes.codegen {
717 codegen.try_build()?;
718 }
719
720 Ok(())
721}
722
723fn to_winres_version(v: &semver::Version) -> u64 {
724 let build = v.build.parse::<u16>().map(u64::from).unwrap_or(0);
725
726 (v.major << 48) | (v.minor << 32) | (v.patch << 16) | build
727}
728
729#[cfg(test)]
730mod tests {
731 use semver::Version;
732
733 #[test]
734 fn version_uses_numeric_build_metadata() {
735 let version = Version::parse("1.2.3+42").unwrap();
736
737 assert_eq!(
738 crate::to_winres_version(&version),
739 (1 << 48) | (2 << 32) | (3 << 16) | 42
740 );
741 }
742
743 #[test]
744 fn version_ignores_non_numeric_composite_build_metadata() {
745 let version = Version::parse("1.2.3+42.sha").unwrap();
746
747 assert_eq!(
748 crate::to_winres_version(&version),
749 (1 << 48) | (2 << 32) | (3 << 16)
750 );
751 }
752
753 #[test]
754 fn version_ignores_non_numeric_build_metadata() {
755 let version = Version::parse("1.2.3+abc").unwrap();
756
757 assert_eq!(
758 crate::to_winres_version(&version),
759 (1 << 48) | (2 << 32) | (3 << 16)
760 );
761 }
762
763 #[test]
764 fn version_ignores_build_metadata_that_does_not_fit_in_u16() {
765 let version = Version::parse("1.2.3+70000").unwrap();
766
767 assert_eq!(
768 crate::to_winres_version(&version),
769 (1 << 48) | (2 << 32) | (3 << 16)
770 );
771 }
772}