Ver código fonte

fix(core/acl): fix `core:default` schema generation (#10971)

* remove dbg! in resources test

* use methods from `fs` and `env` qualified

* share `ACL_MANIFESTS_FILE_NAME` and `CAPABILITIES_FILE_NAME` consts across crates

* simplifiy `Manifest::new` code for better readability

* move reading global api scripts logic next to the function that defines it

* [tauri-build] move acl logic from lib.rs to acl.rs

* use const value for schema instead of enum value with a single variant

* remove unnecessary info from permissions hover

* move related functions next to each other & improve readability of others

* use methods from `fs` and `env` qualified

* fix warning, unused return in test

* document some functions

* improve generated schema for better scope schema completion, simplify, reorganize and document the logic

previously if you had `fs` and `http` plugins added in a project
and then try to write an extended permission for `fs:allow-app-meta`
```json
{
      "identifier": "fs:allow-app-meta",
      "allow": [ <here> ]
}
```
and even though identifier is from `fs` plugin,
the JSON schema suggests `path` and `url`.
Now it will only suggest  relevant field which is `path`

* resolve permissions from other plugins, generate `core:default` as a normal set instead of special logic

* move `PERMISSION_SCHEMAS_FOLDER_NAME` to acl module

* use gneric trait because of MSRV

* ensure `gen/schemas` dir is created

* clippy
Amr Bashir 10 meses atrás
pai
commit
63649d82d2

+ 5 - 0
.changes/core-default-schema.md

@@ -0,0 +1,5 @@
+---
+"tauri": "patch:bug"
+---
+
+Fix schema generation for `core:default` set.

+ 90 - 247
crates/tauri-build/src/acl.rs

@@ -3,35 +3,22 @@
 // SPDX-License-Identifier: MIT
 
 use std::{
-  collections::{BTreeMap, BTreeSet, HashMap},
-  env::current_dir,
-  fs::{copy, create_dir_all, read_to_string, write},
+  collections::{BTreeMap, HashMap},
+  env, fs,
   path::{Path, PathBuf},
 };
 
 use anyhow::{Context, Result};
-use schemars::{
-  schema::{
-    ArrayValidation, InstanceType, Metadata, ObjectValidation, RootSchema, Schema, SchemaObject,
-    SubschemaValidation,
-  },
-  schema_for,
-};
 use tauri_utils::{
   acl::{
-    capability::{Capability, CapabilityFile},
-    manifest::Manifest,
-    APP_ACL_KEY,
+    capability::Capability, manifest::Manifest, schema::CAPABILITIES_SCHEMA_FOLDER_PATH,
+    ACL_MANIFESTS_FILE_NAME, APP_ACL_KEY, CAPABILITIES_FILE_NAME,
   },
   platform::Target,
   write_if_changed,
 };
 
-const CAPABILITIES_SCHEMA_FILE_NAME: &str = "schema.json";
-/// Path of the folder where schemas are saved.
-const CAPABILITIES_SCHEMA_FOLDER_PATH: &str = "gen/schemas";
-const CAPABILITIES_FILE_NAME: &str = "capabilities.json";
-const ACL_MANIFESTS_FILE_NAME: &str = "acl-manifests.json";
+use crate::Attributes;
 
 /// Definition of a plugin that is part of the Tauri application instead of having its own crate.
 ///
@@ -39,7 +26,7 @@ const ACL_MANIFESTS_FILE_NAME: &str = "acl-manifests.json";
 /// To change the glob pattern that is used to find permissions, use [`Self::permissions_path_pattern`].
 ///
 /// To autogenerate permissions for each of the plugin commands, see [`Self::commands`].
-#[derive(Debug, Default)]
+#[derive(Debug, Default, Clone)]
 pub struct InlinedPlugin {
   commands: &'static [&'static str],
   permissions_path_pattern: Option<&'static str>,
@@ -47,7 +34,7 @@ pub struct InlinedPlugin {
 }
 
 /// Variants of a generated default permission that can be used on an [`InlinedPlugin`].
-#[derive(Debug)]
+#[derive(Debug, Clone)]
 pub enum DefaultPermissionRule {
   /// Allow all commands from [`InlinedPlugin::commands`].
   AllowAllCommands,
@@ -95,7 +82,7 @@ impl InlinedPlugin {
 /// To change the glob pattern that is used to find permissions, use [`Self::permissions_path_pattern`].
 ///
 /// To autogenerate permissions for each of the app commands, see [`Self::commands`].
-#[derive(Debug, Default)]
+#[derive(Debug, Default, Clone, Copy)]
 pub struct AppManifest {
   commands: &'static [&'static str],
   permissions_path_pattern: Option<&'static str>,
@@ -124,229 +111,51 @@ impl AppManifest {
   }
 }
 
-fn capabilities_schema(acl_manifests: &BTreeMap<String, Manifest>) -> RootSchema {
-  let mut schema = schema_for!(CapabilityFile);
-
-  fn schema_from(key: &str, id: &str, description: Option<&str>) -> Schema {
-    let command_name = if key == APP_ACL_KEY {
-      id.to_string()
-    } else {
-      format!("{key}:{id}")
-    };
-    Schema::Object(SchemaObject {
-      metadata: Some(Box::new(Metadata {
-        description: description
-          .as_ref()
-          .map(|d| format!("{command_name} -> {d}")),
-        ..Default::default()
-      })),
-      instance_type: Some(InstanceType::String.into()),
-      enum_values: Some(vec![serde_json::Value::String(command_name)]),
-      ..Default::default()
-    })
-  }
-
-  let mut permission_schemas = Vec::new();
-
-  for (key, manifest) in acl_manifests {
-    for (set_id, set) in &manifest.permission_sets {
-      permission_schemas.push(schema_from(key, set_id, Some(&set.description)));
-    }
-
-    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(
-        key,
-        permission_id,
-        permission.description.as_deref(),
-      ));
-    }
-  }
-
-  if let Some(Schema::Object(obj)) = schema.definitions.get_mut("Identifier") {
-    obj.object = None;
-    obj.instance_type = None;
-    obj.metadata.as_mut().map(|metadata| {
-      metadata
-        .description
-        .replace("Permission identifier".to_string());
-      metadata
-    });
-    obj.subschemas.replace(Box::new(SubschemaValidation {
-      one_of: Some(permission_schemas),
-      ..Default::default()
-    }));
-  }
+/// Saves capabilities in a file inside the project, mainly to be read by tauri-cli.
+fn save_capabilities(capabilities: &BTreeMap<String, Capability>) -> Result<PathBuf> {
+  let dir = Path::new(CAPABILITIES_SCHEMA_FOLDER_PATH);
+  fs::create_dir_all(dir)?;
 
-  let mut definitions = Vec::new();
-
-  if let Some(Schema::Object(obj)) = schema.definitions.get_mut("PermissionEntry") {
-    let permission_entry_any_of_schemas = obj.subschemas().any_of.as_mut().unwrap();
-
-    if let Schema::Object(scope_extended_schema_obj) =
-      permission_entry_any_of_schemas.last_mut().unwrap()
-    {
-      let mut global_scope_one_of = Vec::new();
-
-      for (key, manifest) in acl_manifests {
-        if let Some(global_scope_schema) = &manifest.global_scope_schema {
-          let global_scope_schema_def: RootSchema =
-            serde_json::from_value(global_scope_schema.clone())
-              .unwrap_or_else(|e| panic!("invalid JSON schema for plugin {key}: {e}"));
-
-          let global_scope_schema = Schema::Object(SchemaObject {
-            array: Some(Box::new(ArrayValidation {
-              items: Some(Schema::Object(global_scope_schema_def.schema).into()),
-              ..Default::default()
-            })),
-            ..Default::default()
-          });
-
-          definitions.push(global_scope_schema_def.definitions);
-
-          let mut required = BTreeSet::new();
-          required.insert("identifier".to_string());
-
-          let mut object = ObjectValidation {
-            required,
-            ..Default::default()
-          };
-
-          let mut permission_schemas = Vec::new();
-          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)));
-          }
-          for permission in manifest.permissions.values() {
-            permission_schemas.push(schema_from(
-              key,
-              &permission.identifier,
-              permission.description.as_deref(),
-            ));
-          }
-
-          let identifier_schema = Schema::Object(SchemaObject {
-            subschemas: Some(Box::new(SubschemaValidation {
-              one_of: Some(permission_schemas),
-              ..Default::default()
-            })),
-            ..Default::default()
-          });
-
-          object
-            .properties
-            .insert("identifier".to_string(), identifier_schema);
-          object
-            .properties
-            .insert("allow".to_string(), global_scope_schema.clone());
-          object
-            .properties
-            .insert("deny".to_string(), global_scope_schema);
-
-          global_scope_one_of.push(Schema::Object(SchemaObject {
-            instance_type: Some(InstanceType::Object.into()),
-            object: Some(Box::new(object)),
-            ..Default::default()
-          }));
-        }
-      }
-
-      if !global_scope_one_of.is_empty() {
-        scope_extended_schema_obj.object = None;
-        scope_extended_schema_obj
-          .subschemas
-          .replace(Box::new(SubschemaValidation {
-            one_of: Some(global_scope_one_of),
-            ..Default::default()
-          }));
-      };
-    }
-  }
+  let path = dir.join(CAPABILITIES_FILE_NAME);
+  let json = serde_json::to_string(&capabilities)?;
+  write_if_changed(&path, json)?;
 
-  for definitions_map in definitions {
-    schema.definitions.extend(definitions_map);
-  }
-
-  schema
+  Ok(path)
 }
 
