diff --git a/.changes/cli-remove-command.md b/.changes/cli-remove-command.md new file mode 100644 index 000000000..523c08082 --- /dev/null +++ b/.changes/cli-remove-command.md @@ -0,0 +1,6 @@ +--- +"tauri-cli": minor:feat +"@tauri-apps/cli": minor:feat +--- + +Add `tauri remove` to remove plugins from projects. diff --git a/crates/tauri-cli/src/acl/permission/mod.rs b/crates/tauri-cli/src/acl/permission/mod.rs index e65295919..deb82f00b 100644 --- a/crates/tauri-cli/src/acl/permission/mod.rs +++ b/crates/tauri-cli/src/acl/permission/mod.rs @@ -9,7 +9,7 @@ use crate::Result; pub mod add; mod ls; mod new; -mod rm; +pub mod rm; #[derive(Debug, Parser)] #[clap(about = "Manage or create permissions for your app or plugin")] diff --git a/crates/tauri-cli/src/acl/permission/rm.rs b/crates/tauri-cli/src/acl/permission/rm.rs index 4c2e11764..f565b6e56 100644 --- a/crates/tauri-cli/src/acl/permission/rm.rs +++ b/crates/tauri-cli/src/acl/permission/rm.rs @@ -46,13 +46,15 @@ fn rm_permission_files(identifier: &str, dir: &Path) -> Result<()> { permission_file.default = None; } else { let set_len = permission_file.set.len(); - permission_file.set.retain(|s| s.identifier != identifier); + permission_file + .set + .retain(|s| !identifier_match(identifier, &s.identifier)); updated = permission_file.set.len() != set_len; let permission_len = permission_file.permission.len(); permission_file .permission - .retain(|s| s.identifier != identifier); + .retain(|s| !identifier_match(identifier, &s.identifier)); updated = updated || permission_file.permission.len() != permission_len; } @@ -84,7 +86,11 @@ fn rm_permission_from_capabilities(identifier: &str, dir: &Path) -> Result<()> { if let Ok(mut value) = content.parse::() { if let Some(permissions) = value.get_mut("permissions").and_then(|p| p.as_array_mut()) { let prev_len = permissions.len(); - permissions.retain(|p| p.as_str().map(|p| p != identifier).unwrap_or(false)); + permissions.retain(|p| { + p.as_str() + .map(|p| !identifier_match(identifier, p)) + .unwrap_or(false) + }); if prev_len != permissions.len() { std::fs::write(&path, value.to_string())?; log::info!(action = "Removed"; "permission from capability at {}", dunce::simplified(&path).display()); @@ -97,7 +103,11 @@ fn rm_permission_from_capabilities(identifier: &str, dir: &Path) -> Result<()> { if let Ok(mut value) = serde_json::from_slice::(&content) { if let Some(permissions) = value.get_mut("permissions").and_then(|p| p.as_array_mut()) { let prev_len = permissions.len(); - permissions.retain(|p| p.as_str().map(|p| p != identifier).unwrap_or(false)); + permissions.retain(|p| { + p.as_str() + .map(|p| !identifier_match(identifier, p)) + .unwrap_or(false) + }); if prev_len != permissions.len() { std::fs::write(&path, serde_json::to_vec_pretty(&value)?)?; log::info!(action = "Removed"; "permission from capability at {}", dunce::simplified(&path).display()); @@ -113,11 +123,20 @@ fn rm_permission_from_capabilities(identifier: &str, dir: &Path) -> Result<()> { Ok(()) } +fn identifier_match(identifier: &str, permission: &str) -> bool { + match identifier.split_once(':') { + Some((plugin_name, "*")) => permission.contains(plugin_name), + _ => permission == identifier, + } +} + #[derive(Debug, Parser)] #[clap(about = "Remove a permission file, and its reference from any capability")] pub struct Options { /// Permission to remove. - identifier: String, + /// + /// To remove all permissions for a given plugin, provide `:*` + pub identifier: String, } pub fn command(options: Options) -> Result<()> { diff --git a/crates/tauri-cli/src/helpers/cargo.rs b/crates/tauri-cli/src/helpers/cargo.rs index 24628bb9d..ffa4493b7 100644 --- a/crates/tauri-cli/src/helpers/cargo.rs +++ b/crates/tauri-cli/src/helpers/cargo.rs @@ -61,3 +61,33 @@ pub fn install_one(options: CargoInstallOptions) -> crate::Result<()> { Ok(()) } + +#[derive(Debug, Default, Clone, Copy)] +pub struct CargoUninstallOptions<'a> { + pub name: &'a str, + pub cwd: Option<&'a std::path::Path>, + pub target: Option<&'a str>, +} + +pub fn uninstall_one(options: CargoUninstallOptions) -> crate::Result<()> { + let mut cargo = Command::new("cargo"); + cargo.arg("remove"); + + cargo.arg(options.name); + + if let Some(target) = options.target { + cargo.args(["--target", target]); + } + + if let Some(cwd) = options.cwd { + cargo.current_dir(cwd); + } + + log::info!("Uninstalling Cargo dependency \"{}\"...", options.name); + let status = cargo.status().context("failed to run `cargo remove`")?; + if !status.success() { + anyhow::bail!("Failed to remove Cargo dependency"); + } + + Ok(()) +} diff --git a/crates/tauri-cli/src/lib.rs b/crates/tauri-cli/src/lib.rs index 2ab75cf95..4a9e11d3b 100644 --- a/crates/tauri-cli/src/lib.rs +++ b/crates/tauri-cli/src/lib.rs @@ -30,6 +30,7 @@ mod interface; mod migrate; mod mobile; mod plugin; +mod remove; mod signer; use clap::{ArgAction, CommandFactory, FromArgMatches, Parser, Subcommand, ValueEnum}; @@ -146,6 +147,7 @@ enum Commands { Migrate, Info(info::Options), Add(add::Options), + Remove(remove::Options), Plugin(plugin::Cli), Icon(icon::Options), Signer(signer::Cli), @@ -265,6 +267,7 @@ where Commands::Bundle(options) => bundle::command(options, cli.verbose)?, Commands::Dev(options) => dev::command(options)?, Commands::Add(options) => add::command(options)?, + Commands::Remove(options) => remove::command(options)?, Commands::Icon(options) => icon::command(options)?, Commands::Info(options) => info::command(options)?, Commands::Init(options) => init::command(options)?, diff --git a/crates/tauri-cli/src/remove.rs b/crates/tauri-cli/src/remove.rs new file mode 100644 index 000000000..319f60e82 --- /dev/null +++ b/crates/tauri-cli/src/remove.rs @@ -0,0 +1,69 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use clap::Parser; + +use crate::{ + acl, + helpers::{ + app_paths::{resolve_frontend_dir, tauri_dir}, + cargo, + npm::PackageManager, + }, + Result, +}; + +#[derive(Debug, Parser)] +#[clap(about = "Remove a tauri plugin from the project")] +pub struct Options { + /// The plugin to remove. + pub plugin: String, +} + +pub fn command(options: Options) -> Result<()> { + crate::helpers::app_paths::resolve(); + run(options) +} + +pub fn run(options: Options) -> Result<()> { + let plugin = options.plugin; + + let crate_name = format!("tauri-plugin-{plugin}"); + + let mut plugins = crate::helpers::plugins::known_plugins(); + let metadata = plugins.remove(plugin.as_str()).unwrap_or_default(); + + let frontend_dir = resolve_frontend_dir(); + let tauri_dir = tauri_dir(); + + let target_str = metadata + .desktop_only + .then_some(r#"cfg(not(any(target_os = "android", target_os = "ios")))"#) + .or_else(|| { + metadata + .mobile_only + .then_some(r#"cfg(any(target_os = "android", target_os = "ios"))"#) + }); + + cargo::uninstall_one(cargo::CargoUninstallOptions { + name: &crate_name, + cwd: Some(tauri_dir), + target: target_str, + })?; + + if !metadata.rust_only { + if let Some(manager) = frontend_dir.map(PackageManager::from_project) { + let npm_name = format!("@tauri-apps/plugin-{plugin}"); + manager.remove(&[npm_name], tauri_dir)?; + } + + acl::permission::rm::command(acl::permission::rm::Options { + identifier: format!("{plugin}:*"), + })?; + } + + log::info!("Now, you must manually remove the plugin from your Rust code.",); + + Ok(()) +}