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}