-pub fn generate_schema(acl_manifests: &BTreeMap<String, Manifest>, target: Target) -> Result<()> {
-  let schema = capabilities_schema(acl_manifests);
-  let schema_str = serde_json::to_string_pretty(&schema).unwrap();
-  let out_dir = PathBuf::from(CAPABILITIES_SCHEMA_FOLDER_PATH);
-  create_dir_all(&out_dir).context("unable to create schema output directory")?;
-
-  let schema_path = out_dir.join(format!("{target}-{CAPABILITIES_SCHEMA_FILE_NAME}"));
-  if schema_str != read_to_string(&schema_path).unwrap_or_default() {
-    write(&schema_path, schema_str)?;
-
-    copy(
-      schema_path,
-      out_dir.join(format!(
-        "{}-{CAPABILITIES_SCHEMA_FILE_NAME}",
-        if target.is_desktop() {
-          "desktop"
-        } else {
-          "mobile"
-        }
-      )),
-    )?;
-  }
+/// Saves ACL manifests in a file inside the project, mainly to be read by tauri-cli.
+fn save_acl_manifests(acl_manifests: &BTreeMap<String, Manifest>) -> Result<PathBuf> {
+  let dir = Path::new(CAPABILITIES_SCHEMA_FOLDER_PATH);
+  fs::create_dir_all(dir)?;
 
-  Ok(())
-}
+  let path = dir.join(ACL_MANIFESTS_FILE_NAME);
+  let json = serde_json::to_string(&acl_manifests)?;
+  write_if_changed(&path, json)?;
 
-pub fn save_capabilities(capabilities: &BTreeMap<String, Capability>) -> Result<PathBuf> {
-  let capabilities_path =
-    PathBuf::from(CAPABILITIES_SCHEMA_FOLDER_PATH).join(CAPABILITIES_FILE_NAME);
-  let capabilities_json = serde_json::to_string(&capabilities)?;
-  if capabilities_json != read_to_string(&capabilities_path).unwrap_or_default() {
-    std::fs::write(&capabilities_path, capabilities_json)?;
-  }
-  Ok(capabilities_path)
+  Ok(path)
 }
 
-pub fn save_acl_manifests(acl_manifests: &BTreeMap<String, Manifest>) -> Result<PathBuf> {
-  let acl_manifests_path =
-    PathBuf::from(CAPABILITIES_SCHEMA_FOLDER_PATH).join(ACL_MANIFESTS_FILE_NAME);
-  let acl_manifests_json = serde_json::to_string(&acl_manifests)?;
-  if acl_manifests_json != read_to_string(&acl_manifests_path).unwrap_or_default() {
-    std::fs::write(&acl_manifests_path, acl_manifests_json)?;
-  }
-  Ok(acl_manifests_path)
-}
+/// Read plugin permissions and scope schema from env vars
+fn read_plugins_manifests() -> Result<BTreeMap<String, Manifest>> {
+  use tauri_utils::acl;
 
-pub fn get_manifests_from_plugins() -> Result<BTreeMap<String, Manifest>> {
   let permission_map =
-    tauri_utils::acl::build::read_permissions().context("failed to read plugin permissions")?;
-  let mut global_scope_map = tauri_utils::acl::build::read_global_scope_schemas()
-    .context("failed to read global scope schemas")?;
+    acl::build::read_permissions().context("failed to read plugin permissions")?;
+  let mut global_scope_map =
+    acl::build::read_global_scope_schemas().context("failed to read global scope schemas")?;
+
+  let mut manifests = BTreeMap::new();
 
-  let mut processed = BTreeMap::new();
   for (plugin_name, permission_files) in permission_map {
-    let manifest = Manifest::new(permission_files, global_scope_map.remove(&plugin_name));
-    processed.insert(plugin_name, manifest);
+    let global_scope_schema = global_scope_map.remove(&plugin_name);
+    let manifest = Manifest::new(permission_files, global_scope_schema);
+    manifests.insert(plugin_name, manifest);
   }
 
-  Ok(processed)
+  Ok(manifests)
 }
 
-pub fn inline_plugins(
+fn inline_plugins(
   out_dir: &Path,
   inlined_plugins: HashMap<&'static str, InlinedPlugin>,
 ) -> Result<BTreeMap<String, Manifest>> {
@@ -354,7 +163,7 @@ pub fn inline_plugins(
 
   for (name, plugin) in inlined_plugins {
     let plugin_out_dir = out_dir.join("plugins").join(name);
-    create_dir_all(&plugin_out_dir)?;
+    fs::create_dir_all(&plugin_out_dir)?;
 
     let mut permission_files = if plugin.commands.is_empty() {
       Vec::new()
@@ -371,22 +180,22 @@ pub fn inline_plugins(
         DefaultPermissionRule::Allow(permissions) => permissions,
       });
       if let Some(default_permissions) = default_permissions {
-        let default_permission_toml = format!(
+        let default_permissions = default_permissions
+          .iter()
+          .map(|p| format!("\"{p}\""))
+          .collect::<Vec<String>>()
+          .join(",");
+        let default_permission = format!(
           r###"# Automatically generated - DO NOT EDIT!
 [default]
 permissions = [{default_permissions}]
-"###,
-          default_permissions = default_permissions
-            .iter()
-            .map(|p| format!("\"{p}\""))
-            .collect::<Vec<String>>()
-            .join(",")
+"###
         );
 
-        let default_permission_toml_path = plugin_out_dir.join("default.toml");
+        let default_permission_path = plugin_out_dir.join("default.toml");
 
-        write_if_changed(&default_permission_toml_path, default_permission_toml)
-          .unwrap_or_else(|_| panic!("unable to autogenerate {default_permission_toml_path:?}"));
+        write_if_changed(&default_permission_path, default_permission)
+          .unwrap_or_else(|_| panic!("unable to autogenerate {default_permission_path:?}"));
       }
 
       tauri_utils::acl::build::define_permissions(
@@ -430,13 +239,13 @@ permissions = [{default_permissions}]
   Ok(acl_manifests)
 }
 
-pub fn app_manifest_permissions(
+fn app_manifest_permissions(
   out_dir: &Path,
   manifest: AppManifest,
   inlined_plugins: &HashMap<&'static str, InlinedPlugin>,
 ) -> Result<Manifest> {
   let app_out_dir = out_dir.join("app-manifest");
-  create_dir_all(&app_out_dir)?;
+  fs::create_dir_all(&app_out_dir)?;
   let pkg_name = "__app__";
 
   let mut permission_files = if manifest.commands.is_empty() {
@@ -473,7 +282,7 @@ pub fn app_manifest_permissions(
       );
     }
 
-    let permissions_root = current_dir()?.join("permissions");
+    let permissions_root = env::current_dir()?.join("permissions");
     let inlined_plugins_permissions: Vec<_> = inlined_plugins
       .keys()
       .map(|name| permissions_root.join(name))
@@ -501,7 +310,7 @@ pub fn app_manifest_permissions(
   ))
 }
 
-pub fn validate_capabilities(
+fn validate_capabilities(
   acl_manifests: &BTreeMap<String, Manifest>,
   capabilities: &BTreeMap<String, Capability>,
 ) -> Result<()> {
@@ -523,10 +332,6 @@ pub fn validate_capabilities(
       let key = permission_id.get_prefix().unwrap_or(APP_ACL_KEY);
       let permission_name = permission_id.get_base();
 
-      if key == "core" && permission_name == "default" {
-        continue;
-      }
-
       let permission_exists = acl_manifests
         .get(key)
         .map(|manifest| {
@@ -567,3 +372,41 @@ pub fn validate_capabilities(
 
   Ok(())
 }
+
+pub fn build(out_dir: &Path, target: Target, attributes: &Attributes) -> super::Result<()> {
+  let mut acl_manifests = read_plugins_manifests()?;
+
+  let app_manifest = app_manifest_permissions(
+    out_dir,
+    attributes.app_manifest,
+    &attributes.inlined_plugins,
+  )?;
+  if app_manifest.default_permission.is_some()
+    || !app_manifest.permission_sets.is_empty()
+    || !app_manifest.permissions.is_empty()
+  {
+    acl_manifests.insert(APP_ACL_KEY.into(), app_manifest);
+  }
+
+  acl_manifests.extend(inline_plugins(out_dir, attributes.inlined_plugins.clone())?);
+
+  let acl_manifests_path = save_acl_manifests(&acl_manifests)?;
+  fs::copy(acl_manifests_path, out_dir.join(ACL_MANIFESTS_FILE_NAME))?;
+
+  tauri_utils::acl::schema::generate_capability_schema(&acl_manifests, target)?;
+
+  let capabilities = if let Some(pattern) = attributes.capabilities_path_pattern {
+    tauri_utils::acl::build::parse_capabilities(pattern)?
+  } else {
+    println!("cargo:rerun-if-changed=capabilities");
+    tauri_utils::acl::build::parse_capabilities("./capabilities/**/*")?
+  };
+  validate_capabilities(&acl_manifests, &capabilities)?;
+
+  let capabilities_path = save_capabilities(&capabilities)?;
+  fs::copy(capabilities_path, out_dir.join(CAPABILITIES_FILE_NAME))?;
+
+  tauri_utils::plugin::save_global_api_scripts_paths(out_dir);
+
+  Ok(())
+}

+ 24 - 63
crates/tauri-build/src/lib.rs

@@ -17,15 +17,13 @@ pub use anyhow::Result;
 use cargo_toml::Manifest;
 
 use tauri_utils::{
-  acl::{build::parse_capabilities, APP_ACL_KEY},
   config::{BundleResources, Config, WebviewInstallMode},
   resources::{external_binaries, ResourcePaths},
 };
 
 use std::{
   collections::HashMap,
-  env::var_os,
-  fs::copy,
+  env, fs,
   path::{Path, PathBuf},
 };
 
@@ -42,9 +40,6 @@ pub use codegen::context::CodegenContext;
 
 pub use acl::{AppManifest, DefaultPermissionRule, InlinedPlugin};
 
-const ACL_MANIFESTS_FILE_NAME: &str = "acl-manifests.json";
-const CAPABILITIES_FILE_NAME: &str = "capabilities.json";
-
 fn copy_file(from: impl AsRef<Path>, to: impl AsRef<Path>) -> Result<()> {
   let from = from.as_ref();
   let to = to.as_ref();
@@ -55,8 +50,8 @@ fn copy_file(from: impl AsRef<Path>, to: impl AsRef<Path>) -> Result<()> {
     return Err(anyhow::anyhow!("{:?} is not a file", from));
   }
   let dest_dir = to.parent().expect("No data in parent");
-  std::fs::create_dir_all(dest_dir)?;
-  std::fs::copy(from, to)?;
+  fs::create_dir_all(dest_dir)?;
+  fs::copy(from, to)?;
   Ok(())
 }
 
@@ -84,7 +79,7 @@ fn copy_binaries(
 
     let dest = path.join(file_name);
     if dest.exists() {
-      std::fs::remove_file(&dest).unwrap();
+      fs::remove_file(&dest).unwrap();
     }
     copy_file(&src, &dest)?;
   }
@@ -139,16 +134,16 @@ fn copy_dir(from: &Path, to: &Path) -> Result<()> {
     let rel_path = entry.path().strip_prefix(from)?;
     let dest_path = to.join(rel_path);
     if entry.file_type().is_symlink() {
-      let target = std::fs::read_link(entry.path())?;
+      let target = fs::read_link(entry.path())?;
       if entry.path().is_dir() {
         symlink_dir(&target, &dest_path)?;
       } else {
         symlink_file(&target, &dest_path)?;
       }
     } else if entry.file_type().is_dir() {
-      std::fs::create_dir(dest_path)?;
+      fs::create_dir(dest_path)?;
     } else {
-      std::fs::copy(entry.path(), dest_path)?;
+      fs::copy(entry.path(), dest_path)?;
     }
   }
   Ok(())
@@ -168,7 +163,7 @@ fn copy_framework_from(src_dir: &Path, framework: &str, dest_dir: &Path) -> Resu
 
 // Copies the macOS application bundle frameworks to the target folder
 fn copy_frameworks(dest_dir: &Path, frameworks: &[String]) -> Result<()> {
-  std::fs::create_dir_all(dest_dir)
+  fs::create_dir_all(dest_dir)
     .with_context(|| format!("Failed to create frameworks output directory at {dest_dir:?}"))?;
   for framework in frameworks.iter() {
     if framework.ends_with(".framework") {
@@ -420,8 +415,7 @@ impl Attributes {
 }
 
 pub fn is_dev() -> bool {
-  std::env::var("DEP_TAURI_DEV")
-    .expect("missing `cargo:dev` instruction, please update tauri to latest")
+  env::var("DEP_TAURI_DEV").expect("missing `cargo:dev` instruction, please update tauri to latest")
     == "true"
 }
 
@@ -471,19 +465,19 @@ pub fn try_build(attributes: Attributes) -> Result<()> {
   #[cfg(feature = "config-toml")]
   println!("cargo:rerun-if-changed=Tauri.toml");
 
-  let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap();
+  let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap();
   let mobile = target_os == "ios" || target_os == "android";
   cfg_alias("desktop", !mobile);
   cfg_alias("mobile", mobile);
 
-  let target_triple = std::env::var("TARGET").unwrap();
+  let target_triple = env::var("TARGET").unwrap();
   let target = tauri_utils::platform::Target::from_triple(&target_triple);
 
   let mut config = serde_json::from_value(tauri_utils::config::parse::read_from(
     target,
-    std::env::current_dir().unwrap(),
+    env::current_dir().unwrap(),
   )?)?;
-  if let Ok(env) = std::env::var("TAURI_CONFIG") {
+  if let Ok(env) = env::var("TAURI_CONFIG") {
     let merge_config: serde_json::Value = serde_json::from_str(&env)?;
     json_patch::merge(&mut config, &merge_config);
   }
@@ -506,7 +500,7 @@ pub fn try_build(attributes: Attributes) -> Result<()> {
   android_package_prefix.pop();
   println!("cargo:rustc-env=TAURI_ANDROID_PACKAGE_NAME_PREFIX={android_package_prefix}");
 
-  if let Some(project_dir) = var_os("TAURI_ANDROID_PROJECT_PATH").map(PathBuf::from) {
+  if let Some(project_dir) = env::var_os("TAURI_ANDROID_PROJECT_PATH").map(PathBuf::from) {
     mobile::generate_gradle_files(project_dir, &config)?;
   }
 
@@ -514,7 +508,7 @@ pub fn try_build(attributes: Attributes) -> Result<()> {
 
   let ws_path = get_workspace_dir()?;
   let mut manifest =
-    Manifest::<cargo_toml::Value>::from_slice_with_metadata(&std::fs::read("Cargo.toml")?)?;
+    Manifest::<cargo_toml::Value>::from_slice_with_metadata(&fs::read("Cargo.toml")?)?;
 
   if let Ok(ws_manifest) = Manifest::from_path(ws_path.join("Cargo.toml")) {
     Manifest::complete_from_path_and_workspace(
@@ -526,48 +520,15 @@ pub fn try_build(attributes: Attributes) -> Result<()> {
     Manifest::complete_from_path(&mut manifest, Path::new("Cargo.toml"))?;
   }
 
-  let out_dir = PathBuf::from(std::env::var("OUT_DIR").unwrap());
+  let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
 
   manifest::check(&config, &mut manifest)?;
 
-  let mut acl_manifests = acl::get_manifests_from_plugins()?;
-  let app_manifest = acl::app_manifest_permissions(
-    &out_dir,
-    attributes.app_manifest,
-    &attributes.inlined_plugins,
-  )?;
-  if app_manifest.default_permission.is_some()
-    || !app_manifest.permission_sets.is_empty()
-    || !app_manifest.permissions.is_empty()
-  {
-    acl_manifests.insert(APP_ACL_KEY.into(), app_manifest);
-  }
-  acl_manifests.extend(acl::inline_plugins(&out_dir, attributes.inlined_plugins)?);
-
-  std::fs::write(
-    out_dir.join(ACL_MANIFESTS_FILE_NAME),
-    serde_json::to_string(&acl_manifests)?,
-  )?;
-
-  let capabilities = if let Some(pattern) = attributes.capabilities_path_pattern {
-    parse_capabilities(pattern)?
-  } else {
-    println!("cargo:rerun-if-changed=capabilities");
-    parse_capabilities("./capabilities/**/*")?
-  };
-  acl::generate_schema(&acl_manifests, target)?;
-  acl::validate_capabilities(&acl_manifests, &capabilities)?;
-
-  let capabilities_path = acl::save_capabilities(&capabilities)?;
-  copy(capabilities_path, out_dir.join(CAPABILITIES_FILE_NAME))?;
-
-  acl::save_acl_manifests(&acl_manifests)?;
-
-  tauri_utils::plugin::load_global_api_scripts(&out_dir);
+  acl::build(&out_dir, target, &attributes)?;
 
   println!("cargo:rustc-env=TAURI_ENV_TARGET_TRIPLE={target_triple}");
   // when running codegen in this build script, we need to access the env var directly
-  std::env::set_var("TAURI_ENV_TARGET_TRIPLE", &target_triple);
+  env::set_var("TAURI_ENV_TARGET_TRIPLE", &target_triple);
 
   // TODO: far from ideal, but there's no other way to get the target dir, see <https://github.com/rust-lang/cargo/issues/5457>
   let target_dir = out_dir
@@ -612,7 +573,7 @@ pub fn try_build(attributes: Attributes) -> Result<()> {
     if let Some(frameworks) = &config.bundle.macos.frameworks {
       if !frameworks.is_empty() {
         let frameworks_dir = target_dir.parent().unwrap().join("Frameworks");
-        let _ = std::fs::remove_dir_all(&frameworks_dir);
+        let _ = fs::remove_dir_all(&frameworks_dir);
         // copy frameworks to the root `target` folder (instead of `target/debug` for instance)
         // because the rpath is set to `@executable_path/../Frameworks`.
         copy_frameworks(&frameworks_dir, frameworks)?;
@@ -700,17 +661,17 @@ pub fn try_build(attributes: Attributes) -> Result<()> {
       )
     })?;
 
-    let target_env = std::env::var("CARGO_CFG_TARGET_ENV").unwrap();
+    let target_env = env::var("CARGO_CFG_TARGET_ENV").unwrap();
     match target_env.as_str() {
       "gnu" => {
-        let target_arch = match std::env::var("CARGO_CFG_TARGET_ARCH").unwrap().as_str() {
+        let target_arch = match env::var("CARGO_CFG_TARGET_ARCH").unwrap().as_str() {
           "x86_64" => Some("x64"),
           "x86" => Some("x86"),
           "aarch64" => Some("arm64"),
           arch => None,
         };
         if let Some(target_arch) = target_arch {
-          for entry in std::fs::read_dir(target_dir.join("build"))? {
+          for entry in fs::read_dir(target_dir.join("build"))? {
             let path = entry?.path();
             let webview2_loader_path = path
               .join("out")
@@ -718,14 +679,14 @@ pub fn try_build(attributes: Attributes) -> Result<()> {
               .join("WebView2Loader.dll");
             if path.to_string_lossy().contains("webview2-com-sys") && webview2_loader_path.exists()
             {
-              std::fs::copy(webview2_loader_path, target_dir.join("WebView2Loader.dll"))?;
+              fs::copy(webview2_loader_path, target_dir.join("WebView2Loader.dll"))?;
               break;
             }
           }
         }
       }
       "msvc" => {
-        if std::env::var("STATIC_VCRUNTIME").map_or(false, |v| v == "true") {
+        if env::var("STATIC_VCRUNTIME").map_or(false, |v| v == "true") {
           static_vcruntime::build();
         }
       }

+ 1 - 3
crates/tauri-cli/config.schema.json

@@ -1748,9 +1748,7 @@
       "anyOf": [
         {
           "description": "Bundle all targets.",
-          "enum": [
-            "all"
-          ]
+          "const": "all"
         },
         {
           "description": "A list of bundle targets.",

+ 8 - 30
crates/tauri-codegen/src/context.rs

@@ -18,6 +18,7 @@ use proc_macro2::TokenStream;
 use quote::quote;
 use sha2::{Digest, Sha256};
 use syn::Expr;
+use tauri_utils::acl::{ACL_MANIFESTS_FILE_NAME, CAPABILITIES_FILE_NAME};
 use tauri_utils::{
   acl::capability::{Capability, CapabilityFile},
   acl::manifest::Manifest,
@@ -26,13 +27,9 @@ use tauri_utils::{
   config::{CapabilityEntry, Config, FrontendDist, PatternKind},
   html::{inject_nonce_token, parse as parse_html, serialize_node as serialize_html_node, NodeRef},
   platform::Target,
-  plugin::GLOBAL_API_SCRIPT_FILE_LIST_PATH,
   tokens::{map_lit, str_lit},
 };
 
-const ACL_MANIFESTS_FILE_NAME: &str = "acl-manifests.json";
-const CAPABILITIES_FILE_NAME: &str = "capabilities.json";
-
 /// Necessary data needed by [`context_codegen`] to generate code for a Tauri application context.
 pub struct ContextData {
   pub dev: bool,
@@ -450,32 +447,13 @@ pub fn context_codegen(data: ContextData) -> EmbeddedAssetsResult<TokenStream> {
   let resolved = Resolved::resolve(&acl, capabilities, target).expect("failed to resolve ACL");
   let runtime_authority = quote!(#root::ipc::RuntimeAuthority::new(#acl_tokens, #resolved));
 
-  let plugin_global_api_script_file_list_path = out_dir.join(GLOBAL_API_SCRIPT_FILE_LIST_PATH);
-  let plugin_global_api_script =
-    if config.app.with_global_tauri && plugin_global_api_script_file_list_path.exists() {
-      let file_list_str = std::fs::read_to_string(plugin_global_api_script_file_list_path)
-        .expect("failed to read plugin global API script paths");
-      let file_list = serde_json::from_str::<Vec<PathBuf>>(&file_list_str)
-        .expect("failed to parse plugin global API script paths");
-
-      let mut plugins = Vec::new();
-      for path in file_list {
-        plugins.push(std::fs::read_to_string(&path).unwrap_or_else(|e| {
-          panic!(
-            "failed to read plugin global API script {}: {e}",
-            path.display()
-          )
-        }));
-      }
-
-      Some(plugins)
+  let plugin_global_api_scripts = if config.app.with_global_tauri {
+    if let Some(scripts) = tauri_utils::plugin::read_global_api_scripts(&out_dir) {
+      let scripts = scripts.into_iter().map(|s| quote!(#s));
+      quote!(::std::option::Option::Some(&[#(#scripts),*]))
     } else {
-      None
-    };
-
-  let plugin_global_api_script = if let Some(scripts) = plugin_global_api_script {
-    let scripts = scripts.into_iter().map(|s| quote!(#s));
-    quote!(::std::option::Option::Some(&[#(#scripts),*]))
+      quote!(::std::option::Option::None)
+    }
   } else {
     quote!(::std::option::Option::None)
   };
@@ -501,7 +479,7 @@ pub fn context_codegen(data: ContextData) -> EmbeddedAssetsResult<TokenStream> {
       #package_info,
       #pattern,
       #runtime_authority,
-      #plugin_global_api_script
+      #plugin_global_api_scripts
     );
 
     #with_tray_icon_code

+ 5 - 6
crates/tauri-plugin/src/build/mod.rs

@@ -11,7 +11,7 @@ pub mod mobile;
 
 use serde::de::DeserializeOwned;
 
-use std::{env::var, io::Cursor};
+use std::{env, io::Cursor};
 
 const RESERVED_PLUGIN_NAMES: &[&str] = &["core", "tauri"];
 
@@ -20,7 +20,7 @@ pub fn plugin_config<T: DeserializeOwned>(name: &str) -> Option<T> {
     "TAURI_{}_PLUGIN_CONFIG",
     name.to_uppercase().replace('-', "_")
   );
-  if let Ok(config_str) = var(&config_env_var_name) {
+  if let Ok(config_str) = env::var(&config_env_var_name) {
     println!("cargo:rerun-if-env-changed={config_env_var_name}");
     serde_json::from_reader(Cursor::new(config_str))
       .map(Some)
@@ -105,10 +105,9 @@ impl<'a> Builder<'a> {
     let _links = std::env::var("CARGO_MANIFEST_LINKS").map_err(|_| Error::LinksMissing)?;
 
     let autogenerated = Path::new("permissions").join(acl::build::AUTOGENERATED_FOLDER_NAME);
-    let commands_dir = autogenerated.join("commands");
-
     std::fs::create_dir_all(&autogenerated).expect("unable to create permissions dir");
 
+    let commands_dir = autogenerated.join("commands");
     if !self.commands.is_empty() {
       acl::build::autogenerate_command_permissions(&commands_dir, self.commands, "", true);
     }
@@ -120,12 +119,12 @@ impl<'a> Builder<'a> {
     if permissions.is_empty() {
       let _ = std::fs::remove_file(format!(
         "./permissions/{}/{}",
-        acl::build::PERMISSION_SCHEMAS_FOLDER_NAME,
+        acl::PERMISSION_SCHEMAS_FOLDER_NAME,
         acl::PERMISSION_SCHEMA_FILE_NAME
       ));
       let _ = std::fs::remove_file(autogenerated.join(acl::build::PERMISSION_DOCS_FILE_NAME));
     } else {
-      acl::build::generate_schema(&permissions, "./permissions")?;
+      acl::schema::generate_permissions_schema(&permissions, "./permissions")?;
       acl::build::generate_docs(
         &permissions,
         &autogenerated,

+ 1 - 3
crates/tauri-schema-generator/schemas/config.schema.json

@@ -1748,9 +1748,7 @@
       "anyOf": [
         {
           "description": "Bundle all targets.",
-          "enum": [
-            "all"
-          ]
+          "const": "all"
         },
         {
           "description": "A list of bundle targets.",

+ 163 - 264
crates/tauri-utils/src/acl/build.rs

@@ -6,21 +6,16 @@
 
 use std::{
   collections::{BTreeMap, HashMap},
-  env::{current_dir, vars_os},
-  fs::{create_dir_all, read_to_string, write},
+  env, fs,
   path::{Path, PathBuf},
 };
 
 use crate::{acl::Error, write_if_changed};
-use schemars::{
-  schema::{InstanceType, Metadata, RootSchema, Schema, SchemaObject, SubschemaValidation},
-  schema_for,
-};
 
 use super::{
   capability::{Capability, CapabilityFile},
   manifest::PermissionFile,
-  PERMISSION_SCHEMA_FILE_NAME,
+  PERMISSION_SCHEMAS_FOLDER_NAME, PERMISSION_SCHEMA_FILE_NAME,
 };
 
 /// Known name of the folder containing autogenerated permissions.
@@ -35,9 +30,6 @@ pub const GLOBAL_SCOPE_SCHEMA_PATH_KEY: &str = "GLOBAL_SCOPE_SCHEMA_PATH";
 /// Allowed permission file extensions
 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 documentation file
 pub const PERMISSION_DOCS_FILE_NAME: &str = "reference.md";
 
@@ -49,6 +41,21 @@ const CAPABILITIES_SCHEMA_FOLDER_NAME: &str = "schemas";
 
 const CORE_PLUGIN_PERMISSIONS_TOKEN: &str = "__CORE_PLUGIN__";
 
+fn parse_permissions(paths: Vec<PathBuf>) -> Result<Vec<PermissionFile>, Error> {
+  let mut permissions = Vec::new();
+  for path in paths {
+    let permission_file = fs::read_to_string(&path).map_err(Error::ReadFile)?;
+    let ext = path.extension().unwrap().to_string_lossy().to_string();
+    let permission: PermissionFile = match ext.as_str() {
+      "toml" => toml::from_str(&permission_file)?,
+      "json" => serde_json::from_str(&permission_file)?,
+      _ => return Err(Error::UnknownPermissionFormat(ext)),
+    };
+    permissions.push(permission);
+  }
+  Ok(permissions)
+}
+
 /// Write the permissions to a temporary directory and pass it to the immediate consuming crate.
 pub fn define_permissions<F: Fn(&Path) -> bool>(
   pattern: &str,
@@ -71,13 +78,10 @@ pub fn define_permissions<F: Fn(&Path) -> bool>(
     .filter(|p| p.parent().unwrap().file_name().unwrap() != PERMISSION_SCHEMAS_FOLDER_NAME)
     .collect::<Vec<PathBuf>>();
 
-  let permission_files_path =
-    out_dir.join(format!("{}-permission-files", pkg_name.replace(':', "-")));
-  std::fs::write(
-    &permission_files_path,
-    serde_json::to_string(&permission_files)?,
-  )
-  .map_err(Error::WriteFile)?;
+  let pkg_name_valid_path = pkg_name.replace(':', "-");
+  let permission_files_path = out_dir.join(format!("{}-permission-files", pkg_name_valid_path));
+  let permission_files_json = serde_json::to_string(&permission_files)?;
+  fs::write(&permission_files_path, permission_files_json).map_err(Error::WriteFile)?;
 
   if let Some(plugin_name) = pkg_name.strip_prefix("tauri:") {
     println!(
@@ -94,6 +98,40 @@ pub fn define_permissions<F: Fn(&Path) -> bool>(
   parse_permissions(permission_files)
 }
 
+/// Read all permissions listed from the defined cargo cfg key value.
+pub fn read_permissions() -> Result<HashMap<String, Vec<PermissionFile>>, Error> {
+  let mut permissions_map = HashMap::new();
+
+  for (key, value) in env::vars_os() {
+    let key = key.to_string_lossy();
+
+    if let Some(plugin_crate_name_var) = key
+      .strip_prefix("DEP_")
+      .and_then(|v| v.strip_suffix(&format!("_{PERMISSION_FILES_PATH_KEY}")))
+      .map(|v| {
+        v.strip_suffix(CORE_PLUGIN_PERMISSIONS_TOKEN)
+          .and_then(|v| v.strip_prefix("TAURI_"))
+          .unwrap_or(v)
+      })
+    {
+      let permissions_path = PathBuf::from(value);
+      let permissions_str = fs::read_to_string(&permissions_path).map_err(Error::ReadFile)?;
+      let permissions: Vec<PathBuf> = serde_json::from_str(&permissions_str)?;
+      let permissions = parse_permissions(permissions)?;
+
+      let plugin_crate_name = plugin_crate_name_var.to_lowercase().replace('_', "-");
+      let plugin_crate_name = plugin_crate_name
+        .strip_prefix("tauri-plugin-")
+        .map(ToString::to_string)
+        .unwrap_or(plugin_crate_name);
+
+      permissions_map.insert(plugin_crate_name, permissions);
+    }
+  }
+
+  Ok(permissions_map)
+}
+
 /// Define the global scope schema JSON file path if it exists and pass it to the immediate consuming crate.
 pub fn define_global_scope_schema(
   schema: schemars::schema::RootSchema,
@@ -101,7 +139,7 @@ pub fn define_global_scope_schema(
   out_dir: &Path,
 ) -> Result<(), Error> {
   let path = out_dir.join("global-scope.json");
-  write(&path, serde_json::to_vec(&schema)?).map_err(Error::WriteFile)?;
+  fs::write(&path, serde_json::to_vec(&schema)?).map_err(Error::WriteFile)?;
 
   if let Some(plugin_name) = pkg_name.strip_prefix("tauri:") {
     println!(
@@ -115,13 +153,44 @@ pub fn define_global_scope_schema(
   Ok(())
 }
 
+/// Read all global scope schemas listed from the defined cargo cfg key value.
+pub fn read_global_scope_schemas() -> Result<HashMap<String, serde_json::Value>, Error> {
+  let mut schemas_map = HashMap::new();
+
+  for (key, value) in env::vars_os() {
+    let key = key.to_string_lossy();
+
+    if let Some(plugin_crate_name_var) = key
+      .strip_prefix("DEP_")
+      .and_then(|v| v.strip_suffix(&format!("_{GLOBAL_SCOPE_SCHEMA_PATH_KEY}")))
+      .map(|v| {
+        v.strip_suffix(CORE_PLUGIN_PERMISSIONS_TOKEN)
+          .and_then(|v| v.strip_prefix("TAURI_"))
+          .unwrap_or(v)
+      })
+    {
+      let path = PathBuf::from(value);
+      let json = fs::read_to_string(&path).map_err(Error::ReadFile)?;
+      let schema: serde_json::Value = serde_json::from_str(&json)?;
+
+      let plugin_crate_name = plugin_crate_name_var.to_lowercase().replace('_', "-");
+      let plugin_crate_name = plugin_crate_name
+        .strip_prefix("tauri-plugin-")
+        .map(ToString::to_string)
+        .unwrap_or(plugin_crate_name);
+
+      schemas_map.insert(plugin_crate_name, schema);
+    }
+  }
+
+  Ok(schemas_map)
+}
+
 /// Parses all capability files with the given glob pattern.
-pub fn parse_capabilities(
-  capabilities_path_pattern: &str,
-) -> Result<BTreeMap<String, Capability>, Error> {
+pub fn parse_capabilities(pattern: &str) -> Result<BTreeMap<String, Capability>, Error> {
   let mut capabilities_map = BTreeMap::new();
 
-  for path in glob::glob(capabilities_path_pattern)?
+  for path in glob::glob(pattern)?
     .flatten() // filter extension
     .filter(|p| {
       p.extension()
@@ -140,6 +209,7 @@ pub fn parse_capabilities(
             identifier: capability.identifier,
           });
         }
+
         capabilities_map.insert(capability.identifier.clone(), capability);
       }
       CapabilityFile::List(capabilities) | CapabilityFile::NamedList { capabilities } => {
@@ -149,6 +219,7 @@ pub fn parse_capabilities(
               identifier: capability.identifier,
             });
           }
+
           capabilities_map.insert(capability.identifier.clone(), capability);
         }
       }
@@ -158,246 +229,6 @@ pub fn parse_capabilities(
   Ok(capabilities_map)
 }
 
-fn permissions_schema(permissions: &[PermissionFile]) -> RootSchema {
-  let mut schema = schema_for!(PermissionFile);
-
-  fn schema_from(id: &str, description: Option<&str>) -> Schema {
-    Schema::Object(SchemaObject {
-      metadata: Some(Box::new(Metadata {
-        description: description.map(|d| format!("{id} -> {d}")),
-        ..Default::default()
-      })),
-      instance_type: Some(InstanceType::String.into()),
-      enum_values: Some(vec![serde_json::Value::String(id.into())]),
-      ..Default::default()
-    })
-  }
-
-  let mut permission_schemas = Vec::new();
-  for file in permissions {
-    if let Some(permission) = &file.default {
-      permission_schemas.push(schema_from("default", permission.description.as_deref()));
-    }
-
-    permission_schemas.extend(
-      file
-        .set
-        .iter()
-        .map(|set| schema_from(&set.identifier, Some(set.description.as_str())))
-        .collect::<Vec<_>>(),
-    );
-
-    permission_schemas.extend(
-      file
-        .permission
-        .iter()
-        .map(|permission| schema_from(&permission.identifier, permission.description.as_deref()))
-        .collect::<Vec<_>>(),
-    );
-  }
-
-  if let Some(Schema::Object(obj)) = schema.definitions.get_mut("PermissionSet") {
-    if let Some(Schema::Object(permissions_prop_schema)) =
-      obj.object().properties.get_mut("permissions")
-    {
-      permissions_prop_schema.array().items.replace(
-        Schema::Object(SchemaObject {
-          reference: Some("#/definitions/PermissionKind".into()),
-          ..Default::default()
-        })
-        .into(),
-      );
-
-      schema.definitions.insert(
-        "PermissionKind".into(),
-        Schema::Object(SchemaObject {
-          instance_type: Some(InstanceType::String.into()),
-          subschemas: Some(Box::new(SubschemaValidation {
-            one_of: Some(permission_schemas),
-            ..Default::default()
-          })),
-          ..Default::default()
-        }),
-      );
-    }
-  }
-
-  schema
-}
-
-/// Generate and write a schema based on the format of a [`PermissionFile`].
-pub fn generate_schema<P: AsRef<Path>>(
-  permissions: &[PermissionFile],
-  out_dir: P,
-) -> Result<(), Error> {
-  let schema = permissions_schema(permissions);
-  let schema_str = serde_json::to_string_pretty(&schema).unwrap();
-
-  let out_dir = out_dir.as_ref().join(PERMISSION_SCHEMAS_FOLDER_NAME);
-  create_dir_all(&out_dir).expect("unable to create schema output directory");
-
-  let schema_path = out_dir.join(PERMISSION_SCHEMA_FILE_NAME);
-  if schema_str != read_to_string(&schema_path).unwrap_or_default() {
-    write(schema_path, schema_str).map_err(Error::WriteFile)?;
-  }
-
-  Ok(())
-}
-
-/// Generate a markdown documentation page containing the list of permissions of the plugin.
-pub fn generate_docs(
-  permissions: &[PermissionFile],
-  out_dir: &Path,
-  plugin_identifier: &str,
-) -> Result<(), Error> {
-  let mut permission_table = "".to_string();
-  let permission_table_header =
-    "## Permission Table \n\n<table>\n<tr>\n<th>Identifier</th>\n<th>Description</th>\n</tr>\n"
-      .to_string();
-
-  let mut default_permission = "## Default Permission\n\n".to_string();
-  let mut contains_default = false;
-
-  fn docs_from(id: &str, description: Option<&str>, plugin_identifier: &str) -> String {
-    let mut docs = format!("\n<tr>\n<td>\n\n`{plugin_identifier}:{id}`\n\n</td>\n");
-    if let Some(d) = description {
-      docs.push_str(&format!("<td>\n\n{d}\n\n</td>"));
-    }
-    docs.push_str("\n</tr>");
-    docs
-  }
-
-  for permission in permissions {
-    for set in &permission.set {
-      permission_table.push_str(&docs_from(
-        &set.identifier,
-        Some(&set.description),
-        plugin_identifier,
-      ));
-      permission_table.push('\n');
-    }
-
-    if let Some(default) = &permission.default {
-      default_permission.push_str(default.description.as_deref().unwrap_or_default());
-      default_permission.push('\n');
-      default_permission.push('\n');
-      for permission in &default.permissions {
-        default_permission.push_str(&format!("- `{permission}`"));
-        default_permission.push('\n');
-      }
-
-      contains_default = true;
-    }
-
-    for permission in &permission.permission {
-      permission_table.push_str(&docs_from(
-        &permission.identifier,
-        permission.description.as_deref(),
-        plugin_identifier,
-      ));
-      permission_table.push('\n');
-    }
-  }
-  permission_table.push_str("</table>");
-
-  if !contains_default {
-    default_permission = "".to_string();
-  }
-
-  let docs = format!("{default_permission}\n{permission_table_header}\n{permission_table}\n");
-
-  let reference_path = out_dir.join(PERMISSION_DOCS_FILE_NAME);
-  if docs != read_to_string(&reference_path).unwrap_or_default() {
-    std::fs::write(reference_path, docs).map_err(Error::WriteFile)?;
-  }
-
-  Ok(())
-}
-
-/// Read all permissions listed from the defined cargo cfg key value.
-pub fn read_permissions() -> Result<HashMap<String, Vec<PermissionFile>>, Error> {
-  let mut permissions_map = HashMap::new();
-
-  for (key, value) in vars_os() {
-    let key = key.to_string_lossy();
-
-    if let Some(plugin_crate_name_var) = key
-      .strip_prefix("DEP_")
-      .and_then(|v| v.strip_suffix(&format!("_{PERMISSION_FILES_PATH_KEY}")))
-      .map(|v| {
-        v.strip_suffix(CORE_PLUGIN_PERMISSIONS_TOKEN)
-          .and_then(|v| v.strip_prefix("TAURI_"))
-          .unwrap_or(v)
-      })
-    {
-      let permissions_path = PathBuf::from(value);
-      let permissions_str = std::fs::read_to_string(&permissions_path).map_err(Error::ReadFile)?;
-      let permissions: Vec<PathBuf> = serde_json::from_str(&permissions_str)?;
-      let permissions = parse_permissions(permissions)?;
-
-      let plugin_crate_name = plugin_crate_name_var.to_lowercase().replace('_', "-");
-      permissions_map.insert(
-        plugin_crate_name
-          .strip_prefix("tauri-plugin-")
-          .map(|n| n.to_string())
-          .unwrap_or(plugin_crate_name),
-        permissions,
-      );
-    }
-  }
-
-  Ok(permissions_map)
-}
-
-/// Read all global scope schemas listed from the defined cargo cfg key value.
-pub fn read_global_scope_schemas() -> Result<HashMap<String, serde_json::Value>, Error> {
-  let mut permissions_map = HashMap::new();
-
-  for (key, value) in vars_os() {
-    let key = key.to_string_lossy();
-
-    if let Some(plugin_crate_name_var) = key
-      .strip_prefix("DEP_")
-      .and_then(|v| v.strip_suffix(&format!("_{GLOBAL_SCOPE_SCHEMA_PATH_KEY}")))
-      .map(|v| {
-        v.strip_suffix(CORE_PLUGIN_PERMISSIONS_TOKEN)
-          .and_then(|v| v.strip_prefix("TAURI_"))
-          .unwrap_or(v)
-      })
-    {
-      let path = PathBuf::from(value);
-      let json = std::fs::read_to_string(&path).map_err(Error::ReadFile)?;
-      let schema: serde_json::Value = serde_json::from_str(&json)?;
-
-      let plugin_crate_name = plugin_crate_name_var.to_lowercase().replace('_', "-");
-      permissions_map.insert(
-        plugin_crate_name
-          .strip_prefix("tauri-plugin-")
-          .map(|n| n.to_string())
-          .unwrap_or(plugin_crate_name),
-        schema,
-      );
-    }
-  }
-
-  Ok(permissions_map)
-}
-
-fn parse_permissions(paths: Vec<PathBuf>) -> Result<Vec<PermissionFile>, Error> {
-  let mut permissions = Vec::new();
-  for path in paths {
-    let permission_file = std::fs::read_to_string(&path).map_err(Error::ReadFile)?;
-    let ext = path.extension().unwrap().to_string_lossy().to_string();
-    let permission: PermissionFile = match ext.as_str() {
-      "toml" => toml::from_str(&permission_file)?,
-      "json" => serde_json::from_str(&permission_file)?,
-      _ => return Err(Error::UnknownPermissionFormat(ext)),
-    };
-    permissions.push(permission);
-  }
-  Ok(permissions)
-}
-
 /// Permissions that are generated from commands using [`autogenerate_command_permissions`].
 pub struct AutogeneratedPermissions {
   /// The allow permissions generated from commands.
@@ -414,11 +245,11 @@ pub fn autogenerate_command_permissions(
   schema_ref: bool,
 ) -> AutogeneratedPermissions {
   if !path.exists() {
-    create_dir_all(path).expect("unable to create autogenerated commands dir");
+    fs::create_dir_all(path).expect("unable to create autogenerated commands dir");
   }
 
   let schema_entry = if schema_ref {
-    let cwd = current_dir().unwrap();
+    let cwd = env::current_dir().unwrap();
     let components_len = path.strip_prefix(&cwd).unwrap_or(path).components().count();
     let schema_path = (1..components_len)
       .map(|_| "..")
@@ -473,3 +304,71 @@ commands.deny = ["{command}"]
 
   autogenerated
 }
+
+const PERMISSION_TABLE_HEADER: &str =
+  "## Permission Table \n\n<table>\n<tr>\n<th>Identifier</th>\n<th>Description</th>\n</tr>\n";
+
+/// Generate a markdown documentation page containing the list of permissions of the plugin.
+pub fn generate_docs(
+  permissions: &[PermissionFile],
+  out_dir: &Path,
+  plugin_identifier: &str,
+) -> Result<(), Error> {
+  let mut permission_table = "".to_string();
+
+  let mut default_permission = "## Default Permission\n\n".to_string();
+  let mut contains_default = false;
+
+  fn docs_from(id: &str, description: Option<&str>, plugin_identifier: &str) -> String {
+    let mut docs = format!("\n<tr>\n<td>\n\n`{plugin_identifier}:{id}`\n\n</td>\n");
+    if let Some(d) = description {
+      docs.push_str(&format!("<td>\n\n{d}\n\n</td>"));
+    }
+    docs.push_str("\n</tr>");
+    docs
+  }
+
+  for permission in permissions {
+    for set in &permission.set {
+      permission_table.push_str(&docs_from(
+        &set.identifier,
+        Some(&set.description),
+        plugin_identifier,
+      ));
+      permission_table.push('\n');
+    }
+
+    if let Some(default) = &permission.default {
+      contains_default = true;
+
+      default_permission.push_str(default.description.as_deref().unwrap_or_default());
+      default_permission.push('\n');
+      default_permission.push('\n');
+      for permission in &default.permissions {
+        default_permission.push_str(&format!("- `{permission}`"));
+        default_permission.push('\n');
+      }
+    }
+
+    for permission in &permission.permission {
+      permission_table.push_str(&docs_from(
+        &permission.identifier,
+        permission.description.as_deref(),
+        plugin_identifier,
+      ));
+      permission_table.push('\n');
+    }
+  }
+
+  if !contains_default {
+    default_permission = "".to_string();
+  }
+
+  let docs =
+    format!("{default_permission}\n{PERMISSION_TABLE_HEADER}\n{permission_table}</table>\n");
+
+  let reference_path = out_dir.join(PERMISSION_DOCS_FILE_NAME);
+  write_if_changed(reference_path, docs).map_err(Error::WriteFile)?;
+
+  Ok(())
+}

+ 11 - 0
crates/tauri-utils/src/acl/capability.rs

@@ -195,6 +195,17 @@ pub struct Capability {
   pub platforms: Option<Vec<Target>>,
 }
 
+impl Capability {
+  /// Whether this capability should be active based on the platform target or not.
+  pub fn is_active(&self, target: &Target) -> bool {
+    self
+      .platforms
+      .as_ref()
+      .map(|platforms| platforms.contains(target))
+      .unwrap_or(true)
+  }
+}
+
 #[cfg(feature = "schema")]
 fn unique_permission(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
   use schemars::schema;

+ 40 - 24
crates/tauri-utils/src/acl/manifest.rs

@@ -7,6 +7,8 @@
 use std::{collections::BTreeMap, num::NonZeroU64};
 
 use super::{Permission, PermissionSet};
+#[cfg(feature = "schema")]
+use schemars::schema::*;
 use serde::{Deserialize, Serialize};
 
 /// The default permission set of the plugin.
@@ -44,7 +46,7 @@ pub struct PermissionFile {
 }
 
 /// Plugin manifest.
-#[derive(Debug, Serialize, Deserialize)]
+#[derive(Debug, Serialize, Deserialize, Default)]
 pub struct Manifest {
   /// Default permission.
   pub default_permission: Option<PermissionSet>,
@@ -80,36 +82,50 @@ impl Manifest {
         });
       }
 
-      manifest.permissions.extend(
-        permission_file
-          .permission
-          .into_iter()
-          .map(|p| (p.identifier.clone(), p))
-          .collect::<BTreeMap<_, _>>(),
-      );
+      for permission in permission_file.permission {
+        let key = permission.identifier.clone();
+        manifest.permissions.insert(key, permission);
+      }
 
-      manifest.permission_sets.extend(
-        permission_file
-          .set
-          .into_iter()
-          .map(|set| {
-            (
-              set.identifier.clone(),
-              PermissionSet {
-                identifier: set.identifier,
-                description: set.description,
-                permissions: set.permissions,
-              },
-            )
-          })
-          .collect::<BTreeMap<_, _>>(),
-      );
+      for set in permission_file.set {
+        let key = set.identifier.clone();
+        manifest.permission_sets.insert(key, set);
+      }
     }
 
     manifest
   }
 }
 
+#[cfg(feature = "schema")]
+type ScopeSchema = (Schema, schemars::Map<String, Schema>);
+
+#[cfg(feature = "schema")]
+impl Manifest {
+  /// Return scope schema and extra schema definitions for this plugin manifest.
+  pub fn global_scope_schema(&self) -> Result<Option<ScopeSchema>, super::Error> {
+    self
+      .global_scope_schema
+      .as_ref()
+      .map(|s| {
+        serde_json::from_value::<RootSchema>(s.clone()).map(|s| {
+          // convert RootSchema to Schema
+          let scope_schema = Schema::Object(SchemaObject {
+            array: Some(Box::new(ArrayValidation {
+              items: Some(Schema::Object(s.schema).into()),
+              ..Default::default()
+            })),
+            ..Default::default()
+          });
+
+          (scope_schema, s.definitions)
+        })
+      })
+      .transpose()
+      .map_err(Into::into)
+  }
+}
+
 #[cfg(feature = "build")]
 mod build {
   use proc_macro2::TokenStream;

+ 24 - 1
crates/tauri-utils/src/acl/mod.rs

@@ -30,10 +30,16 @@ use crate::platform::Target;
 
 pub use self::{identifier::*, value::*};
 
+/// 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 ACL key for the app permissions.
 pub const APP_ACL_KEY: &str = "__app-acl__";
+/// Known acl manifests file
+pub const ACL_MANIFESTS_FILE_NAME: &str = "acl-manifests.json";
+/// Known capabilityies file
+pub const CAPABILITIES_FILE_NAME: &str = "capabilities.json";
 
 #[cfg(feature = "build")]
 pub mod build;
@@ -41,6 +47,8 @@ pub mod capability;
 pub mod identifier;
 pub mod manifest;
 pub mod resolved;
+#[cfg(feature = "schema")]
+pub mod schema;
 pub mod value;
 
 /// Possible errors while processing ACL files.
@@ -74,6 +82,10 @@ pub enum Error {
   #[error("failed to create file: {0}")]
   CreateFile(std::io::Error),
 
+  /// IO error while creating a dir
+  #[error("failed to create dir: {0}")]
+  CreateDir(std::io::Error),
+
   /// [`cargo_metadata`] was not able to complete successfully
   #[cfg(feature = "build")]
   #[error("failed to execute: {0}")]
@@ -185,7 +197,7 @@ impl Scopes {
 /// It can enable commands to be accessible in the frontend of the application.
 ///
 /// If the scope is defined it can be used to fine grain control the access of individual or multiple commands.
-#[derive(Debug, Serialize, Deserialize)]
+#[derive(Debug, Serialize, Deserialize, Default)]
 #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
 pub struct Permission {
   /// The version of the permission.
@@ -214,6 +226,17 @@ pub struct Permission {
   pub platforms: Option<Vec<Target>>,
 }
 
+impl Permission {
+  /// Whether this permission should be active based on the platform target or not.
+  pub fn is_active(&self, target: &Target) -> bool {
+    self
+      .platforms
+      .as_ref()
+      .map(|platforms| platforms.contains(target))
+      .unwrap_or(true)
+  }
+}
+
 /// A set of direct permissions grouped together under a new name.
 #[derive(Debug, Serialize, Deserialize)]
 #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]

+ 286 - 150
crates/tauri-utils/src/acl/resolved.rs

@@ -11,24 +11,13 @@ use crate::platform::Target;
 use super::{
   capability::{Capability, PermissionEntry},
   manifest::Manifest,
-  Commands, Error, ExecutionContext, Permission, PermissionSet, Scopes, Value, APP_ACL_KEY,
+  Commands, Error, ExecutionContext, Identifier, Permission, PermissionSet, Scopes, Value,
+  APP_ACL_KEY,
 };
 
 /// A key for a scope, used to link a [`ResolvedCommand#structfield.scope`] to the store [`Resolved#structfield.scopes`].
 pub type ScopeKey = u64;
 
-const CORE_PLUGINS: &[&str] = &[
-  "core:app",
-  "core:event",
-  "core:image",
-  "core:menu",
-  "core:path",
-  "core:resources",
-  "core:tray",
-  "core:webview",
-  "core:window",
-];
-
 /// Metadata for what referenced a [`ResolvedCommand`].
 #[cfg(debug_assertions)]
 #[derive(Default, Clone, PartialEq, Eq)]
@@ -103,39 +92,17 @@ impl Resolved {
     let mut global_scope: BTreeMap<String, Vec<Scopes>> = BTreeMap::new();
 
     // resolve commands
-    for capability in capabilities.values_mut() {
-      if !capability
-        .platforms
-        .as_ref()
-        .map(|platforms| platforms.contains(&target))
-        .unwrap_or(true)
-      {
-        continue;
-      }
-
-      if let Some(core_default_index) = capability.permissions.iter().position(|permission| {
-        matches!(
-          permission,
-          PermissionEntry::PermissionRef(i) if i.get() == "core:default"
-        )
-      }) {
-        capability.permissions.remove(core_default_index);
-        for plugin in CORE_PLUGINS {
-          capability.permissions.push(PermissionEntry::PermissionRef(
-            format!("{plugin}:default").try_into().unwrap(),
-          ));
-        }
-      }
-
+    for capability in capabilities.values_mut().filter(|c| c.is_active(&target)) {
       with_resolved_permissions(
         capability,
         acl,
         target,
         |ResolvedPermission {
            key,
-           permission_name,
            commands,
            scope,
+           #[cfg_attr(not(debug_assertions), allow(unused))]
+           permission_name,
          }| {
           if commands.allow.is_empty() && commands.deny.is_empty() {
             // global scope
@@ -236,6 +203,46 @@ fn parse_glob_patterns(mut raw: Vec<String>) -> Result<Vec<glob::Pattern>, Error
   Ok(patterns)
 }
 
+fn resolve_command(
+  commands: &mut BTreeMap<String, Vec<ResolvedCommand>>,
+  command: String,
+  capability: &Capability,
+  scope_id: Option<ScopeKey>,
+  #[cfg(debug_assertions)] referenced_by_permission_identifier: String,
+) -> Result<(), Error> {
+  let mut contexts = Vec::new();
+  if capability.local {
+    contexts.push(ExecutionContext::Local);
+  }
+  if let Some(remote) = &capability.remote {
+    contexts.extend(remote.urls.iter().map(|url| {
+      ExecutionContext::Remote {
+        url: url
+          .parse()
+          .unwrap_or_else(|e| panic!("invalid URL pattern for remote URL {url}: {e}")),
+      }
+    }));
+  }
+
+  for context in contexts {
+    let resolved_list = commands.entry(command.clone()).or_default();
+
+    resolved_list.push(ResolvedCommand {
+      context,
+      #[cfg(debug_assertions)]
+      referenced_by: ResolvedCommandReference {
+        capability: capability.identifier.clone(),
+        permission: referenced_by_permission_identifier.clone(),
+      },
+      windows: parse_glob_patterns(capability.windows.clone())?,
+      webviews: parse_glob_patterns(capability.webviews.clone())?,
+      scope_id,
+    });
+  }
+
+  Ok(())
+}
+
 struct ResolvedPermission<'a> {
   key: &'a str,
   permission_name: &'a str,
@@ -243,6 +250,8 @@ struct ResolvedPermission<'a> {
   scope: Scopes,
 }
 
+/// Iterate over permissions in a capability, resolving permission sets if necessary
+/// to produce a [`ResolvedPermission`] and calling the provided callback with it.
 fn with_resolved_permissions<F: FnMut(ResolvedPermission<'_>) -> Result<(), Error>>(
   capability: &Capability,
   acl: &BTreeMap<String, Manifest>,
@@ -251,43 +260,39 @@ fn with_resolved_permissions<F: FnMut(ResolvedPermission<'_>) -> Result<(), Erro
 ) -> Result<(), Error> {
   for permission_entry in &capability.permissions {
     let permission_id = permission_entry.identifier();
-    let permission_name = permission_id.get_base();
-
-    let key = permission_id.get_prefix().unwrap_or(APP_ACL_KEY);
 
-    let permissions = get_permissions(key, permission_name, acl)?
+    let permissions = get_permissions(permission_id, acl)?
       .into_iter()
-      .filter(|p| {
-        p.platforms
-          .as_ref()
-          .map(|platforms| platforms.contains(&target))
-          .unwrap_or(true)
-      })
-      .collect::<Vec<_>>();
-
-    let mut resolved_scope = Scopes::default();
-    let mut commands = Commands::default();
+      .filter(|p| p.permission.is_active(&target));
 
-    if let PermissionEntry::ExtendedPermission {
-      identifier: _,
-      scope,
-    } = permission_entry
+    for TraversedPermission {
+      key,
+      permission_name,
+      permission,
+    } in permissions
     {
-      if let Some(allow) = scope.allow.clone() {
-        resolved_scope
-          .allow
-          .get_or_insert_with(Default::default)
-          .extend(allow);
-      }
-      if let Some(deny) = scope.deny.clone() {
-        resolved_scope
-          .deny
-          .get_or_insert_with(Default::default)
-          .extend(deny);
+      let mut resolved_scope = Scopes::default();
+      let mut commands = Commands::default();
+
+      if let PermissionEntry::ExtendedPermission {
+        identifier: _,
+        scope,
+      } = permission_entry
+      {
+        if let Some(allow) = scope.allow.clone() {
+          resolved_scope
+            .allow
+            .get_or_insert_with(Default::default)
+            .extend(allow);
+        }
+        if let Some(deny) = scope.deny.clone() {
+          resolved_scope
+            .deny
+            .get_or_insert_with(Default::default)
+            .extend(deny);
+        }
       }
-    }
 
-    for permission in permissions {
       if let Some(allow) = permission.scope.allow.clone() {
         resolved_scope
           .allow
@@ -303,74 +308,110 @@ fn with_resolved_permissions<F: FnMut(ResolvedPermission<'_>) -> Result<(), Erro
 
       commands.allow.extend(permission.commands.allow.clone());
       commands.deny.extend(permission.commands.deny.clone());
-    }
 
-    f(ResolvedPermission {
-      key,
-      permission_name,
-      commands,
-      scope: resolved_scope,
-    })?;
+      f(ResolvedPermission {
+        key: &key,
+        permission_name: &permission_name,
+        commands,
+        scope: resolved_scope,
+      })?;
+    }
   }
 
   Ok(())
 }
 
-fn resolve_command(
-  commands: &mut BTreeMap<String, Vec<ResolvedCommand>>,
-  command: String,
-  capability: &Capability,
-  scope_id: Option<ScopeKey>,
-  #[cfg(debug_assertions)] referenced_by_permission_identifier: String,
-) -> Result<(), Error> {
-  let mut contexts = Vec::new();
-  if capability.local {
-    contexts.push(ExecutionContext::Local);
-  }
-  if let Some(remote) = &capability.remote {
-    contexts.extend(remote.urls.iter().map(|url| {
-      ExecutionContext::Remote {
-        url: url
-          .parse()
-          .unwrap_or_else(|e| panic!("invalid URL pattern for remote URL {url}: {e}")),
-      }
-    }));
-  }
+#[derive(Debug)]
+struct TraversedPermission<'a> {
+  key: String,
+  permission_name: String,
+  permission: &'a Permission,
+}
 
-  for context in contexts {
-    let resolved_list = commands.entry(command.clone()).or_default();
+fn get_permissions<'a>(
+  permission_id: &Identifier,
+  acl: &'a BTreeMap<String, Manifest>,
+) -> Result<Vec<TraversedPermission<'a>>, Error> {
+  let key = permission_id.get_prefix().unwrap_or(APP_ACL_KEY);
+  let permission_name = permission_id.get_base();
 
-    resolved_list.push(ResolvedCommand {
-      context,
-      #[cfg(debug_assertions)]
-      referenced_by: ResolvedCommandReference {
-        capability: capability.identifier.clone(),
-        permission: referenced_by_permission_identifier.clone(),
-      },
-      windows: parse_glob_patterns(capability.windows.clone())?,
-      webviews: parse_glob_patterns(capability.webviews.clone())?,
-      scope_id,
-    });
-  }
+  let manifest = acl.get(key).ok_or_else(|| Error::UnknownManifest {
+    key: display_perm_key(key).to_string(),
+    available: acl.keys().cloned().collect::<Vec<_>>().join(", "),
+  })?;
 
-  Ok(())
+  if permission_name == "default" {
+    manifest
+      .default_permission
+      .as_ref()
+      .map(|default| get_permission_set_permissions(permission_id, acl, manifest, default))
+      .unwrap_or_else(|| Ok(Default::default()))
+  } else if let Some(set) = manifest.permission_sets.get(permission_name) {
+    get_permission_set_permissions(permission_id, acl, manifest, set)
+  } else if let Some(permission) = manifest.permissions.get(permission_name) {
+    Ok(vec![TraversedPermission {
+      key: key.to_string(),
+      permission_name: permission_name.to_string(),
+      permission,
+    }])
+  } else {
+    Err(Error::UnknownPermission {
+      key: display_perm_key(key).to_string(),
+      permission: permission_name.to_string(),
+    })
+  }
 }
 
 // get the permissions from a permission set
 fn get_permission_set_permissions<'a>(
+  permission_id: &Identifier,
+  acl: &'a BTreeMap<String, Manifest>,
   manifest: &'a Manifest,
   set: &'a PermissionSet,
-) -> Result<Vec<&'a Permission>, Error> {
+) -> Result<Vec<TraversedPermission<'a>>, Error> {
+  let key = permission_id.get_prefix().unwrap_or(APP_ACL_KEY);
+
   let mut permissions = Vec::new();
 
-  for p in &set.permissions {
-    if let Some(permission) = manifest.permissions.get(p) {
-      permissions.push(permission);
-    } else if let Some(permission_set) = manifest.permission_sets.get(p) {
-      permissions.extend(get_permission_set_permissions(manifest, permission_set)?);
+  for perm in &set.permissions {
+    // a set could include permissions from other plugins
+    // for example `dialog:default`, could include `fs:default`
+    // in this case `perm = "fs:default"` which is not a permission
+    // in the dialog manifest so we check if `perm` still have a prefix (i.e `fs:`)
+    // and if so, we resolve this prefix from `acl` first before proceeding
+    let id = Identifier::try_from(perm.clone()).expect("invalid identifier in permission set?");
+    let (manifest, permission_id, key, permission_name) =
+      if let Some((new_key, manifest)) = id.get_prefix().and_then(|k| acl.get(k).map(|m| (k, m))) {
+        (manifest, &id, new_key, id.get_base())
+      } else {
+        (manifest, permission_id, key, perm.as_str())
+      };
+
+    if permission_name == "default" {
+      permissions.extend(
+        manifest
+          .default_permission
+          .as_ref()
+          .map(|default| get_permission_set_permissions(permission_id, acl, manifest, default))
+          .transpose()?
+          .unwrap_or_default(),
+      );
+    } else if let Some(permission) = manifest.permissions.get(permission_name) {
+      permissions.push(TraversedPermission {
+        key: key.to_string(),
+        permission_name: permission_name.to_string(),
+        permission,
+      });
+    } else if let Some(permission_set) = manifest.permission_sets.get(permission_name) {
+      permissions.extend(get_permission_set_permissions(
+        permission_id,
+        acl,
+        manifest,
+        permission_set,
+      )?);
     } else {
       return Err(Error::SetPermissionNotFound {
-        permission: p.to_string(),
+        permission: permission_name.to_string(),
         set: set.identifier.clone(),
       });
     }
@@ -379,39 +420,12 @@ fn get_permission_set_permissions<'a>(
   Ok(permissions)
 }
 
-fn get_permissions<'a>(
-  key: &'a str,
-  permission_name: &'a str,
-  acl: &'a BTreeMap<String, Manifest>,
-) -> Result<Vec<&'a Permission>, Error> {
-  let manifest = acl.get(key).ok_or_else(|| Error::UnknownManifest {
-    key: if key == APP_ACL_KEY {
-      "app manifest".to_string()
-    } else {
-      key.to_string()
-    },
-    available: acl.keys().cloned().collect::<Vec<_>>().join(", "),
-  })?;
-
-  if permission_name == "default" {
-    manifest
-      .default_permission
-      .as_ref()
-      .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) {
-    Ok(vec![permission])
+#[inline]
+fn display_perm_key(prefix: &str) -> &str {
+  if prefix == APP_ACL_KEY {
+    "app manifest"
   } else {
-    Err(Error::UnknownPermission {
-      key: if key == APP_ACL_KEY {
-        "app manifest".to_string()
-      } else {
-        key.to_string()
-      },
-      permission: permission_name.to_string(),
-    })
+    prefix
   }
 }
 
@@ -533,3 +547,125 @@ mod build {
     }
   }
 }
+
+#[cfg(test)]
+mod tests {
+
+  use super::{get_permissions, Identifier, Manifest, Permission, PermissionSet};
+
+  fn manifest<const P: usize, const S: usize>(
+    name: &str,
+    permissions: [&str; P],
+    default_set: Option<&[&str]>,
+    sets: [(&str, &[&str]); S],
+  ) -> (String, Manifest) {
+    (
+      name.to_string(),
+      Manifest {
+        default_permission: default_set.map(|perms| PermissionSet {
+          identifier: "default".to_string(),
+          description: "default set".to_string(),
+          permissions: perms.iter().map(|s| s.to_string()).collect(),
+        }),
+        permissions: permissions
+          .iter()
+          .map(|p| {
+            (
+              p.to_string(),
+              Permission {
+                identifier: p.to_string(),
+                ..Default::default()
+              },
+            )
+          })
+          .collect(),
+        permission_sets: sets
+          .iter()
+          .map(|(s, perms)| {
+            (
+              s.to_string(),
+              PermissionSet {
+                identifier: s.to_string(),
+                description: format!("{s} set"),
+                permissions: perms.iter().map(|s| s.to_string()).collect(),
+              },
+            )
+          })
+          .collect(),
+        ..Default::default()
+      },
+    )
+  }
+
+  fn id(id: &str) -> Identifier {
+    Identifier::try_from(id.to_string()).unwrap()
+  }
+
+  #[test]
+  fn resolves_permissions_from_other_plugins() {
+    let acl = [
+      manifest(
+        "fs",
+        ["read", "write", "rm", "exist"],
+        Some(&["read", "exist"]),
+        [],
+      ),
+      manifest(
+        "http",
+        ["fetch", "fetch-cancel"],
+        None,
+        [("fetch-with-cancel", &["fetch", "fetch-cancel"])],
+      ),
+      manifest(
+        "dialog",
+        ["open", "save"],
+        None,
+        [(
+          "extra",
+          &[
+            "save",
+            "fs:default",
+            "fs:write",
+            "http:default",
+            "http:fetch-with-cancel",
+          ],
+        )],
+      ),
+    ]
+    .into();
+
+    let permissions = get_permissions(&id("fs:default"), &acl).unwrap();
+    assert_eq!(permissions.len(), 2);
+    assert_eq!(permissions[0].key, "fs");
+    assert_eq!(permissions[0].permission_name, "read");
+    assert_eq!(permissions[1].key, "fs");
+    assert_eq!(permissions[1].permission_name, "exist");
+
+    let permissions = get_permissions(&id("fs:rm"), &acl).unwrap();
+    assert_eq!(permissions.len(), 1);
+    assert_eq!(permissions[0].key, "fs");
+    assert_eq!(permissions[0].permission_name, "rm");
+
+    let permissions = get_permissions(&id("http:fetch-with-cancel"), &acl).unwrap();
+    assert_eq!(permissions.len(), 2);
+    assert_eq!(permissions[0].key, "http");
+    assert_eq!(permissions[0].permission_name, "fetch");
+    assert_eq!(permissions[1].key, "http");
+    assert_eq!(permissions[1].permission_name, "fetch-cancel");
+
+    let permissions = get_permissions(&id("dialog:extra"), &acl).unwrap();
+    assert_eq!(permissions.len(), 6);
+    assert_eq!(permissions[0].key, "dialog");
+    assert_eq!(permissions[0].permission_name, "save");
+    assert_eq!(permissions[1].key, "fs");
+    assert_eq!(permissions[1].permission_name, "read");
+    assert_eq!(permissions[2].key, "fs");
+    assert_eq!(permissions[2].permission_name, "exist");
+    assert_eq!(permissions[3].key, "fs");
+    assert_eq!(permissions[3].permission_name, "write");
+    assert_eq!(permissions[4].key, "http");
+    assert_eq!(permissions[4].permission_name, "fetch");
+    assert_eq!(permissions[5].key, "http");
+    assert_eq!(permissions[5].permission_name, "fetch-cancel");
+  }
+}

+ 345 - 0
crates/tauri-utils/src/acl/schema.rs

@@ -0,0 +1,345 @@
+// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
+
+//! Schema generation for ACL items.
+
+use std::{
+  collections::{btree_map::Values, BTreeMap},
+  fs,
+  path::{Path, PathBuf},
+  slice::Iter,
+};
+
+use schemars::schema::*;
+
+use super::{Error, PERMISSION_SCHEMAS_FOLDER_NAME};
+use crate::{platform::Target, write_if_changed};
+
+use super::{
+  capability::CapabilityFile,
+  manifest::{Manifest, PermissionFile},
+  Permission, PermissionSet, PERMISSION_SCHEMA_FILE_NAME,
+};
+
+/// Capability schema file name.
+pub const CAPABILITIES_SCHEMA_FILE_NAME: &str = "schema.json";
+/// Path of the folder where schemas are saved.
+pub const CAPABILITIES_SCHEMA_FOLDER_PATH: &str = "gen/schemas";
+
+// TODO: once MSRV is high enough, remove generic and use impl <trait>
+// see https://github.com/tauri-apps/tauri/commit/b5561d74aee431f93c0c5b0fa6784fc0a956effe#diff-7c31d393f83cae149122e74ad44ac98e7d70ffb45c9e5b0a94ec52881b6f1cebR30-R42
+/// Permission schema generator trait
+pub trait PermissionSchemaGenerator<
+  'a,
+  Ps: Iterator<Item = &'a PermissionSet>,
+  P: Iterator<Item = &'a Permission>,
+>
+{
+  /// Whether has a default permission set or not.
+  fn has_default_permission_set(&self) -> bool;
+
+  /// Default permission set description if any.
+  fn default_set_description(&self) -> Option<&str>;
+
+  /// Permissions sets to generate schema for.
+  fn permission_sets(&'a self) -> Ps;
+
+  /// Permissions to generate schema for.
+  fn permissions(&'a self) -> P;
+
+  /// A utility function to generate a schema for a permission identifier
+  fn perm_id_schema(name: Option<&str>, id: &str, description: Option<&str>) -> Schema {
+    let command_name = match name {
+      Some(name) if name == super::APP_ACL_KEY => id.to_string(),
+      Some(name) => format!("{name}:{id}"),
+      _ => id.to_string(),
+    };
+
+    Schema::Object(SchemaObject {
+      metadata: Some(Box::new(Metadata {
+        description: description.map(ToString::to_string),
+        ..Default::default()
+      })),
+      instance_type: Some(InstanceType::String.into()),
+      const_value: Some(serde_json::Value::String(command_name)),
+      ..Default::default()
+    })
+  }
+
+  /// Generate schemas for all possible permissions.
+  fn gen_possible_permission_schemas(&'a self, name: Option<&str>) -> Vec<Schema> {
+    let mut permission_schemas = Vec::new();
+
+    // schema for default set
+    if self.has_default_permission_set() {
+      let default = Self::perm_id_schema(name, "default", self.default_set_description());
+      permission_schemas.push(default);
+    }
+
+    // schema for each permission set
+    for set in self.permission_sets() {
+      let schema = Self::perm_id_schema(name, &set.identifier, Some(&set.description));
+      permission_schemas.push(schema);
+    }
+
+    // schema for each permission
+    for perm in self.permissions() {
+      let schema = Self::perm_id_schema(name, &perm.identifier, perm.description.as_deref());
+      permission_schemas.push(schema);
+    }
+
+    permission_schemas
+  }
+}
+
+impl<'a>
+  PermissionSchemaGenerator<
+    'a,
+    Values<'a, std::string::String, PermissionSet>,
+    Values<'a, std::string::String, Permission>,
+  > for Manifest
+{
+  fn has_default_permission_set(&self) -> bool {
+    self.default_permission.is_some()
+  }
+
+  fn default_set_description(&self) -> Option<&str> {
+    self
+      .default_permission
+      .as_ref()
+      .map(|d| d.description.as_str())
+  }
+
+  fn permission_sets(&'a self) -> Values<'a, std::string::String, PermissionSet> {
+    self.permission_sets.values()
+  }
+
+  fn permissions(&'a self) -> Values<'a, std::string::String, Permission> {
+    self.permissions.values()
+  }
+}
+
+impl<'a> PermissionSchemaGenerator<'a, Iter<'a, PermissionSet>, Iter<'a, Permission>>
+  for PermissionFile
+{
+  fn has_default_permission_set(&self) -> bool {
+    self.default.is_some()
+  }
+
+  fn default_set_description(&self) -> Option<&str> {
+    self.default.as_ref().and_then(|d| d.description.as_deref())
+  }
+
+  fn permission_sets(&'a self) -> Iter<'a, PermissionSet> {
+    self.set.iter()
+  }
+
+  fn permissions(&'a self) -> Iter<'a, Permission> {
+    self.permission.iter()
+  }
+}
+
+/// Collect and include all possible identifiers in `Identifier` defintion in the schema
+fn extend_identifier_schema(schema: &mut RootSchema, acl: &BTreeMap<String, Manifest>) {
+  if let Some(Schema::Object(identifier_schema)) = schema.definitions.get_mut("Identifier") {
+    let permission_schemas = acl
+      .iter()
+      .flat_map(|(name, manifest)| manifest.gen_possible_permission_schemas(Some(name)))
+      .collect::<Vec<_>>();
+
+    let new_subschemas = Box::new(SubschemaValidation {
+      one_of: Some(permission_schemas.clone()),
+      ..Default::default()
+    });
+
+    identifier_schema.subschemas = Some(new_subschemas);
+    identifier_schema.object = None;
+    identifier_schema.instance_type = None;
+    identifier_schema.metadata().description = Some("Permission identifier".to_string());
+  }
+}
+
+/// Collect permission schemas and its associated scope schema and schema definitons from plugins
+/// and replace `PermissionEntry` extend object syntax with a new schema that does conditional
+/// checks to serve the relavent scope schema for the right permissions schema, in a nutshell, it
+/// will look something like this:
+/// ```text
+/// PermissionEntry {
+///   anyOf {
+///     String,  // default string syntax
+///     Object { // extended object syntax
+///       allOf { // JSON allOf is used but actually means anyOf
+///         {
+///           "if": "identifier" property anyOf "fs" plugin permission,
+///           "then": add "allow" and "deny" properties that match "fs" plugin scope schema
+///         },
+///         {
+///           "if": "identifier" property anyOf "http" plugin permission,
+///           "then": add "allow" and "deny" properties that match "http" plugin scope schema
+///         },
+///         ...etc,
+///         {
+///           No "if" or "then", just "allow" and "deny" properties with default "#/definitions/Value"
+///         },
+///       }
+///     }
+///   }
+/// }
+/// ```
+fn extend_permission_entry_schema(root_schema: &mut RootSchema, acl: &BTreeMap<String, Manifest>) {
+  const IDENTIFIER: &str = "identifier";
+  const ALLOW: &str = "allow";
+  const DENY: &str = "deny";
+
+  let mut collected_defs = vec![];
+
+  if let Some(Schema::Object(obj)) = root_schema.definitions.get_mut("PermissionEntry") {
+    let any_of = obj.subschemas().any_of.as_mut().unwrap();
+    let Schema::Object(extened_permission_entry) = any_of.last_mut().unwrap() else {
+      unreachable!("PermissionsEntry should be an object not a boolean");
+    };
+
+    // remove default properties and save it to be added later as a fallback
+    let obj = extened_permission_entry.object.as_mut().unwrap();
+    let default_properties = std::mem::take(&mut obj.properties);
+
+    let defaut_identifier = default_properties.get(IDENTIFIER).cloned().unwrap();
+    let default_identifier = (IDENTIFIER.to_string(), defaut_identifier);
+
+    let mut all_of = vec![];
+
+    let schemas = acl.iter().filter_map(|(name, manifest)| {
+      manifest
+        .global_scope_schema()
+        .unwrap_or_else(|e| panic!("invalid JSON schema for plugin {name}: {e}"))
+        .map(|s| (s, manifest.gen_possible_permission_schemas(Some(name))))
+    });
+
+    for ((scope_schema, defs), acl_perm_schema) in schemas {
+      let mut perm_schema = SchemaObject::default();
+      perm_schema.subschemas().any_of = Some(acl_perm_schema);
+
+      let mut if_schema = SchemaObject::default();
+      if_schema.object().properties = [(IDENTIFIER.to_string(), perm_schema.into())].into();
+
+      let mut then_schema = SchemaObject::default();
+      then_schema.object().properties = [
+        (ALLOW.to_string(), scope_schema.clone()),
+        (DENY.to_string(), scope_schema.clone()),
+      ]
+      .into();
+
+      let mut obj = SchemaObject::default();
+      obj.object().properties = [default_identifier.clone()].into();
+      obj.subschemas().if_schema = Some(Box::new(if_schema.into()));
+      obj.subschemas().then_schema = Some(Box::new(then_schema.into()));
+
+      all_of.push(Schema::Object(obj));
+      collected_defs.extend(defs);
+    }
+
+    // add back default properties as a fallback
+    let mut default_obj = SchemaObject::default();
+    default_obj.object().properties = default_properties;
+    all_of.push(Schema::Object(default_obj));
+
+    // replace extended PermissionEntry with the new schema
+    extened_permission_entry.subschemas().all_of = Some(all_of);
+  }
+
+  // extend root schema with definitions collected from plugins
+  root_schema.definitions.extend(collected_defs);
+}
+
+/// Generate schema for CapabilityFile with all possible plugins permissions
+pub fn generate_capability_schema(
+  acl: &BTreeMap<String, Manifest>,
+  target: Target,
+) -> crate::Result<()> {
+  let mut schema = schemars::schema_for!(CapabilityFile);
+
+  extend_identifier_schema(&mut schema, acl);
+  extend_permission_entry_schema(&mut schema, acl);
+
+  let schema_str = serde_json::to_string_pretty(&schema).unwrap();
+  let out_dir = PathBuf::from(CAPABILITIES_SCHEMA_FOLDER_PATH);
+  fs::create_dir_all(&out_dir)?;
+
+  let schema_path = out_dir.join(format!("{target}-{CAPABILITIES_SCHEMA_FILE_NAME}"));
+  if schema_str != fs::read_to_string(&schema_path).unwrap_or_default() {
+    fs::write(&schema_path, schema_str)?;
+
+    fs::copy(
+      schema_path,
+      out_dir.join(format!(
+        "{}-{CAPABILITIES_SCHEMA_FILE_NAME}",
+        if target.is_desktop() {
+          "desktop"
+        } else {
+          "mobile"
+        }
+      )),
+    )?;
+  }
+
+  Ok(())
+}
+
+/// Extend schema with collected permissions from the passed [`PermissionFile`]s.
+fn extend_permission_file_schema(schema: &mut RootSchema, permissions: &[PermissionFile]) {
+  // collect possible permissions
+  let permission_schemas = permissions
+    .iter()
+    .flat_map(|p| p.gen_possible_permission_schemas(None))
+    .collect();
+
+  if let Some(Schema::Object(obj)) = schema.definitions.get_mut("PermissionSet") {
+    let permissions_obj = obj.object().properties.get_mut("permissions");
+    if let Some(Schema::Object(permissions_obj)) = permissions_obj {
+      // replace the permissions property schema object
+      // from a mere string to a referecnce to `PermissionKind`
+      permissions_obj.array().items.replace(
+        Schema::Object(SchemaObject {
+          reference: Some("#/definitions/PermissionKind".into()),
+          ..Default::default()
+        })
+        .into(),
+      );
+
+      // add the new `PermissionKind` definition in the schema that
+      // is a list of all possible permissions collected
+      schema.definitions.insert(
+        "PermissionKind".into(),
+        Schema::Object(SchemaObject {
+          instance_type: Some(InstanceType::String.into()),
+          subschemas: Some(Box::new(SubschemaValidation {
+            one_of: Some(permission_schemas),
+            ..Default::default()
+          })),
+          ..Default::default()
+        }),
+      );
+    }
+  }
+}
+
+/// Generate and write a schema based on the format of a [`PermissionFile`].
+pub fn generate_permissions_schema<P: AsRef<Path>>(
+  permissions: &[PermissionFile],
+  out_dir: P,
+) -> Result<(), Error> {
+  let mut schema = schemars::schema_for!(PermissionFile);
+
+  extend_permission_file_schema(&mut schema, permissions);
+
+  let schema_str = serde_json::to_string_pretty(&schema)?;
+
+  let out_dir = out_dir.as_ref().join(PERMISSION_SCHEMAS_FOLDER_NAME);
+  fs::create_dir_all(&out_dir).map_err(Error::CreateDir)?;
+
+  let schema_path = out_dir.join(PERMISSION_SCHEMA_FILE_NAME);
+  write_if_changed(&schema_path, schema_str).map_err(Error::WriteFile)?;
+
+  Ok(())
+}

+ 1 - 1
crates/tauri-utils/src/config.rs

@@ -212,7 +212,7 @@ impl schemars::JsonSchema for BundleTarget {
   fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
     let any_of = vec![
       schemars::schema::SchemaObject {
-        enum_values: Some(vec!["all".into()]),
+        const_value: Some("all".into()),
         metadata: Some(Box::new(schemars::schema::Metadata {
           description: Some("Bundle all targets.".to_owned()),
           ..Default::default()

+ 30 - 2
crates/tauri-utils/src/plugin.rs

@@ -10,6 +10,7 @@ pub use build::*;
 mod build {
   use std::{
     env::vars_os,
+    fs,
     path::{Path, PathBuf},
   };
 
@@ -30,7 +31,7 @@ mod build {
 
   /// Collects the path of all the global API scripts defined with [`define_global_api_script_path`]
   /// and saves them to the out dir with filename [`GLOBAL_API_SCRIPT_FILE_LIST_PATH`].
-  pub fn load_global_api_scripts(out_dir: &Path) {
+  pub fn save_global_api_scripts_paths(out_dir: &Path) {
     let mut scripts = Vec::new();
 
     for (key, value) in vars_os() {
@@ -42,10 +43,37 @@ mod build {
       }
     }
 
-    std::fs::write(
+    fs::write(
       out_dir.join(GLOBAL_API_SCRIPT_FILE_LIST_PATH),
       serde_json::to_string(&scripts).expect("failed to serialize global API script paths"),
     )
     .expect("failed to write global API script");
   }
+
+  /// Read global api scripts from [`GLOBAL_API_SCRIPT_FILE_LIST_PATH`]
+  pub fn read_global_api_scripts(out_dir: &Path) -> Option<Vec<String>> {
+    let global_scripts_path = out_dir.join(GLOBAL_API_SCRIPT_FILE_LIST_PATH);
+    if !global_scripts_path.exists() {
+      return None;
+    }
+
+    let global_scripts_str = fs::read_to_string(global_scripts_path)
+      .expect("failed to read plugin global API script paths");
+    let global_scripts = serde_json::from_str::<Vec<PathBuf>>(&global_scripts_str)
+      .expect("failed to parse plugin global API script paths");
+
+    Some(
+      global_scripts
+        .into_iter()
+        .map(|p| {
+          fs::read_to_string(&p).unwrap_or_else(|e| {
+            panic!(
+              "failed to read plugin global API script {}: {e}",
+              p.display()
+            )
+          })
+        })
+        .collect(),
+    )
+  }
 }

+ 0 - 2
crates/tauri-utils/src/resources.rs

@@ -580,8 +580,6 @@ mod tests {
     .iter()
     .collect::<Vec<_>>();
 
-    dbg!(&resources);
-
     assert_eq!(resources.len(), 4);
 
     assert!(resources.iter().all(|r| r.is_err()));

+ 55 - 20
crates/tauri/build.rs

@@ -5,12 +5,8 @@
 use heck::AsShoutySnakeCase;
 use tauri_utils::write_if_changed;
 
-use std::env::var_os;
-use std::fs::create_dir_all;
-use std::fs::read_dir;
-use std::fs::read_to_string;
 use std::{
-  env::var,
+  env, fs,
   path::{Path, PathBuf},
   sync::{Mutex, OnceLock},
 };
@@ -18,7 +14,6 @@ use std::{
 static CHECKED_FEATURES: OnceLock<Mutex<Vec<String>>> = OnceLock::new();
 const PLUGINS: &[(&str, &[(&str, bool)])] = &[
   // (plugin_name, &[(command, enabled-by_default)])
-  // note that when adding new core plugins, they must be added to the ACL resolver aswell
   (
     "core:path",
     &[
@@ -240,7 +235,7 @@ fn main() {
   alias("desktop", !mobile);
   alias("mobile", mobile);
 
-  let out_dir = PathBuf::from(var("OUT_DIR").unwrap());
+  let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
 
   let checked_features_out_path = out_dir.join("checked_features");
   std::fs::write(
@@ -278,12 +273,12 @@ fn main() {
         PathBuf::from(env_var("CARGO_MANIFEST_DIR")).join("mobile/android-codegen");
       println!("cargo:rerun-if-changed={}", kotlin_files_path.display());
       let kotlin_files =
-        read_dir(kotlin_files_path).expect("failed to read Android codegen directory");
+        fs::read_dir(kotlin_files_path).expect("failed to read Android codegen directory");
 
       for file in kotlin_files {
         let file = file.unwrap();
 
-        let content = read_to_string(file.path())
+        let content = fs::read_to_string(file.path())
           .expect("failed to read kotlin file as string")
           .replace("{{package}}", &package)
           .replace("{{library}}", &library);
@@ -296,10 +291,11 @@ fn main() {
       }
     }
 
-    if let Some(project_dir) = var_os("TAURI_ANDROID_PROJECT_PATH").map(PathBuf::from) {
+    if let Some(project_dir) = env::var_os("TAURI_ANDROID_PROJECT_PATH").map(PathBuf::from) {
       let tauri_proguard = include_str!("./mobile/proguard-tauri.pro").replace(
         "$PACKAGE",
-        &var("WRY_ANDROID_PACKAGE").expect("missing `WRY_ANDROID_PACKAGE` environment variable"),
+        &env::var("WRY_ANDROID_PACKAGE")
+          .expect("missing `WRY_ANDROID_PACKAGE` environment variable"),
       );
       std::fs::write(
         project_dir.join("app").join("proguard-tauri.pro"),
@@ -326,12 +322,12 @@ fn main() {
   define_permissions(&out_dir);
 }
 
-fn define_permissions(out_dir: &Path) {
-  let license_header = r"# Copyright 2019-2024 Tauri Programme within The Commons Conservancy
+const LICENSE_HEADER: &str = r"# Copyright 2019-2024 Tauri Programme within The Commons Conservancy
 # SPDX-License-Identifier: Apache-2.0
 # SPDX-License-Identifier: MIT
 ";
 
+fn define_permissions(out_dir: &Path) {
   for (plugin, commands) in PLUGINS {
     let plugin_directory_name = plugin.strip_prefix("core:").unwrap_or(plugin);
     let permissions_out_dir = out_dir.join("permissions").join(plugin_directory_name);
@@ -342,7 +338,7 @@ fn define_permissions(out_dir: &Path) {
     tauri_utils::acl::build::autogenerate_command_permissions(
       &commands_dir,
       &commands.iter().map(|(cmd, _)| *cmd).collect::<Vec<_>>(),
-      license_header,
+      LICENSE_HEADER,
       false,
     );
     let default_permissions = commands
@@ -356,7 +352,7 @@ fn define_permissions(out_dir: &Path) {
       .join(", ");
 
     let default_toml = format!(
-      r###"{license_header}# Automatically generated - DO NOT EDIT!
+      r###"{LICENSE_HEADER}# Automatically generated - DO NOT EDIT!
 
 [default]
 description = "Default permissions for the plugin."
@@ -365,10 +361,8 @@ permissions = [{default_permissions}]
     );
 
     let out_path = autogenerated.join("default.toml");
-    if default_toml != read_to_string(&out_path).unwrap_or_default() {
-      std::fs::write(out_path, default_toml)
-        .unwrap_or_else(|_| panic!("unable to autogenerate default permissions"));
-    }
+    write_if_changed(out_path, default_toml)
+      .unwrap_or_else(|_| panic!("unable to autogenerate default permissions"));
 
     let permissions = tauri_utils::acl::build::define_permissions(
       &permissions_out_dir
@@ -384,7 +378,7 @@ permissions = [{default_permissions}]
     let docs_out_dir = Path::new("permissions")
       .join(plugin_directory_name)
       .join("autogenerated");
-    create_dir_all(&docs_out_dir).expect("failed to create plugin documentation directory");
+    fs::create_dir_all(&docs_out_dir).expect("failed to create plugin documentation directory");
     tauri_utils::acl::build::generate_docs(
       &permissions,
       &docs_out_dir,
@@ -392,6 +386,47 @@ permissions = [{default_permissions}]
     )
     .expect("failed to generate plugin documentation page");
   }
+
+  define_default_permission_set(out_dir);
+}
+
+fn define_default_permission_set(out_dir: &Path) {
+  let permissions_out_dir = out_dir.join("permissions");
+  fs::create_dir_all(&permissions_out_dir)
+    .expect("failed to create core:default permissions directory");
+
+  let default_toml = permissions_out_dir.join("default.toml");
+  let toml_content = format!(
+    r#"# {LICENSE_HEADER}
+
+[default]
+description = """Default core plugins set which includes:
+{}
+"""
+permissions = [{}]
+"#,
+    PLUGINS
+      .iter()
+      .map(|(k, _)| format!("- '{k}:default'"))
+      .collect::<Vec<_>>()
+      .join("\n"),
+    PLUGINS
+      .iter()
+      .map(|(k, _)| format!("'{k}:default'"))
+      .collect::<Vec<_>>()
+      .join(",")
+  );
+
+  write_if_changed(&default_toml, toml_content)
+    .unwrap_or_else(|_| panic!("unable to autogenerate core:default set"));
+
+  let _ = tauri_utils::acl::build::define_permissions(
+    &permissions_out_dir.join("*.toml").to_string_lossy(),
+    "tauri:core",
+    out_dir,
+    |_| true,
+  )
+  .unwrap_or_else(|e| panic!("failed to define permissions for `core:default` : {e}"));
 }
 
 fn embed_manifest_for_tests() {

+ 8 - 16
examples/api/src-tauri/tauri-plugin-sample/permissions/schemas/schema.json

@@ -295,32 +295,24 @@
       "type": "string",
       "oneOf": [
         {
-          "description": "allow-ping -> Enables the ping command without any pre-configured scope.",
+          "description": "Enables the ping command without any pre-configured scope.",
           "type": "string",
-          "enum": [
-            "allow-ping"
-          ]
+          "const": "allow-ping"
         },
         {
-          "description": "deny-ping -> Denies the ping command without any pre-configured scope.",
+          "description": "Denies the ping command without any pre-configured scope.",
           "type": "string",
-          "enum": [
-            "deny-ping"
-          ]
+          "const": "deny-ping"
         },
         {
-          "description": "global-scope -> Sets a global scope.",
+          "description": "Sets a global scope.",
           "type": "string",
-          "enum": [
-            "global-scope"
-          ]
+          "const": "global-scope"
         },
         {
-          "description": "allow-ping-scoped -> Enables the ping command with a test scope.",
+          "description": "Enables the ping command with a test scope.",
           "type": "string",
-          "enum": [
-            "allow-ping-scoped"
-          ]
+          "const": "allow-ping-scoped"
         }
       ]
     }