From f18b469ed2bfd6a530ccce65a68784a47328646b Mon Sep 17 00:00:00 2001 From: Tony Date: Sun, 7 Dec 2025 17:14:01 +0800 Subject: [PATCH 1/6] feat(core): allow config app dir override --- .changes/app-directories-override.md | 5 +++ crates/tauri-cli/config.schema.json | 7 ++++ .../schemas/config.schema.json | 7 ++++ crates/tauri-utils/src/config.rs | 25 ++++++++++++ crates/tauri/src/manager/webview.rs | 6 +-- crates/tauri/src/path/desktop.rs | 38 +++++++++++++++++++ 6 files changed, 83 insertions(+), 5 deletions(-) create mode 100644 .changes/app-directories-override.md diff --git a/.changes/app-directories-override.md b/.changes/app-directories-override.md new file mode 100644 index 000000000000..31b0cfe8d525 --- /dev/null +++ b/.changes/app-directories-override.md @@ -0,0 +1,5 @@ +--- +"tauri": minor:feat +--- + +Introduce a new config `appDirectoriesOverride` that changes what `app_*_dir` APIs return, useful for putting all your data besides the executable for testing or as portable apps diff --git a/crates/tauri-cli/config.schema.json b/crates/tauri-cli/config.schema.json index 1d19c20eaede..2e9361053d10 100644 --- a/crates/tauri-cli/config.schema.json +++ b/crates/tauri-cli/config.schema.json @@ -217,6 +217,13 @@ "description": "If set to true \"identifier\" will be set as GTK app ID (on systems that use GTK).", "default": false, "type": "boolean" + }, + "appDirectoriesOverride": { + "description": "Override the path returned from `app_*_dir` APIs,\n this is useful for making portable apps that stores all the data inside a single place\n\n Note: relative paths are resolved based on the app's executable path\n\n ## Example:\n\n To put all the data besides your current executable:\n\n ```json\n {\n \"app\": {\n \"appDirectoriesOverride\": \"./\"\n }\n }\n ```\n\n `app.path().app_local_data_dir()` should now return `${current_exe_dir}/`\n\n ## Platform-specific:\n\n - **Android**: Unsupported.", + "type": [ + "string", + "null" + ] } }, "additionalProperties": false diff --git a/crates/tauri-schema-generator/schemas/config.schema.json b/crates/tauri-schema-generator/schemas/config.schema.json index 1d19c20eaede..2e9361053d10 100644 --- a/crates/tauri-schema-generator/schemas/config.schema.json +++ b/crates/tauri-schema-generator/schemas/config.schema.json @@ -217,6 +217,13 @@ "description": "If set to true \"identifier\" will be set as GTK app ID (on systems that use GTK).", "default": false, "type": "boolean" + }, + "appDirectoriesOverride": { + "description": "Override the path returned from `app_*_dir` APIs,\n this is useful for making portable apps that stores all the data inside a single place\n\n Note: relative paths are resolved based on the app's executable path\n\n ## Example:\n\n To put all the data besides your current executable:\n\n ```json\n {\n \"app\": {\n \"appDirectoriesOverride\": \"./\"\n }\n }\n ```\n\n `app.path().app_local_data_dir()` should now return `${current_exe_dir}/`\n\n ## Platform-specific:\n\n - **Android**: Unsupported.", + "type": [ + "string", + "null" + ] } }, "additionalProperties": false diff --git a/crates/tauri-utils/src/config.rs b/crates/tauri-utils/src/config.rs index 37c77abff8ac..f0892498efde 100644 --- a/crates/tauri-utils/src/config.rs +++ b/crates/tauri-utils/src/config.rs @@ -2763,6 +2763,30 @@ pub struct AppConfig { /// If set to true "identifier" will be set as GTK app ID (on systems that use GTK). #[serde(rename = "enableGTKAppId", alias = "enable-gtk-app-id", default)] pub enable_gtk_app_id: bool, + /// Override the path returned from `app_*_dir` APIs, + /// this is useful for making portable apps that stores all the data inside a single place + /// + /// Note: relative paths are resolved based on the app's executable path + /// + /// ## Example: + /// + /// To put all the data besides your current executable: + /// + /// ```json + /// { + /// "app": { + /// "appDirectoriesOverride": "./" + /// } + /// } + /// ``` + /// + /// `app.path().app_local_data_dir()` should now return `${current_exe_dir}/` + /// + /// ## Platform-specific: + /// + /// - **Android**: Unsupported. + #[serde(alias = "app-directories-override")] + pub app_directories_override: Option, } impl AppConfig { @@ -4119,6 +4143,7 @@ mod test { macos_private_api: false, with_global_tauri: false, enable_gtk_app_id: false, + app_directories_override: None, }; // create a build config diff --git a/crates/tauri/src/manager/webview.rs b/crates/tauri/src/manager/webview.rs index 623b31d24724..9dafd1bfd362 100644 --- a/crates/tauri/src/manager/webview.rs +++ b/crates/tauri/src/manager/webview.rs @@ -506,11 +506,7 @@ impl WebviewManager { // but we do respect user-specification #[cfg(any(target_os = "linux", target_os = "windows"))] if pending.webview_attributes.data_directory.is_none() { - let local_app_data = manager.path().resolve( - &app_manager.config.identifier, - crate::path::BaseDirectory::LocalData, - ); - if let Ok(user_data_dir) = local_app_data { + if let Ok(user_data_dir) = manager.path().app_local_data_dir() { pending.webview_attributes.data_directory = Some(user_data_dir); } } diff --git a/crates/tauri/src/path/desktop.rs b/crates/tauri/src/path/desktop.rs index 96ea0bd7e20c..016fed093e65 100644 --- a/crates/tauri/src/path/desktop.rs +++ b/crates/tauri/src/path/desktop.rs @@ -236,6 +236,10 @@ impl PathResolver { /// /// Resolves to [`config_dir`](Self::config_dir)`/${bundle_identifier}`. pub fn app_config_dir(&self) -> Result { + if let Some(app_directories_override) = self.app_directories_override() { + return app_directories_override; + } + dirs::config_dir() .ok_or(Error::UnknownPath) .map(|dir| dir.join(&self.0.config().identifier)) @@ -245,6 +249,10 @@ impl PathResolver { /// /// Resolves to [`data_dir`](Self::data_dir)`/${bundle_identifier}`. pub fn app_data_dir(&self) -> Result { + if let Some(app_directories_override) = self.app_directories_override() { + return app_directories_override; + } + dirs::data_dir() .ok_or(Error::UnknownPath) .map(|dir| dir.join(&self.0.config().identifier)) @@ -254,6 +262,10 @@ impl PathResolver { /// /// Resolves to [`local_data_dir`](Self::local_data_dir)`/${bundle_identifier}`. pub fn app_local_data_dir(&self) -> Result { + if let Some(app_directories_override) = self.app_directories_override() { + return app_directories_override; + } + dirs::data_local_dir() .ok_or(Error::UnknownPath) .map(|dir| dir.join(&self.0.config().identifier)) @@ -263,6 +275,10 @@ impl PathResolver { /// /// Resolves to [`cache_dir`](Self::cache_dir)`/${bundle_identifier}`. pub fn app_cache_dir(&self) -> Result { + if let Some(app_directories_override) = self.app_directories_override() { + return Ok(app_directories_override?.join("caches")); + } + dirs::cache_dir() .ok_or(Error::UnknownPath) .map(|dir| dir.join(&self.0.config().identifier)) @@ -276,6 +292,10 @@ impl PathResolver { /// - **macOS:** Resolves to [`home_dir`](Self::home_dir)`/Library/Logs/${bundle_identifier}` /// - **Windows:** Resolves to [`local_data_dir`](Self::local_data_dir)`/${bundle_identifier}/logs`. pub fn app_log_dir(&self) -> Result { + if let Some(app_directories_override) = self.app_directories_override() { + return Ok(app_directories_override?.join("logs")); + } + #[cfg(target_os = "macos")] let path = dirs::home_dir() .ok_or(Error::UnknownPath) @@ -293,4 +313,22 @@ impl PathResolver { pub fn temp_dir(&self) -> Result { Ok(std::env::temp_dir()) } + + /// Resolves the `app_directories_override` based on `current_exe` if it exists + fn app_directories_override(&self) -> Option> { + self + .0 + .config() + .app + .app_directories_override + .as_ref() + .map(|app_directories_override| { + Ok( + tauri_utils::platform::current_exe()? + .parent() + .expect("current executable doesn't have a parent directory") + .join(app_directories_override), + ) + }) + } } From 631b50d7c245b77dd0ee60ac270cbd72ae9c0b11 Mon Sep 17 00:00:00 2001 From: Tony Date: Sun, 7 Dec 2025 17:20:09 +0800 Subject: [PATCH 2/6] Fix mock context --- crates/tauri/src/test/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/tauri/src/test/mod.rs b/crates/tauri/src/test/mod.rs index 6c1311bc3eb7..0125448dd01a 100644 --- a/crates/tauri/src/test/mod.rs +++ b/crates/tauri/src/test/mod.rs @@ -121,6 +121,7 @@ pub fn mock_context>(assets: A) -> crate::Context { tray_icon: None, macos_private_api: false, enable_gtk_app_id: false, + app_directories_override: None, }, bundle: Default::default(), build: Default::default(), From e51f3a61887c51d69e57cfb2f992d1f0100f835a Mon Sep 17 00:00:00 2001 From: Tony Date: Sun, 7 Dec 2025 17:31:52 +0800 Subject: [PATCH 3/6] Fix ToTokens impl --- crates/tauri-utils/src/config.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/crates/tauri-utils/src/config.rs b/crates/tauri-utils/src/config.rs index f0892498efde..86c16d69e1e4 100644 --- a/crates/tauri-utils/src/config.rs +++ b/crates/tauri-utils/src/config.rs @@ -4053,6 +4053,13 @@ mod build { let macos_private_api = self.macos_private_api; let with_global_tauri = self.with_global_tauri; let enable_gtk_app_id = self.enable_gtk_app_id; + let app_directories_override = opt_lit( + self + .app_directories_override + .as_ref() + .map(path_buf_lit) + .as_ref(), + ); literal_struct!( tokens, @@ -4062,7 +4069,8 @@ mod build { tray_icon, macos_private_api, with_global_tauri, - enable_gtk_app_id + enable_gtk_app_id, + app_directories_override ); } } From b4040fb91593849a230325a70a76ef262c0f7956 Mon Sep 17 00:00:00 2001 From: Tony Date: Sun, 7 Dec 2025 19:09:19 +0800 Subject: [PATCH 4/6] Use `current_binary` instead of `current_exe` --- crates/tauri/src/path/desktop.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/tauri/src/path/desktop.rs b/crates/tauri/src/path/desktop.rs index 016fed093e65..e65ee53a9726 100644 --- a/crates/tauri/src/path/desktop.rs +++ b/crates/tauri/src/path/desktop.rs @@ -324,7 +324,7 @@ impl PathResolver { .as_ref() .map(|app_directories_override| { Ok( - tauri_utils::platform::current_exe()? + crate::process::current_binary(&self.0.env())? .parent() .expect("current executable doesn't have a parent directory") .join(app_directories_override), From 6c4a886b90954c21be5a2ee738f2533773e0c71f Mon Sep 17 00:00:00 2001 From: Tony Date: Sun, 7 Dec 2025 20:11:03 +0800 Subject: [PATCH 5/6] Allow resolving variables like `$DESKTOP` --- crates/tauri-cli/config.schema.json | 2 +- .../schemas/config.schema.json | 2 +- crates/tauri-utils/src/config.rs | 6 +- crates/tauri/src/path/desktop.rs | 57 ++++++++++++------- 4 files changed, 44 insertions(+), 23 deletions(-) diff --git a/crates/tauri-cli/config.schema.json b/crates/tauri-cli/config.schema.json index 2e9361053d10..d8200ab9c9de 100644 --- a/crates/tauri-cli/config.schema.json +++ b/crates/tauri-cli/config.schema.json @@ -219,7 +219,7 @@ "type": "boolean" }, "appDirectoriesOverride": { - "description": "Override the path returned from `app_*_dir` APIs,\n this is useful for making portable apps that stores all the data inside a single place\n\n Note: relative paths are resolved based on the app's executable path\n\n ## Example:\n\n To put all the data besides your current executable:\n\n ```json\n {\n \"app\": {\n \"appDirectoriesOverride\": \"./\"\n }\n }\n ```\n\n `app.path().app_local_data_dir()` should now return `${current_exe_dir}/`\n\n ## Platform-specific:\n\n - **Android**: Unsupported.", + "description": "Override the path returned from `app_*_dir` APIs,\n this is useful for making portable apps that stores all the data inside a single place\n\n Note:\n - Relative paths are resolved based on the app's executable path,\n - The path can start with a variable that resolves to a system base directory.\n The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DOCUMENT`, `$DOWNLOAD`, `$PICTURE`,\n `$PUBLIC`, `$VIDEO`, `$RESOURCE`, `$TEMP`, `$HOME`, `$DESKTOP`, `$EXE`, `$FONT`, `$RUNTIME`, `$TEMPLATE`\n\n ## Example:\n\n To put all the data besides your current executable:\n\n ```json\n {\n \"app\": {\n \"appDirectoriesOverride\": \"./\"\n }\n }\n ```\n\n `app.path().app_local_data_dir()` should now return `${current_exe_dir}/`\n\n ## Platform-specific:\n\n - **Android**: Unsupported.", "type": [ "string", "null" diff --git a/crates/tauri-schema-generator/schemas/config.schema.json b/crates/tauri-schema-generator/schemas/config.schema.json index 2e9361053d10..d8200ab9c9de 100644 --- a/crates/tauri-schema-generator/schemas/config.schema.json +++ b/crates/tauri-schema-generator/schemas/config.schema.json @@ -219,7 +219,7 @@ "type": "boolean" }, "appDirectoriesOverride": { - "description": "Override the path returned from `app_*_dir` APIs,\n this is useful for making portable apps that stores all the data inside a single place\n\n Note: relative paths are resolved based on the app's executable path\n\n ## Example:\n\n To put all the data besides your current executable:\n\n ```json\n {\n \"app\": {\n \"appDirectoriesOverride\": \"./\"\n }\n }\n ```\n\n `app.path().app_local_data_dir()` should now return `${current_exe_dir}/`\n\n ## Platform-specific:\n\n - **Android**: Unsupported.", + "description": "Override the path returned from `app_*_dir` APIs,\n this is useful for making portable apps that stores all the data inside a single place\n\n Note:\n - Relative paths are resolved based on the app's executable path,\n - The path can start with a variable that resolves to a system base directory.\n The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DOCUMENT`, `$DOWNLOAD`, `$PICTURE`,\n `$PUBLIC`, `$VIDEO`, `$RESOURCE`, `$TEMP`, `$HOME`, `$DESKTOP`, `$EXE`, `$FONT`, `$RUNTIME`, `$TEMPLATE`\n\n ## Example:\n\n To put all the data besides your current executable:\n\n ```json\n {\n \"app\": {\n \"appDirectoriesOverride\": \"./\"\n }\n }\n ```\n\n `app.path().app_local_data_dir()` should now return `${current_exe_dir}/`\n\n ## Platform-specific:\n\n - **Android**: Unsupported.", "type": [ "string", "null" diff --git a/crates/tauri-utils/src/config.rs b/crates/tauri-utils/src/config.rs index 86c16d69e1e4..1c544a491b78 100644 --- a/crates/tauri-utils/src/config.rs +++ b/crates/tauri-utils/src/config.rs @@ -2766,7 +2766,11 @@ pub struct AppConfig { /// Override the path returned from `app_*_dir` APIs, /// this is useful for making portable apps that stores all the data inside a single place /// - /// Note: relative paths are resolved based on the app's executable path + /// Note: + /// - Relative paths are resolved based on the app's executable path, + /// - The path can start with a variable that resolves to a system base directory. + /// The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DOCUMENT`, `$DOWNLOAD`, `$PICTURE`, + /// `$PUBLIC`, `$VIDEO`, `$RESOURCE`, `$TEMP`, `$HOME`, `$DESKTOP`, `$EXE`, `$FONT`, `$RUNTIME`, `$TEMPLATE` /// /// ## Example: /// diff --git a/crates/tauri/src/path/desktop.rs b/crates/tauri/src/path/desktop.rs index e65ee53a9726..f5a912bea8e3 100644 --- a/crates/tauri/src/path/desktop.rs +++ b/crates/tauri/src/path/desktop.rs @@ -3,7 +3,7 @@ // SPDX-License-Identifier: MIT use super::{Error, Result}; -use crate::{AppHandle, Manager, Runtime}; +use crate::{path::BaseDirectory, AppHandle, Manager, Runtime}; use std::path::{Path, PathBuf}; /// The path resolver is a helper class for general and application-specific path APIs. @@ -236,7 +236,7 @@ impl PathResolver { /// /// Resolves to [`config_dir`](Self::config_dir)`/${bundle_identifier}`. pub fn app_config_dir(&self) -> Result { - if let Some(app_directories_override) = self.app_directories_override() { + if let Some(app_directories_override) = self.resolve_app_directories_override() { return app_directories_override; } @@ -249,7 +249,7 @@ impl PathResolver { /// /// Resolves to [`data_dir`](Self::data_dir)`/${bundle_identifier}`. pub fn app_data_dir(&self) -> Result { - if let Some(app_directories_override) = self.app_directories_override() { + if let Some(app_directories_override) = self.resolve_app_directories_override() { return app_directories_override; } @@ -262,7 +262,7 @@ impl PathResolver { /// /// Resolves to [`local_data_dir`](Self::local_data_dir)`/${bundle_identifier}`. pub fn app_local_data_dir(&self) -> Result { - if let Some(app_directories_override) = self.app_directories_override() { + if let Some(app_directories_override) = self.resolve_app_directories_override() { return app_directories_override; } @@ -275,7 +275,7 @@ impl PathResolver { /// /// Resolves to [`cache_dir`](Self::cache_dir)`/${bundle_identifier}`. pub fn app_cache_dir(&self) -> Result { - if let Some(app_directories_override) = self.app_directories_override() { + if let Some(app_directories_override) = self.resolve_app_directories_override() { return Ok(app_directories_override?.join("caches")); } @@ -292,7 +292,7 @@ impl PathResolver { /// - **macOS:** Resolves to [`home_dir`](Self::home_dir)`/Library/Logs/${bundle_identifier}` /// - **Windows:** Resolves to [`local_data_dir`](Self::local_data_dir)`/${bundle_identifier}/logs`. pub fn app_log_dir(&self) -> Result { - if let Some(app_directories_override) = self.app_directories_override() { + if let Some(app_directories_override) = self.resolve_app_directories_override() { return Ok(app_directories_override?.join("logs")); } @@ -315,20 +315,37 @@ impl PathResolver { } /// Resolves the `app_directories_override` based on `current_exe` if it exists - fn app_directories_override(&self) -> Option> { - self - .0 - .config() - .app - .app_directories_override - .as_ref() - .map(|app_directories_override| { - Ok( - crate::process::current_binary(&self.0.env())? - .parent() - .expect("current executable doesn't have a parent directory") - .join(app_directories_override), - ) + fn resolve_app_directories_override(&self) -> Option> { + let app_directories_override = self.0.config().app.app_directories_override.as_ref(); + app_directories_override.map(|app_directories_override| { + if let Some(base_directory) = app_directories_override + .components() + .next() + .and_then(|str| BaseDirectory::from_variable(&str.as_os_str().to_str()?)) + { + return if matches!( + base_directory, + BaseDirectory::AppCache + | BaseDirectory::AppConfig + | BaseDirectory::AppData + | BaseDirectory::AppLocalData + | BaseDirectory::AppLog + ) { + // TODO: Maybe add a new variant? + Err(crate::Error::UnknownPath) + } else { + self.parse(app_directories_override) + }; + } + + Ok(if app_directories_override.is_absolute() { + app_directories_override.clone() + } else { + crate::process::current_binary(&self.0.env())? + .parent() + .ok_or(crate::Error::NoParent)? + .join(app_directories_override) }) + }) } } From a08aa1e45ab78a81e2c2229778bc0e7c2bc5a5aa Mon Sep 17 00:00:00 2001 From: Tony Date: Sun, 7 Dec 2025 20:14:34 +0800 Subject: [PATCH 6/6] Clippy --- crates/tauri/src/path/desktop.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/tauri/src/path/desktop.rs b/crates/tauri/src/path/desktop.rs index f5a912bea8e3..caf797613bc2 100644 --- a/crates/tauri/src/path/desktop.rs +++ b/crates/tauri/src/path/desktop.rs @@ -321,7 +321,7 @@ impl PathResolver { if let Some(base_directory) = app_directories_override .components() .next() - .and_then(|str| BaseDirectory::from_variable(&str.as_os_str().to_str()?)) + .and_then(|str| BaseDirectory::from_variable(str.as_os_str().to_str()?)) { return if matches!( base_directory,