feat: add a new option to remove unused commands (#12890)

* Add a new option to remove unused commands

* Fix compile

* Add markers to all core plugins

* Clippy

* Add allow unused when running with this

* Use build script to generate allowed-commands.json

* Clean up and add proper reruns

* Wrong path

* Revert to #[cfg_attr(not(debug_assertions), allow(unused))]

* Add change files

* Some more docs

* Add version requirement note

* Avoid rerun if no capabilities folder

* Remove unused box

* small cleanup

* fix channel

* implement for app handler too

* rely on core:default for channel perms

* Move this feature to config

* Docs change

* Forget one last remove_unused_commands

* Remove removeUnusedCommands from helloworld

* tell handler that the app ACL manifest exists

* update change file

* update doc

* update change file

* Use a struct to pass the data instead of env var

* Clippy

* Fix can't exclude inlined plugins on Windows
due to UNC paths...

* Apply suggestion from code review

* Remove remove on empty to tauri-build

* Revert "Remove remove on empty to tauri-build"

This reverts commit b727dd621e.

* Centralize remove_file(allowed_commands_file_path)

* Escape glob pattern

* update change file

* remove unused commands for dev too

* Update crates/tauri-utils/src/config.rs

Co-authored-by: Fabian-Lars <github@fabianlars.de>

* regen schema

---------

Co-authored-by: Lucas Nogueira <lucas@tauri.app>
Co-authored-by: Fabian-Lars <github@fabianlars.de>
This commit is contained in:
Tony 2025-03-16 00:46:08 +08:00 committed by GitHub
parent 5591a4f0b4
commit 013f8f6523
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
40 changed files with 695 additions and 274 deletions

View File

@ -0,0 +1,5 @@
---
tauri-cli: 'minor:feat'
---
Reads `build > removeUnusedCommands` from the config file and pass in the environment variables on the build command to trigger the build scripts and macros to remove unused commands based on the capabilities you defined. For this to work on inlined plugins you must add a `#![plugin(<insert_plugin_name>)]` inside the `tauri::generate_handler![]` usage and the app manifest must be set.

View File

@ -0,0 +1,10 @@
---
tauri: 'minor:feat'
tauri-build: 'minor:feat'
tauri-codegen: 'minor:feat'
tauri-macros: 'minor:feat'
tauri-plugin: 'minor:feat'
tauri-utils: 'minor:feat'
---
Added `build > removeUnusedCommands` to trigger the build scripts and macros to remove unused commands based on the capabilities you defined. Note this won't be accounting for dynamically added ACLs so make sure to check it when using this.

1
Cargo.lock generated
View File

@ -8909,6 +8909,7 @@ name = "tauri-utils"
version = "2.2.0"
dependencies = [
"aes-gcm",
"anyhow",
"brotli",
"cargo_metadata",
"ctor",

View File

@ -70,3 +70,4 @@ opt-level = "s"
[patch.crates-io]
schemars_derive = { git = 'https://github.com/tauri-apps/schemars.git', branch = 'feat/preserve-description-newlines' }
tauri = { path = "./crates/tauri" }
tauri-plugin = { path = "./crates/tauri-plugin" }

View File

@ -11,7 +11,9 @@ use std::{
use anyhow::{Context, Result};
use tauri_utils::{
acl::{
capability::Capability, manifest::Manifest, schema::CAPABILITIES_SCHEMA_FOLDER_PATH,
capability::Capability,
manifest::{Manifest, PermissionFile},
schema::CAPABILITIES_SCHEMA_FOLDER_PATH,
ACL_MANIFESTS_FILE_NAME, APP_ACL_KEY, CAPABILITIES_FILE_NAME,
},
platform::Target,
@ -155,11 +157,17 @@ fn read_plugins_manifests() -> Result<BTreeMap<String, Manifest>> {
Ok(manifests)
}
struct InlinedPuginsAcl {
manifests: BTreeMap<String, Manifest>,
permission_files: BTreeMap<String, Vec<PermissionFile>>,
}
fn inline_plugins(
out_dir: &Path,
inlined_plugins: HashMap<&'static str, InlinedPlugin>,
) -> Result<BTreeMap<String, Manifest>> {
) -> Result<InlinedPuginsAcl> {
let mut acl_manifests = BTreeMap::new();
let mut permission_files_map = BTreeMap::new();
for (name, plugin) in inlined_plugins {
let plugin_out_dir = out_dir.join("plugins").join(name);
@ -236,18 +244,29 @@ permissions = [{default_permissions}]
)?);
}
permission_files_map.insert(name.into(), permission_files.clone());
let manifest = tauri_utils::acl::manifest::Manifest::new(permission_files, None);
acl_manifests.insert(name.into(), manifest);
}
Ok(acl_manifests)
Ok(InlinedPuginsAcl {
manifests: acl_manifests,
permission_files: permission_files_map,
})
}
#[derive(Debug)]
struct AppManifestAcl {
manifest: Manifest,
permission_files: Vec<PermissionFile>,
}
fn app_manifest_permissions(
out_dir: &Path,
manifest: AppManifest,
inlined_plugins: &HashMap<&'static str, InlinedPlugin>,
) -> Result<Manifest> {
) -> Result<AppManifestAcl> {
let app_out_dir = out_dir.join("app-manifest");
fs::create_dir_all(&app_out_dir)?;
let pkg_name = "__app__";
@ -290,6 +309,7 @@ fn app_manifest_permissions(
let inlined_plugins_permissions: Vec<_> = inlined_plugins
.keys()
.map(|name| permissions_root.join(name))
.flat_map(|p| p.canonicalize())
.collect();
permission_files.extend(tauri_utils::acl::build::define_permissions(
@ -308,10 +328,10 @@ fn app_manifest_permissions(
)?);
}
Ok(tauri_utils::acl::manifest::Manifest::new(
permission_files,
None,
))
Ok(AppManifestAcl {
permission_files: permission_files.clone(),
manifest: tauri_utils::acl::manifest::Manifest::new(permission_files, None),
})
}
fn validate_capabilities(
@ -380,19 +400,21 @@ fn validate_capabilities(
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(
let app_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);
let has_app_manifest = app_acl.manifest.default_permission.is_some()
|| !app_acl.manifest.permission_sets.is_empty()
|| !app_acl.manifest.permissions.is_empty();
if has_app_manifest {
acl_manifests.insert(APP_ACL_KEY.into(), app_acl.manifest);
}
acl_manifests.extend(inline_plugins(out_dir, attributes.inlined_plugins.clone())?);
let inline_plugins_acl = inline_plugins(out_dir, attributes.inlined_plugins.clone())?;
acl_manifests.extend(inline_plugins_acl.manifests);
let acl_manifests_path = save_acl_manifests(&acl_manifests)?;
fs::copy(acl_manifests_path, out_dir.join(ACL_MANIFESTS_FILE_NAME))?;
@ -412,5 +434,12 @@ pub fn build(out_dir: &Path, target: Target, attributes: &Attributes) -> super::
tauri_utils::plugin::save_global_api_scripts_paths(out_dir);
let mut permissions_map = inline_plugins_acl.permission_files;
if has_app_manifest {
permissions_map.insert(APP_ACL_KEY.to_string(), app_acl.permission_files);
}
tauri_utils::acl::build::generate_allowed_commands(out_dir, permissions_map)?;
Ok(())
}

View File

@ -456,12 +456,6 @@ pub fn try_build(attributes: Attributes) -> Result<()> {
use anyhow::anyhow;
println!("cargo:rerun-if-env-changed=TAURI_CONFIG");
#[cfg(feature = "config-json")]
println!("cargo:rerun-if-changed=tauri.conf.json");
#[cfg(feature = "config-json5")]
println!("cargo:rerun-if-changed=tauri.conf.json5");
#[cfg(feature = "config-toml")]
println!("cargo:rerun-if-changed=Tauri.toml");
let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap();
let mobile = target_os == "ios" || target_os == "android";
@ -471,12 +465,11 @@ pub fn try_build(attributes: Attributes) -> Result<()> {
let target_triple = env::var("TARGET").unwrap();
let target = tauri_utils::platform::Target::from_triple(&target_triple);
let (config, merged_config_path) =
tauri_utils::config::parse::read_from(target, env::current_dir().unwrap())?;
if let Some(merged_config_path) = merged_config_path {
println!("cargo:rerun-if-changed={}", merged_config_path.display());
let (mut config, config_paths) =
tauri_utils::config::parse::read_from(target, &env::current_dir().unwrap())?;
for config_file_path in config_paths {
println!("cargo:rerun-if-changed={}", config_file_path.display());
}
let mut config = serde_json::from_value(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);

View File

@ -69,7 +69,9 @@
},
"build": {
"description": "The build configuration.",
"default": {},
"default": {
"removeUnusedCommands": false
},
"allOf": [
{
"$ref": "#/definitions/BuildConfig"
@ -1797,6 +1799,11 @@
"items": {
"type": "string"
}
},
"removeUnusedCommands": {
"description": "Try to remove unused commands registered from plugins base on the ACL list during `tauri build`,\n the way it works is that tauri-cli will read this and set the environment variables for the build script and macros,\n and they'll try to get all the allowed commands and remove the rest\n\n Note:\n - This won't be accounting for dynamically added ACLs so make sure to check it when using this\n - This feature requires tauri-plugin 2.1 and tauri 2.4",
"default": false,
"type": "boolean"
}
},
"additionalProperties": false

View File

@ -182,7 +182,7 @@ pub fn setup(
return Err(anyhow::anyhow!(
"The configured frontendDist includes the `{:?}` {}. Please isolate your web assets on a separate folder and update `tauri.conf.json > build > frontendDist`.",
out_folders,
if out_folders.len() == 1 { "folder" }else { "folders" }
if out_folders.len() == 1 { "folder" } else { "folders" }
)
);
}

View File

@ -6,6 +6,7 @@ use itertools::Itertools;
use json_patch::merge;
use serde_json::Value as JsonValue;
use tauri_utils::acl::REMOVE_UNUSED_COMMANDS_ENV_VAR;
pub use tauri_utils::{config::*, platform::Target};
use std::{
@ -153,7 +154,7 @@ fn get_internal(
let mut extensions = HashMap::new();
if let Some((platform_config, config_path)) =
tauri_utils::config::parse::read_platform(target, tauri_dir.to_path_buf())?
tauri_utils::config::parse::read_platform(target, tauri_dir)?
{
merge(&mut config, &platform_config);
extensions.insert(
@ -213,6 +214,10 @@ fn get_internal(
);
}
if config.build.remove_unused_commands {
std::env::set_var(REMOVE_UNUSED_COMMANDS_ENV_VAR, tauri_dir);
}
*config_handle().lock().unwrap() = Some(ConfigMetadata {
target,
inner: config,

View File

@ -18,13 +18,13 @@ 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,
acl::resolved::Resolved,
acl::{
get_capabilities, manifest::Manifest, resolved::Resolved, ACL_MANIFESTS_FILE_NAME,
CAPABILITIES_FILE_NAME,
},
assets::AssetKey,
config::{CapabilityEntry, Config, FrontendDist, PatternKind},
config::{Config, FrontendDist, PatternKind},
html::{inject_nonce_token, parse as parse_html, serialize_node as serialize_html_node, NodeRef},
platform::Target,
tokens::{map_lit, str_lit},
@ -386,34 +386,14 @@ pub fn context_codegen(data: ContextData) -> EmbeddedAssetsResult<TokenStream> {
};
let capabilities_file_path = out_dir.join(CAPABILITIES_FILE_NAME);
let mut capabilities_from_files: BTreeMap<String, Capability> = if capabilities_file_path.exists()
{
let capabilities_file =
std::fs::read_to_string(capabilities_file_path).expect("failed to read capabilities");
serde_json::from_str(&capabilities_file).expect("failed to parse capabilities")
} else {
Default::default()
};
let capabilities = get_capabilities(
&config,
Some(&capabilities_file_path),
additional_capabilities.as_deref(),
)
.unwrap();
let mut capabilities = if config.app.security.capabilities.is_empty() {
capabilities_from_files
} else {
let mut capabilities = BTreeMap::new();
for capability_entry in &config.app.security.capabilities {
match capability_entry {
CapabilityEntry::Inlined(capability) => {
capabilities.insert(capability.identifier.clone(), capability.clone());
}
CapabilityEntry::Reference(id) => {
let capability = capabilities_from_files
.remove(id)
.unwrap_or_else(|| panic!("capability with identifier {id} not found"));
capabilities.insert(id.clone(), capability);
}
}
}
capabilities
};
let resolved = Resolved::resolve(&acl, capabilities, target).expect("failed to resolve ACL");
let acl_tokens = map_lit(
quote! { ::std::collections::BTreeMap },
@ -422,29 +402,6 @@ pub fn context_codegen(data: ContextData) -> EmbeddedAssetsResult<TokenStream> {
identity,
);
if let Some(paths) = additional_capabilities {
for path in paths {
let capability = CapabilityFile::load(&path)
.unwrap_or_else(|e| panic!("failed to read capability {}: {e}", path.display()));
match capability {
CapabilityFile::Capability(c) => {
capabilities.insert(c.identifier.clone(), c);
}
CapabilityFile::List(capabilities_list)
| CapabilityFile::NamedList {
capabilities: capabilities_list,
} => {
capabilities.extend(
capabilities_list
.into_iter()
.map(|c| (c.identifier.clone(), c)),
);
}
}
}
}
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_scripts = if config.app.with_global_tauri {

View File

@ -78,7 +78,7 @@ pub fn get_config(path: &Path) -> Result<(Config, PathBuf), CodegenConfigError>
// already unlikely unless the developer goes out of their way to run the cli on a different
// project than the target crate.
let mut config =
serde_json::from_value(tauri_utils::config::parse::read_from(target, parent.clone())?.0)?;
serde_json::from_value(tauri_utils::config::parse::read_from(target, &parent)?.0)?;
if let Ok(env) = std::env::var("TAURI_CONFIG") {
let merge_config: serde_json::Value =

View File

@ -31,33 +31,109 @@ pub struct Handler {
impl Parse for Handler {
fn parse(input: &ParseBuffer<'_>) -> syn::Result<Self> {
let command_defs = input.parse_terminated(CommandDef::parse, Token![,])?;
let plugin_name = try_get_plugin_name(input)?;
let mut command_defs = input
.parse_terminated(CommandDef::parse, Token![,])?
.into_iter()
.collect();
filter_unused_commands(plugin_name, &mut command_defs);
let mut commands = Vec::new();
let mut wrappers = Vec::new();
// parse the command names and wrappers from the passed paths
let (commands, wrappers) = command_defs
.iter()
.map(|command_def| {
let mut wrapper = command_def.path.clone();
let last = super::path_to_command(&mut wrapper);
for command_def in &command_defs {
let mut wrapper = command_def.path.clone();
let last = super::path_to_command(&mut wrapper);
// the name of the actual command function
let command = last.ident.clone();
// the name of the actual command function
let command = last.ident.clone();
// set the path to the command function wrapper
last.ident = super::format_command_wrapper(&command);
// set the path to the command function wrapper
last.ident = super::format_command_wrapper(&command);
(command, wrapper)
})
.unzip();
commands.push(command);
wrappers.push(wrapper);
}
Ok(Self {
command_defs: command_defs.into_iter().collect(), // remove punctuation separators
command_defs,
commands,
wrappers,
})
}
}
/// Try to get the plugin name by parsing the input for a `#![plugin(...)]` attribute,
/// if it's not present, try getting it from `CARGO_PKG_NAME` enviroment variable
fn try_get_plugin_name(input: &ParseBuffer<'_>) -> Result<Option<String>, syn::Error> {
if let Ok(attrs) = input.call(Attribute::parse_inner) {
for attr in attrs {
if attr.path().is_ident("plugin") {
// Parse the content inside #![plugin(...)]
let plugin_name = attr.parse_args::<Ident>()?.to_string();
return Ok(Some(if plugin_name == "__TAURI_CHANNEL__" {
plugin_name
} else {
plugin_name.replace("_", "-")
}));
}
}
}
Ok(
std::env::var("CARGO_PKG_NAME")
.ok()
.and_then(|var| var.strip_prefix("tauri-plugin-").map(String::from)),
)
}
fn filter_unused_commands(plugin_name: Option<String>, command_defs: &mut Vec<CommandDef>) {
let allowed_commands = tauri_utils::acl::read_allowed_commands();
let Some(allowed_commands) = allowed_commands else {
return;
};
if plugin_name.is_none() && !allowed_commands.has_app_acl {
// All application commands are allowed if we don't have an application ACL
//
// note that inline plugins without the #![plugin()] attribute would also get to this check
// which means inline plugins must have an app manifest to get proper unused command removal
return;
}
let mut unused_commands = Vec::new();
let command_prefix = if let Some(plugin_name) = &plugin_name {
format!("plugin:{plugin_name}|")
} else {
"".into()
};
command_defs.retain(|command_def| {
let mut wrapper = command_def.path.clone();
let last = super::path_to_command(&mut wrapper);
// the name of the actual command function
let command_name = &last.ident;
let command = format!("{command_prefix}{command_name}");
let is_allowed = allowed_commands.commands.contains(&command);
if !is_allowed {
unused_commands.push(command_name.to_string());
}
is_allowed
});
if !unused_commands.is_empty() {
let plugin_display_name = plugin_name.as_deref().unwrap_or("application");
let unused_commands_display = unused_commands.join(", ");
println!("Removed unused commands from {plugin_display_name}: {unused_commands_display}",);
}
}
impl From<Handler> for proc_macro::TokenStream {
fn from(
Handler {

View File

@ -16,6 +16,7 @@ use syn::{
spanned::Spanned,
Expr, ExprLit, FnArg, ItemFn, Lit, Meta, Pat, Token, Visibility,
};
use tauri_utils::acl::REMOVE_UNUSED_COMMANDS_ENV_VAR;
enum WrapperAttributeKind {
Meta(Meta),
@ -261,12 +262,21 @@ pub fn wrapper(attributes: TokenStream, item: TokenStream) -> TokenStream {
quote!()
};
// Allow this to be unused when we're building with `build > removeUnusedCommands` for dead code elimination
let maybe_allow_unused = if var(REMOVE_UNUSED_COMMANDS_ENV_VAR).is_ok() {
quote!(#[allow(unused)])
} else {
TokenStream2::default()
};
// Rely on rust 2018 edition to allow importing a macro from a path.
quote!(
#async_command_check
#maybe_allow_unused
#function
#maybe_allow_unused
#maybe_macro_export
#[doc(hidden)]
macro_rules! #wrapper {

View File

@ -43,7 +43,11 @@ pub fn mobile_entry_point(attributes: TokenStream, item: TokenStream) -> TokenSt
/// Accepts a list of command functions. Creates a handler that allows commands to be called from JS with invoke().
///
/// You can optionally annotate the commands with a inner attribute tag `#![plugin(your_plugin_name)]`
/// for `build > removeUnusedCommands` to work for plugins not defined in a standalone crate like `tauri-plugin-fs`
///
/// # Examples
///
/// ```rust,ignore
/// use tauri_macros::{command, generate_handler};
/// #[command]
@ -58,7 +62,9 @@ pub fn mobile_entry_point(attributes: TokenStream, item: TokenStream) -> TokenSt
/// let _handler = generate_handler![command_one, command_two];
/// }
/// ```
///
/// # Stability
///
/// The output of this macro is managed internally by Tauri,
/// and should not be accessed directly on normal applications.
/// It may have breaking changes in the future.

View File

@ -2,7 +2,10 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use std::path::{Path, PathBuf};
use std::{
collections::BTreeMap,
path::{Path, PathBuf},
};
use anyhow::Result;
use tauri_utils::acl::{self, Error};
@ -132,6 +135,10 @@ impl<'a> Builder<'a> {
)?;
}
let mut permissions_map = BTreeMap::new();
permissions_map.insert(name.clone(), permissions);
tauri_utils::acl::build::generate_allowed_commands(&out_dir, permissions_map)?;
if let Some(global_scope_schema) = self.global_scope_schema {
acl::build::define_global_scope_schema(global_scope_schema, &name, &out_dir)?;
}

View File

@ -69,7 +69,9 @@
},
"build": {
"description": "The build configuration.",
"default": {},
"default": {
"removeUnusedCommands": false
},
"allOf": [
{
"$ref": "#/definitions/BuildConfig"
@ -1797,6 +1799,11 @@
"items": {
"type": "string"
}
},
"removeUnusedCommands": {
"description": "Try to remove unused commands registered from plugins base on the ACL list during `tauri build`,\n the way it works is that tauri-cli will read this and set the environment variables for the build script and macros,\n and they'll try to get all the allowed commands and remove the rest\n\n Note:\n - This won't be accounting for dynamically added ACLs so make sure to check it when using this\n - This feature requires tauri-plugin 2.1 and tauri 2.4",
"default": false,
"type": "boolean"
}
},
"additionalProperties": false

View File

@ -15,6 +15,7 @@ rust-version.workspace = true
[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
anyhow = "1"
thiserror = "2"
phf = { version = "0.11", features = ["macros"] }
brotli = { version = "7", optional = true, default-features = false, features = [

View File

@ -10,12 +10,17 @@ use std::{
path::{Path, PathBuf},
};
use crate::{acl::Error, write_if_changed};
use crate::{
acl::{has_app_manifest, AllowedCommands, Error},
config::Config,
write_if_changed,
};
use super::{
capability::{Capability, CapabilityFile},
manifest::PermissionFile,
PERMISSION_SCHEMAS_FOLDER_NAME, PERMISSION_SCHEMA_FILE_NAME,
ALLOWED_COMMANDS_FILE_NAME, PERMISSION_SCHEMAS_FOLDER_NAME, PERMISSION_SCHEMA_FILE_NAME,
REMOVE_UNUSED_COMMANDS_ENV_VAR,
};
/// Known name of the folder containing autogenerated permissions.
@ -126,7 +131,11 @@ pub fn read_permissions() -> Result<HashMap<String, Vec<PermissionFile>>, Error>
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 = if plugin_crate_name_var == "CORE:__TAURI_CHANNEL__" {
"core:__TAURI_CHANNEL__".to_string()
} else {
plugin_crate_name_var.to_lowercase().replace('_', "-")
};
let plugin_crate_name = plugin_crate_name
.strip_prefix("tauri-plugin-")
.map(ToString::to_string)
@ -379,3 +388,116 @@ pub fn generate_docs(
Ok(())
}
// TODO: We have way too many duplicated code around getting the config files, e.g.
// - crates/tauri-codegen/src/lib.rs (`get_config`)
// - crates/tauri-build/src/lib.rs (`try_build`)
// - crates/tauri-cli/src/helpers/config.rs (`get_internal`)
/// Generate allowed commands file for the `generate_handler` macro to remove never allowed commands
pub fn generate_allowed_commands(
out_dir: &Path,
permissions_map: BTreeMap<String, Vec<PermissionFile>>,
) -> Result<(), anyhow::Error> {
println!("cargo:rerun-if-env-changed={REMOVE_UNUSED_COMMANDS_ENV_VAR}");
let allowed_commands_file_path = out_dir.join(ALLOWED_COMMANDS_FILE_NAME);
let remove_unused_commands_env_var = std::env::var(REMOVE_UNUSED_COMMANDS_ENV_VAR);
let should_generate_allowed_commands =
remove_unused_commands_env_var.is_ok() && !permissions_map.is_empty();
if !should_generate_allowed_commands {
let _ = std::fs::remove_file(allowed_commands_file_path);
return Ok(());
}
// It's safe to `unwrap` here since we have checked if the result is ok above
let config_directory = PathBuf::from(remove_unused_commands_env_var.unwrap());
let capabilities_path = config_directory.join("capabilities");
// Cargo re-builds if the variable points to an empty path,
// so we check for exists here
// see https://github.com/rust-lang/cargo/issues/4213
if capabilities_path.exists() {
println!("cargo:rerun-if-changed={}", capabilities_path.display());
}
let mut capabilities = crate::acl::build::parse_capabilities(&format!(
"{}/**/*",
glob::Pattern::escape(&capabilities_path.to_string_lossy())
))?;
let target_triple = env::var("TARGET")?;
let target = crate::platform::Target::from_triple(&target_triple);
let (mut config, config_paths) = crate::config::parse::read_from(target, &config_directory)?;
for config_file_path in config_paths {
println!("cargo:rerun-if-changed={}", config_file_path.display());
}
if let Ok(env) = std::env::var("TAURI_CONFIG") {
let merge_config: serde_json::Value = serde_json::from_str(&env)?;
json_patch::merge(&mut config, &merge_config);
}
println!("cargo:rerun-if-env-changed=TAURI_CONFIG");
// Set working directory to where `tauri.config.json` is, so that relative paths in it are parsed correctly.
let old_cwd = std::env::current_dir()?;
std::env::set_current_dir(config_directory)?;
let config: Config = serde_json::from_value(config)?;
// Reset working directory.
std::env::set_current_dir(old_cwd)?;
let acl: BTreeMap<String, crate::acl::manifest::Manifest> = permissions_map
.into_iter()
.map(|(key, permissions)| {
let key = key
.strip_prefix("tauri-plugin-")
.unwrap_or(&key)
.to_string();
let manifest = crate::acl::manifest::Manifest::new(permissions, None);
(key, manifest)
})
.collect();
capabilities.extend(crate::acl::get_capabilities(&config, None, None)?);
let permission_entries = capabilities
.into_iter()
.flat_map(|(_, capabilities)| capabilities.permissions);
let mut allowed_commands = AllowedCommands {
has_app_acl: has_app_manifest(&acl),
..Default::default()
};
for permission_entry in permission_entries {
let Ok(permissions) =
crate::acl::resolved::get_permissions(permission_entry.identifier(), &acl)
else {
continue;
};
for permission in permissions {
let plugin_name = permission.key;
let allowed_command_names = &permission.permission.commands.allow;
for allowed_command in allowed_command_names {
let command_name = if plugin_name == crate::acl::APP_ACL_KEY {
allowed_command.to_string()
} else if let Some(core_plugin_name) = plugin_name.strip_prefix("core:") {
format!("plugin:{core_plugin_name}|{allowed_command}")
} else {
format!("plugin:{plugin_name}|{allowed_command}")
};
allowed_commands.commands.insert(command_name);
}
}
}
write_if_changed(
allowed_commands_file_path,
serde_json::to_string(&allowed_commands)?,
)?;
Ok(())
}

View File

@ -163,6 +163,22 @@ impl TryFrom<String> for Identifier {
}
let is_core_identifier = value.starts_with(CORE_PLUGIN_IDENTIFIER_PREFIX);
let is_core_channel_plugin = value.starts_with("core:__TAURI_CHANNEL__:");
if is_core_channel_plugin {
return Ok(Self {
separator: NonZeroU8::new(
value.len() as u8
- value
.chars()
.rev()
.position(|c| c as u8 == IDENTIFIER_SEPARATOR)
.unwrap() as u8
- 1,
),
inner: value,
});
}
let mut bytes = value.bytes();

View File

@ -14,7 +14,7 @@ use serde::{Deserialize, Serialize};
/// The default permission set of the plugin.
///
/// Works similarly to a permission with the "default" identifier.
#[derive(Debug, Deserialize, Serialize)]
#[derive(Debug, Clone, Deserialize, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct DefaultPermission {
/// The version of the permission.
@ -30,7 +30,7 @@ pub struct DefaultPermission {
}
/// Permission file that can define a default permission, a set of permissions or a list of inlined permissions.
#[derive(Debug, Deserialize, Serialize)]
#[derive(Debug, Clone, Deserialize, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct PermissionFile {
/// The default permission set for the plugin

View File

@ -21,12 +21,24 @@
//! [ignore unknown fields when destructuring]: https://doc.rust-lang.org/book/ch18-03-pattern-syntax.html#ignoring-remaining-parts-of-a-value-with-
//! [Struct Update Syntax]: https://doc.rust-lang.org/book/ch05-01-defining-structs.html#creating-instances-from-other-instances-with-struct-update-syntax
use anyhow::Context;
use capability::{Capability, CapabilityFile};
use serde::{Deserialize, Serialize};
use std::{num::NonZeroU64, path::PathBuf, str::FromStr, sync::Arc};
use std::{
collections::{BTreeMap, HashSet},
fs,
num::NonZeroU64,
path::{Path, PathBuf},
str::FromStr,
sync::Arc,
};
use thiserror::Error;
use url::Url;
use crate::platform::Target;
use crate::{
config::{CapabilityEntry, Config},
platform::Target,
};
pub use self::{identifier::*, value::*};
@ -40,6 +52,11 @@ pub const APP_ACL_KEY: &str = "__app-acl__";
pub const ACL_MANIFESTS_FILE_NAME: &str = "acl-manifests.json";
/// Known capabilityies file
pub const CAPABILITIES_FILE_NAME: &str = "capabilities.json";
/// Allowed commands file name
pub const ALLOWED_COMMANDS_FILE_NAME: &str = "allowed-commands.json";
/// Set by the CLI with when `build > removeUnusedCommands` is set for dead code elimination,
/// the value is set to the config's directory
pub const REMOVE_UNUSED_COMMANDS_ENV_VAR: &str = "REMOVE_UNUSED_COMMANDS";
#[cfg(feature = "build")]
pub mod build;
@ -155,7 +172,7 @@ pub enum Error {
/// Allowed and denied commands inside a permission.
///
/// If two commands clash inside of `allow` and `deny`, it should be denied by default.
#[derive(Debug, Default, Serialize, Deserialize)]
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct Commands {
/// Allowed command.
@ -202,7 +219,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, Default)]
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct Permission {
/// The version of the permission.
@ -243,7 +260,7 @@ impl Permission {
}
/// A set of direct permissions grouped together under a new name.
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct PermissionSet {
/// A unique identifier for the permission.
@ -327,6 +344,92 @@ pub enum ExecutionContext {
},
}
/// Test if the app has an application manifest from the ACL
pub fn has_app_manifest(acl: &BTreeMap<String, crate::acl::manifest::Manifest>) -> bool {
acl.contains_key(APP_ACL_KEY)
}
/// Get the capabilities from the config file
pub fn get_capabilities(
config: &Config,
pre_built_capabilities_file_path: Option<&Path>,
additional_capability_files: Option<&[PathBuf]>,
) -> anyhow::Result<BTreeMap<String, Capability>> {
let mut capabilities_from_files: BTreeMap<String, Capability> = BTreeMap::new();
if let Some(capabilities_file_path) = pre_built_capabilities_file_path {
if capabilities_file_path.exists() {
let capabilities_file =
std::fs::read_to_string(capabilities_file_path).context("failed to read capabilities")?;
capabilities_from_files =
serde_json::from_str(&capabilities_file).context("failed to parse capabilities")?;
}
}
let mut capabilities = if config.app.security.capabilities.is_empty() {
capabilities_from_files
} else {
let mut capabilities = BTreeMap::new();
for capability_entry in &config.app.security.capabilities {
match capability_entry {
CapabilityEntry::Inlined(capability) => {
capabilities.insert(capability.identifier.clone(), capability.clone());
}
CapabilityEntry::Reference(id) => {
let capability = capabilities_from_files
.remove(id)
.with_context(|| format!("capability with identifier {id} not found"))?;
capabilities.insert(id.clone(), capability);
}
}
}
capabilities
};
if let Some(paths) = additional_capability_files {
for path in paths {
let capability = CapabilityFile::load(path)
.with_context(|| format!("failed to read capability {}", path.display()))?;
match capability {
CapabilityFile::Capability(c) => {
capabilities.insert(c.identifier.clone(), c);
}
CapabilityFile::List(capabilities_list)
| CapabilityFile::NamedList {
capabilities: capabilities_list,
} => {
capabilities.extend(
capabilities_list
.into_iter()
.map(|c| (c.identifier.clone(), c)),
);
}
}
}
}
Ok(capabilities)
}
/// Allowed commands used to communicate between `generate_handle` and `generate_allowed_commands` through json files
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct AllowedCommands {
/// The commands allowed
pub commands: HashSet<String>,
/// Has application ACL or not
pub has_app_acl: bool,
}
/// Try to reads allowed commands from the out dir made by our build script
pub fn read_allowed_commands() -> Option<AllowedCommands> {
let out_file = std::env::var("OUT_DIR")
.map(PathBuf::from)
.ok()?
.join(ALLOWED_COMMANDS_FILE_NAME);
let file = fs::read_to_string(&out_file).ok()?;
let json = serde_json::from_str(&file).ok()?;
Some(json)
}
#[cfg(test)]
mod tests {
use crate::acl::RemoteUrlPattern;

View File

@ -321,14 +321,19 @@ fn with_resolved_permissions<F: FnMut(ResolvedPermission<'_>) -> Result<(), Erro
Ok(())
}
/// Traversed permission
#[derive(Debug)]
struct TraversedPermission<'a> {
key: String,
permission_name: String,
permission: &'a Permission,
pub struct TraversedPermission<'a> {
/// Plugin name without the tauri-plugin- prefix
pub key: String,
/// Permission's name
pub permission_name: String,
/// Permission details
pub permission: &'a Permission,
}
fn get_permissions<'a>(
/// Expand a permissions id based on the ACL to get the associated permissions (e.g. expand some-plugin:default)
pub fn get_permissions<'a>(
permission_id: &Identifier,
acl: &'a BTreeMap<String, Manifest>,
) -> Result<Vec<TraversedPermission<'a>>, Error> {

View File

@ -2687,6 +2687,15 @@ pub struct BuildConfig {
pub before_bundle_command: Option<HookCommand>,
/// Features passed to `cargo` commands.
pub features: Option<Vec<String>>,
/// Try to remove unused commands registered from plugins base on the ACL list during `tauri build`,
/// the way it works is that tauri-cli will read this and set the environment variables for the build script and macros,
/// and they'll try to get all the allowed commands and remove the rest
///
/// Note:
/// - This won't be accounting for dynamically added ACLs so make sure to check it when using this
/// - This feature requires tauri-plugin 2.1 and tauri 2.4
#[serde(alias = "remove-unused-commands", default)]
pub remove_unused_commands: bool,
}
#[derive(Debug, PartialEq, Eq)]
@ -2847,7 +2856,7 @@ pub struct Config {
#[serde(default)]
pub app: AppConfig,
/// The build configuration.
#[serde(default = "default_build")]
#[serde(default)]
pub build: BuildConfig,
/// The bundler configuration.
#[serde(default)]
@ -2864,18 +2873,6 @@ pub struct Config {
#[cfg_attr(feature = "schema", derive(JsonSchema))]
pub struct PluginConfig(pub HashMap<String, JsonValue>);
fn default_build() -> BuildConfig {
BuildConfig {
runner: None,
dev_url: None,
frontend_dist: None,
before_dev_command: None,
before_build_command: None,
before_bundle_command: None,
features: None,
}
}
/// Implement `ToTokens` for all config structs, allowing a literal `Config` to be built.
///
/// This allows for a build script to output the values in a `Config` to a `TokenStream`, which can
@ -3267,6 +3264,7 @@ mod build {
let before_build_command = quote!(None);
let before_bundle_command = quote!(None);
let features = quote!(None);
let remove_unused_commands = quote!(false);
literal_struct!(
tokens,
@ -3277,7 +3275,8 @@ mod build {
before_dev_command,
before_build_command,
before_bundle_command,
features
features,
remove_unused_commands
);
}
}
@ -3594,6 +3593,7 @@ mod test {
before_build_command: None,
before_bundle_command: None,
features: None,
remove_unused_commands: false,
};
// create a bundle config

View File

@ -174,20 +174,17 @@ pub fn is_configuration_file(target: Target, path: &Path) -> bool {
/// - `tauri.ios.conf.json[5]` or `Tauri.ios.toml` on iOS
/// Merging the configurations using [JSON Merge Patch (RFC 7396)].
///
/// Returns the raw configuration and the platform config path, if any.
/// Returns the raw configuration and used config paths.
///
/// [JSON Merge Patch (RFC 7396)]: https://datatracker.ietf.org/doc/html/rfc7396.
pub fn read_from(
target: Target,
root_dir: PathBuf,
) -> Result<(Value, Option<PathBuf>), ConfigError> {
let mut config: Value = parse_value(target, root_dir.join("tauri.conf.json"))?.0;
pub fn read_from(target: Target, root_dir: &Path) -> Result<(Value, Vec<PathBuf>), ConfigError> {
let (mut config, config_file_path) = parse_value(target, root_dir.join("tauri.conf.json"))?;
let mut config_paths = vec![config_file_path];
if let Some((platform_config, path)) = read_platform(target, root_dir)? {
config_paths.push(path);
merge(&mut config, &platform_config);
Ok((config, Some(path)))
} else {
Ok((config, None))
}
Ok((config, config_paths))
}
/// Reads the platform-specific configuration file from the given root directory if it exists.
@ -195,7 +192,7 @@ pub fn read_from(
/// Check [`read_from`] for more information.
pub fn read_platform(
target: Target,
root_dir: PathBuf,
root_dir: &Path,
) -> Result<Option<(Value, PathBuf)>, ConfigError> {
let platform_config_path = root_dir.join(ConfigFormat::Json.into_platform_file_name(target));
if does_supported_file_name_exist(target, &platform_config_path) {

View File

@ -6,6 +6,7 @@ use heck::AsShoutySnakeCase;
use tauri_utils::write_if_changed;
use std::{
collections::BTreeMap,
env, fs,
path::{Path, PathBuf},
sync::{Mutex, OnceLock},
@ -14,6 +15,7 @@ use std::{
static CHECKED_FEATURES: OnceLock<Mutex<Vec<String>>> = OnceLock::new();
const PLUGINS: &[(&str, &[(&str, bool)])] = &[
// (plugin_name, &[(command, enabled-by_default)])
("core:__TAURI_CHANNEL__", &[("fetch", true)]),
(
"core:path",
&[
@ -334,7 +336,8 @@ fn main() {
}
}
define_permissions(&out_dir);
let permissions = define_permissions(&out_dir);
tauri_utils::acl::build::generate_allowed_commands(&out_dir, permissions).unwrap();
}
const LICENSE_HEADER: &str = r"# Copyright 2019-2024 Tauri Programme within The Commons Conservancy
@ -342,7 +345,10 @@ const LICENSE_HEADER: &str = r"# Copyright 2019-2024 Tauri Programme within The
# SPDX-License-Identifier: MIT
";
fn define_permissions(out_dir: &Path) {
fn define_permissions(
out_dir: &Path,
) -> BTreeMap<String, Vec<tauri_utils::acl::manifest::PermissionFile>> {
let mut all_permissions = BTreeMap::new();
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);
@ -402,12 +408,18 @@ permissions = [{default_permissions}]
plugin.strip_prefix("tauri-plugin-").unwrap_or(plugin),
)
.expect("failed to generate plugin documentation page");
all_permissions.insert(plugin.to_string(), permissions);
}
define_default_permission_set(out_dir);
let default_permissions = define_default_permission_set(out_dir);
all_permissions.insert("core".to_string(), default_permissions);
all_permissions
}
fn define_default_permission_set(out_dir: &Path) {
fn define_default_permission_set(
out_dir: &Path,
) -> Vec<tauri_utils::acl::manifest::PermissionFile> {
let permissions_out_dir = out_dir.join("permissions");
fs::create_dir_all(&permissions_out_dir)
.expect("failed to create core:default permissions directory");
@ -437,7 +449,7 @@ permissions = [{}]
write_if_changed(default_toml, toml_content)
.unwrap_or_else(|_| panic!("unable to autogenerate core:default set"));
let _ = tauri_utils::acl::build::define_permissions(
tauri_utils::acl::build::define_permissions(
&PathBuf::from(glob::Pattern::escape(
&permissions_out_dir.to_string_lossy(),
))
@ -447,7 +459,7 @@ permissions = [{}]
out_dir,
|_| true,
)
.unwrap_or_else(|e| panic!("failed to define permissions for `core:default` : {e}"));
.unwrap_or_else(|e| panic!("failed to define permissions for `core:default` : {e}"))
}
fn embed_manifest_for_tests() {

View File

@ -0,0 +1,41 @@
## Default Permission
Default permissions for the plugin.
- `allow-fetch`
## Permission Table
<table>
<tr>
<th>Identifier</th>
<th>Description</th>
</tr>
<tr>
<td>
`core:__TAURI_CHANNEL__:allow-fetch`
</td>
<td>
Enables the fetch command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:__TAURI_CHANNEL__:deny-fetch`
</td>
<td>
Denies the fetch command without any pre-configured scope.
</td>
</tr>
</table>

View File

@ -86,6 +86,7 @@ pub async fn set_app_theme<R: Runtime>(app: AppHandle<R>, theme: Option<Theme>)
pub fn init<R: Runtime>() -> TauriPlugin<R> {
Builder::new("app")
.invoke_handler(crate::generate_handler![
#![plugin(app)]
version,
name,
tauri_version,

View File

@ -77,6 +77,9 @@ async fn emit_to<R: Runtime>(
/// Initializes the event plugin.
pub(crate) fn init<R: Runtime>() -> TauriPlugin<R> {
Builder::new("event")
.invoke_handler(crate::generate_handler![listen, unlisten, emit, emit_to])
.invoke_handler(crate::generate_handler![
#![plugin(event)]
listen, unlisten, emit, emit_to
])
.build()
}

View File

@ -81,6 +81,7 @@ fn size<R: Runtime>(webview: Webview<R>, rid: ResourceId) -> crate::Result<Size>
pub fn init<R: Runtime>() -> TauriPlugin<R> {
Builder::new("image")
.invoke_handler(crate::generate_handler![
#![plugin(image)]
new, from_bytes, from_path, rgba, size
])
.build()

View File

@ -9,6 +9,7 @@ use std::sync::Arc;
use serde::de::DeserializeOwned;
use serde::Serialize;
use tauri_utils::acl::has_app_manifest;
use tauri_utils::acl::{
capability::{Capability, CapabilityFile, PermissionEntry},
manifest::Manifest,
@ -247,7 +248,7 @@ impl RuntimeAuthority {
}
pub(crate) fn has_app_manifest(&self) -> bool {
self.acl.contains_key(APP_ACL_KEY)
has_app_manifest(&self.acl)
}
#[doc(hidden)]

View File

@ -254,6 +254,9 @@ fn fetch(
pub fn plugin<R: Runtime>() -> TauriPlugin<R> {
PluginBuilder::new(CHANNEL_PLUGIN_NAME)
.invoke_handler(crate::generate_handler![fetch])
.invoke_handler(crate::generate_handler![
#![plugin(__TAURI_CHANNEL__)]
fetch
])
.build()
}

View File

@ -883,6 +883,7 @@ pub(crate) fn init<R: Runtime>() -> TauriPlugin<R> {
}
})
.invoke_handler(crate::generate_handler![
#![plugin(menu)]
new,
append,
prepend,

View File

@ -221,6 +221,7 @@ pub(crate) fn init<R: Runtime>() -> TauriPlugin<R> {
Builder::new("path")
.invoke_handler(crate::generate_handler![
#![plugin(path)]
resolve_directory,
resolve,
normalize,

View File

@ -24,6 +24,9 @@ fn close<R: Runtime>(webview: Webview<R>, rid: ResourceId) -> crate::Result<()>
pub(crate) fn init<R: Runtime>() -> TauriPlugin<R> {
Builder::new("resources")
.invoke_handler(crate::generate_handler![close])
.invoke_handler(crate::generate_handler![
#![plugin(resources)]
close
])
.build()
}

View File

@ -227,6 +227,7 @@ fn set_show_menu_on_left_click<R: Runtime>(
pub(crate) fn init<R: Runtime>() -> TauriPlugin<R> {
Builder::new("tray")
.invoke_handler(crate::generate_handler![
#![plugin(tray)]
new,
get_by_id,
remove_by_id,

View File

@ -1432,10 +1432,7 @@ fn main() {
});
// we only check ACL on plugin commands or if the app defined its ACL manifest
if (plugin_command.is_some() || has_app_acl_manifest)
&& request.cmd != crate::ipc::channel::FETCH_CHANNEL_DATA_COMMAND
&& invoke.acl.is_none()
{
if (plugin_command.is_some() || has_app_acl_manifest) && invoke.acl.is_none() {
#[cfg(debug_assertions)]
{
let (key, command_name) = plugin_command

View File

@ -277,41 +277,38 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
}
builder
.invoke_handler(|invoke| {
.invoke_handler(
#[cfg(desktop)]
{
let handler: Box<dyn Fn(crate::ipc::Invoke<R>) -> bool> =
Box::new(crate::generate_handler![
desktop_commands::create_webview,
desktop_commands::create_webview_window,
// getters
desktop_commands::get_all_webviews,
desktop_commands::webview_position,
desktop_commands::webview_size,
// setters
desktop_commands::webview_close,
desktop_commands::set_webview_size,
desktop_commands::set_webview_position,
desktop_commands::set_webview_focus,
desktop_commands::set_webview_background_color,
desktop_commands::set_webview_zoom,
desktop_commands::webview_hide,
desktop_commands::webview_show,
desktop_commands::print,
desktop_commands::reparent,
desktop_commands::clear_all_browsing_data,
#[cfg(any(debug_assertions, feature = "devtools"))]
desktop_commands::internal_toggle_devtools,
]);
handler(invoke)
}
crate::generate_handler![
#![plugin(webview)]
desktop_commands::create_webview,
desktop_commands::create_webview_window,
// getters
desktop_commands::get_all_webviews,
desktop_commands::webview_position,
desktop_commands::webview_size,
// setters
desktop_commands::webview_close,
desktop_commands::set_webview_size,
desktop_commands::set_webview_position,
desktop_commands::set_webview_focus,
desktop_commands::set_webview_background_color,
desktop_commands::set_webview_zoom,
desktop_commands::webview_hide,
desktop_commands::webview_show,
desktop_commands::print,
desktop_commands::reparent,
desktop_commands::clear_all_browsing_data,
#[cfg(any(debug_assertions, feature = "devtools"))]
desktop_commands::internal_toggle_devtools,
],
#[cfg(mobile)]
{
|invoke| {
invoke
.resolver
.reject("Webview API not available on mobile");
true
}
})
},
)
.build()
}

View File

@ -242,97 +242,94 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
Builder::new("window")
.js_init_script(init_script)
.invoke_handler(|invoke| {
.invoke_handler(
#[cfg(desktop)]
{
let handler: Box<dyn Fn(crate::ipc::Invoke<R>) -> bool> =
Box::new(crate::generate_handler![
desktop_commands::create,
// getters
desktop_commands::get_all_windows,
desktop_commands::scale_factor,
desktop_commands::inner_position,
desktop_commands::outer_position,
desktop_commands::inner_size,
desktop_commands::outer_size,
desktop_commands::is_fullscreen,
desktop_commands::is_minimized,
desktop_commands::is_maximized,
desktop_commands::is_focused,
desktop_commands::is_decorated,
desktop_commands::is_resizable,
desktop_commands::is_maximizable,
desktop_commands::is_minimizable,
desktop_commands::is_closable,
desktop_commands::is_visible,
desktop_commands::is_enabled,
desktop_commands::title,
desktop_commands::current_monitor,
desktop_commands::primary_monitor,
desktop_commands::monitor_from_point,
desktop_commands::available_monitors,
desktop_commands::cursor_position,
desktop_commands::theme,
desktop_commands::is_always_on_top,
// setters
desktop_commands::center,
desktop_commands::request_user_attention,
desktop_commands::set_resizable,
desktop_commands::set_maximizable,
desktop_commands::set_minimizable,
desktop_commands::set_closable,
desktop_commands::set_title,
desktop_commands::maximize,
desktop_commands::unmaximize,
desktop_commands::minimize,
desktop_commands::unminimize,
desktop_commands::show,
desktop_commands::hide,
desktop_commands::close,
desktop_commands::destroy,
desktop_commands::set_decorations,
desktop_commands::set_shadow,
desktop_commands::set_effects,
desktop_commands::set_always_on_top,
desktop_commands::set_always_on_bottom,
desktop_commands::set_content_protected,
desktop_commands::set_size,
desktop_commands::set_min_size,
desktop_commands::set_max_size,
desktop_commands::set_size_constraints,
desktop_commands::set_position,
desktop_commands::set_fullscreen,
desktop_commands::set_focus,
desktop_commands::set_enabled,
desktop_commands::set_skip_taskbar,
desktop_commands::set_cursor_grab,
desktop_commands::set_cursor_visible,
desktop_commands::set_cursor_icon,
desktop_commands::set_cursor_position,
desktop_commands::set_ignore_cursor_events,
desktop_commands::start_dragging,
desktop_commands::start_resize_dragging,
desktop_commands::set_badge_count,
#[cfg(target_os = "macos")]
desktop_commands::set_badge_label,
desktop_commands::set_progress_bar,
#[cfg(target_os = "windows")]
desktop_commands::set_overlay_icon,
desktop_commands::set_icon,
desktop_commands::set_visible_on_all_workspaces,
desktop_commands::set_background_color,
desktop_commands::set_title_bar_style,
desktop_commands::set_theme,
desktop_commands::toggle_maximize,
desktop_commands::internal_toggle_maximize,
]);
handler(invoke)
}
crate::generate_handler![
#![plugin(window)]
desktop_commands::create,
// getters
desktop_commands::get_all_windows,
desktop_commands::scale_factor,
desktop_commands::inner_position,
desktop_commands::outer_position,
desktop_commands::inner_size,
desktop_commands::outer_size,
desktop_commands::is_fullscreen,
desktop_commands::is_minimized,
desktop_commands::is_maximized,
desktop_commands::is_focused,
desktop_commands::is_decorated,
desktop_commands::is_resizable,
desktop_commands::is_maximizable,
desktop_commands::is_minimizable,
desktop_commands::is_closable,
desktop_commands::is_visible,
desktop_commands::is_enabled,
desktop_commands::title,
desktop_commands::current_monitor,
desktop_commands::primary_monitor,
desktop_commands::monitor_from_point,
desktop_commands::available_monitors,
desktop_commands::cursor_position,
desktop_commands::theme,
desktop_commands::is_always_on_top,
// setters
desktop_commands::center,
desktop_commands::request_user_attention,
desktop_commands::set_resizable,
desktop_commands::set_maximizable,
desktop_commands::set_minimizable,
desktop_commands::set_closable,
desktop_commands::set_title,
desktop_commands::maximize,
desktop_commands::unmaximize,
desktop_commands::minimize,
desktop_commands::unminimize,
desktop_commands::show,
desktop_commands::hide,
desktop_commands::close,
desktop_commands::destroy,
desktop_commands::set_decorations,
desktop_commands::set_shadow,
desktop_commands::set_effects,
desktop_commands::set_always_on_top,
desktop_commands::set_always_on_bottom,
desktop_commands::set_content_protected,
desktop_commands::set_size,
desktop_commands::set_min_size,
desktop_commands::set_max_size,
desktop_commands::set_size_constraints,
desktop_commands::set_position,
desktop_commands::set_fullscreen,
desktop_commands::set_focus,
desktop_commands::set_enabled,
desktop_commands::set_skip_taskbar,
desktop_commands::set_cursor_grab,
desktop_commands::set_cursor_visible,
desktop_commands::set_cursor_icon,
desktop_commands::set_cursor_position,
desktop_commands::set_ignore_cursor_events,
desktop_commands::start_dragging,
desktop_commands::start_resize_dragging,
desktop_commands::set_badge_count,
#[cfg(target_os = "macos")]
desktop_commands::set_badge_label,
desktop_commands::set_progress_bar,
#[cfg(target_os = "windows")]
desktop_commands::set_overlay_icon,
desktop_commands::set_icon,
desktop_commands::set_visible_on_all_workspaces,
desktop_commands::set_background_color,
desktop_commands::set_title_bar_style,
desktop_commands::set_theme,
desktop_commands::toggle_maximize,
desktop_commands::internal_toggle_maximize,
],
#[cfg(mobile)]
{
|invoke| {
invoke.resolver.reject("Window API not available on mobile");
true
}
})
},
)
.build()
}

View File

@ -44,6 +44,9 @@ pub fn popup<R: tauri::Runtime>(
pub fn init<R: Runtime>() -> TauriPlugin<R> {
Builder::new("app-menu")
.invoke_handler(tauri::generate_handler![popup, toggle])
.invoke_handler(tauri::generate_handler![
#![plugin(app_menu)]
popup, toggle
])
.build()
}

View File

@ -7,7 +7,8 @@
"frontendDist": "../dist",
"devUrl": "http://localhost:1420",
"beforeDevCommand": "pnpm dev",
"beforeBuildCommand": "pnpm build"
"beforeBuildCommand": "pnpm build",
"removeUnusedCommands": true
},
"app": {
"withGlobalTauri": true,