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..d8200ab9c9de 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:\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" + ] } }, "additionalProperties": false diff --git a/crates/tauri-schema-generator/schemas/config.schema.json b/crates/tauri-schema-generator/schemas/config.schema.json index 1d19c20eaede..d8200ab9c9de 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:\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" + ] } }, "additionalProperties": false diff --git a/crates/tauri-utils/src/config.rs b/crates/tauri-utils/src/config.rs index 37c77abff8ac..1c544a491b78 100644 --- a/crates/tauri-utils/src/config.rs +++ b/crates/tauri-utils/src/config.rs @@ -2763,6 +2763,34 @@ 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, + /// - 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: + /// + /// 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 { @@ -4029,6 +4057,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, @@ -4038,7 +4073,8 @@ mod build { tray_icon, macos_private_api, with_global_tauri, - enable_gtk_app_id + enable_gtk_app_id, + app_directories_override ); } } @@ -4119,6 +4155,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..caf797613bc2 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,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.resolve_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.resolve_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.resolve_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.resolve_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.resolve_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,39 @@ 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 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) + }) + }) + } } 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(),