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 <lucas@tauri.studio>
This commit is contained in:
Amr Bashir 2024-02-26 19:42:13 +02:00 committed by GitHub
parent 9be314f07a
commit 06d63d67a0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
30 changed files with 949 additions and 124 deletions

View File

@ -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`

View File

@ -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 {

View File

@ -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";

View File

@ -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<CapabilityRemote>,
/// 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<PermissionEntry>,
/// 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<Target>,
}

View File

@ -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<Vec<Value>>,
}
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<NonZeroU64>,
/// 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<String>,
/// 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,
}

View File

@ -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<DefaultPermission>,
/// A list of permissions sets defined
#[serde(default)]
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub set: Vec<PermissionSet>,
/// A list of inlined permissions

15
tooling/cli/Cargo.lock generated
View File

@ -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"

View File

@ -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]

View File

@ -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),
}
}

View File

@ -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<String>,
/// Capability description
#[clap(long)]
description: Option<String>,
/// Capability windows
#[clap(long)]
windows: Option<Vec<String>>,
/// Capability permissions
#[clap(long)]
permission: Option<Vec<String>>,
/// Output file format.
#[clap(long, default_value_t = FileFormat::Json)]
format: FileFormat,
/// The output file.
#[clap(short, long)]
out: Option<PathBuf>,
}
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::<String>("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::<String>(
"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<String> = match options.permission.map(FromIterator::from_iter) {
Some(p) => p,
None => prompts::input::<String>(
"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(())
}

View File

@ -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<S: Serialize>(&self, s: &S) -> crate::Result<String> {
let contents = match self {
Self::Json => serde_json::to_string_pretty(s)?,
Self::Toml => toml_edit::ser::to_string_pretty(s)?,
};
Ok(contents)
}
}

View File

@ -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<String> {
Ok(match self {
TomlOrJson::Toml(t) => t.to_string(),
TomlOrJson::Json(j) => serde_json::to_string_pretty(&j)?,
})
}
}
fn capability_from_path<P: AsRef<Path>>(path: P) -> Option<TomlOrJson> {
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::<toml_edit::Document>().ok())
.map(TomlOrJson::Toml),
Some("json") => std::fs::read(&path)
.ok()
.and_then(|c| serde_json::from_slice::<serde_json::Value>(&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<String>,
}
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::<Vec<_>>();
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::<Vec<_>>()
.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(())
}

View File

@ -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<String>,
/// Permission identifier filter.
#[clap(short, long)]
filter: Option<String>,
}
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::<BTreeMap<String, Manifest>>(&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::<Vec<_>>()
.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::<Vec<_>>()
.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::<Vec<_>>()
.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::<Vec<_>>()
.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")
}
}

View File

@ -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),
}
}

View File

@ -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<String>,
/// Permission description
#[clap(long)]
description: Option<String>,
/// List of commands to allow
#[clap(short, long, use_value_delimiter = true)]
allow: Option<Vec<String>>,
/// List of commands to deny
#[clap(short, long, use_value_delimiter = true)]
deny: Option<Vec<String>>,
/// Output file format.
#[clap(long, default_value_t = FileFormat::Json)]
format: FileFormat,
/// The output file.
#[clap(short, long)]
out: Option<PathBuf>,
}
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::<String>("What's the permission description?", None, false, true)?
.and_then(|d| if d.is_empty() { None } else { Some(d) }),
};
let allow: Vec<String> = options
.allow
.map(FromIterator::from_iter)
.unwrap_or_default();
let deny: Vec<String> = 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(())
}

View File

@ -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::<toml_edit::Document>() {
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::<serde_json::Value>(&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(())
}

View File

@ -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<String>,
/// 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

View File

@ -66,14 +66,16 @@ fn lookup<F: Fn(&PathBuf) -> bool>(dir: &Path, checker: F) -> Option<PathBuf> {
None
}
fn get_tauri_dir() -> PathBuf {
let cwd = current_dir().expect("failed to read cwd");
pub fn tauri_dir_opt() -> Option<PathBuf> {
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<PathBuf> {
pub fn app_dir() -> &'static PathBuf {
static APP_DIR: OnceLock<PathBuf> = 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()))
}

View File

@ -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;

View File

@ -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<T>(
prompt: &str,
initial: Option<T>,
skip: bool,
allow_empty: bool,
) -> Result<Option<T>>
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<bool>) -> Result<bool> {
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<T: ToString>(
prompt: &str,
items: &[T],
defaults: Option<&[bool]>,
) -> Result<Vec<usize>> {
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)
}

View File

@ -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> {
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 "<current dir>/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<T>(
prompt: &str,
initial: Option<T>,
skip: bool,
allow_empty: bool,
) -> Result<Option<T>>
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)
}
}

View File

@ -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<I: CommandFactory>(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)?,

View File

@ -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<Options> for BuildOptions {
@ -76,7 +79,7 @@ impl From<Options> for BuildOptions {
bundles: None,
config: options.config,
args: Vec::new(),
ci: false,
ci: options.ci,
}
}
}

View File

@ -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)]

View File

@ -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(())
}

View File

@ -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<Options> for BuildOptions {
@ -72,7 +75,7 @@ impl From<Options> for BuildOptions {
bundles: None,
config: options.config,
args: Vec::new(),
ci: false,
ci: options.ci,
}
}
}

View File

@ -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)]

View File

@ -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,

View File

@ -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<T>(
prompt: &str,
initial: Option<T>,
skip: bool,
allow_empty: bool,
) -> Result<Option<T>>
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)
}
}

View File

@ -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());