Skip to main content

tauri_build/
lib.rs

1// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
2// SPDX-License-Identifier: Apache-2.0
3// SPDX-License-Identifier: MIT
4
5//! This applies the macros at build-time in order to rig some special features needed by `cargo`.
6
7#![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
87/// Copies resources to a path.
88fn 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    // avoid copying the resource if target is the same as source
96    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/// Makes a symbolic link to a directory.
111#[cfg(windows)]
112fn symlink_dir(src: &Path, dst: &Path) -> std::io::Result<()> {
113  std::os::windows::fs::symlink_dir(src, dst)
114}
115
116/// Makes a symbolic link to a file.
117#[cfg(unix)]
118fn symlink_file(src: &Path, dst: &Path) -> std::io::Result<()> {
119  std::os::unix::fs::symlink(src, dst)
120}
121
122/// Makes a symbolic link to a file.
123#[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
150// Copies the framework under `{src_dir}/{framework}.framework` to `{dest_dir}/{framework}.framework`.
151fn 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
162// Copies the macOS application bundle frameworks to the target folder
163fn 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
204// creates a cfg alias if `has_feature` is true.
205// `alias` must be a snake case string.
206fn 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/// Attributes used on Windows.
214#[allow(dead_code)]
215#[derive(Debug)]
216pub struct WindowsAttributes {
217  window_icon_path: Option<PathBuf>,
218  /// A string containing an [application manifest] to be included with the application on Windows.
219  ///
220  /// Defaults to:
221  /// ```text
222  #[doc = include_str!("windows-app-manifest.xml")]
223  /// ```
224  ///
225  /// ## Warning
226  ///
227  /// if you are using tauri's dialog APIs, you need to specify a dependency on Common Control v6 by adding the following to your custom manifest:
228  /// ```text
229  ///  <dependency>
230  ///    <dependentAssembly>
231  ///      <assemblyIdentity
232  ///        type="win32"
233  ///        name="Microsoft.Windows.Common-Controls"
234  ///        version="6.0.0.0"
235  ///        processorArchitecture="*"
236  ///        publicKeyToken="6595b64144ccf1df"
237  ///        language="*"
238  ///      />
239  ///    </dependentAssembly>
240  ///  </dependency>
241  /// ```
242  ///
243  /// [application manifest]: https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests
244  app_manifest: Option<String>,
245  /// A series of strings containing additional .rc content to be appended to the generated resource file on Windows.
246  append_rc_content: Vec<String>,
247}
248
249impl Default for WindowsAttributes {
250  fn default() -> Self {
251    Self::new()
252  }
253}
254
255impl WindowsAttributes {
256  /// Creates the default attribute set.
257  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  /// Creates the default attribute set without the default app manifest.
266  #[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  /// Sets the icon to use on the window. Currently only used on Windows.
276  /// It must be in `ico` format. Defaults to `icons/icon.ico`.
277  #[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  /// Sets the [application manifest] to be included with the application on Windows.
286  ///
287  /// Defaults to:
288  /// ```text
289  #[doc = include_str!("windows-app-manifest.xml")]
290  /// ```
291  ///
292  /// ## Warning
293  ///
294  /// if you are using tauri's dialog APIs, you need to specify a dependency on Common Control v6 by adding the following to your custom manifest:
295  /// ```text
296  ///  <dependency>
297  ///    <dependentAssembly>
298  ///      <assemblyIdentity
299  ///        type="win32"
300  ///        name="Microsoft.Windows.Common-Controls"
301  ///        version="6.0.0.0"
302  ///        processorArchitecture="*"
303  ///        publicKeyToken="6595b64144ccf1df"
304  ///        language="*"
305  ///      />
306  ///    </dependentAssembly>
307  ///  </dependency>
308  /// ```
309  ///
310  /// # Example
311  ///
312  /// The following manifest will brand the exe as requesting administrator privileges.
313  /// Thus, every time it is executed, a Windows UAC dialog will appear.
314  ///
315  /// ```rust,no_run
316  /// let mut windows = tauri_build::WindowsAttributes::new();
317  /// windows = windows.app_manifest(r#"
318  /// <assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
319  ///   <trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
320  ///       <security>
321  ///           <requestedPrivileges>
322  ///               <requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
323  ///           </requestedPrivileges>
324  ///       </security>
325  ///   </trustInfo>
326  /// </assembly>
327  /// "#);
328  /// let attrs =  tauri_build::Attributes::new().windows_attributes(windows);
329  /// tauri_build::try_build(attrs).expect("failed to run build script");
330  /// ```
331  ///
332  /// Note that you can move the manifest contents to a separate file and use `include_str!("manifest.xml")`
333  /// instead of the inline string.
334  ///
335  /// [manifest]: https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests
336  #[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  /// Append additional .rc content to the generated resource file on Windows.
343  /// This can be called multiple times to append multiple contents.
344  #[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/// The attributes used on the build.
352#[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  /// Creates the default attribute set.
365  pub fn new() -> Self {
366    Self::default()
367  }
368
369  /// Sets the icon to use on the window. Currently only used on Windows.
370  #[must_use]
371  pub fn windows_attributes(mut self, windows_attributes: WindowsAttributes) -> Self {
372    self.windows_attributes = windows_attributes;
373    self
374  }
375
376  /// Set the glob pattern to be used to find the capabilities.
377  ///
378  /// **WARNING:** The `removeUnusedCommands` option does not work with a custom capabilities path.
379  ///
380  /// **Note:** You must emit [rerun-if-changed] instructions for your capabilities directory.
381  ///
382  /// [rerun-if-changed]: https://doc.rust-lang.org/cargo/reference/build-scripts.html#rerun-if-changed
383  #[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  /// Adds the given plugin to the list of inlined plugins (a plugin that is part of your application).
390  ///
391  /// See [`InlinedPlugin`] for more information.
392  pub fn plugin(mut self, name: &'static str, plugin: InlinedPlugin) -> Self {
393    self.inlined_plugins.insert(name, plugin);
394    self
395  }
396
397  /// Adds the given list of plugins to the list of inlined plugins (a plugin that is part of your application).
398  ///
399  /// See [`InlinedPlugin`] for more information.
400  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  /// Sets the application manifest for the Access Control List.
409  ///
410  /// See [`AppManifest`] for more information.
411  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
431/// Run all build time helpers for your Tauri Application.
432///
433/// To provide extra configuration, such as [`AppManifest::commands`]
434/// for fine-grained control over command permissions, see [`try_build`].
435/// See [`Attributes`] for the complete list of configuration options.
436///
437/// # Platforms
438///
439/// [`build()`] should be called inside of `build.rs` regardless of the platform, so **DO NOT** use a [conditional compilation]
440/// check that prevents it from running on any of your targets.
441///
442/// Platform specific code is handled by the helpers automatically.
443///
444/// A build script is required in order to activate some cargo environmental variables that are
445/// used when generating code and embedding assets.
446///
447/// # Panics
448///
449/// If any of the build time helpers fail, they will [`std::panic!`] with the related error message.
450/// This is typically desirable when running inside a build script; see [`try_build`] for no panics.
451///
452/// [conditional compilation]: https://web.mit.edu/rust-lang_v1.25/arch/amd64_ubuntu1404/share/doc/rust/html/book/first-edition/conditional-compilation.html
453pub 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/// Same as [`build()`], but takes an extra configuration argument, and does not panic.
468#[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    // Update Android manifest with file associations
514    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  // when running codegen in this build script, we need to access the env var directly
534  env::set_var("TAURI_ENV_TARGET_TRIPLE", &target_triple);
535
536  // TODO: far from ideal, but there's no other way to get the target dir, see <https://github.com/rust-lang/cargo/issues/5457>
537  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 to the root `target` folder (instead of `target/debug` for instance)
581        // because the rpath is set to `@executable_path/../Frameworks`.
582        copy_frameworks(&frameworks_dir, frameworks)?;
583
584        // If we have frameworks, we need to set the @rpath
585        // https://github.com/tauri-apps/tauri/issues/7710
586        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}