diff --git a/.changes/permissions-add-target-specific.md b/.changes/permissions-add-target-specific.md new file mode 100644 index 000000000000..2f89bd0274c1 --- /dev/null +++ b/.changes/permissions-add-target-specific.md @@ -0,0 +1,7 @@ +--- +"tauri-cli": patch:enhance +"@tauri-apps/cli": patch:enhance +--- + +`permission add` and `add` commands now check if the plugin is known and if it is either desktop or mobile only +we add the permission to a target-specific capability. diff --git a/tooling/cli/src/acl/permission/add.rs b/tooling/cli/src/acl/permission/add.rs index e5eadd4d1555..6a8ca1d0f979 100644 --- a/tooling/cli/src/acl/permission/add.rs +++ b/tooling/cli/src/acl/permission/add.rs @@ -31,14 +31,36 @@ impl TomlOrJson { } } - fn insert_permission(&mut self, idenitifer: String) { + fn platforms(&self) -> Option> { + match self { + TomlOrJson::Toml(t) => t.get("platforms").and_then(|k| { + k.as_array() + .and_then(|array| array.iter().map(|v| v.as_str()).collect()) + }), + TomlOrJson::Json(j) => j.get("platforms").and_then(|k| { + if let Some(array) = k.as_array() { + let mut items = Vec::new(); + for item in array { + if let Some(s) = item.as_str() { + items.push(s); + } + } + Some(items) + } else { + None + } + }), + } + } + + fn insert_permission(&mut self, identifier: String) { match self { TomlOrJson::Toml(t) => { let permissions = t.entry("permissions").or_insert_with(|| { toml_edit::Item::Value(toml_edit::Value::Array(toml_edit::Array::new())) }); if let Some(permissions) = permissions.as_array_mut() { - permissions.push(idenitifer) + permissions.push(identifier) }; } @@ -48,7 +70,7 @@ impl TomlOrJson { .entry("permissions") .or_insert_with(|| serde_json::Value::Array(Vec::new())); if let Some(permissions) = permissions.as_array_mut() { - permissions.push(serde_json::Value::String(idenitifer)) + permissions.push(serde_json::Value::String(identifier)) }; } } @@ -100,7 +122,13 @@ pub fn command(options: Options) -> Result<()> { ); } - let capabilities = std::fs::read_dir(&capabilities_dir)? + let known_plugins = crate::helpers::plugins::known_plugins(); + let known_plugin = options + .identifier + .split_once(':') + .and_then(|(plugin, _permission)| known_plugins.get(&plugin)); + + let capabilities_iter = std::fs::read_dir(&capabilities_dir)? .flatten() .filter(|e| e.file_type().map(|e| e.is_file()).unwrap_or_default()) .filter_map(|e| { @@ -109,8 +137,66 @@ pub fn command(options: Options) -> Result<()> { Some(c) => (c == capability.identifier()).then_some((capability, path)), None => Some((capability, path)), }) - }) - .collect::>(); + }); + + let (desktop_only, mobile_only) = known_plugin + .map(|p| (p.desktop_only, p.mobile_only)) + .unwrap_or_default(); + + let expected_capability_config = if desktop_only { + Some(( + vec![ + tauri_utils::platform::Target::MacOS.to_string(), + tauri_utils::platform::Target::Windows.to_string(), + tauri_utils::platform::Target::Linux.to_string(), + ], + "desktop", + )) + } else if mobile_only { + Some(( + vec![ + tauri_utils::platform::Target::Android.to_string(), + tauri_utils::platform::Target::Ios.to_string(), + ], + "mobile", + )) + } else { + None + }; + + let capabilities = if let Some((expected_platforms, target_name)) = expected_capability_config { + let mut capabilities = capabilities_iter + .filter(|(capability, _path)| { + capability.platforms().map_or( + false, /* allows any target, so we should skip it since we're adding a target-specific plugin */ + |platforms| { + // all platforms must be in the expected platforms list + platforms.iter().all(|p| expected_platforms.contains(&p.to_string())) + }, + ) + }) + .collect::>(); + + if capabilities.is_empty() { + let identifier = format!("{target_name}-capability"); + let capability_path = capabilities_dir.join(target_name).with_extension("json"); + log::info!( + "Capability matching platforms {expected_platforms:?} not found, creating {}", + capability_path.display() + ); + capabilities.push(( + TomlOrJson::Json(serde_json::json!({ + "identifier": identifier, + "platforms": expected_platforms + })), + capability_path, + )); + } + + capabilities + } else { + capabilities_iter.collect::>() + }; let mut capabilities = if capabilities.len() > 1 { let selections = prompts::multiselect( @@ -132,6 +218,11 @@ pub fn command(options: Options) -> Result<()> { .as_slice(), None, )?; + + if selections.is_empty() { + anyhow::bail!("You did not select any capabilities to update"); + } + selections .into_iter() .map(|idx| capabilities[idx].clone()) @@ -140,6 +231,10 @@ pub fn command(options: Options) -> Result<()> { capabilities }; + if capabilities.is_empty() { + anyhow::bail!("Could not find a capability to update"); + } + for (capability, path) in &mut capabilities { capability.insert_permission(options.identifier.clone()); std::fs::write(&*path, capability.to_string()?)?; diff --git a/tooling/cli/src/add.rs b/tooling/cli/src/add.rs index 6ffd3f2f097a..e5309780e5d3 100644 --- a/tooling/cli/src/add.rs +++ b/tooling/cli/src/add.rs @@ -16,61 +16,7 @@ use crate::{ Result, }; -use std::{collections::HashMap, process::Command}; - -#[derive(Default)] -struct PluginMetadata { - desktop_only: bool, - mobile_only: bool, - rust_only: bool, - builder: bool, -} - -// known plugins with particular cases -fn plugins() -> HashMap<&'static str, PluginMetadata> { - let mut plugins: HashMap<&'static str, PluginMetadata> = HashMap::new(); - - // desktop-only - for p in [ - "authenticator", - "autostart", - "cli", - "global-shortcut", - "positioner", - "single-instance", - "updater", - "window-state", - ] { - plugins.entry(p).or_default().desktop_only = true; - } - - // mobile-only - for p in ["barcode-scanner", "biometric", "nfc"] { - plugins.entry(p).or_default().mobile_only = true; - } - - // uses builder pattern - for p in [ - "global-shortcut", - "localhost", - "log", - "sql", - "store", - "stronghold", - "updater", - "window-state", - ] { - plugins.entry(p).or_default().builder = true; - } - - // rust-only - #[allow(clippy::single_element_loop)] - for p in ["localhost", "persisted-scope", "single-instance"] { - plugins.entry(p).or_default().rust_only = true; - } - - plugins -} +use std::process::Command; #[derive(Debug, Parser)] #[clap(about = "Add a tauri plugin to the project")] @@ -104,7 +50,7 @@ pub fn command(options: Options) -> Result<()> { let crate_name = format!("tauri-plugin-{plugin}"); let npm_name = format!("@tauri-apps/plugin-{plugin}"); - let mut plugins = plugins(); + let mut plugins = crate::helpers::plugins::known_plugins(); let metadata = plugins.remove(plugin).unwrap_or_default(); let app_dir = resolve_app_dir(); diff --git a/tooling/cli/src/helpers/mod.rs b/tooling/cli/src/helpers/mod.rs index aa55463d519d..58bdd06edd90 100644 --- a/tooling/cli/src/helpers/mod.rs +++ b/tooling/cli/src/helpers/mod.rs @@ -9,6 +9,7 @@ pub mod config; pub mod flock; pub mod framework; pub mod npm; +pub mod plugins; pub mod prompts; pub mod template; pub mod updater_signature; diff --git a/tooling/cli/src/helpers/plugins.rs b/tooling/cli/src/helpers/plugins.rs new file mode 100644 index 000000000000..52280635bae8 --- /dev/null +++ b/tooling/cli/src/helpers/plugins.rs @@ -0,0 +1,59 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::collections::HashMap; + +#[derive(Default)] +pub struct PluginMetadata { + pub desktop_only: bool, + pub mobile_only: bool, + pub rust_only: bool, + pub builder: bool, +} + +// known plugins with particular cases +pub fn known_plugins() -> HashMap<&'static str, PluginMetadata> { + let mut plugins: HashMap<&'static str, PluginMetadata> = HashMap::new(); + + // desktop-only + for p in [ + "authenticator", + "autostart", + "cli", + "global-shortcut", + "positioner", + "single-instance", + "updater", + "window-state", + ] { + plugins.entry(p).or_default().desktop_only = true; + } + + // mobile-only + for p in ["barcode-scanner", "biometric", "nfc", "haptics"] { + plugins.entry(p).or_default().mobile_only = true; + } + + // uses builder pattern + for p in [ + "global-shortcut", + "localhost", + "log", + "sql", + "store", + "stronghold", + "updater", + "window-state", + ] { + plugins.entry(p).or_default().builder = true; + } + + // rust-only + #[allow(clippy::single_element_loop)] + for p in ["localhost", "persisted-scope", "single-instance"] { + plugins.entry(p).or_default().rust_only = true; + } + + plugins +}