Skip to main content

tauri_utils/
platform.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//! Platform helper functions.
6
7use std::{fmt::Display, path::PathBuf};
8
9use serde::{Deserialize, Serialize};
10
11use crate::{Env, PackageInfo, config::BundleType};
12
13mod starting_binary;
14
15/// URI prefix of a Tauri asset.
16///
17/// This is referenced in the Tauri Android library,
18/// which resolves these assets to a file descriptor.
19#[cfg(target_os = "android")]
20pub const ANDROID_ASSET_PROTOCOL_URI_PREFIX: &str = "asset://localhost/";
21
22/// Resource id of the application icon that `tauri-build` embeds into Windows executables
23/// and that `tauri::image::Image::from_app_icon_resource` reads back.
24///
25/// `32512` has no special meaning here: it was picked because we misunderstood
26/// `IDI_APPLICATION` (`MAKEINTRESOURCE(32512)`) to be the id an application icon must use,
27/// which is not the case. See <https://devblogs.microsoft.com/oldnewthing/20250423-00/?p=111106>.
28pub const WINDOWS_APP_ICON_RESOURCE_ID: u16 = 32512;
29
30/// Platform target.
31#[derive(PartialEq, Eq, Copy, Debug, Clone, Serialize, Deserialize)]
32#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
33#[serde(rename_all = "camelCase")]
34#[non_exhaustive]
35pub enum Target {
36  /// MacOS.
37  #[serde(rename = "macOS")]
38  MacOS,
39  /// Windows.
40  Windows,
41  /// Linux.
42  Linux,
43  /// Android.
44  Android,
45  /// iOS.
46  #[serde(rename = "iOS")]
47  Ios,
48}
49
50impl Display for Target {
51  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52    write!(
53      f,
54      "{}",
55      match self {
56        Self::MacOS => "macOS",
57        Self::Windows => "windows",
58        Self::Linux => "linux",
59        Self::Android => "android",
60        Self::Ios => "iOS",
61      }
62    )
63  }
64}
65
66impl Target {
67  /// Parses the target from the given target triple.
68  pub fn from_triple(target: &str) -> Self {
69    if target.contains("darwin") {
70      Self::MacOS
71    } else if target.contains("windows") {
72      Self::Windows
73    } else if target.contains("android") {
74      Self::Android
75    } else if target.contains("ios") {
76      Self::Ios
77    } else {
78      Self::Linux
79    }
80  }
81
82  /// Gets the current build target.
83  pub fn current() -> Self {
84    if cfg!(target_os = "macos") {
85      Self::MacOS
86    } else if cfg!(target_os = "windows") {
87      Self::Windows
88    } else if cfg!(target_os = "ios") {
89      Self::Ios
90    } else if cfg!(target_os = "android") {
91      Self::Android
92    } else {
93      Self::Linux
94    }
95  }
96
97  /// Whether the target is mobile or not.
98  pub fn is_mobile(&self) -> bool {
99    matches!(self, Target::Android | Target::Ios)
100  }
101
102  /// Whether the target is desktop or not.
103  pub fn is_desktop(&self) -> bool {
104    !self.is_mobile()
105  }
106}
107
108/// Retrieves the currently running binary's path, taking into account security considerations.
109///
110/// The path is cached as soon as possible (before even `main` runs) and that value is returned
111/// repeatedly instead of fetching the path every time. It is possible for the path to not be found,
112/// or explicitly disabled (see following macOS specific behavior).
113///
114/// # Platform-specific behavior
115///
116/// On `macOS`, this function will return an error if the original path contained any symlinks
117/// due to less protection on macOS regarding symlinks. This behavior can be disabled by setting the
118/// `process-relaunch-dangerous-allow-symlink-macos` feature, although it is *highly discouraged*.
119///
120/// # Security
121///
122/// If the above platform-specific behavior does **not** take place, this function uses the
123/// following resolution.
124///
125/// We canonicalize the path we received from [`std::env::current_exe`] to resolve any soft links.
126/// This avoids the usual issue of needing the file to exist at the passed path because a valid
127/// current executable result for our purpose should always exist. Notably,
128/// [`std::env::current_exe`] also has a security section that goes over a theoretical attack using
129/// hard links. Let's cover some specific topics that relate to different ways an attacker might
130/// try to trick this function into returning the wrong binary path.
131///
132/// ## Symlinks ("Soft Links")
133///
134/// [`std::path::Path::canonicalize`] is used to resolve symbolic links to the original path,
135/// including nested symbolic links (`link2 -> link1 -> bin`). On macOS, any results that include
136/// a symlink are rejected by default due to lesser symlink protections. This can be disabled,
137/// **although discouraged**, with the `process-relaunch-dangerous-allow-symlink-macos` feature.
138///
139/// ## Hard Links
140///
141/// A [Hard Link] is a named entry that points to a file in the file system.
142/// On most systems, this is what you would think of as a "file". The term is
143/// used on filesystems that allow multiple entries to point to the same file.
144/// The linked [Hard Link] Wikipedia page provides a decent overview.
145///
146/// In short, unless the attacker was able to create the link with elevated
147/// permissions, it should generally not be possible for them to hard link
148/// to a file they do not have permissions to - with exception to possible
149/// operating system exploits.
150///
151/// There are also some platform-specific information about this below.
152///
153/// ### Windows
154///
155/// Windows requires a permission to be set for the user to create a symlink
156/// or a hard link, regardless of ownership status of the target. Elevated
157/// permissions users have the ability to create them.
158///
159/// ### macOS
160///
161/// macOS allows for the creation of symlinks and hard links to any file.
162/// Accessing through those links will fail if the user who owns the links
163/// does not have the proper permissions on the original file.
164///
165/// ### Linux
166///
167/// Linux allows for the creation of symlinks to any file. Accessing the
168/// symlink will fail if the user who owns the symlink does not have the
169/// proper permissions on the original file.
170///
171/// Linux additionally provides a kernel hardening feature since version
172/// 3.6 (30 September 2012). Most distributions since then have enabled
173/// the protection (setting `fs.protected_hardlinks = 1`) by default, which
174/// means that a vast majority of desktop Linux users should have it enabled.
175/// **The feature prevents the creation of hardlinks that the user does not own
176/// or have read/write access to.** [See the patch that enabled this].
177///
178/// [Hard Link]: https://en.wikipedia.org/wiki/Hard_link
179/// [See the patch that enabled this]: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=800179c9b8a1e796e441674776d11cd4c05d61d7
180pub fn current_exe() -> std::io::Result<PathBuf> {
181  self::starting_binary::STARTING_BINARY.cloned()
182}
183
184/// Try to determine the current target triple.
185///
186/// Returns a target triple (e.g. `x86_64-unknown-linux-gnu` or `i686-pc-windows-msvc`) or an
187/// `Error::Config` if the current config cannot be determined or is not some combination of the
188/// following values:
189/// `linux, mac, windows` -- `i686, x86, armv7` -- `gnu, musl, msvc`
190///
191/// * Errors:
192///     * Unexpected system config
193pub fn target_triple() -> crate::Result<String> {
194  let arch = if cfg!(target_arch = "x86") {
195    "i686"
196  } else if cfg!(target_arch = "x86_64") {
197    "x86_64"
198  } else if cfg!(target_arch = "arm") {
199    "armv7"
200  } else if cfg!(target_arch = "aarch64") {
201    "aarch64"
202  } else if cfg!(target_arch = "riscv64") {
203    "riscv64"
204  } else {
205    return Err(crate::Error::Architecture);
206  };
207
208  let os = if cfg!(target_os = "linux") {
209    "unknown-linux"
210  } else if cfg!(target_os = "macos") {
211    "apple-darwin"
212  } else if cfg!(target_os = "windows") {
213    "pc-windows"
214  } else if cfg!(target_os = "freebsd") {
215    "unknown-freebsd"
216  } else {
217    return Err(crate::Error::Os);
218  };
219
220  let os = if cfg!(target_os = "macos") || cfg!(target_os = "freebsd") {
221    String::from(os)
222  } else {
223    let env = if cfg!(target_env = "gnu") {
224      "gnu"
225    } else if cfg!(target_env = "musl") {
226      "musl"
227    } else if cfg!(target_env = "msvc") {
228      "msvc"
229    } else {
230      return Err(crate::Error::Environment);
231    };
232
233    format!("{os}-{env}")
234  };
235
236  Ok(format!("{arch}-{os}"))
237}
238
239#[cfg(all(not(test), not(target_os = "android")))]
240fn is_cargo_output_directory(path: &std::path::Path) -> bool {
241  path.join(".cargo-lock").exists()
242}
243
244#[cfg(test)]
245const CARGO_OUTPUT_DIRECTORIES: &[&str] = &["debug", "release", "custom-profile"];
246
247#[cfg(test)]
248fn is_cargo_output_directory(path: &std::path::Path) -> bool {
249  let Some(last_component) = path.components().next_back() else {
250    return false;
251  };
252  CARGO_OUTPUT_DIRECTORIES
253    .iter()
254    .any(|dirname| &last_component.as_os_str() == dirname)
255}
256
257/// Computes the resource directory of the current environment.
258///
259/// ## Platform-specific
260///
261/// - **Windows:** Resolves to the directory that contains the main executable.
262/// - **Linux:** When running in an AppImage, the `APPDIR` variable will be set to
263///   the mounted location of the app, and the resource dir will be `${APPDIR}/usr/lib/${exe_name}`.
264///   If not running in an AppImage, the path is `/usr/lib/${exe_name}`.
265///   When running the app from `src-tauri/target/(debug|release)/`, the path is `${exe_dir}/../lib/${exe_name}`.
266/// - **macOS:** Resolves to `${exe_dir}/../Resources` (inside .app).
267/// - **iOS:** Resolves to `${exe_dir}/assets`.
268/// - **Android:** Currently the resources are stored in the APK as assets so it's not a normal file system path,
269///   we return a special URI prefix `asset://localhost/` here that can be used with the [file system plugin](https://tauri.app/plugin/file-system/),
270///   with that, you can read the files through [`FsExt::fs`](https://docs.rs/tauri-plugin-fs/latest/tauri_plugin_fs/trait.FsExt.html#tymethod.fs)
271///   like this: `app.fs().read_to_string(app.path().resource_dir().unwrap().join("resource"));`
272pub fn resource_dir(package_info: &PackageInfo, env: &Env) -> crate::Result<PathBuf> {
273  #[cfg(target_os = "android")]
274  return resource_dir_android(package_info, env);
275  #[cfg(not(target_os = "android"))]
276  {
277    let exe = current_exe()?;
278    resource_dir_from(exe, package_info, env)
279  }
280}
281
282#[cfg(target_os = "android")]
283fn resource_dir_android(_package_info: &PackageInfo, _env: &Env) -> crate::Result<PathBuf> {
284  Ok(PathBuf::from(ANDROID_ASSET_PROTOCOL_URI_PREFIX))
285}
286
287#[cfg(not(target_os = "android"))]
288#[allow(unused_variables)]
289fn resource_dir_from<P: AsRef<std::path::Path>>(
290  exe: P,
291  package_info: &PackageInfo,
292  env: &Env,
293) -> crate::Result<PathBuf> {
294  let exe_dir = exe.as_ref().parent().expect("failed to get exe directory");
295  let curr_dir = exe_dir.display().to_string();
296
297  let parts: Vec<&str> = curr_dir.split(std::path::MAIN_SEPARATOR).collect();
298  let len = parts.len();
299
300  // Check if running from the Cargo output directory, which means it's an executable in a development machine
301  // We check if the binary is inside a `target` folder which can be either `target/$profile` or `target/$triple/$profile`
302  // and see if there's a .cargo-lock file along the executable
303  // This ensures the check is safer so it doesn't affect apps in production
304  // Windows also includes the resources in the executable folder so we check that too
305  if cfg!(target_os = "windows")
306    || ((len >= 2 && parts[len - 2] == "target") || (len >= 3 && parts[len - 3] == "target"))
307      && is_cargo_output_directory(exe_dir)
308  {
309    return Ok(exe_dir.to_path_buf());
310  }
311
312  #[allow(unused_mut, unused_assignments)]
313  let mut res = Err(crate::Error::UnsupportedPlatform);
314
315  #[cfg(target_os = "linux")]
316  {
317    // (canonicalize checks for existence, so there's no need for an extra check)
318    res = if let Ok(bundle_dir) = exe_dir
319      .join(format!("../lib/{}", package_info.name))
320      .canonicalize()
321    {
322      Ok(bundle_dir)
323    } else if let Some(appdir) = &env.appdir {
324      let appdir: &std::path::Path = appdir.as_ref();
325      Ok(PathBuf::from(format!(
326        "{}/usr/lib/{}",
327        appdir.display(),
328        package_info.name
329      )))
330    } else {
331      // running bundle
332      Ok(PathBuf::from(format!("/usr/lib/{}", package_info.name)))
333    };
334  }
335
336  #[cfg(target_os = "macos")]
337  {
338    res = exe_dir
339      .join("../Resources")
340      .canonicalize()
341      .map_err(Into::into);
342  }
343
344  #[cfg(target_os = "ios")]
345  {
346    res = exe_dir.join("assets").canonicalize().map_err(Into::into);
347  }
348
349  res
350}
351
352// Variable holding the type of bundle the executable is stored in. This is modified by binary
353// patching during build
354#[used]
355// Marked as `mut` because it could get optimized away without it,
356// see https://github.com/tauri-apps/tauri/pull/13812
357static mut __TAURI_BUNDLE_TYPE: &str = "__TAURI_BUNDLE_TYPE_VAR_UNK";
358
359/// Get the type of the bundle current binary is packaged in.
360/// If the bundle type is unknown, it returns [`Option::None`].
361pub fn bundle_type() -> Option<BundleType> {
362  unsafe {
363    match __TAURI_BUNDLE_TYPE {
364      "__TAURI_BUNDLE_TYPE_VAR_DEB" => Some(BundleType::Deb),
365      "__TAURI_BUNDLE_TYPE_VAR_RPM" => Some(BundleType::Rpm),
366      "__TAURI_BUNDLE_TYPE_VAR_APP" => Some(BundleType::AppImage),
367      "__TAURI_BUNDLE_TYPE_VAR_MSI" => Some(BundleType::Msi),
368      "__TAURI_BUNDLE_TYPE_VAR_NSS" => Some(BundleType::Nsis),
369      _ => {
370        if cfg!(target_os = "macos") {
371          Some(BundleType::App)
372        } else {
373          None
374        }
375      }
376    }
377  }
378}
379
380#[cfg(any(feature = "build", feature = "build-2"))]
381mod build {
382  use proc_macro2::TokenStream;
383  use quote::{ToTokens, TokenStreamExt, quote};
384
385  use super::*;
386
387  impl ToTokens for Target {
388    fn to_tokens(&self, tokens: &mut TokenStream) {
389      let prefix = quote! { ::tauri::utils::platform::Target };
390
391      tokens.append_all(match self {
392        Self::MacOS => quote! { #prefix::MacOS },
393        Self::Linux => quote! { #prefix::Linux },
394        Self::Windows => quote! { #prefix::Windows },
395        Self::Android => quote! { #prefix::Android },
396        Self::Ios => quote! { #prefix::Ios },
397      });
398    }
399  }
400}
401
402#[cfg(test)]
403mod tests {
404  use std::path::PathBuf;
405
406  use crate::{Env, PackageInfo};
407
408  #[test]
409  #[cfg(not(target_os = "android"))]
410  fn resolve_resource_dir() {
411    let package_info = PackageInfo {
412      name: "MyApp".into(),
413      version: "1.0.0".parse().unwrap(),
414      authors: "",
415      description: "",
416      crate_name: "my-app",
417    };
418    let env = Env::default();
419
420    let path = PathBuf::from("/path/to/target/aarch64-apple-darwin/debug/app");
421    let resource_dir = super::resource_dir_from(&path, &package_info, &env).unwrap();
422    assert_eq!(resource_dir, path.parent().unwrap());
423
424    let path = PathBuf::from("/path/to/target/custom-profile/app");
425    let resource_dir = super::resource_dir_from(&path, &package_info, &env).unwrap();
426    assert_eq!(resource_dir, path.parent().unwrap());
427
428    let path = PathBuf::from("/path/to/target/release/app");
429    let resource_dir = super::resource_dir_from(&path, &package_info, &env).unwrap();
430    assert_eq!(resource_dir, path.parent().unwrap());
431
432    let path = PathBuf::from("/path/to/target/unknown-profile/app");
433    #[allow(clippy::needless_borrows_for_generic_args)]
434    let resource_dir = super::resource_dir_from(&path, &package_info, &env);
435    #[cfg(target_os = "macos")]
436    assert!(resource_dir.is_err());
437    #[cfg(target_os = "linux")]
438    assert_eq!(resource_dir.unwrap(), PathBuf::from("/usr/lib/MyApp"));
439    #[cfg(windows)]
440    assert_eq!(resource_dir.unwrap(), path.parent().unwrap());
441  }
442}