feat(cli/add): add default permission to capabilities (#9124)

* feat(cli/add): add default permission to capabilities

also cleanup `tauri add` command

* license headers & clippy

* print permission name

* do not error out if default permission is not set

---------

Co-authored-by: Lucas Nogueira <lucas@tauri.app>
This commit is contained in:
Amr Bashir 2024-03-13 16:58:25 +02:00 committed by GitHub
parent acdd76833d
commit 7213b9e472
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 307 additions and 249 deletions

View File

@ -0,0 +1,6 @@
---
"tauri-build": patch:enhance
"tauri-utils": patch:enhance
---
Fallback to an empty permission set if the plugin did not define its `default` permissions.

View File

@ -0,0 +1,6 @@
---
'tauri-cli': 'patch:feat'
'@tauri-apps/cli': 'patch:feat'
---
Add default permission for a plugin to capabilities when using `tauri add <plugin>`.

View File

@ -131,13 +131,14 @@ fn capabilities_schema(acl_manifests: &BTreeMap<String, Manifest>) -> RootSchema
permission_schemas.push(schema_from(key, set_id, Some(&set.description)));
}
if let Some(default) = &manifest.default_permission {
permission_schemas.push(schema_from(
key,
"default",
Some(default.description.as_ref()),
));
}
permission_schemas.push(schema_from(
key,
"default",
manifest
.default_permission
.as_ref()
.map(|d| d.description.as_ref()),
));
for (permission_id, permission) in &manifest.permissions {
permission_schemas.push(schema_from(
@ -198,9 +199,14 @@ fn capabilities_schema(acl_manifests: &BTreeMap<String, Manifest>) -> RootSchema
};
let mut permission_schemas = Vec::new();
if let Some(default) = &manifest.default_permission {
permission_schemas.push(schema_from(key, "default", Some(&default.description)));
}
permission_schemas.push(schema_from(
key,
"default",
manifest
.default_permission
.as_ref()
.map(|d| d.description.as_ref()),
));
for set in manifest.permission_sets.values() {
permission_schemas.push(schema_from(key, &set.identifier, Some(&set.description)));
}
@ -471,12 +477,10 @@ pub fn validate_capabilities(
let permission_exists = acl_manifests
.get(key)
.map(|manifest| {
if permission_name == "default" {
manifest.default_permission.is_some()
} else {
manifest.permissions.contains_key(permission_name)
|| manifest.permission_sets.contains_key(permission_name)
}
// the default permission is always treated as valid, the CLI automatically adds it on the `tauri add` command
permission_name == "default"
|| manifest.permissions.contains_key(permission_name)
|| manifest.permission_sets.contains_key(permission_name)
})
.unwrap_or(false);

View File

@ -367,15 +367,8 @@ fn get_permissions<'a>(
manifest
.default_permission
.as_ref()
.ok_or_else(|| Error::UnknownPermission {
key: if key == APP_ACL_KEY {
"app manifest".to_string()
} else {
key.to_string()
},
permission: permission_name.to_string(),
})
.and_then(|default| get_permission_set_permissions(manifest, default))
.map(|default| get_permission_set_permissions(manifest, default))
.unwrap_or_else(|| Ok(Vec::new()))
} else if let Some(set) = manifest.permission_sets.get(permission_name) {
get_permission_set_permissions(manifest, set)
} else if let Some(permission) = manifest.permissions.get(permission_name) {

View File

@ -80,10 +80,10 @@ fn capability_from_path<P: AsRef<Path>>(path: P) -> Option<TomlOrJson> {
#[derive(Debug, Parser)]
#[clap(about = "Add a permission to capabilities")]
pub struct Options {
/// Permission to remove.
identifier: String,
/// Permission to add.
pub identifier: String,
/// Capability to add the permission to.
capability: Option<String>,
pub capability: Option<String>,
}
pub fn command(options: Options) -> Result<()> {
@ -114,7 +114,10 @@ pub fn command(options: Options) -> Result<()> {
let mut capabilities = if capabilities.len() > 1 {
let selections = prompts::multiselect(
"Choose which capabilities to add the permission to:",
&format!(
"Choose which capabilities to add the permission `{}` to:",
options.identifier
),
capabilities
.iter()
.map(|(c, p)| {

View File

@ -6,7 +6,7 @@ use clap::{Parser, Subcommand};
use crate::Result;
mod add;
pub mod add;
mod ls;
mod new;
mod rm;

View File

@ -2,15 +2,15 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use anyhow::Context;
use clap::Parser;
use colored::Colorize;
use regex::Regex;
use crate::{
acl,
helpers::{
app_paths::{app_dir, tauri_dir},
cross_command,
cargo,
npm::PackageManager,
},
Result,
@ -18,165 +18,6 @@ use crate::{
use std::{collections::HashMap, process::Command};
#[derive(Debug, Parser)]
#[clap(about = "Add a tauri plugin to the project")]
pub struct Options {
/// The plugin to add.
pub plugin: String,
/// Git tag to use.
#[clap(short, long)]
pub tag: Option<String>,
/// Git rev to use.
#[clap(short, long)]
pub rev: Option<String>,
/// Git branch to use.
#[clap(short, long)]
pub branch: Option<String>,
}
pub fn command(options: Options) -> Result<()> {
let plugin = options.plugin;
let plugin_snake_case = plugin.replace('-', "_");
let crate_name = format!("tauri-plugin-{plugin}");
let npm_name = format!("@tauri-apps/plugin-{plugin}");
let mut plugins = plugins();
let metadata = plugins.remove(plugin.as_str()).unwrap_or_default();
let tauri_dir = tauri_dir();
let mut cargo = Command::new("cargo");
cargo.current_dir(&tauri_dir).arg("add").arg(&crate_name);
if options.tag.is_some() || options.rev.is_some() || options.branch.is_some() {
cargo
.arg("--git")
.arg("https://github.com/tauri-apps/plugins-workspace");
}
if metadata.desktop_only {
cargo
.arg("--target")
.arg(r#"cfg(not(any(target_os = "android", target_os = "ios")))"#);
}
let npm_spec = match (options.tag, options.rev, options.branch) {
(Some(tag), None, None) => {
cargo.args(["--tag", &tag]);
format!("tauri-apps/tauri-plugin-{plugin}#{tag}")
}
(None, Some(rev), None) => {
cargo.args(["--rev", &rev]);
format!("tauri-apps/tauri-plugin-{plugin}#{rev}")
}
(None, None, Some(branch)) => {
cargo.args(["--branch", &branch]);
format!("tauri-apps/tauri-plugin-{plugin}#{branch}")
}
(None, None, None) => npm_name,
_ => anyhow::bail!("Only one of --tag, --rev and --branch can be specified"),
};
log::info!("Installing Cargo dependency {crate_name}...");
let status = cargo.status().context("failed to run `cargo add`")?;
if !status.success() {
anyhow::bail!("Failed to install Cargo dependency");
}
if !metadata.rust_only {
if let Some(manager) = std::panic::catch_unwind(app_dir)
.map(Some)
.unwrap_or_default()
.map(PackageManager::from_project)
.and_then(|managers| managers.into_iter().next())
{
let mut cmd = match manager {
PackageManager::Npm => cross_command("npm"),
PackageManager::Pnpm => cross_command("pnpm"),
PackageManager::Yarn => cross_command("yarn"),
PackageManager::YarnBerry => cross_command("yarn"),
PackageManager::Bun => cross_command("bun"),
};
cmd.arg("add").arg(&npm_spec);
log::info!("Installing NPM dependency {npm_spec}...");
let status = cmd
.status()
.with_context(|| format!("failed to run {manager}"))?;
if !status.success() {
anyhow::bail!("Failed to install NPM dependency");
}
}
}
// add plugin init code to main.rs or lib.rs
let plugin_init_fn = if plugin == "stronghold" {
"Builder::new(|pass| todo!()).build()"
} else if metadata.builder {
"Builder::new().build()"
} else {
"init()"
};
let plugin_init = format!(".plugin(tauri_plugin_{plugin_snake_case}::{plugin_init_fn})");
let re = Regex::new(r"(tauri\s*::\s*Builder\s*::\s*default\(\))(\s*)")?;
for file in [tauri_dir.join("src/main.rs"), tauri_dir.join("src/lib.rs")] {
let contents = std::fs::read_to_string(&file)?;
if contents.contains(&plugin_init) {
log::info!(
"Plugin initialization code already found on {}",
file.display()
);
return Ok(());
}
if re.is_match(&contents) {
let out = re.replace(&contents, format!("$1$2{plugin_init}$2"));
log::info!("Adding plugin to {}", file.display());
std::fs::write(file, out.as_bytes())?;
// run cargo fmt
log::info!("Running `cargo fmt`...");
let _ = Command::new("cargo")
.arg("fmt")
.current_dir(&tauri_dir)
.status();
return Ok(());
}
}
let builder_code = if metadata.builder {
format!(r#"+ .plugin(tauri_plugin_{plugin_snake_case}::Builder::new().build())"#,)
} else {
format!(r#"+ .plugin(tauri_plugin_{plugin_snake_case}::init())"#)
};
let rust_code = format!(
r#" {}
{}
{}"#,
"tauri::Builder::default()".dimmed(),
builder_code.normal().green(),
r#".invoke_handler(tauri::generate_handler![])
.run(tauri::generate_context!())
.expect("error while running tauri application");"#
.dimmed(),
);
log::warn!(
"Couldn't find `{}` in `{}` or `{}`, you must enable the plugin in your Rust code manually:\n\n{}",
"tauri::Builder".cyan(),
"main.rs".cyan(),
"lib.rs".cyan(),
rust_code
);
Ok(())
}
#[derive(Default)]
struct PluginMetadata {
desktop_only: bool,
@ -221,3 +62,137 @@ fn plugins() -> HashMap<&'static str, PluginMetadata> {
plugins
}
#[derive(Debug, Parser)]
#[clap(about = "Add a tauri plugin to the project")]
pub struct Options {
/// The plugin to add.
pub plugin: String,
/// Git tag to use.
#[clap(short, long)]
pub tag: Option<String>,
/// Git rev to use.
#[clap(short, long)]
pub rev: Option<String>,
/// Git branch to use.
#[clap(short, long)]
pub branch: Option<String>,
}
pub fn command(options: Options) -> Result<()> {
let plugin = options.plugin;
let plugin_snake_case = plugin.replace('-', "_");
let crate_name = format!("tauri-plugin-{plugin}");
let npm_name = format!("@tauri-apps/plugin-{plugin}");
let mut plugins = plugins();
let metadata = plugins.remove(plugin.as_str()).unwrap_or_default();
let tauri_dir = tauri_dir();
cargo::install_one(cargo::CargoInstallOptions {
name: &crate_name,
branch: options.branch.as_deref(),
rev: options.rev.as_deref(),
tag: options.tag.as_deref(),
cwd: Some(&tauri_dir),
target: metadata
.desktop_only
.then_some(r#"cfg(not(any(target_os = "android", target_os = "ios")))"#),
})?;
if !metadata.rust_only {
if let Some(manager) = std::panic::catch_unwind(app_dir)
.map(Some)
.unwrap_or_default()
.map(PackageManager::from_project)
.and_then(|managers| managers.into_iter().next())
{
let npm_spec = match (options.tag, options.rev, options.branch) {
(Some(tag), None, None) => {
format!("tauri-apps/tauri-plugin-{plugin}#{tag}")
}
(None, Some(rev), None) => {
format!("tauri-apps/tauri-plugin-{plugin}#{rev}")
}
(None, None, Some(branch)) => {
format!("tauri-apps/tauri-plugin-{plugin}#{branch}")
}
(None, None, None) => npm_name,
_ => anyhow::bail!("Only one of --tag, --rev and --branch can be specified"),
};
manager.install(&[npm_spec])?;
}
}
let _ = acl::permission::add::command(acl::permission::add::Options {
identifier: format!("{plugin}:default"),
capability: None,
});
// add plugin init code to main.rs or lib.rs
let plugin_init_fn = if plugin == "stronghold" {
"Builder::new(|pass| todo!()).build()"
} else if metadata.builder {
"Builder::new().build()"
} else {
"init()"
};
let plugin_init = format!(".plugin(tauri_plugin_{plugin_snake_case}::{plugin_init_fn})");
let re = Regex::new(r"(tauri\s*::\s*Builder\s*::\s*default\(\))(\s*)")?;
for file in [tauri_dir.join("src/main.rs"), tauri_dir.join("src/lib.rs")] {
let contents = std::fs::read_to_string(&file)?;
if contents.contains(&plugin_init) {
log::info!(
"Plugin initialization code already found on {}",
file.display()
);
return Ok(());
}
if re.is_match(&contents) {
let out = re.replace(&contents, format!("$1$2{plugin_init}$2"));
log::info!("Adding plugin to {}", file.display());
std::fs::write(file, out.as_bytes())?;
// run cargo fmt
log::info!("Running `cargo fmt`...");
let _ = Command::new("cargo")
.arg("fmt")
.current_dir(&tauri_dir)
.status();
return Ok(());
}
}
let builder_code = if metadata.builder {
format!(r#"+ .plugin(tauri_plugin_{plugin_snake_case}::Builder::new().build())"#,)
} else {
format!(r#"+ .plugin(tauri_plugin_{plugin_snake_case}::init())"#)
};
let rust_code = format!(
r#" {}
{}
{}"#,
"tauri::Builder::default()".dimmed(),
builder_code.normal().green(),
r#".invoke_handler(tauri::generate_handler![])
.run(tauri::generate_context!())
.expect("error while running tauri application");"#
.dimmed(),
);
log::warn!(
"Couldn't find `{}` in `{}` or `{}`, you must enable the plugin in your Rust code manually:\n\n{}",
"tauri::Builder".cyan(),
"main.rs".cyan(),
"lib.rs".cyan(),
rust_code
);
Ok(())
}

View File

@ -0,0 +1,87 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use std::{path::Path, process::Command};
use anyhow::Context;
#[derive(Debug, Default, Clone, Copy)]
pub struct CargoInstallOptions<'a> {
pub name: &'a str,
pub rev: Option<&'a str>,
pub tag: Option<&'a str>,
pub branch: Option<&'a str>,
pub cwd: Option<&'a std::path::Path>,
pub target: Option<&'a str>,
}
pub fn install(dependencies: &[String], cwd: Option<&Path>) -> crate::Result<()> {
let dependencies_str = if dependencies.len() > 1 {
"dependencies"
} else {
"dependency"
};
log::info!(
"Installing Cargo {dependencies_str} {}...",
dependencies
.iter()
.map(|d| format!("\"{d}\""))
.collect::<Vec<_>>()
.join(", ")
);
let mut cmd = Command::new("cargo");
cmd.arg("add").args(dependencies);
if let Some(cwd) = cwd {
cmd.current_dir(cwd);
}
let status = cmd.status().with_context(|| "failed to run cargo")?;
if !status.success() {
anyhow::bail!("Failed to install Cargo {dependencies_str}");
}
Ok(())
}
pub fn install_one(options: CargoInstallOptions) -> crate::Result<()> {
let mut cargo = Command::new("cargo");
cargo.args(["add", options.name]);
if options.tag.is_some() || options.rev.is_some() || options.branch.is_some() {
cargo.args(["--git", "https://github.com/tauri-apps/plugins-workspace"]);
}
match (options.tag, options.rev, options.branch) {
(Some(tag), None, None) => {
cargo.args(["--tag", &tag]);
}
(None, Some(rev), None) => {
cargo.args(["--rev", &rev]);
}
(None, None, Some(branch)) => {
cargo.args(["--branch", &branch]);
}
(None, None, None) => {}
_ => anyhow::bail!("Only one of --tag, --rev and --branch can be specified"),
};
if let Some(target) = options.target {
cargo.args(["--target", target]);
}
if let Some(cwd) = options.cwd {
cargo.current_dir(cwd);
}
log::info!("Installing Cargo dependency \"{}\"...", options.name);
let status = cargo.status().context("failed to run `cargo add`")?;
if !status.success() {
anyhow::bail!("Failed to install Cargo dependency");
}
Ok(())
}

View File

@ -3,6 +3,7 @@
// SPDX-License-Identifier: MIT
pub mod app_paths;
pub mod cargo;
pub mod config;
pub mod flock;
pub mod framework;

View File

@ -2,8 +2,10 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use crate::{helpers::cross_command, Result};
use std::{fmt::Display, path::Path, process::ExitStatus};
use anyhow::Context;
use crate::helpers::cross_command;
use std::{fmt::Display, path::Path, process::Command};
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum PackageManager {
@ -69,48 +71,42 @@ impl PackageManager {
found
}
pub fn install(&self, dependencies: &[String]) -> Result<ExitStatus> {
fn cross_command(&self) -> Command {
match self {
PackageManager::Yarn => {
let mut cmd = cross_command("yarn");
cmd
.arg("add")
.args(dependencies)
.status()
.map_err(Into::into)
}
PackageManager::YarnBerry => {
let mut cmd = cross_command("yarn");
cmd
.arg("add")
.args(dependencies)
.status()
.map_err(Into::into)
}
PackageManager::Npm => {
let mut cmd = cross_command("npm");
cmd
.arg("install")
.args(dependencies)
.status()
.map_err(Into::into)
}
PackageManager::Pnpm => {
let mut cmd = cross_command("pnpm");
cmd
.arg("install")
.args(dependencies)
.status()
.map_err(Into::into)
}
PackageManager::Bun => {
let mut cmd = cross_command("bun");
cmd
.arg("install")
.args(dependencies)
.status()
.map_err(Into::into)
}
PackageManager::Yarn => cross_command("yarn"),
PackageManager::YarnBerry => cross_command("yarn"),
PackageManager::Npm => cross_command("npm"),
PackageManager::Pnpm => cross_command("pnpm"),
PackageManager::Bun => cross_command("bun"),
}
}
pub fn install(&self, dependencies: &[String]) -> crate::Result<()> {
let dependencies_str = if dependencies.len() > 1 {
"dependencies"
} else {
"dependency"
};
log::info!(
"Installing NPM {dependencies_str} {}...",
dependencies
.iter()
.map(|d| format!("\"{d}\""))
.collect::<Vec<_>>()
.join(", ")
);
let status = self
.cross_command()
.arg("add")
.args(dependencies)
.status()
.with_context(|| format!("failed to run {self}"))?;
if !status.success() {
anyhow::bail!("Failed to install NPM {dependencies_str}");
}
Ok(())
}
}

View File

@ -3,14 +3,13 @@
// SPDX-License-Identifier: MIT
use crate::{
helpers::{app_paths::walk_builder, npm::PackageManager},
helpers::{app_paths::walk_builder, cargo, npm::PackageManager},
Result,
};
use std::{
fs::{read_to_string, write},
path::Path,
process::Command,
};
const CORE_API_MODULES: &[&str] = &["dpi", "event", "path", "core", "window", "mocks"];
@ -78,23 +77,11 @@ pub fn migrate(app_dir: &Path, tauri_dir: &Path) -> Result<()> {
}
if !new_npm_packages.is_empty() {
log::info!(
"Installing NPM packages for plugins: {}",
new_npm_packages.join(", ")
);
pm.install(&new_npm_packages)?;
}
if !new_cargo_packages.is_empty() {
log::info!(
"Installing Cargo dependencies for plugins: {}",
new_cargo_packages.join(", ")
);
Command::new("cargo")
.arg("add")
.args(new_cargo_packages)
.current_dir(tauri_dir)
.status()?;
cargo::install(&new_cargo_packages, Some(tauri_dir))?;
}
Ok(())