snapshot of https://github.com/tauri-apps/tauri.git (dev, 2026-09-23; blobs > 131072B omitted)
Some checks failed
Audit / audit-js (push) Failing after 0s
bench / bench (map[os:ubuntu-latest runner:xvfb-run --auto-servernum target:x86_64-unknown-linux-gnu], nightly) (push) Failing after 0s
covector version or publish / run-integration-tests (ubuntu-latest) (push) Failing after 0s
Audit / audit-rust (push) Failing after 0s
test `@tauri-apps/cli` / test (ubuntu-latest) (push) Failing after 0s
test `tauri-cli` / test (map[os:ubuntu-latest target:x86_64-unknown-linux-gnu]) (push) Failing after 0s
test core / test (map[args:--all-features key:all], map[cargo:cargo command:test os:ubuntu-latest target:x86_64-unknown-linux-gnu toolchain:1.90]) (push) Failing after 0s
test core / test (map[args:--no-default-features key:no-default], map[cargo:cargo command:test os:ubuntu-latest target:x86_64-unknown-linux-gnu toolchain:1.90]) (push) Failing after 0s
test core / test (map[args:--no-default-features key:no-default], map[cargo:cross command:build os:ubuntu-latest target:aarch64-linux-android toolchain:1.90]) (push) Failing after 0s
deploy schema worker / deploy (push) Failing after 0s
lint rust / clippy (map[cargo:cargo include: os:ubuntu-latest target:x86_64-unknown-linux-gnu]) (push) Failing after 0s
lint rust / clippy (map[cargo:cross include:--package api os:ubuntu-latest target:aarch64-linux-android]) (push) Failing after 0s
supply chain health status / check rust dependencies with cargo vet (push) Failing after 0s
test `@tauri-apps/api` e2e (mobile) / android (push) Failing after 0s
test `@tauri-apps/api` e2e / test (map[name:Linux os:ubuntu-latest]) (push) Failing after 0s
test core / test (map[args:--all-features key:all], map[cargo:cross command:build os:ubuntu-latest target:aarch64-linux-android toolchain:1.90]) (push) Failing after 0s
Udeps / changes (push) Failing after 0s
Udeps / setup (push) Has been skipped
Udeps / udeps (push) Has been skipped
bench / bench (map[os:macos-latest runner: target:aarch64-apple-darwin], nightly) (push) Has been cancelled
bench / bench (map[os:windows-latest runner: target:x86_64-pc-windows-msvc], nightly) (push) Has been cancelled
covector version or publish / run-integration-tests (macos-latest) (push) Has been cancelled
covector version or publish / run-integration-tests (windows-latest) (push) Has been cancelled
covector version or publish / version-or-publish (push) Has been cancelled
test `@tauri-apps/cli` / test (macos-latest) (push) Has been cancelled
test `tauri-cli` / test (map[args:--no-default-features --features native-tls-vendored os:windows-latest restore-only:true target:aarch64-pc-windows-msvc]) (push) Has been cancelled
test `tauri-cli` / test (map[os:macos-latest target:x86_64-apple-darwin]) (push) Has been cancelled
test `tauri-cli` / test (map[os:windows-latest target:x86_64-pc-windows-msvc]) (push) Has been cancelled
test core / test (map[args:--all-features key:all], map[cargo:cargo command:build os:macos-latest target:aarch64-apple-ios toolchain:1.90]) (push) Has been cancelled
test core / test (map[args:--all-features key:all], map[cargo:cargo command:test os:macos-14 target:aarch64-apple-darwin toolchain:1.90]) (push) Has been cancelled
lint rust / clippy (map[cargo:cargo include: os:macos-latest target:aarch64-apple-darwin]) (push) Has been cancelled
lint rust / clippy (map[cargo:cargo include: os:windows-latest target:x86_64-pc-windows-msvc]) (push) Has been cancelled
lint rust / clippy (map[cargo:cargo include:--package api os:macos-latest target:aarch64-apple-ios]) (push) Has been cancelled
test `@tauri-apps/api` e2e (mobile) / ios (push) Has been cancelled
test `@tauri-apps/api` e2e / test (map[name:Windows os:windows-latest]) (push) Has been cancelled
test `@tauri-apps/api` e2e / test (map[name:macOS os:macos-latest]) (push) Has been cancelled
test `@tauri-apps/cli` / test (windows-latest) (push) Has been cancelled
test core / test (map[args:--all-features key:all], map[cargo:cargo command:test os:windows-latest target:x86_64-pc-windows-msvc toolchain:1.90]) (push) Has been cancelled
test core / test (map[args:--no-default-features key:no-default], map[cargo:cargo command:build os:macos-latest target:aarch64-apple-ios toolchain:1.90]) (push) Has been cancelled
test core / test (map[args:--no-default-features key:no-default], map[cargo:cargo command:test os:macos-14 target:aarch64-apple-darwin toolchain:1.90]) (push) Has been cancelled
test core / test (map[args:--no-default-features key:no-default], map[cargo:cargo command:test os:windows-latest target:x86_64-pc-windows-msvc toolchain:1.90]) (push) Has been cancelled
Some checks failed
Audit / audit-js (push) Failing after 0s
bench / bench (map[os:ubuntu-latest runner:xvfb-run --auto-servernum target:x86_64-unknown-linux-gnu], nightly) (push) Failing after 0s
covector version or publish / run-integration-tests (ubuntu-latest) (push) Failing after 0s
Audit / audit-rust (push) Failing after 0s
test `@tauri-apps/cli` / test (ubuntu-latest) (push) Failing after 0s
test `tauri-cli` / test (map[os:ubuntu-latest target:x86_64-unknown-linux-gnu]) (push) Failing after 0s
test core / test (map[args:--all-features key:all], map[cargo:cargo command:test os:ubuntu-latest target:x86_64-unknown-linux-gnu toolchain:1.90]) (push) Failing after 0s
test core / test (map[args:--no-default-features key:no-default], map[cargo:cargo command:test os:ubuntu-latest target:x86_64-unknown-linux-gnu toolchain:1.90]) (push) Failing after 0s
test core / test (map[args:--no-default-features key:no-default], map[cargo:cross command:build os:ubuntu-latest target:aarch64-linux-android toolchain:1.90]) (push) Failing after 0s
deploy schema worker / deploy (push) Failing after 0s
lint rust / clippy (map[cargo:cargo include: os:ubuntu-latest target:x86_64-unknown-linux-gnu]) (push) Failing after 0s
lint rust / clippy (map[cargo:cross include:--package api os:ubuntu-latest target:aarch64-linux-android]) (push) Failing after 0s
supply chain health status / check rust dependencies with cargo vet (push) Failing after 0s
test `@tauri-apps/api` e2e (mobile) / android (push) Failing after 0s
test `@tauri-apps/api` e2e / test (map[name:Linux os:ubuntu-latest]) (push) Failing after 0s
test core / test (map[args:--all-features key:all], map[cargo:cross command:build os:ubuntu-latest target:aarch64-linux-android toolchain:1.90]) (push) Failing after 0s
Udeps / changes (push) Failing after 0s
Udeps / setup (push) Has been skipped
Udeps / udeps (push) Has been skipped
bench / bench (map[os:macos-latest runner: target:aarch64-apple-darwin], nightly) (push) Has been cancelled
bench / bench (map[os:windows-latest runner: target:x86_64-pc-windows-msvc], nightly) (push) Has been cancelled
covector version or publish / run-integration-tests (macos-latest) (push) Has been cancelled
covector version or publish / run-integration-tests (windows-latest) (push) Has been cancelled
covector version or publish / version-or-publish (push) Has been cancelled
test `@tauri-apps/cli` / test (macos-latest) (push) Has been cancelled
test `tauri-cli` / test (map[args:--no-default-features --features native-tls-vendored os:windows-latest restore-only:true target:aarch64-pc-windows-msvc]) (push) Has been cancelled
test `tauri-cli` / test (map[os:macos-latest target:x86_64-apple-darwin]) (push) Has been cancelled
test `tauri-cli` / test (map[os:windows-latest target:x86_64-pc-windows-msvc]) (push) Has been cancelled
test core / test (map[args:--all-features key:all], map[cargo:cargo command:build os:macos-latest target:aarch64-apple-ios toolchain:1.90]) (push) Has been cancelled
test core / test (map[args:--all-features key:all], map[cargo:cargo command:test os:macos-14 target:aarch64-apple-darwin toolchain:1.90]) (push) Has been cancelled
lint rust / clippy (map[cargo:cargo include: os:macos-latest target:aarch64-apple-darwin]) (push) Has been cancelled
lint rust / clippy (map[cargo:cargo include: os:windows-latest target:x86_64-pc-windows-msvc]) (push) Has been cancelled
lint rust / clippy (map[cargo:cargo include:--package api os:macos-latest target:aarch64-apple-ios]) (push) Has been cancelled
test `@tauri-apps/api` e2e (mobile) / ios (push) Has been cancelled
test `@tauri-apps/api` e2e / test (map[name:Windows os:windows-latest]) (push) Has been cancelled
test `@tauri-apps/api` e2e / test (map[name:macOS os:macos-latest]) (push) Has been cancelled
test `@tauri-apps/cli` / test (windows-latest) (push) Has been cancelled
test core / test (map[args:--all-features key:all], map[cargo:cargo command:test os:windows-latest target:x86_64-pc-windows-msvc toolchain:1.90]) (push) Has been cancelled
test core / test (map[args:--no-default-features key:no-default], map[cargo:cargo command:build os:macos-latest target:aarch64-apple-ios toolchain:1.90]) (push) Has been cancelled
test core / test (map[args:--no-default-features key:no-default], map[cargo:cargo command:test os:macos-14 target:aarch64-apple-darwin toolchain:1.90]) (push) Has been cancelled
test core / test (map[args:--no-default-features key:no-default], map[cargo:cargo command:test os:windows-latest target:x86_64-pc-windows-msvc toolchain:1.90]) (push) Has been cancelled
This commit is contained in:
185
crates/tauri-macros/src/command/handler.rs
Normal file
185
crates/tauri-macros/src/command/handler.rs
Normal file
@@ -0,0 +1,185 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use quote::{format_ident, quote};
|
||||
use syn::{
|
||||
Attribute, Ident, Path, Token,
|
||||
parse::{Parse, ParseBuffer, ParseStream},
|
||||
};
|
||||
|
||||
struct CommandDef {
|
||||
path: Path,
|
||||
attrs: Vec<Attribute>,
|
||||
}
|
||||
|
||||
impl Parse for CommandDef {
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
let attrs = input.call(Attribute::parse_outer)?;
|
||||
let path = input.parse()?;
|
||||
|
||||
Ok(CommandDef { path, attrs })
|
||||
}
|
||||
}
|
||||
|
||||
/// The items parsed from [`generate_handle!`](crate::generate_handle).
|
||||
pub struct Handler {
|
||||
command_defs: Vec<CommandDef>,
|
||||
commands: Vec<Ident>,
|
||||
wrappers: Vec<Path>,
|
||||
}
|
||||
|
||||
impl Parse for Handler {
|
||||
fn parse(input: &ParseBuffer<'_>) -> syn::Result<Self> {
|
||||
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
|
||||
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();
|
||||
|
||||
// set the path to the command function wrapper
|
||||
last.ident = super::format_command_wrapper(&command);
|
||||
|
||||
commands.push(command);
|
||||
wrappers.push(wrapper);
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
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` environment 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__" {
|
||||
// TODO: Remove this in v3
|
||||
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;
|
||||
};
|
||||
|
||||
// TODO: Remove this in v3
|
||||
if plugin_name.as_deref() == Some("__TAURI_CHANNEL__") {
|
||||
// Always allowed
|
||||
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 {
|
||||
command_defs,
|
||||
commands,
|
||||
wrappers,
|
||||
}: Handler,
|
||||
) -> Self {
|
||||
let cmd = format_ident!("__tauri_cmd__");
|
||||
let invoke = format_ident!("__tauri_invoke__");
|
||||
let mut paths: Vec<Path> = Vec::new();
|
||||
let mut attrs: Vec<Vec<Attribute>> = Vec::new();
|
||||
let mut command_name_macros: Vec<proc_macro2::TokenStream> = Vec::new();
|
||||
for (def, command) in command_defs.into_iter().zip(commands) {
|
||||
let path = def.path;
|
||||
let attrs_vec = def.attrs;
|
||||
|
||||
let mut command_name_macro_path = path.clone();
|
||||
let last = command_name_macro_path
|
||||
.segments
|
||||
.last_mut()
|
||||
.expect("path has at least one segment");
|
||||
last.ident = format_ident!("__tauri_command_name_{command}");
|
||||
|
||||
paths.push(path);
|
||||
attrs.push(attrs_vec);
|
||||
// Call the macro to get the command name string literal
|
||||
command_name_macros.push(quote!(#command_name_macro_path!()));
|
||||
}
|
||||
|
||||
quote::quote!(move |#invoke| {
|
||||
let #cmd = #invoke.message.command();
|
||||
match #cmd {
|
||||
#(#(#attrs)* #command_name_macros => #wrappers!(#paths, #invoke),)*
|
||||
_ => {
|
||||
return false;
|
||||
},
|
||||
}
|
||||
})
|
||||
.into()
|
||||
}
|
||||
}
|
||||
24
crates/tauri-macros/src/command/mod.rs
Normal file
24
crates/tauri-macros/src/command/mod.rs
Normal file
@@ -0,0 +1,24 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use proc_macro2::Ident;
|
||||
use syn::{Path, PathSegment};
|
||||
|
||||
pub use self::{handler::Handler, wrapper::wrapper};
|
||||
|
||||
mod handler;
|
||||
mod wrapper;
|
||||
|
||||
/// The autogenerated wrapper ident.
|
||||
fn format_command_wrapper(function: &Ident) -> Ident {
|
||||
quote::format_ident!("__cmd__{}", function)
|
||||
}
|
||||
|
||||
/// This function will panic if the passed [`syn::Path`] does not have any segments.
|
||||
fn path_to_command(path: &mut Path) -> &mut PathSegment {
|
||||
path
|
||||
.segments
|
||||
.last_mut()
|
||||
.expect("parsed syn::Path has no segment")
|
||||
}
|
||||
524
crates/tauri-macros/src/command/wrapper.rs
Normal file
524
crates/tauri-macros/src/command/wrapper.rs
Normal file
@@ -0,0 +1,524 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use std::env::var;
|
||||
|
||||
use heck::{ToLowerCamelCase, ToSnakeCase};
|
||||
use proc_macro::TokenStream;
|
||||
use proc_macro2::{Ident, Span, TokenStream as TokenStream2};
|
||||
use quote::{format_ident, quote, quote_spanned};
|
||||
use syn::{
|
||||
Expr, ExprLit, FnArg, ItemFn, Lit, Meta, Pat, Token, Visibility,
|
||||
ext::IdentExt,
|
||||
parse::{Parse, ParseStream},
|
||||
parse_macro_input,
|
||||
punctuated::Punctuated,
|
||||
spanned::Spanned,
|
||||
};
|
||||
use tauri_utils::acl::REMOVE_UNUSED_COMMANDS_ENV_VAR;
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
enum WrapperAttributeKind {
|
||||
Meta(Meta),
|
||||
Async,
|
||||
}
|
||||
|
||||
impl Parse for WrapperAttributeKind {
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
match input.parse::<Meta>() {
|
||||
Ok(m) => Ok(Self::Meta(m)),
|
||||
Err(e) => match input.parse::<Token![async]>() {
|
||||
Ok(_) => Ok(Self::Async),
|
||||
Err(_) => Err(e),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct WrapperAttributes {
|
||||
root: TokenStream2,
|
||||
execution_context: ExecutionContext,
|
||||
argument_case: ArgumentCase,
|
||||
rename: RenamePolicy,
|
||||
}
|
||||
|
||||
impl Parse for WrapperAttributes {
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
let mut wrapper_attributes = WrapperAttributes {
|
||||
root: quote!(::tauri),
|
||||
execution_context: ExecutionContext::Blocking,
|
||||
argument_case: ArgumentCase::Camel,
|
||||
rename: RenamePolicy::Keep,
|
||||
};
|
||||
|
||||
let attrs = Punctuated::<WrapperAttributeKind, Token![,]>::parse_terminated(input)?;
|
||||
for attr in attrs {
|
||||
match attr {
|
||||
WrapperAttributeKind::Meta(Meta::List(_)) => {
|
||||
return Err(syn::Error::new(input.span(), "unexpected list input"));
|
||||
}
|
||||
WrapperAttributeKind::Meta(Meta::NameValue(v)) => {
|
||||
if v.path.is_ident("rename_all") {
|
||||
if let Expr::Lit(ExprLit {
|
||||
lit: Lit::Str(s),
|
||||
attrs: _,
|
||||
}) = v.value
|
||||
{
|
||||
wrapper_attributes.argument_case = match s.value().as_str() {
|
||||
"snake_case" => ArgumentCase::Snake,
|
||||
"camelCase" => ArgumentCase::Camel,
|
||||
_ => {
|
||||
return Err(syn::Error::new(
|
||||
s.span(),
|
||||
"expected \"camelCase\" or \"snake_case\"",
|
||||
));
|
||||
}
|
||||
};
|
||||
}
|
||||
} else if v.path.is_ident("rename") {
|
||||
if let Expr::Lit(ExprLit {
|
||||
lit: Lit::Str(s), ..
|
||||
}) = v.value
|
||||
{
|
||||
let lit = s.value();
|
||||
wrapper_attributes.rename = RenamePolicy::Rename(quote!(#lit));
|
||||
} else {
|
||||
return Err(syn::Error::new(
|
||||
v.span(),
|
||||
"expected string literal for rename",
|
||||
));
|
||||
}
|
||||
} else if v.path.is_ident("root") {
|
||||
if let Expr::Lit(ExprLit {
|
||||
lit: Lit::Str(s),
|
||||
attrs: _,
|
||||
}) = v.value
|
||||
{
|
||||
let lit = s.value();
|
||||
|
||||
wrapper_attributes.root = if lit == "crate" {
|
||||
quote!($crate)
|
||||
} else {
|
||||
let ident = Ident::new(&lit, Span::call_site());
|
||||
quote!(#ident)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
WrapperAttributeKind::Meta(Meta::Path(_)) => {
|
||||
return Err(syn::Error::new(
|
||||
input.span(),
|
||||
"unexpected input, expected one of `rename_all`, `rename`, `root`, `async`",
|
||||
));
|
||||
}
|
||||
WrapperAttributeKind::Async => {
|
||||
wrapper_attributes.execution_context = ExecutionContext::Async;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(wrapper_attributes)
|
||||
}
|
||||
}
|
||||
|
||||
/// The execution context of the command.
|
||||
enum ExecutionContext {
|
||||
Async,
|
||||
Blocking,
|
||||
}
|
||||
|
||||
/// The case of each argument name.
|
||||
#[derive(Copy, Clone)]
|
||||
enum ArgumentCase {
|
||||
Snake,
|
||||
Camel,
|
||||
}
|
||||
|
||||
/// The rename policy for the command.
|
||||
enum RenamePolicy {
|
||||
Keep,
|
||||
Rename(TokenStream2),
|
||||
}
|
||||
|
||||
/// The bindings we attach to `tauri::Invoke`.
|
||||
struct Invoke {
|
||||
message: Ident,
|
||||
resolver: Ident,
|
||||
acl: Ident,
|
||||
}
|
||||
|
||||
/// Create a new [`Wrapper`] from the function and the generated code parsed from the function.
|
||||
pub fn wrapper(attributes: TokenStream, item: TokenStream) -> TokenStream {
|
||||
let mut attrs = parse_macro_input!(attributes as WrapperAttributes);
|
||||
let function = parse_macro_input!(item as ItemFn);
|
||||
let wrapper = super::format_command_wrapper(&function.sig.ident);
|
||||
let visibility = &function.vis;
|
||||
|
||||
if function.sig.asyncness.is_some() {
|
||||
attrs.execution_context = ExecutionContext::Async;
|
||||
}
|
||||
|
||||
// macros used with `pub use my_macro;` need to be exported with `#[macro_export]`.
|
||||
let maybe_macro_export = match &function.vis {
|
||||
Visibility::Public(_) | Visibility::Restricted(_) => {
|
||||
quote!(#[macro_export])
|
||||
}
|
||||
_ => TokenStream2::default(),
|
||||
};
|
||||
|
||||
let invoke = Invoke {
|
||||
message: format_ident!("__tauri_message__"),
|
||||
resolver: format_ident!("__tauri_resolver__"),
|
||||
acl: format_ident!("__tauri_acl__"),
|
||||
};
|
||||
|
||||
// Tauri currently doesn't support async commands that take a reference as input and don't return
|
||||
// a result. See: https://github.com/tauri-apps/tauri/issues/2533
|
||||
//
|
||||
// For now, we provide an informative error message to the user in that case. Once #2533 is
|
||||
// resolved, this check can be removed.
|
||||
let mut async_command_check = TokenStream2::new();
|
||||
if function.sig.asyncness.is_some() {
|
||||
// This check won't catch all possible problems but it should catch the most common ones.
|
||||
let mut ref_argument_span = None;
|
||||
|
||||
for arg in &function.sig.inputs {
|
||||
if let syn::FnArg::Typed(pat) = arg {
|
||||
match &*pat.ty {
|
||||
syn::Type::Reference(_) => {
|
||||
ref_argument_span = Some(pat.span());
|
||||
}
|
||||
syn::Type::Path(path) => {
|
||||
// Check if the type contains a lifetime argument
|
||||
let last = path.path.segments.last().unwrap();
|
||||
if let syn::PathArguments::AngleBracketed(args) = &last.arguments {
|
||||
if args
|
||||
.args
|
||||
.iter()
|
||||
.any(|arg| matches!(arg, syn::GenericArgument::Lifetime(_)))
|
||||
{
|
||||
ref_argument_span = Some(pat.span());
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if let Some(span) = ref_argument_span {
|
||||
if let syn::ReturnType::Type(_, return_type) = &function.sig.output {
|
||||
// To check if the return type is `Result` we require it to check a trait that is
|
||||
// only implemented by `Result`. That way we don't exclude renamed result types
|
||||
// which we wouldn't otherwise be able to detect purely from the token stream.
|
||||
// The "error message" displayed to the user is simply the trait name.
|
||||
let diagnostic = quote!(#[diagnostic::on_unimplemented(message = "async commands that contain references as inputs must return a `Result`")]);
|
||||
|
||||
async_command_check = quote_spanned! {return_type.span() =>
|
||||
#[allow(unreachable_code, clippy::diverging_sub_expression, clippy::used_underscore_binding)]
|
||||
const _: () = if false {
|
||||
#diagnostic
|
||||
trait AsyncCommandMustReturnResult {}
|
||||
impl<A, B> AsyncCommandMustReturnResult for ::std::result::Result<A, B> {}
|
||||
let _check: #return_type = unreachable!();
|
||||
let _: &dyn AsyncCommandMustReturnResult = &_check;
|
||||
};
|
||||
};
|
||||
} else {
|
||||
return quote_spanned! {
|
||||
span => compile_error!("async commands that contain references as inputs must return a `Result`");
|
||||
}.into();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let plugin_name = var("CARGO_PKG_NAME")
|
||||
.expect("missing `CARGO_PKG_NAME` environment variable")
|
||||
.strip_prefix("tauri-plugin-")
|
||||
.map(|name| quote!(::core::option::Option::Some(#name)))
|
||||
.unwrap_or_else(|| quote!(::core::option::Option::None));
|
||||
|
||||
let body = match attrs.execution_context {
|
||||
ExecutionContext::Async => body_async(&plugin_name, &function, &invoke, &attrs)
|
||||
.unwrap_or_else(syn::Error::into_compile_error),
|
||||
ExecutionContext::Blocking => body_blocking(&plugin_name, &function, &invoke, &attrs)
|
||||
.unwrap_or_else(syn::Error::into_compile_error),
|
||||
};
|
||||
|
||||
let Invoke {
|
||||
message,
|
||||
resolver,
|
||||
acl,
|
||||
} = invoke;
|
||||
|
||||
let root = attrs.root;
|
||||
|
||||
let kind = match attrs.execution_context {
|
||||
ExecutionContext::Async if function.sig.asyncness.is_none() => "sync_threadpool",
|
||||
ExecutionContext::Async => "async",
|
||||
ExecutionContext::Blocking => "sync",
|
||||
};
|
||||
|
||||
let loc = function.span().start();
|
||||
let line = loc.line;
|
||||
let col = loc.column;
|
||||
|
||||
let maybe_span = if cfg!(feature = "tracing") {
|
||||
quote!({
|
||||
let _span = tracing::debug_span!(
|
||||
"ipc::request::handler",
|
||||
cmd = #message.command(),
|
||||
kind = #kind,
|
||||
loc.line = #line,
|
||||
loc.col = #col,
|
||||
is_internal = false,
|
||||
)
|
||||
.entered();
|
||||
})
|
||||
} else {
|
||||
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()
|
||||
};
|
||||
|
||||
// Always define a hidden macro that returns the externally invoked command name.
|
||||
// This lets the handler match on the renamed string while the original function
|
||||
// identifier remains usable in `generate_handler![original_fn_name]`.
|
||||
let command_name_macro_ident = format_ident!("__tauri_command_name_{}", function.sig.ident);
|
||||
let command_name_value = if let RenamePolicy::Rename(ref rename) = attrs.rename {
|
||||
quote!(#rename)
|
||||
} else {
|
||||
let ident = &function.sig.ident;
|
||||
quote!(stringify!(#ident))
|
||||
};
|
||||
|
||||
// Rely on rust 2018 edition to allow importing a macro from a path.
|
||||
quote!(
|
||||
#async_command_check
|
||||
|
||||
#maybe_allow_unused
|
||||
#function
|
||||
|
||||
// Command name macro used by the handler for pattern matching.
|
||||
// This macro returns the command name string literal (renamed or original).
|
||||
#maybe_allow_unused
|
||||
#maybe_macro_export
|
||||
#[doc(hidden)]
|
||||
macro_rules! #command_name_macro_ident {
|
||||
() => {
|
||||
#command_name_value
|
||||
};
|
||||
}
|
||||
|
||||
#maybe_allow_unused
|
||||
#maybe_macro_export
|
||||
#[doc(hidden)]
|
||||
macro_rules! #wrapper {
|
||||
// double braces because the item is expected to be a block expression
|
||||
($path:path, $invoke:ident) => {
|
||||
// The IIFE here is for preventing stack overflow on Windows,
|
||||
// see https://github.com/tauri-apps/tauri/issues/12488
|
||||
{
|
||||
move || {
|
||||
#[allow(unused_imports)]
|
||||
use #root::ipc::private::*;
|
||||
// prevent warnings when the body is a `compile_error!` or if the command has no arguments
|
||||
#[allow(unused_variables)]
|
||||
let #root::ipc::Invoke { message: #message, resolver: #resolver, acl: #acl } = $invoke;
|
||||
|
||||
#maybe_span
|
||||
|
||||
#body
|
||||
}
|
||||
}()
|
||||
};
|
||||
}
|
||||
|
||||
// allow the macro to be resolved with the same path as the command function
|
||||
#[allow(unused_imports)]
|
||||
#visibility use {#wrapper, #command_name_macro_ident};
|
||||
)
|
||||
.into()
|
||||
}
|
||||
|
||||
/// Generates an asynchronous command response from the arguments and return value of a function.
|
||||
///
|
||||
/// See the [`tauri::command`] module for all the items and traits that make this possible.
|
||||
///
|
||||
/// [`tauri::command`]: https://docs.rs/tauri/*/tauri/runtime/index.html
|
||||
fn body_async(
|
||||
plugin_name: &TokenStream2,
|
||||
function: &ItemFn,
|
||||
invoke: &Invoke,
|
||||
attributes: &WrapperAttributes,
|
||||
) -> syn::Result<TokenStream2> {
|
||||
let Invoke {
|
||||
message,
|
||||
resolver,
|
||||
acl,
|
||||
} = invoke;
|
||||
parse_args(plugin_name, function, message, acl, attributes).map(|args| {
|
||||
#[cfg(feature = "tracing")]
|
||||
quote! {
|
||||
use tracing::Instrument;
|
||||
|
||||
let span = tracing::debug_span!("ipc::request::run");
|
||||
#resolver.respond_async_serialized(async move {
|
||||
let result = $path(#(#args?),*);
|
||||
let kind = (&result).async_kind();
|
||||
kind.future(result).await
|
||||
}
|
||||
.instrument(span));
|
||||
return true;
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "tracing"))]
|
||||
quote! {
|
||||
#resolver.respond_async_serialized(async move {
|
||||
let result = $path(#(#args?),*);
|
||||
let kind = (&result).async_kind();
|
||||
kind.future(result).await
|
||||
});
|
||||
return true;
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Generates a blocking command response from the arguments and return value of a function.
|
||||
///
|
||||
/// See the [`tauri::command`] module for all the items and traits that make this possible.
|
||||
///
|
||||
/// [`tauri::command`]: https://docs.rs/tauri/*/tauri/runtime/index.html
|
||||
fn body_blocking(
|
||||
plugin_name: &TokenStream2,
|
||||
function: &ItemFn,
|
||||
invoke: &Invoke,
|
||||
attributes: &WrapperAttributes,
|
||||
) -> syn::Result<TokenStream2> {
|
||||
let Invoke {
|
||||
message,
|
||||
resolver,
|
||||
acl,
|
||||
} = invoke;
|
||||
let args = parse_args(plugin_name, function, message, acl, attributes)?;
|
||||
|
||||
// the body of a `match` to early return any argument that wasn't successful in parsing.
|
||||
let match_body = quote!({
|
||||
Ok(arg) => arg,
|
||||
Err(err) => { #resolver.invoke_error(err); return true },
|
||||
});
|
||||
|
||||
let maybe_span = if cfg!(feature = "tracing") {
|
||||
quote!(let _span = tracing::debug_span!("ipc::request::run").entered();)
|
||||
} else {
|
||||
quote!()
|
||||
};
|
||||
|
||||
Ok(quote! {
|
||||
#maybe_span
|
||||
let result = $path(#(match #args #match_body),*);
|
||||
let kind = (&result).blocking_kind();
|
||||
kind.block(result, #resolver);
|
||||
return true;
|
||||
})
|
||||
}
|
||||
|
||||
/// Parse all arguments for the command wrapper to use from the signature of the command function.
|
||||
fn parse_args(
|
||||
plugin_name: &TokenStream2,
|
||||
function: &ItemFn,
|
||||
message: &Ident,
|
||||
acl: &Ident,
|
||||
attributes: &WrapperAttributes,
|
||||
) -> syn::Result<Vec<TokenStream2>> {
|
||||
function
|
||||
.sig
|
||||
.inputs
|
||||
.iter()
|
||||
.map(|arg| {
|
||||
parse_arg(
|
||||
plugin_name,
|
||||
&function.sig.ident,
|
||||
arg,
|
||||
message,
|
||||
acl,
|
||||
attributes,
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Transform a [`FnArg`] into a command argument.
|
||||
fn parse_arg(
|
||||
plugin_name: &TokenStream2,
|
||||
command: &Ident,
|
||||
arg: &FnArg,
|
||||
message: &Ident,
|
||||
acl: &Ident,
|
||||
attributes: &WrapperAttributes,
|
||||
) -> syn::Result<TokenStream2> {
|
||||
// we have no use for self arguments
|
||||
let mut arg = match arg {
|
||||
FnArg::Typed(arg) => arg.pat.as_ref().clone(),
|
||||
FnArg::Receiver(arg) => {
|
||||
return Err(syn::Error::new(
|
||||
arg.span(),
|
||||
"unable to use self as a command function parameter",
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
// we only support patterns that allow us to extract some sort of keyed identifier
|
||||
let mut key = match &mut arg {
|
||||
Pat::Ident(arg) => arg.ident.unraw().to_string(),
|
||||
Pat::Wild(_) => "".into(), // we always convert to camelCase, so "_" will end up empty anyways
|
||||
Pat::Struct(s) => super::path_to_command(&mut s.path).ident.to_string(),
|
||||
Pat::TupleStruct(s) => super::path_to_command(&mut s.path).ident.to_string(),
|
||||
err => {
|
||||
return Err(syn::Error::new(
|
||||
err.span(),
|
||||
"only named, wildcard, struct, and tuple struct arguments allowed",
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
// also catch self arguments that use FnArg::Typed syntax
|
||||
if key == "self" {
|
||||
return Err(syn::Error::new(
|
||||
key.span(),
|
||||
"unable to use self as a command function parameter",
|
||||
));
|
||||
}
|
||||
|
||||
match attributes.argument_case {
|
||||
ArgumentCase::Camel => {
|
||||
key = key.to_lower_camel_case();
|
||||
}
|
||||
ArgumentCase::Snake => {
|
||||
key = key.to_snake_case();
|
||||
}
|
||||
}
|
||||
|
||||
let root = &attributes.root;
|
||||
let command_name = if let RenamePolicy::Rename(r) = &attributes.rename {
|
||||
quote!(stringify!(#r))
|
||||
} else {
|
||||
quote!(stringify!(#command))
|
||||
};
|
||||
|
||||
Ok(quote!(#root::ipc::CommandArg::from_command(
|
||||
#root::ipc::CommandItem {
|
||||
plugin: #plugin_name,
|
||||
name: #command_name,
|
||||
key: #key,
|
||||
message: &#message,
|
||||
acl: &#acl,
|
||||
}
|
||||
)))
|
||||
}
|
||||
169
crates/tauri-macros/src/context.rs
Normal file
169
crates/tauri-macros/src/context.rs
Normal file
@@ -0,0 +1,169 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use proc_macro2::{Ident, Span, TokenStream};
|
||||
use quote::{ToTokens, quote};
|
||||
use std::path::PathBuf;
|
||||
use syn::{
|
||||
Expr, ExprLit, Lit, LitBool, LitStr, Meta, PathArguments, PathSegment, Token,
|
||||
parse::{Parse, ParseBuffer},
|
||||
punctuated::Punctuated,
|
||||
};
|
||||
use tauri_codegen::{ContextData, context_codegen, get_config};
|
||||
use tauri_utils::{config::parse::does_supported_file_name_exist, platform::Target};
|
||||
|
||||
pub(crate) struct ContextItems {
|
||||
config_file: PathBuf,
|
||||
root: syn::Path,
|
||||
capabilities: Option<Vec<PathBuf>>,
|
||||
assets: Option<Expr>,
|
||||
test: bool,
|
||||
}
|
||||
|
||||
impl Parse for ContextItems {
|
||||
fn parse(input: &ParseBuffer<'_>) -> syn::parse::Result<Self> {
|
||||
let target = std::env::var("TARGET")
|
||||
.or_else(|_| std::env::var("TAURI_ENV_TARGET_TRIPLE"))
|
||||
.as_deref()
|
||||
.map(Target::from_triple)
|
||||
.unwrap_or_else(|_| Target::current());
|
||||
|
||||
let mut root = None;
|
||||
let mut capabilities = None;
|
||||
let mut assets = None;
|
||||
let mut test = false;
|
||||
let config_file = input.parse::<LitStr>().ok().map(|raw| {
|
||||
let _ = input.parse::<Token![,]>();
|
||||
let path = PathBuf::from(raw.value());
|
||||
if path.is_relative() {
|
||||
std::env::var("CARGO_MANIFEST_DIR")
|
||||
.map(|m| PathBuf::from(m).join(path))
|
||||
.map_err(|e| e.to_string())
|
||||
} else {
|
||||
Ok(path)
|
||||
}
|
||||
.and_then(|path| {
|
||||
if does_supported_file_name_exist(target, &path) {
|
||||
Ok(path)
|
||||
} else {
|
||||
Err(format!(
|
||||
"no file at path {} exists, expected tauri config file",
|
||||
path.display()
|
||||
))
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
while let Ok(meta) = input.parse::<Meta>() {
|
||||
match meta {
|
||||
Meta::Path(p) => {
|
||||
root.replace(p);
|
||||
}
|
||||
Meta::NameValue(v) => {
|
||||
let ident = v.path.require_ident()?;
|
||||
match ident.to_string().as_str() {
|
||||
"capabilities" => {
|
||||
if let Expr::Array(array) = v.value {
|
||||
capabilities.replace(
|
||||
array
|
||||
.elems
|
||||
.into_iter()
|
||||
.map(|e| {
|
||||
if let Expr::Lit(ExprLit {
|
||||
attrs: _,
|
||||
lit: Lit::Str(s),
|
||||
}) = e
|
||||
{
|
||||
Ok(s.value().into())
|
||||
} else {
|
||||
Err(syn::Error::new(
|
||||
input.span(),
|
||||
"unexpected expression for capability",
|
||||
))
|
||||
}
|
||||
})
|
||||
.collect::<Result<Vec<_>, syn::Error>>()?,
|
||||
);
|
||||
} else {
|
||||
return Err(syn::Error::new(
|
||||
input.span(),
|
||||
"unexpected value for capabilities",
|
||||
));
|
||||
}
|
||||
}
|
||||
"assets" => {
|
||||
assets.replace(v.value);
|
||||
}
|
||||
"test" => {
|
||||
if let Expr::Lit(ExprLit {
|
||||
lit: Lit::Bool(LitBool { value, .. }),
|
||||
..
|
||||
}) = v.value
|
||||
{
|
||||
test = value;
|
||||
} else {
|
||||
return Err(syn::Error::new(input.span(), "unexpected value for test"));
|
||||
}
|
||||
}
|
||||
name => {
|
||||
return Err(syn::Error::new(
|
||||
input.span(),
|
||||
format!("unknown attribute {name}"),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
Meta::List(_) => {
|
||||
return Err(syn::Error::new(input.span(), "unexpected list input"));
|
||||
}
|
||||
}
|
||||
|
||||
let _ = input.parse::<Token![,]>();
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
config_file: config_file
|
||||
.unwrap_or_else(|| {
|
||||
std::env::var("CARGO_MANIFEST_DIR")
|
||||
.map(|m| PathBuf::from(m).join("tauri.conf.json"))
|
||||
.map_err(|e| e.to_string())
|
||||
})
|
||||
.map_err(|e| input.error(e))?,
|
||||
root: root.unwrap_or_else(|| {
|
||||
let mut segments = Punctuated::new();
|
||||
segments.push(PathSegment {
|
||||
ident: Ident::new("tauri", Span::call_site()),
|
||||
arguments: PathArguments::None,
|
||||
});
|
||||
syn::Path {
|
||||
leading_colon: Some(Token)),
|
||||
segments,
|
||||
}
|
||||
}),
|
||||
capabilities,
|
||||
assets,
|
||||
test,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn generate_context(context: ContextItems) -> TokenStream {
|
||||
let context = get_config(&context.config_file)
|
||||
.map_err(|e| e.to_string())
|
||||
.map(|(config, config_parent)| ContextData {
|
||||
dev: cfg!(not(feature = "custom-protocol")),
|
||||
config,
|
||||
config_parent,
|
||||
root: context.root.to_token_stream(),
|
||||
capabilities: context.capabilities,
|
||||
assets: context.assets,
|
||||
test: context.test,
|
||||
})
|
||||
.and_then(|data| context_codegen(data).map_err(|e| e.to_string()));
|
||||
|
||||
match context {
|
||||
Ok(code) => code,
|
||||
Err(error) => quote!(compile_error!(#error)),
|
||||
}
|
||||
}
|
||||
394
crates/tauri-macros/src/lib.rs
Normal file
394
crates/tauri-macros/src/lib.rs
Normal file
@@ -0,0 +1,394 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//! Create macros for `tauri::Context`, invoke handler and commands leveraging the `tauri-codegen` crate.
|
||||
//!
|
||||
//! Don't depend on this crate directly, use the re-exported types from tauri instead.
|
||||
|
||||
#![doc(
|
||||
html_logo_url = "https://github.com/tauri-apps/tauri/raw/dev/.github/icon.png",
|
||||
html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/.github/icon.png"
|
||||
)]
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::context::ContextItems;
|
||||
use proc_macro::TokenStream;
|
||||
use quote::{ToTokens, quote};
|
||||
use syn::{LitStr, parse_macro_input, parse2};
|
||||
use tauri_codegen::image::CachedIcon;
|
||||
|
||||
mod command;
|
||||
mod menu;
|
||||
mod mobile;
|
||||
mod runtime;
|
||||
|
||||
#[macro_use]
|
||||
mod context;
|
||||
|
||||
/// Mark a function as a command handler. It creates a wrapper function with the necessary glue code.
|
||||
///
|
||||
/// The wrapped function can then be passed to [`generate_handler!`] so it can be called
|
||||
/// from the frontend with `invoke()`.
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// #[tauri::command]
|
||||
/// fn greet(name: String) -> String {
|
||||
/// format!("Hello, {name}!")
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// # Options
|
||||
///
|
||||
/// The attribute accepts a comma separated list of the following options:
|
||||
///
|
||||
/// ## `async`
|
||||
///
|
||||
/// Runs the command on the async runtime instead of blocking the main thread.
|
||||
///
|
||||
/// `async fn` commands are always executed asynchronously, so this option is only needed
|
||||
/// for synchronous functions that should not run on the main thread:
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// #[tauri::command(async)]
|
||||
/// fn expensive_computation() -> u64 {
|
||||
/// // the body runs on the async runtime, so the main thread is not blocked
|
||||
/// 42
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// ## `rename_all`
|
||||
///
|
||||
/// Sets the case convention used to match the command arguments with the keys of the
|
||||
/// payload sent by the frontend. Either `"camelCase"` (default) or `"snake_case"`.
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// // called from JavaScript with `invoke("send_message", { messageBody: "Hello" })`
|
||||
/// #[tauri::command]
|
||||
/// fn send_message(message_body: String) {}
|
||||
///
|
||||
/// // called from JavaScript with `invoke("send_message", { message_body: "Hello" })`
|
||||
/// #[tauri::command(rename_all = "snake_case")]
|
||||
/// fn send_message_snake(message_body: String) {}
|
||||
/// ```
|
||||
///
|
||||
/// ## `rename`
|
||||
///
|
||||
/// Changes the name used to call the command from the frontend.
|
||||
/// By default it is the name of the function.
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// // called from JavaScript with `invoke("greetUser")`
|
||||
/// // and still registered as `generate_handler![greet_user]`
|
||||
/// #[tauri::command(rename = "greetUser")]
|
||||
/// fn greet_user() {}
|
||||
/// ```
|
||||
///
|
||||
/// ## `root`
|
||||
///
|
||||
/// Path to the `tauri` crate, used when it is renamed in `Cargo.toml` or re-exported
|
||||
/// by another crate. Defaults to `::tauri`, and the special value `"crate"` resolves
|
||||
/// to `$crate` (used internally by Tauri itself).
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// // Cargo.toml: tauri_framework = { package = "tauri", version = "2" }
|
||||
/// #[tauri::command(root = "tauri_framework")]
|
||||
/// fn my_command() {}
|
||||
/// ```
|
||||
///
|
||||
/// # Inline plugins
|
||||
///
|
||||
/// When the command belongs to a plugin that is part of your application instead of a
|
||||
/// standalone crate, annotate it in the [`generate_handler!`] list with the
|
||||
/// `#![plugin(your_plugin_name)]` inner attribute so `build > removeUnusedCommands` can
|
||||
/// match it against the plugin permissions.
|
||||
///
|
||||
/// # 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.
|
||||
#[proc_macro_attribute]
|
||||
pub fn command(attributes: TokenStream, item: TokenStream) -> TokenStream {
|
||||
command::wrapper(attributes, item)
|
||||
}
|
||||
|
||||
/// Marks a function as the entry point of a mobile application.
|
||||
///
|
||||
/// It must be applied to the function that builds and runs your Tauri application on the
|
||||
/// library target (`run()` on `src-tauri/src/lib.rs` for apps created by the Tauri CLI),
|
||||
/// which is the function the generated Android and iOS projects call on startup.
|
||||
///
|
||||
/// The canonical usage only applies it on mobile targets, so the same function can be called
|
||||
/// by the `main.rs` of the desktop binary:
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// #[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
/// pub fn run() {
|
||||
/// tauri::Builder::default()
|
||||
/// .run(tauri::generate_context!())
|
||||
/// .expect("error while running tauri application");
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// The macro generates a `start_app` C symbol (checked by the Tauri CLI) that catches
|
||||
/// panics instead of unwinding across the FFI boundary, blocks on the function when it is
|
||||
/// `async`, sets up the stdout logger on iOS and the JNI bindings on Android using the
|
||||
/// package name derived from the `identifier` in your Tauri configuration.
|
||||
///
|
||||
/// Because the Android package name is read from environment variables set by `tauri-build`,
|
||||
/// your application must have a build script calling [`tauri_build::build`] - otherwise the
|
||||
/// macro fails to compile with a `env var not set` error.
|
||||
///
|
||||
/// [`tauri_build::build`]: https://docs.rs/tauri-build/latest/tauri_build/fn.build.html
|
||||
#[proc_macro_attribute]
|
||||
pub fn mobile_entry_point(attributes: TokenStream, item: TokenStream) -> TokenStream {
|
||||
mobile::entry_point(attributes, item)
|
||||
}
|
||||
|
||||
/// 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]
|
||||
/// fn command_one() {
|
||||
/// println!("command one called");
|
||||
/// }
|
||||
/// #[command]
|
||||
/// fn command_two() {
|
||||
/// println!("command two called");
|
||||
/// }
|
||||
/// fn main() {
|
||||
/// 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.
|
||||
#[proc_macro]
|
||||
pub fn generate_handler(item: TokenStream) -> TokenStream {
|
||||
parse_macro_input!(item as command::Handler).into()
|
||||
}
|
||||
|
||||
/// Reads a Tauri config file and generates a `::tauri::Context` based on the content.
|
||||
///
|
||||
/// The context embeds the frontend assets, the application icons, the resolved Access Control List
|
||||
/// and the parsed configuration into the binary, and is passed to
|
||||
/// `tauri::Builder::run`/`tauri::Builder::build`.
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// tauri::Builder::default()
|
||||
/// .run(tauri::generate_context!())
|
||||
/// .expect("error while running tauri application");
|
||||
/// ```
|
||||
///
|
||||
/// # Options
|
||||
///
|
||||
/// All options are optional and can be combined in a comma separated list.
|
||||
///
|
||||
/// ## Configuration file path
|
||||
///
|
||||
/// A string literal as the first argument sets the path of the Tauri configuration file to read,
|
||||
/// relative to `CARGO_MANIFEST_DIR`. Defaults to `tauri.conf.json` on the crate directory.
|
||||
/// Platform specific configuration files (e.g. `tauri.windows.conf.json`) that sit next to it are
|
||||
/// merged as usual.
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// tauri::generate_context!("../tauri.conf.json");
|
||||
/// ```
|
||||
///
|
||||
/// ## Root path
|
||||
///
|
||||
/// A path (any item that is not a `key = value` pair) changes the crate path the generated code
|
||||
/// refers to. Defaults to `::tauri`, and is only needed when the `tauri` crate is renamed,
|
||||
/// re-exported by another crate, or is the crate being compiled (`crate`).
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// tauri::generate_context!("../tauri.conf.json", ::my_framework::tauri);
|
||||
/// ```
|
||||
///
|
||||
/// ## `capabilities`
|
||||
///
|
||||
/// A list of additional capability files to include in the generated Access Control List,
|
||||
/// on top of the ones defined in the `capabilities` directory and in the
|
||||
/// `app > security > capabilities` configuration value.
|
||||
///
|
||||
/// Each item is a path (relative to the current working directory of the compiler,
|
||||
/// usually the crate directory) to a JSON or TOML file containing a capability,
|
||||
/// a list of capabilities or a named list of capabilities.
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// tauri::generate_context!(capabilities = ["./capabilities/extra.json"]);
|
||||
/// ```
|
||||
///
|
||||
/// ## `assets`
|
||||
///
|
||||
/// An expression resolving to a custom [`tauri::Assets`] implementation, used instead of
|
||||
/// embedding the files from `build > frontendDist`. Useful for serving the frontend from a
|
||||
/// custom source, or for skipping asset embedding on tests.
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// tauri::generate_context!(assets = tauri::test::noop_assets());
|
||||
/// ```
|
||||
///
|
||||
/// ## `test`
|
||||
///
|
||||
/// When `true`, skips code generation that misbehaves when the context is created inside a
|
||||
/// test binary - currently the `Info.plist` embedding performed on macOS development builds.
|
||||
/// Defaults to `false`.
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// let context = tauri::generate_context!("../tauri.conf.json", test = true);
|
||||
/// ```
|
||||
///
|
||||
/// [`tauri::Assets`]: https://docs.rs/tauri/latest/tauri/trait.Assets.html
|
||||
///
|
||||
/// # 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.
|
||||
#[proc_macro]
|
||||
pub fn generate_context(items: TokenStream) -> TokenStream {
|
||||
// this macro is exported from the context module
|
||||
let path = parse_macro_input!(items as ContextItems);
|
||||
context::generate_context(path).into()
|
||||
}
|
||||
|
||||
/// Adds the default type for the last parameter (assumed to be runtime) for a specific feature.
|
||||
///
|
||||
/// e.g. To default the runtime generic to type `crate::Wry` when the `wry` feature is enabled, the
|
||||
/// syntax would look like `#[default_runtime(crate::Wry, wry)`. This is **always** set for the last
|
||||
/// generic, so make sure the last generic is the runtime when using this macro.
|
||||
#[doc(hidden)]
|
||||
#[proc_macro_attribute]
|
||||
pub fn default_runtime(attributes: TokenStream, input: TokenStream) -> TokenStream {
|
||||
let attributes = parse_macro_input!(attributes as runtime::Attributes);
|
||||
let input = parse_macro_input!(input as runtime::Input);
|
||||
runtime::default_runtime(attributes, input).into()
|
||||
}
|
||||
|
||||
/// Accepts a closure-like syntax to call arbitrary code on a menu item
|
||||
/// after matching against `kind` and retrieving it from `resources_table` using `rid`.
|
||||
///
|
||||
/// You can optionally pass a 5th parameter to select which item kinds
|
||||
/// to match against, by providing a `|` separated list of item kinds
|
||||
/// ```ignore
|
||||
/// do_menu_item!(resources_table, rid, kind, |i| i.set_text(text), Check | Submenu);
|
||||
/// ```
|
||||
/// You could also provide a negated list
|
||||
/// ```ignore
|
||||
/// do_menu_item!(resources_table, rid, kind, |i| i.set_text(text), !Check);
|
||||
/// do_menu_item!(resources_table, rid, kind, |i| i.set_text(text), !Check | !Submenu);
|
||||
/// ```
|
||||
/// but you can't have mixed negations and positive kinds.
|
||||
/// ```ignore
|
||||
/// do_menu_item!(resources_table, rid, kind, |i| i.set_text(text), !Check | Submenu);
|
||||
/// ```
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```ignore
|
||||
/// let rid = 23;
|
||||
/// let kind = ItemKind::Check;
|
||||
/// let resources_table = app.resources_table();
|
||||
/// do_menu_item!(resources_table, rid, kind, |i| i.set_text(text))
|
||||
/// ```
|
||||
/// which will expand into:
|
||||
/// ```ignore
|
||||
/// let rid = 23;
|
||||
/// let kind = ItemKind::Check;
|
||||
/// let resources_table = app.resources_table();
|
||||
/// match kind {
|
||||
/// ItemKind::Submenu => {
|
||||
/// let i = resources_table.get::<Submenu<R>>(rid)?;
|
||||
/// i.set_text(text)
|
||||
/// }
|
||||
/// ItemKind::MenuItem => {
|
||||
/// let i = resources_table.get::<MenuItem<R>>(rid)?;
|
||||
/// i.set_text(text)
|
||||
/// }
|
||||
/// ItemKind::Predefined => {
|
||||
/// let i = resources_table.get::<PredefinedMenuItem<R>>(rid)?;
|
||||
/// i.set_text(text)
|
||||
/// }
|
||||
/// ItemKind::Check => {
|
||||
/// let i = resources_table.get::<CheckMenuItem<R>>(rid)?;
|
||||
/// i.set_text(text)
|
||||
/// }
|
||||
/// ItemKind::Icon => {
|
||||
/// let i = resources_table.get::<IconMenuItem<R>>(rid)?;
|
||||
/// i.set_text(text)
|
||||
/// }
|
||||
/// _ => return Err(crate::Error::UnexpectedMenuKind),
|
||||
/// }
|
||||
/// ```
|
||||
#[doc(hidden)]
|
||||
#[proc_macro]
|
||||
pub fn do_menu_item(input: TokenStream) -> TokenStream {
|
||||
let tokens = parse_macro_input!(input as menu::DoMenuItemInput);
|
||||
menu::do_menu_item(tokens).into()
|
||||
}
|
||||
|
||||
/// Convert a .png or .ico icon to an Image
|
||||
/// for things like `tauri::tray::TrayIconBuilder` to consume,
|
||||
/// relative paths are resolved from `CARGO_MANIFEST_DIR`, not current file
|
||||
///
|
||||
/// ### Examples
|
||||
///
|
||||
/// ```ignore
|
||||
/// const APP_ICON: Image<'_> = include_image!("./icons/32x32.png");
|
||||
///
|
||||
/// // then use it with tray
|
||||
/// TrayIconBuilder::new().icon(APP_ICON).build().unwrap();
|
||||
///
|
||||
/// // or with window
|
||||
/// WebviewWindowBuilder::new(app, "main", WebviewUrl::default())
|
||||
/// .icon(APP_ICON)
|
||||
/// .unwrap()
|
||||
/// .build()
|
||||
/// .unwrap();
|
||||
///
|
||||
/// // or with any other functions that takes `Image` struct
|
||||
/// ```
|
||||
///
|
||||
/// Note: this stores the image in raw pixels to the final binary,
|
||||
/// so keep the icon size (width and height) small
|
||||
/// or else it's going to bloat your final executable
|
||||
#[proc_macro]
|
||||
pub fn include_image(tokens: TokenStream) -> TokenStream {
|
||||
let path = match parse2::<LitStr>(tokens.into()) {
|
||||
Ok(path) => path,
|
||||
Err(err) => return err.into_compile_error().into(),
|
||||
};
|
||||
let path = PathBuf::from(path.value());
|
||||
let resolved_path = if path.is_relative() {
|
||||
if let Ok(base_dir) = std::env::var("CARGO_MANIFEST_DIR").map(PathBuf::from) {
|
||||
base_dir.join(path)
|
||||
} else {
|
||||
return quote!(compile_error!("$CARGO_MANIFEST_DIR is not defined")).into();
|
||||
}
|
||||
} else {
|
||||
path
|
||||
};
|
||||
if !resolved_path.exists() {
|
||||
let error_string = format!(
|
||||
"Provided Image path \"{}\" doesn't exists",
|
||||
resolved_path.display()
|
||||
);
|
||||
return quote!(compile_error!(#error_string)).into();
|
||||
}
|
||||
|
||||
match CachedIcon::new("e!(::tauri), &resolved_path).map_err(|error| error.to_string()) {
|
||||
Ok(icon) => icon.into_token_stream(),
|
||||
Err(error) => quote!(compile_error!(#error)),
|
||||
}
|
||||
.into()
|
||||
}
|
||||
136
crates/tauri-macros/src/menu.rs
Normal file
136
crates/tauri-macros/src/menu.rs
Normal file
@@ -0,0 +1,136 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use proc_macro2::{Ident, Span, TokenStream};
|
||||
use quote::quote;
|
||||
use syn::{
|
||||
Expr, Token,
|
||||
parse::{Parse, ParseStream},
|
||||
punctuated::Punctuated,
|
||||
};
|
||||
|
||||
pub struct DoMenuItemInput {
|
||||
resources_table: Ident,
|
||||
rid: Ident,
|
||||
kind: Ident,
|
||||
var: Ident,
|
||||
expr: Expr,
|
||||
kinds: Vec<NegatedIdent>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct NegatedIdent {
|
||||
negated: bool,
|
||||
ident: Ident,
|
||||
}
|
||||
|
||||
impl NegatedIdent {
|
||||
fn new(ident: &str) -> Self {
|
||||
Self {
|
||||
negated: false,
|
||||
ident: Ident::new(ident, Span::call_site()),
|
||||
}
|
||||
}
|
||||
|
||||
fn is_negated(&self) -> bool {
|
||||
self.negated
|
||||
}
|
||||
}
|
||||
|
||||
impl Parse for NegatedIdent {
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
let negated_token = input.parse::<Token![!]>();
|
||||
let ident: Ident = input.parse()?;
|
||||
Ok(NegatedIdent {
|
||||
negated: negated_token.is_ok(),
|
||||
ident,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Parse for DoMenuItemInput {
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
let resources_table: Ident = input.parse()?;
|
||||
let _: Token![,] = input.parse()?;
|
||||
let rid: Ident = input.parse()?;
|
||||
let _: Token![,] = input.parse()?;
|
||||
let kind: Ident = input.parse()?;
|
||||
let _: Token![,] = input.parse()?;
|
||||
let _: Token![|] = input.parse()?;
|
||||
let var: Ident = input.parse()?;
|
||||
let _: Token![|] = input.parse()?;
|
||||
let expr: Expr = input.parse()?;
|
||||
let _: syn::Result<Token![,]> = input.parse();
|
||||
let kinds = Punctuated::<NegatedIdent, Token![|]>::parse_terminated(input)?;
|
||||
|
||||
Ok(Self {
|
||||
resources_table,
|
||||
rid,
|
||||
kind,
|
||||
var,
|
||||
expr,
|
||||
kinds: kinds.into_iter().collect(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn do_menu_item(input: DoMenuItemInput) -> TokenStream {
|
||||
let DoMenuItemInput {
|
||||
rid,
|
||||
resources_table,
|
||||
kind,
|
||||
expr,
|
||||
var,
|
||||
mut kinds,
|
||||
} = input;
|
||||
|
||||
let defaults = vec![
|
||||
NegatedIdent::new("Submenu"),
|
||||
NegatedIdent::new("MenuItem"),
|
||||
NegatedIdent::new("Predefined"),
|
||||
NegatedIdent::new("Check"),
|
||||
NegatedIdent::new("Icon"),
|
||||
];
|
||||
|
||||
if kinds.is_empty() {
|
||||
kinds.extend(defaults.clone());
|
||||
}
|
||||
|
||||
let has_negated = kinds.iter().any(|n| n.is_negated());
|
||||
if has_negated {
|
||||
kinds.extend(defaults);
|
||||
kinds.sort_by(|a, b| a.ident.cmp(&b.ident));
|
||||
kinds.dedup_by(|a, b| a.ident == b.ident);
|
||||
}
|
||||
|
||||
let (kinds, types): (Vec<Ident>, Vec<Ident>) = kinds
|
||||
.into_iter()
|
||||
.filter_map(|nident| {
|
||||
if nident.is_negated() {
|
||||
None
|
||||
} else {
|
||||
match nident.ident {
|
||||
i if i == "MenuItem" => Some((i, Ident::new("MenuItem", Span::call_site()))),
|
||||
i if i == "Submenu" => Some((i, Ident::new("Submenu", Span::call_site()))),
|
||||
i if i == "Predefined" => Some((i, Ident::new("PredefinedMenuItem", Span::call_site()))),
|
||||
i if i == "Check" => Some((i, Ident::new("CheckMenuItem", Span::call_site()))),
|
||||
i if i == "Icon" => Some((i, Ident::new("IconMenuItem", Span::call_site()))),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
})
|
||||
.unzip();
|
||||
|
||||
quote! {
|
||||
match #kind {
|
||||
#(
|
||||
ItemKind::#kinds => {
|
||||
let #var = #resources_table.get::<#types<R>>(#rid)?;
|
||||
#expr
|
||||
}
|
||||
)*
|
||||
_ => return Err(crate::Error::UnexpectedMenuKind),
|
||||
}
|
||||
}
|
||||
}
|
||||
95
crates/tauri-macros/src/mobile.rs
Normal file
95
crates/tauri-macros/src/mobile.rs
Normal file
@@ -0,0 +1,95 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use proc_macro::TokenStream;
|
||||
use proc_macro2::TokenStream as TokenStream2;
|
||||
use quote::{format_ident, quote};
|
||||
use std::env::var;
|
||||
use syn::{ItemFn, parse_macro_input, spanned::Spanned};
|
||||
|
||||
fn get_env_var(name: &str, error: &mut Option<TokenStream2>, function: &ItemFn) -> TokenStream2 {
|
||||
match var(name) {
|
||||
Ok(value) => {
|
||||
let ident = format_ident!("{value}");
|
||||
quote!(#ident)
|
||||
}
|
||||
Err(_) => {
|
||||
error.replace(
|
||||
syn::Error::new(
|
||||
function.span(),
|
||||
format!("`{name}` env var not set, do you have a build script with tauri-build?",),
|
||||
)
|
||||
.into_compile_error(),
|
||||
);
|
||||
quote!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn entry_point(_attributes: TokenStream, item: TokenStream) -> TokenStream {
|
||||
let function = parse_macro_input!(item as ItemFn);
|
||||
let function_name = function.sig.ident.clone();
|
||||
|
||||
let mut error = None;
|
||||
let domain = get_env_var("TAURI_ANDROID_PACKAGE_NAME_PREFIX", &mut error, &function);
|
||||
let app_name = get_env_var("TAURI_ANDROID_PACKAGE_NAME_APP_NAME", &mut error, &function);
|
||||
|
||||
let (wrapper, wrapper_name) = if function.sig.asyncness.is_some() {
|
||||
let wrapper_name = syn::Ident::new(&format!("{function_name}_wrapper"), function_name.span());
|
||||
(
|
||||
quote! {
|
||||
#function
|
||||
|
||||
fn #wrapper_name() {
|
||||
::tauri::async_runtime::block_on(#function_name());
|
||||
}
|
||||
},
|
||||
wrapper_name,
|
||||
)
|
||||
} else {
|
||||
(
|
||||
quote! {
|
||||
#function
|
||||
},
|
||||
function_name,
|
||||
)
|
||||
};
|
||||
|
||||
if let Some(e) = error {
|
||||
quote!(#e).into()
|
||||
} else {
|
||||
quote!(
|
||||
fn stop_unwind<F: FnOnce() -> T, T>(f: F) -> T {
|
||||
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)) {
|
||||
Ok(t) => t,
|
||||
Err(err) => {
|
||||
eprintln!("attempt to unwind out of `rust` with err: {:?}", err);
|
||||
std::process::abort()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#wrapper
|
||||
|
||||
fn _start_app() {
|
||||
#[cfg(target_os = "ios")]
|
||||
::tauri::log_stdout();
|
||||
#[cfg(target_os = "android")]
|
||||
{
|
||||
::tauri::android_binding!(#domain, #app_name, _start_app, ::tauri::wry);
|
||||
}
|
||||
stop_unwind(#wrapper_name);
|
||||
}
|
||||
|
||||
// be careful when renaming this, the `start_app` symbol is checked by the CLI
|
||||
#[cfg(not(target_os = "android"))]
|
||||
#[unsafe(no_mangle)]
|
||||
#[inline(never)]
|
||||
pub extern "C" fn start_app() {
|
||||
_start_app()
|
||||
}
|
||||
)
|
||||
.into()
|
||||
}
|
||||
}
|
||||
105
crates/tauri-macros/src/runtime.rs
Normal file
105
crates/tauri-macros/src/runtime.rs
Normal file
@@ -0,0 +1,105 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::{ToTokens, quote};
|
||||
use syn::parse::{Parse, ParseStream};
|
||||
use syn::{
|
||||
DeriveInput, Error, GenericParam, Ident, ItemTrait, ItemType, Token, Type, TypeParam, parse_quote,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) enum Input {
|
||||
Derive(DeriveInput),
|
||||
Trait(ItemTrait),
|
||||
Type(ItemType),
|
||||
}
|
||||
|
||||
impl Parse for Input {
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
input
|
||||
.parse::<DeriveInput>()
|
||||
.map(Self::Derive)
|
||||
.or_else(|_| input.parse().map(Self::Trait))
|
||||
.or_else(|_| input.parse().map(Self::Type))
|
||||
.map_err(|_| {
|
||||
Error::new(
|
||||
input.span(),
|
||||
"default_runtime only supports `struct`, `enum`, `type`, or `trait` definitions",
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Input {
|
||||
fn last_param_mut(&mut self) -> Option<&mut GenericParam> {
|
||||
match self {
|
||||
Input::Derive(d) => d.generics.params.last_mut(),
|
||||
Input::Trait(t) => t.generics.params.last_mut(),
|
||||
Input::Type(t) => t.generics.params.last_mut(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for Input {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
match self {
|
||||
Input::Derive(d) => d.to_tokens(tokens),
|
||||
Input::Trait(t) => t.to_tokens(tokens),
|
||||
Input::Type(t) => t.to_tokens(tokens),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The default runtime type to enable when the provided feature is enabled.
|
||||
pub(crate) struct Attributes {
|
||||
default_type: Type,
|
||||
feature: Ident,
|
||||
}
|
||||
|
||||
impl Parse for Attributes {
|
||||
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
|
||||
let default_type = input.parse()?;
|
||||
input.parse::<Token![,]>()?;
|
||||
Ok(Attributes {
|
||||
default_type,
|
||||
feature: input.parse()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn default_runtime(attributes: Attributes, input: Input) -> TokenStream {
|
||||
// create a new copy to manipulate for the wry feature flag
|
||||
let mut wry = input.clone();
|
||||
let wry_runtime = wry
|
||||
.last_param_mut()
|
||||
.expect("default_runtime requires the item to have at least 1 generic parameter");
|
||||
|
||||
// set the default value of the last generic parameter to the provided runtime type
|
||||
match wry_runtime {
|
||||
GenericParam::Type(
|
||||
param @ TypeParam {
|
||||
eq_token: None,
|
||||
default: None,
|
||||
..
|
||||
},
|
||||
) => {
|
||||
param.eq_token = Some(parse_quote!(=));
|
||||
param.default = Some(attributes.default_type);
|
||||
}
|
||||
_ => {
|
||||
panic!("DefaultRuntime requires the last parameter to not have a default value")
|
||||
}
|
||||
};
|
||||
|
||||
let feature = attributes.feature.to_string();
|
||||
|
||||
quote!(
|
||||
#[cfg(feature = #feature)]
|
||||
#wry
|
||||
|
||||
#[cfg(not(feature = #feature))]
|
||||
#input
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user