From 06d63d67a061459dd533ddcae755922427a6dfc5 Mon Sep 17 00:00:00 2001 From: Amr Bashir Date: Mon, 26 Feb 2024 19:42:13 +0200 Subject: [PATCH] feat(cli): add new acl subcommands (#8827) * unify `CI` var handling, and lay foundation for `permission` subcommand * feat(cli/init&new): create `permissions` directory by default for plugins * generate permissions with consistent pathing on windows and unix * `pemrission create` initial implementation * add ls command * finalize `permission create` subcommand * `permission rm` subcommand * `permission add` subcommand * remove empty `permission copy` subcommand * clippy * `capability create` subcommand and move modules under `acl` directory * fix multiselect for `permission add` when capabilty doesn't have identifier * clippy * `create` -> `new` and change file * license headers * more license headers * clippy * Discard changes to examples/resources/src-tauri/.gitignore * fix build * cleanup --------- Co-authored-by: Lucas Nogueira --- .changes/cli-acl-subcommands.md | 12 ++ core/tauri-plugin/src/build/mod.rs | 2 +- core/tauri-utils/src/acl/build.rs | 4 +- core/tauri-utils/src/acl/capability.rs | 3 +- core/tauri-utils/src/acl/mod.rs | 13 ++- core/tauri-utils/src/acl/plugin.rs | 6 +- tooling/cli/Cargo.lock | 15 +-- tooling/cli/Cargo.toml | 5 +- tooling/cli/src/acl/capability/mod.rs | 28 +++++ tooling/cli/src/acl/capability/new.rs | 141 ++++++++++++++++++++++ tooling/cli/src/acl/mod.rs | 41 +++++++ tooling/cli/src/acl/permission/add.rs | 147 +++++++++++++++++++++++ tooling/cli/src/acl/permission/ls.rs | 148 ++++++++++++++++++++++++ tooling/cli/src/acl/permission/mod.rs | 39 +++++++ tooling/cli/src/acl/permission/new.rs | 113 ++++++++++++++++++ tooling/cli/src/acl/permission/rm.rs | 137 ++++++++++++++++++++++ tooling/cli/src/build.rs | 3 +- tooling/cli/src/helpers/app_paths.rs | 36 +++--- tooling/cli/src/helpers/mod.rs | 1 + tooling/cli/src/helpers/prompts.rs | 57 +++++++++ tooling/cli/src/init.rs | 46 ++------ tooling/cli/src/lib.rs | 5 + tooling/cli/src/mobile/android/build.rs | 5 +- tooling/cli/src/mobile/android/mod.rs | 2 +- tooling/cli/src/mobile/init.rs | 10 +- tooling/cli/src/mobile/ios/build.rs | 5 +- tooling/cli/src/mobile/ios/mod.rs | 2 +- tooling/cli/src/plugin/android.rs | 7 +- tooling/cli/src/plugin/init.rs | 36 +----- tooling/cli/src/signer/generate.rs | 4 +- 30 files changed, 949 insertions(+), 124 deletions(-) create mode 100644 .changes/cli-acl-subcommands.md create mode 100644 tooling/cli/src/acl/capability/mod.rs create mode 100644 tooling/cli/src/acl/capability/new.rs create mode 100644 tooling/cli/src/acl/mod.rs create mode 100644 tooling/cli/src/acl/permission/add.rs create mode 100644 tooling/cli/src/acl/permission/ls.rs create mode 100644 tooling/cli/src/acl/permission/mod.rs create mode 100644 tooling/cli/src/acl/permission/new.rs create mode 100644 tooling/cli/src/acl/permission/rm.rs create mode 100644 tooling/cli/src/helpers/prompts.rs diff --git a/.changes/cli-acl-subcommands.md b/.changes/cli-acl-subcommands.md new file mode 100644 index 000000000..44895acfc --- /dev/null +++ b/.changes/cli-acl-subcommands.md @@ -0,0 +1,12 @@ +--- +'tauri-cli': 'patch:feat' +'@tauri-apps/cli': 'patch:feat' +--- + +Add new subcommands for managing permissions and cababilities: + +- `tauri permission new` +- `tauri permission add` +- `tauri permission rm` +- `tauri permission ls` +- `tauri capability new` diff --git a/core/tauri-plugin/src/build/mod.rs b/core/tauri-plugin/src/build/mod.rs index a3f0dd297..de65ab608 100644 --- a/core/tauri-plugin/src/build/mod.rs +++ b/core/tauri-plugin/src/build/mod.rs @@ -105,7 +105,7 @@ impl<'a> Builder<'a> { let _ = std::fs::remove_file(format!( "./permissions/{}/{}", acl::build::PERMISSION_SCHEMAS_FOLDER_NAME, - acl::build::PERMISSION_SCHEMA_FILE_NAME + acl::PERMISSION_SCHEMA_FILE_NAME )); let _ = std::fs::remove_file(autogenerated.join(acl::build::PERMISSION_DOCS_FILE_NAME)); } else { diff --git a/core/tauri-utils/src/acl/build.rs b/core/tauri-utils/src/acl/build.rs index b922ce674..3287337dc 100644 --- a/core/tauri-utils/src/acl/build.rs +++ b/core/tauri-utils/src/acl/build.rs @@ -20,6 +20,7 @@ use schemars::{ use super::{ capability::{Capability, CapabilityFile}, plugin::PermissionFile, + PERMISSION_SCHEMA_FILE_NAME, }; /// Known name of the folder containing autogenerated permissions. @@ -37,9 +38,6 @@ pub const PERMISSION_FILE_EXTENSIONS: &[&str] = &["json", "toml"]; /// Known foldername of the permission schema files pub const PERMISSION_SCHEMAS_FOLDER_NAME: &str = "schemas"; -/// Known filename of the permission schema JSON file -pub const PERMISSION_SCHEMA_FILE_NAME: &str = "schema.json"; - /// Known filename of the permission documentation file pub const PERMISSION_DOCS_FILE_NAME: &str = "reference.md"; diff --git a/core/tauri-utils/src/acl/capability.rs b/core/tauri-utils/src/acl/capability.rs index e5dd69cfa..927a317c9 100644 --- a/core/tauri-utils/src/acl/capability.rs +++ b/core/tauri-utils/src/acl/capability.rs @@ -57,6 +57,7 @@ pub struct Capability { #[serde(default)] pub description: String, /// Configure remote URLs that can use the capability permissions. + #[serde(default, skip_serializing_if = "Option::is_none")] pub remote: Option, /// Whether this capability is enabled for local app URLs or not. Defaults to `true`. #[serde(default = "default_capability_local")] @@ -74,7 +75,7 @@ pub struct Capability { /// List of permissions attached to this capability. Must include the plugin name as prefix in the form of `${plugin-name}:${permission-name}`. pub permissions: Vec, /// Target platforms this capability applies. By default all platforms applies. - #[serde(default = "default_platforms")] + #[serde(default = "default_platforms", skip_serializing_if = "Vec::is_empty")] pub platforms: Vec, } diff --git a/core/tauri-utils/src/acl/mod.rs b/core/tauri-utils/src/acl/mod.rs index e354ef9ea..33affe027 100644 --- a/core/tauri-utils/src/acl/mod.rs +++ b/core/tauri-utils/src/acl/mod.rs @@ -11,6 +11,9 @@ use thiserror::Error; pub use self::{identifier::*, value::*}; +/// Known filename of the permission schema JSON file +pub const PERMISSION_SCHEMA_FILE_NAME: &str = "schema.json"; + #[cfg(feature = "build")] pub mod build; pub mod capability; @@ -142,6 +145,12 @@ pub struct Scopes { pub deny: Option>, } +impl Scopes { + fn is_empty(&self) -> bool { + self.allow.is_none() && self.deny.is_none() + } +} + /// Descriptions of explicit privileges of commands. /// /// It can enable commands to be accessible in the frontend of the application. @@ -151,12 +160,14 @@ pub struct Scopes { #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] pub struct Permission { /// The version of the permission. + #[serde(skip_serializing_if = "Option::is_none")] pub version: Option, /// A unique identifier for the permission. pub identifier: String, /// Human-readable description of what the permission does. + #[serde(skip_serializing_if = "Option::is_none")] pub description: Option, /// Allowed or denied commands when using this permission. @@ -164,7 +175,7 @@ pub struct Permission { pub commands: Commands, /// Allowed or denied scoped when using this permission. - #[serde(default)] + #[serde(default, skip_serializing_if = "Scopes::is_empty")] pub scope: Scopes, } diff --git a/core/tauri-utils/src/acl/plugin.rs b/core/tauri-utils/src/acl/plugin.rs index 9f427651b..63eec392f 100644 --- a/core/tauri-utils/src/acl/plugin.rs +++ b/core/tauri-utils/src/acl/plugin.rs @@ -12,7 +12,7 @@ use serde::{Deserialize, Serialize}; /// The default permission set of the plugin. /// /// Works similarly to a permission with the "default" identifier. -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Serialize)] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] pub struct DefaultPermission { /// The version of the permission. @@ -26,14 +26,14 @@ pub struct DefaultPermission { } /// Permission file that can define a default permission, a set of permissions or a list of inlined permissions. -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Serialize)] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] pub struct PermissionFile { /// The default permission set for the plugin pub default: Option, /// A list of permissions sets defined - #[serde(default)] + #[serde(default, skip_serializing_if = "Vec::is_empty")] pub set: Vec, /// A list of inlined permissions diff --git a/tooling/cli/Cargo.lock b/tooling/cli/Cargo.lock index 01bacfe0c..348433372 100644 --- a/tooling/cli/Cargo.lock +++ b/tooling/cli/Cargo.lock @@ -4112,6 +4112,7 @@ version = "1.0.113" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69801b70b1c3dac963ecb03a364ba0ceda9cf60c71cfe475e99864759c8b8a79" dependencies = [ + "indexmap 2.2.3", "itoa 1.0.10", "ryu", "serde", @@ -4697,6 +4698,7 @@ dependencies = [ "ctrlc", "dialoguer", "duct", + "dunce", "env_logger", "glob", "handlebars 5.1.0", @@ -4737,7 +4739,7 @@ dependencies = [ "thiserror", "tokio", "toml 0.8.10", - "toml_edit 0.21.1", + "toml_edit 0.22.6", "unicode-width", "ureq", "url", @@ -5103,17 +5105,6 @@ dependencies = [ "winnow 0.5.40", ] -[[package]] -name = "toml_edit" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" -dependencies = [ - "indexmap 2.2.3", - "toml_datetime", - "winnow 0.5.40", -] - [[package]] name = "toml_edit" version = "0.22.6" diff --git a/tooling/cli/Cargo.toml b/tooling/cli/Cargo.toml index 206c408bd..cbec7bf71 100644 --- a/tooling/cli/Cargo.toml +++ b/tooling/cli/Cargo.toml @@ -52,12 +52,12 @@ anyhow = "1.0" tauri-bundler = { version = "2.0.1-beta.0", default-features = false, path = "../bundler" } colored = "2.0" serde = { version = "1.0", features = [ "derive" ] } -serde_json = "1.0" +serde_json = { version = "1.0", features = [ "preserve_order" ] } notify = "6.1" notify-debouncer-mini = "0.4" shared_child = "1.0" duct = "0.13" -toml_edit = "0.21" +toml_edit = { version = "0.22", features = [ "serde" ] } json-patch = "1.2" tauri-utils = { version = "2.0.0-beta.4", path = "../../core/tauri-utils", features = [ "isolation", "schema", "config-json5", "config-toml" ] } tauri-utils-v1 = { version = "1", package = "tauri-utils", features = [ "isolation", "schema", "config-json5", "config-toml" ] } @@ -93,6 +93,7 @@ itertools = "0.11" local-ip-address = "0.5" css-color = "0.2" resvg = "0.36.0" +dunce = "1" glob = "0.3" [target."cfg(windows)".dependencies] diff --git a/tooling/cli/src/acl/capability/mod.rs b/tooling/cli/src/acl/capability/mod.rs new file mode 100644 index 000000000..8696dc1f6 --- /dev/null +++ b/tooling/cli/src/acl/capability/mod.rs @@ -0,0 +1,28 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use clap::{Parser, Subcommand}; + +use crate::Result; + +mod new; + +#[derive(Debug, Parser)] +#[clap(about = "Manage or create capabilities for your app")] +pub struct Cli { + #[clap(subcommand)] + command: Commands, +} + +#[derive(Subcommand, Debug)] +enum Commands { + #[clap(alias = "create")] + New(new::Options), +} + +pub fn command(cli: Cli) -> Result<()> { + match cli.command { + Commands::New(options) => new::command(options), + } +} diff --git a/tooling/cli/src/acl/capability/new.rs b/tooling/cli/src/acl/capability/new.rs new file mode 100644 index 000000000..f7a81bb9e --- /dev/null +++ b/tooling/cli/src/acl/capability/new.rs @@ -0,0 +1,141 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::{collections::HashSet, path::PathBuf}; + +use clap::Parser; +use tauri_utils::acl::capability::{Capability, PermissionEntry}; + +use crate::{ + acl::FileFormat, + helpers::{app_paths::tauri_dir, prompts}, + Result, +}; + +#[derive(Debug, Parser)] +#[clap(about = "Create a new permission file")] +pub struct Options { + /// Capability identifier. + identifier: Option, + /// Capability description + #[clap(long)] + description: Option, + /// Capability windows + #[clap(long)] + windows: Option>, + /// Capability permissions + #[clap(long)] + permission: Option>, + /// Output file format. + #[clap(long, default_value_t = FileFormat::Json)] + format: FileFormat, + /// The output file. + #[clap(short, long)] + out: Option, +} + +pub fn command(options: Options) -> Result<()> { + let identifier = match options.identifier { + Some(i) => i, + None => prompts::input("What's the capability identifier?", None, false, false)?.unwrap(), + }; + + let description = match options.description { + Some(d) => Some(d), + None => prompts::input::("What's the capability description?", None, false, true)? + .and_then(|d| if d.is_empty() { None } else { Some(d) }), + }; + + let windows = match options.windows.map(FromIterator::from_iter) { + Some(w) => w, + None => prompts::input::( + "Which windows should be affected by this? (comma separated)", + Some("main".into()), + false, + false, + )? + .and_then(|d| { + if d.is_empty() { + None + } else { + Some(d.split(',').map(ToString::to_string).collect()) + } + }) + .unwrap_or_default(), + }; + + let permissions: HashSet = match options.permission.map(FromIterator::from_iter) { + Some(p) => p, + None => prompts::input::( + "What permissions to enable? (comma separated)", + None, + false, + true, + )? + .and_then(|p| { + if p.is_empty() { + None + } else { + Some(p.split(',').map(ToString::to_string).collect()) + } + }) + .unwrap_or_default(), + }; + + let capability = Capability { + identifier, + description: description.unwrap_or_default(), + remote: None, + local: true, + windows, + webviews: Vec::new(), + permissions: permissions + .into_iter() + .map(|p| { + PermissionEntry::PermissionRef( + p.clone() + .try_into() + .unwrap_or_else(|_| panic!("invalid permission {}", p)), + ) + }) + .collect(), + platforms: Vec::new(), + }; + + let path = match options.out { + Some(o) => o.canonicalize()?, + None => { + let dir = tauri_dir(); + let capabilities_dir = dir.join("capabilities"); + capabilities_dir.join(format!( + "{}.{}", + capability.identifier, + options.format.extension() + )) + } + }; + + if path.exists() { + let msg = format!( + "Capability already exists at {}", + dunce::simplified(&path).display() + ); + let overwrite = prompts::confirm(&format!("{msg}, overwrite?"), Some(false))?; + if overwrite { + std::fs::remove_file(&path)?; + } else { + anyhow::bail!(msg); + } + } + + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent)?; + } + + std::fs::write(&path, options.format.serialize(&capability)?)?; + + log::info!(action = "Created"; "capability at {}", dunce::simplified(&path).display()); + + Ok(()) +} diff --git a/tooling/cli/src/acl/mod.rs b/tooling/cli/src/acl/mod.rs new file mode 100644 index 000000000..a6f987b45 --- /dev/null +++ b/tooling/cli/src/acl/mod.rs @@ -0,0 +1,41 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use serde::Serialize; +use std::fmt::Display; + +pub mod capability; +pub mod permission; + +#[derive(Debug, clap::ValueEnum, Clone)] +enum FileFormat { + Json, + Toml, +} + +impl Display for FileFormat { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Json => write!(f, "json"), + Self::Toml => write!(f, "toml"), + } + } +} + +impl FileFormat { + pub fn extension(&self) -> &'static str { + match self { + Self::Json => "json", + Self::Toml => "toml", + } + } + + pub fn serialize(&self, s: &S) -> crate::Result { + let contents = match self { + Self::Json => serde_json::to_string_pretty(s)?, + Self::Toml => toml_edit::ser::to_string_pretty(s)?, + }; + Ok(contents) + } +} diff --git a/tooling/cli/src/acl/permission/add.rs b/tooling/cli/src/acl/permission/add.rs new file mode 100644 index 000000000..bd724bd6e --- /dev/null +++ b/tooling/cli/src/acl/permission/add.rs @@ -0,0 +1,147 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::path::Path; + +use clap::Parser; + +use crate::{ + helpers::{app_paths::tauri_dir_opt, prompts}, + Result, +}; + +#[derive(Clone)] +enum TomlOrJson { + Toml(toml_edit::Document), + Json(serde_json::Value), +} + +impl TomlOrJson { + fn identifier(&self) -> &str { + match self { + TomlOrJson::Toml(t) => t + .get("identifier") + .and_then(|k| k.as_str()) + .unwrap_or_default(), + TomlOrJson::Json(j) => j + .get("identifier") + .and_then(|k| k.as_str()) + .unwrap_or_default(), + } + } + + fn insert_permission(&mut self, idenitifer: 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) + }; + } + + TomlOrJson::Json(j) => { + if let Some(o) = j.as_object_mut() { + let permissions = o + .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)) + }; + } + } + }; + } + + fn to_string(&self) -> Result { + Ok(match self { + TomlOrJson::Toml(t) => t.to_string(), + TomlOrJson::Json(j) => serde_json::to_string_pretty(&j)?, + }) + } +} + +fn capability_from_path>(path: P) -> Option { + match path.as_ref().extension().and_then(|o| o.to_str()) { + Some("toml") => std::fs::read_to_string(&path) + .ok() + .and_then(|c| c.parse::().ok()) + .map(TomlOrJson::Toml), + Some("json") => std::fs::read(&path) + .ok() + .and_then(|c| serde_json::from_slice::(&c).ok()) + .map(TomlOrJson::Json), + _ => None, + } +} + +#[derive(Debug, Parser)] +#[clap(about = "Add a permission to capabilities")] +pub struct Options { + /// Permission to remove. + identifier: String, + /// Capability to add the permission to. + capability: Option, +} + +pub fn command(options: Options) -> Result<()> { + let dir = match tauri_dir_opt() { + Some(t) => t, + None => std::env::current_dir()?, + }; + + let capabilities_dir = dir.join("capabilities"); + if !capabilities_dir.exists() { + anyhow::bail!( + "Couldn't find capabilities directory at {}", + dunce::simplified(&capabilities_dir).display() + ); + } + + let capabilities = std::fs::read_dir(&capabilities_dir)? + .flatten() + .filter(|e| e.file_type().map(|e| e.is_file()).unwrap_or_default()) + .filter_map(|e| { + let path = e.path(); + capability_from_path(&path).and_then(|capability| match &options.capability { + Some(c) => (c == capability.identifier()).then_some((capability, path)), + None => Some((capability, path)), + }) + }) + .collect::>(); + + let mut capabilities = if capabilities.len() > 1 { + let selections = prompts::multiselect( + "Choose which capabilities to add the permission to:", + capabilities + .iter() + .map(|(c, p)| { + let id = c.identifier(); + if id.is_empty() { + dunce::simplified(p).to_str().unwrap_or_default() + } else { + id + } + }) + .collect::>() + .as_slice(), + None, + )?; + selections + .into_iter() + .map(|idx| capabilities[idx].clone()) + .collect() + } else { + capabilities + }; + + for (capability, path) in &mut capabilities { + capability.insert_permission(options.identifier.clone()); + std::fs::write(&path, capability.to_string()?)?; + log::info!(action = "Added"; "permission `{}` to `{}` at {}", options.identifier, capability.identifier(), dunce::simplified(path).display()); + } + + Ok(()) +} diff --git a/tooling/cli/src/acl/permission/ls.rs b/tooling/cli/src/acl/permission/ls.rs new file mode 100644 index 000000000..499a9f4f6 --- /dev/null +++ b/tooling/cli/src/acl/permission/ls.rs @@ -0,0 +1,148 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use clap::Parser; + +use crate::{helpers::app_paths::tauri_dir, Result}; +use colored::Colorize; +use tauri_utils::acl::plugin::Manifest; + +use std::{collections::BTreeMap, fs::read_to_string}; + +#[derive(Debug, Parser)] +#[clap(about = "List permissions available to your application")] +pub struct Options { + /// Name of the plugin to list permissions. + plugin: Option, + /// Permission identifier filter. + #[clap(short, long)] + filter: Option, +} + +pub fn command(options: Options) -> Result<()> { + let tauri_dir = tauri_dir(); + let plugin_manifests_path = tauri_dir + .join("gen") + .join("schemas") + .join("plugin-manifests.json"); + + if plugin_manifests_path.exists() { + let plugin_manifest_json = read_to_string(&plugin_manifests_path)?; + let acl = serde_json::from_str::>(&plugin_manifest_json)?; + + for (plugin, manifest) in acl { + if options + .plugin + .as_ref() + .map(|p| p != &plugin) + .unwrap_or_default() + { + continue; + } + + let mut permissions = Vec::new(); + + if let Some(default) = manifest.default_permission { + if options + .filter + .as_ref() + .map(|f| "default".contains(f)) + .unwrap_or(true) + { + permissions.push(format!( + "{}:{}\n{}\nPermissions: {}", + plugin.magenta(), + "default".cyan(), + default.description, + default + .permissions + .iter() + .map(|c| c.cyan().to_string()) + .collect::>() + .join(", ") + )); + } + } + + for set in manifest.permission_sets.values() { + if options + .filter + .as_ref() + .map(|f| set.identifier.contains(f)) + .unwrap_or(true) + { + permissions.push(format!( + "{}:{}\n{}\nPermissions: {}", + plugin.magenta(), + set.identifier.cyan(), + set.description, + set + .permissions + .iter() + .map(|c| c.cyan().to_string()) + .collect::>() + .join(", ") + )); + } + } + + for permission in manifest.permissions.into_values() { + if options + .filter + .as_ref() + .map(|f| permission.identifier.contains(f)) + .unwrap_or(true) + { + permissions.push(format!( + "{}:{}{}{}{}", + plugin.magenta(), + permission.identifier.cyan(), + permission + .description + .map(|d| format!("\n{d}")) + .unwrap_or_default(), + if permission.commands.allow.is_empty() { + "".to_string() + } else { + format!( + "\n{}: {}", + "Allow commands".bold(), + permission + .commands + .allow + .iter() + .map(|c| c.green().to_string()) + .collect::>() + .join(", ") + ) + }, + if permission.commands.deny.is_empty() { + "".to_string() + } else { + format!( + "\n{}: {}", + "Deny commands".bold(), + permission + .commands + .deny + .iter() + .map(|c| c.red().to_string()) + .collect::>() + .join(", ") + ) + }, + )); + } + } + + if !permissions.is_empty() { + println!("{}\n", permissions.join("\n\n")); + } + } + + Ok(()) + } else { + anyhow::bail!("permission file not found, please build your application once first") + } +} diff --git a/tooling/cli/src/acl/permission/mod.rs b/tooling/cli/src/acl/permission/mod.rs new file mode 100644 index 000000000..27661e662 --- /dev/null +++ b/tooling/cli/src/acl/permission/mod.rs @@ -0,0 +1,39 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use clap::{Parser, Subcommand}; + +use crate::Result; + +mod add; +mod ls; +mod new; +mod rm; + +#[derive(Debug, Parser)] +#[clap(about = "Manage or create permissions for your app or plugin")] +pub struct Cli { + #[clap(subcommand)] + command: Commands, +} + +#[derive(Subcommand, Debug)] +enum Commands { + #[clap(alias = "create")] + New(new::Options), + Add(add::Options), + #[clap(alias = "remove")] + Rm(rm::Options), + #[clap(alias = "list")] + Ls(ls::Options), +} + +pub fn command(cli: Cli) -> Result<()> { + match cli.command { + Commands::New(options) => new::command(options), + Commands::Add(options) => add::command(options), + Commands::Rm(options) => rm::command(options), + Commands::Ls(options) => ls::command(options), + } +} diff --git a/tooling/cli/src/acl/permission/new.rs b/tooling/cli/src/acl/permission/new.rs new file mode 100644 index 000000000..298a878ed --- /dev/null +++ b/tooling/cli/src/acl/permission/new.rs @@ -0,0 +1,113 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::path::PathBuf; + +use clap::Parser; + +use crate::{ + acl::FileFormat, + helpers::{app_paths::tauri_dir_opt, prompts}, + Result, +}; + +use tauri_utils::acl::{plugin::PermissionFile, Commands, Permission}; + +#[derive(Debug, Parser)] +#[clap(about = "Create a new permission file")] +pub struct Options { + /// Permission identifier. + identifier: Option, + /// Permission description + #[clap(long)] + description: Option, + /// List of commands to allow + #[clap(short, long, use_value_delimiter = true)] + allow: Option>, + /// List of commands to deny + #[clap(short, long, use_value_delimiter = true)] + deny: Option>, + /// Output file format. + #[clap(long, default_value_t = FileFormat::Json)] + format: FileFormat, + /// The output file. + #[clap(short, long)] + out: Option, +} + +pub fn command(options: Options) -> Result<()> { + let identifier = match options.identifier { + Some(i) => i, + None => prompts::input("What's the permission identifier?", None, false, false)?.unwrap(), + }; + + let description = match options.description { + Some(d) => Some(d), + None => prompts::input::("What's the permission description?", None, false, true)? + .and_then(|d| if d.is_empty() { None } else { Some(d) }), + }; + + let allow: Vec = options + .allow + .map(FromIterator::from_iter) + .unwrap_or_default(); + let deny: Vec = options + .deny + .map(FromIterator::from_iter) + .unwrap_or_default(); + + let permission = Permission { + version: None, + identifier, + description, + commands: Commands { allow, deny }, + scope: Default::default(), + }; + + let path = match options.out { + Some(o) => o.canonicalize()?, + None => { + let dir = match tauri_dir_opt() { + Some(t) => t, + None => std::env::current_dir()?, + }; + let permissions_dir = dir.join("permissions"); + permissions_dir.join(format!( + "{}.{}", + permission.identifier, + options.format.extension() + )) + } + }; + + if path.exists() { + let msg = format!( + "Permission already exists at {}", + dunce::simplified(&path).display() + ); + let overwrite = prompts::confirm(&format!("{msg}, overwrite?"), Some(false))?; + if overwrite { + std::fs::remove_file(&path)?; + } else { + anyhow::bail!(msg); + } + } + + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent)?; + } + + std::fs::write( + &path, + options.format.serialize(&PermissionFile { + default: None, + set: Vec::new(), + permission: vec![permission], + })?, + )?; + + log::info!(action = "Created"; "permission at {}", dunce::simplified(&path).display()); + + Ok(()) +} diff --git a/tooling/cli/src/acl/permission/rm.rs b/tooling/cli/src/acl/permission/rm.rs new file mode 100644 index 000000000..595fef661 --- /dev/null +++ b/tooling/cli/src/acl/permission/rm.rs @@ -0,0 +1,137 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::path::Path; + +use clap::Parser; +use tauri_utils::acl::{plugin::PermissionFile, PERMISSION_SCHEMA_FILE_NAME}; + +use crate::{acl::FileFormat, helpers::app_paths::tauri_dir_opt, Result}; + +fn rm_permission_files(identifier: &str, dir: &Path) -> Result<()> { + for entry in std::fs::read_dir(dir)?.flatten() { + let file_type = entry.file_type()?; + let path = entry.path(); + if file_type.is_dir() { + rm_permission_files(identifier, &path)?; + } else { + if path + .file_name() + .map(|name| name == PERMISSION_SCHEMA_FILE_NAME) + .unwrap_or_default() + { + continue; + } + + let (mut permission_file, format): (PermissionFile, FileFormat) = + match path.extension().and_then(|o| o.to_str()) { + Some("toml") => { + let content = std::fs::read_to_string(&path)?; + (toml::from_str(&content)?, FileFormat::Toml) + } + Some("json") => { + let content = std::fs::read(&path)?; + (serde_json::from_slice(&content)?, FileFormat::Json) + } + _ => { + continue; + } + }; + + let mut updated; + + if identifier == "default" { + updated = permission_file.default.is_some(); + permission_file.default = None; + } else { + let set_len = permission_file.set.len(); + permission_file.set.retain(|s| s.identifier != identifier); + updated = permission_file.set.len() != set_len; + + let permission_len = permission_file.permission.len(); + permission_file + .permission + .retain(|s| s.identifier != identifier); + updated = updated || permission_file.permission.len() != permission_len; + } + + // if the file is empty, let's remove it + if permission_file.default.is_none() + && permission_file.set.is_empty() + && permission_file.permission.is_empty() + { + std::fs::remove_file(&path)?; + log::info!(action = "Removed"; "file {}", dunce::simplified(&path).display()); + } else if updated { + std::fs::write(&path, format.serialize(&permission_file)?)?; + log::info!(action = "Removed"; "permission {identifier} from {}", dunce::simplified(&path).display()); + } + } + } + + Ok(()) +} + +fn rm_permission_from_capabilities(identifier: &str, dir: &Path) -> Result<()> { + for entry in std::fs::read_dir(dir)?.flatten() { + let file_type = entry.file_type()?; + if file_type.is_file() { + let path = entry.path(); + match path.extension().and_then(|o| o.to_str()) { + Some("toml") => { + let content = std::fs::read_to_string(&path)?; + 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)); + if prev_len != permissions.len() { + std::fs::write(&path, value.to_string())?; + log::info!(action = "Removed"; "permission from capability at {}", dunce::simplified(&path).display()); + } + } + } + } + Some("json") => { + let content = std::fs::read(&path)?; + 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)); + 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()); + } + } + } + } + _ => {} + } + } + } + + Ok(()) +} + +#[derive(Debug, Parser)] +#[clap(about = "Remove a permission file, and its reference from any capability")] +pub struct Options { + /// Permission to remove. + identifier: String, +} + +pub fn command(options: Options) -> Result<()> { + let permissions_dir = std::env::current_dir()?.join("permissions"); + if permissions_dir.exists() { + rm_permission_files(&options.identifier, &permissions_dir)?; + } + + if let Some(tauri_dir) = tauri_dir_opt() { + let capabilities_dir = tauri_dir.join("capabilities"); + if capabilities_dir.exists() { + rm_permission_from_capabilities(&options.identifier, &capabilities_dir)?; + } + } + + Ok(()) +} diff --git a/tooling/cli/src/build.rs b/tooling/cli/src/build.rs index 17aacfb09..664313e43 100644 --- a/tooling/cli/src/build.rs +++ b/tooling/cli/src/build.rs @@ -60,12 +60,11 @@ pub struct Options { /// Command line arguments passed to the runner. Use `--` to explicitly mark the start of the arguments. pub args: Vec, /// Skip prompting for values - #[clap(long)] + #[clap(long, env = "CI")] pub ci: bool, } pub fn command(mut options: Options, verbosity: u8) -> Result<()> { - options.ci = options.ci || std::env::var("CI").is_ok(); let ci = options.ci; let target = options diff --git a/tooling/cli/src/helpers/app_paths.rs b/tooling/cli/src/helpers/app_paths.rs index dacbb0eb6..63578340f 100644 --- a/tooling/cli/src/helpers/app_paths.rs +++ b/tooling/cli/src/helpers/app_paths.rs @@ -66,14 +66,16 @@ fn lookup bool>(dir: &Path, checker: F) -> Option { None } -fn get_tauri_dir() -> PathBuf { - let cwd = current_dir().expect("failed to read cwd"); +pub fn tauri_dir_opt() -> Option { + let Ok(cwd) = current_dir() else { + return None; + }; if cwd.join(ConfigFormat::Json.into_file_name()).exists() || cwd.join(ConfigFormat::Json5.into_file_name()).exists() || cwd.join(ConfigFormat::Toml.into_file_name()).exists() { - return cwd; + return Some(cwd); } let src_tauri = cwd.join("src-tauri"); @@ -83,12 +85,23 @@ fn get_tauri_dir() -> PathBuf { .exists() || src_tauri.join(ConfigFormat::Toml.into_file_name()).exists() { - return src_tauri; + return Some(src_tauri); } - lookup(&cwd, |path| folder_has_configuration_file(Target::Linux, path) || is_configuration_file(Target::Linux, path)) - .map(|p| if p.is_dir() { p } else { p.parent().unwrap().to_path_buf() }) - .unwrap_or_else(|| + lookup(&cwd, |path| { + folder_has_configuration_file(Target::Linux, path) || is_configuration_file(Target::Linux, path) + }) + .map(|p| { + if p.is_dir() { + p + } else { + p.parent().unwrap().to_path_buf() + } + }) +} + +pub fn tauri_dir() -> PathBuf { + tauri_dir_opt().unwrap_or_else(|| panic!("Couldn't recognize the current folder as a Tauri project. It must contain a `{}`, `{}` or `{}` file in any subfolder.", ConfigFormat::Json.into_file_name(), ConfigFormat::Json5.into_file_name(), @@ -116,11 +129,6 @@ fn get_app_dir() -> Option { pub fn app_dir() -> &'static PathBuf { static APP_DIR: OnceLock = OnceLock::new(); - APP_DIR.get_or_init(|| { - get_app_dir().unwrap_or_else(|| get_tauri_dir().parent().unwrap().to_path_buf()) - }) -} - -pub fn tauri_dir() -> PathBuf { - get_tauri_dir() + APP_DIR + .get_or_init(|| get_app_dir().unwrap_or_else(|| tauri_dir().parent().unwrap().to_path_buf())) } diff --git a/tooling/cli/src/helpers/mod.rs b/tooling/cli/src/helpers/mod.rs index daa29d7bd..f3e59ead7 100644 --- a/tooling/cli/src/helpers/mod.rs +++ b/tooling/cli/src/helpers/mod.rs @@ -7,6 +7,7 @@ pub mod config; pub mod flock; pub mod framework; pub mod npm; +pub mod prompts; pub mod template; pub mod updater_signature; pub mod web_dev_server; diff --git a/tooling/cli/src/helpers/prompts.rs b/tooling/cli/src/helpers/prompts.rs new file mode 100644 index 000000000..08753671a --- /dev/null +++ b/tooling/cli/src/helpers/prompts.rs @@ -0,0 +1,57 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::{fmt::Display, str::FromStr}; + +use crate::Result; + +pub fn input( + prompt: &str, + initial: Option, + skip: bool, + allow_empty: bool, +) -> Result> +where + T: Clone + FromStr + Display + ToString, + T::Err: Display + std::fmt::Debug, +{ + if skip { + Ok(initial) + } else { + let theme = dialoguer::theme::ColorfulTheme::default(); + let mut builder = dialoguer::Input::with_theme(&theme) + .with_prompt(prompt) + .allow_empty(allow_empty); + + if let Some(v) = initial { + builder = builder.with_initial_text(v.to_string()); + } + + builder.interact_text().map(Some).map_err(Into::into) + } +} + +pub fn confirm(prompt: &str, default: Option) -> Result { + let theme = dialoguer::theme::ColorfulTheme::default(); + let mut builder = dialoguer::Confirm::with_theme(&theme).with_prompt(prompt); + if let Some(default) = default { + builder = builder.default(default); + } + builder.interact().map_err(Into::into) +} + +pub fn multiselect( + prompt: &str, + items: &[T], + defaults: Option<&[bool]>, +) -> Result> { + let theme = dialoguer::theme::ColorfulTheme::default(); + let mut builder = dialoguer::MultiSelect::with_theme(&theme) + .with_prompt(prompt) + .items(items); + if let Some(defaults) = defaults { + builder = builder.defaults(defaults); + } + builder.interact().map_err(Into::into) +} diff --git a/tooling/cli/src/init.rs b/tooling/cli/src/init.rs index e08f7ae59..5d23318e2 100644 --- a/tooling/cli/src/init.rs +++ b/tooling/cli/src/init.rs @@ -5,23 +5,20 @@ use crate::{ helpers::{ framework::{infer_from_package_json as infer_framework, Framework}, - resolve_tauri_path, template, + prompts, resolve_tauri_path, template, }, VersionMetadata, }; use std::{ collections::BTreeMap, env::current_dir, - fmt::Display, fs::{read_to_string, remove_dir_all}, path::PathBuf, - str::FromStr, }; use crate::Result; use anyhow::Context; use clap::Parser; -use dialoguer::Input; use handlebars::{to_json, Handlebars}; use include_dir::{include_dir, Dir}; use log::warn; @@ -33,7 +30,7 @@ const TAURI_CONF_TEMPLATE: &str = include_str!("../templates/tauri.conf.json"); #[clap(about = "Initialize a Tauri project in an existing directory")] pub struct Options { /// Skip prompting for values - #[clap(long)] + #[clap(long, env = "CI")] ci: bool, /// Force init to overwrite the src-tauri folder #[clap(short, long)] @@ -76,7 +73,6 @@ struct InitDefaults { impl Options { fn load(mut self) -> Result { - self.ci = self.ci || std::env::var("CI").is_ok(); let package_json_path = PathBuf::from(&self.directory).join("package.json"); let init_defaults = if package_json_path.exists() { @@ -92,7 +88,7 @@ impl Options { }; self.app_name = self.app_name.map(|s| Ok(Some(s))).unwrap_or_else(|| { - request_input( + prompts::input( "What is your app name?", init_defaults.app_name.clone(), self.ci, @@ -101,7 +97,7 @@ impl Options { })?; self.window_title = self.window_title.map(|s| Ok(Some(s))).unwrap_or_else(|| { - request_input( + prompts::input( "What should the window title be?", init_defaults.app_name.clone(), self.ci, @@ -109,7 +105,7 @@ impl Options { ) })?; - self.frontend_dist = self.frontend_dist.map(|s| Ok(Some(s))).unwrap_or_else(|| request_input( + self.frontend_dist = self.frontend_dist.map(|s| Ok(Some(s))).unwrap_or_else(|| prompts::input( r#"Where are your web assets (HTML/CSS/JS) located, relative to the "/src-tauri/tauri.conf.json" file that will be created?"#, init_defaults.framework.as_ref().map(|f| f.frontend_dist()), self.ci, @@ -117,7 +113,7 @@ impl Options { ))?; self.dev_url = self.dev_url.map(|s| Ok(Some(s))).unwrap_or_else(|| { - request_input( + prompts::input( "What is the url of your dev server?", init_defaults.framework.map(|f| f.dev_url()), self.ci, @@ -129,7 +125,7 @@ impl Options { .before_dev_command .map(|s| Ok(Some(s))) .unwrap_or_else(|| { - request_input( + prompts::input( "What is your frontend dev command?", Some("npm run dev".to_string()), self.ci, @@ -140,7 +136,7 @@ impl Options { .before_build_command .map(|s| Ok(Some(s))) .unwrap_or_else(|| { - request_input( + prompts::input( "What is your frontend build command?", Some("npm run build".to_string()), self.ci, @@ -283,29 +279,3 @@ pub fn command(mut options: Options) -> Result<()> { Ok(()) } - -fn request_input( - prompt: &str, - initial: Option, - skip: bool, - allow_empty: bool, -) -> Result> -where - T: Clone + FromStr + Display + ToString, - T::Err: Display + std::fmt::Debug, -{ - if skip { - Ok(initial) - } else { - let theme = dialoguer::theme::ColorfulTheme::default(); - let mut builder = Input::with_theme(&theme) - .with_prompt(prompt) - .allow_empty(allow_empty); - - if let Some(v) = initial { - builder = builder.with_initial_text(v.to_string()); - } - - builder.interact_text().map(Some).map_err(Into::into) - } -} diff --git a/tooling/cli/src/lib.rs b/tooling/cli/src/lib.rs index 3ddbc8ce4..da1d080a1 100644 --- a/tooling/cli/src/lib.rs +++ b/tooling/cli/src/lib.rs @@ -14,6 +14,7 @@ use anyhow::Context; pub use anyhow::Result; +mod acl; mod add; mod build; mod completions; @@ -145,6 +146,8 @@ enum Commands { Icon(icon::Options), Signer(signer::Cli), Completions(completions::Options), + Permission(acl::permission::Cli), + Capability(acl::capability::Cli), } fn format_error(err: clap::Error) -> clap::Error { @@ -247,6 +250,8 @@ where Commands::Plugin(cli) => plugin::command(cli)?, Commands::Signer(cli) => signer::command(cli)?, Commands::Completions(options) => completions::command(options, cli_)?, + Commands::Permission(options) => acl::permission::command(options)?, + Commands::Capability(options) => acl::capability::command(options)?, Commands::Android(c) => mobile::android::command(c, cli.verbose)?, #[cfg(target_os = "macos")] Commands::Ios(c) => mobile::ios::command(c, cli.verbose)?, diff --git a/tooling/cli/src/mobile/android/build.rs b/tooling/cli/src/mobile/android/build.rs index 8ab64f8d9..0c84363c0 100644 --- a/tooling/cli/src/mobile/android/build.rs +++ b/tooling/cli/src/mobile/android/build.rs @@ -64,6 +64,9 @@ pub struct Options { /// Open Android Studio #[clap(short, long)] pub open: bool, + /// Skip prompting for values + #[clap(long, env = "CI")] + pub ci: bool, } impl From for BuildOptions { @@ -76,7 +79,7 @@ impl From for BuildOptions { bundles: None, config: options.config, args: Vec::new(), - ci: false, + ci: options.ci, } } } diff --git a/tooling/cli/src/mobile/android/mod.rs b/tooling/cli/src/mobile/android/mod.rs index bb61b16ab..90955ccd6 100644 --- a/tooling/cli/src/mobile/android/mod.rs +++ b/tooling/cli/src/mobile/android/mod.rs @@ -57,7 +57,7 @@ pub struct Cli { #[clap(about = "Initialize Android target in the project")] pub struct InitOptions { /// Skip prompting for values - #[clap(long)] + #[clap(long, env = "CI")] ci: bool, /// Skips installing rust toolchains via rustup #[clap(long)] diff --git a/tooling/cli/src/mobile/init.rs b/tooling/cli/src/mobile/init.rs index 0f4dd51b5..03f4b8733 100644 --- a/tooling/cli/src/mobile/init.rs +++ b/tooling/cli/src/mobile/init.rs @@ -38,14 +38,8 @@ pub fn command( ) -> Result<()> { let wrapper = TextWrapper::default(); - exec( - target, - &wrapper, - ci || var_os("CI").is_some(), - reinstall_deps, - skip_targets_install, - ) - .map_err(|e| anyhow::anyhow!("{:#}", e))?; + exec(target, &wrapper, ci, reinstall_deps, skip_targets_install) + .map_err(|e| anyhow::anyhow!("{:#}", e))?; Ok(()) } diff --git a/tooling/cli/src/mobile/ios/build.rs b/tooling/cli/src/mobile/ios/build.rs index 07a59ca22..f8d741592 100644 --- a/tooling/cli/src/mobile/ios/build.rs +++ b/tooling/cli/src/mobile/ios/build.rs @@ -60,6 +60,9 @@ pub struct Options { /// Open Xcode #[clap(short, long)] pub open: bool, + /// Skip prompting for values + #[clap(long, env = "CI")] + pub ci: bool, } impl From for BuildOptions { @@ -72,7 +75,7 @@ impl From for BuildOptions { bundles: None, config: options.config, args: Vec::new(), - ci: false, + ci: options.ci, } } } diff --git a/tooling/cli/src/mobile/ios/mod.rs b/tooling/cli/src/mobile/ios/mod.rs index 055ec0513..57cac6ddf 100644 --- a/tooling/cli/src/mobile/ios/mod.rs +++ b/tooling/cli/src/mobile/ios/mod.rs @@ -63,7 +63,7 @@ pub struct Cli { #[clap(about = "Initialize iOS target in the project")] pub struct InitOptions { /// Skip prompting for values - #[clap(long)] + #[clap(long, env = "CI")] ci: bool, /// Reinstall dependencies #[clap(short, long)] diff --git a/tooling/cli/src/plugin/android.rs b/tooling/cli/src/plugin/android.rs index 08994f673..7b2ee45d4 100644 --- a/tooling/cli/src/plugin/android.rs +++ b/tooling/cli/src/plugin/android.rs @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use crate::{helpers::template, Result}; +use crate::{ + helpers::{prompts::input, template}, + Result, +}; use clap::{Parser, Subcommand}; use handlebars::Handlebars; @@ -56,7 +59,7 @@ pub fn command(cli: Cli) -> Result<()> { return Err(anyhow::anyhow!("android folder already exists")); } - let plugin_id = super::init::request_input( + let plugin_id = input( "What should be the Android Package ID for your plugin?", Some(format!("com.plugin.{}", plugin_name)), false, diff --git a/tooling/cli/src/plugin/init.rs b/tooling/cli/src/plugin/init.rs index fe484274a..7f631c0ab 100644 --- a/tooling/cli/src/plugin/init.rs +++ b/tooling/cli/src/plugin/init.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT +use crate::helpers::prompts::input; use crate::Result; use crate::{ helpers::{resolve_tauri_path, template}, @@ -9,7 +10,6 @@ use crate::{ }; use anyhow::Context; use clap::Parser; -use dialoguer::Input; use handlebars::{to_json, Handlebars}; use heck::{ToKebabCase, ToPascalCase, ToSnakeCase}; use include_dir::{include_dir, Dir}; @@ -18,10 +18,8 @@ use std::{ collections::BTreeMap, env::current_dir, ffi::OsStr, - fmt::Display, fs::{create_dir_all, remove_dir_all, File, OpenOptions}, path::{Component, Path, PathBuf}, - str::FromStr, }; pub const TEMPLATE_DIR: Dir<'_> = include_dir!("templates/plugin"); @@ -145,7 +143,7 @@ pub fn command(mut options: Options) -> Result<()> { } let plugin_id = if options.android || options.mobile { - let plugin_id = request_input( + let plugin_id = input( "What should be the Android Package ID for your plugin?", Some(format!("com.plugin.{}", plugin_name)), false, @@ -218,6 +216,10 @@ pub fn command(mut options: Options) -> Result<()> { ) .with_context(|| "failed to render plugin Android template")?; } + + std::fs::create_dir(template_target_path.join("permissions")) + .with_context(|| "failed to create `permissions` directory")?; + Ok(()) } @@ -278,29 +280,3 @@ pub fn generate_android_out_file( Ok(None) } } - -pub fn request_input( - prompt: &str, - initial: Option, - skip: bool, - allow_empty: bool, -) -> Result> -where - T: Clone + FromStr + Display + ToString, - T::Err: Display + std::fmt::Debug, -{ - if skip { - Ok(initial) - } else { - let theme = dialoguer::theme::ColorfulTheme::default(); - let mut builder = Input::with_theme(&theme) - .with_prompt(prompt) - .allow_empty(allow_empty); - - if let Some(v) = initial { - builder = builder.with_initial_text(v.to_string()); - } - - builder.interact_text().map(Some).map_err(Into::into) - } -} diff --git a/tooling/cli/src/signer/generate.rs b/tooling/cli/src/signer/generate.rs index 8bc3e9bcf..990ddc41c 100644 --- a/tooling/cli/src/signer/generate.rs +++ b/tooling/cli/src/signer/generate.rs @@ -23,13 +23,11 @@ pub struct Options { #[clap(short, long)] force: bool, /// Skip prompting for values - #[clap(long)] + #[clap(long, env = "CI")] ci: bool, } pub fn command(mut options: Options) -> Result<()> { - options.ci = options.ci || std::env::var("CI").is_ok(); - if options.ci && options.password.is_none() { log::warn!("Generating new private key without password. For security reasons, we recommend setting a password instead."); options.password.replace("".into());