Files
dioxus/packages/manganis/manganis-macro/src/lib.rs
T
eeb60f7173 permissions, manganis::ffi, full Info.plist/AndroidManifest.xml customization (#4842)
* initial version

* make custom permission work without const-vec change

* wiring up with cli

* yey symbol found

* wip

* holy moly

* proper ui

* cleanup

* okay

* wip cleanup

* reduce boilerplate code into mobile-core

* Delete copilot-instructions.md

* revert hello-world example for low diff

* fix android build in mobile-core

* rename permission to static_permission

* small success with popup location on android

* android popup working

* Refactor permission checks for readability

Improves code formatting in has_location_permission and check_permission functions by splitting long lines and updating function signatures for better readability. No functional changes were made.

* wip consolidate all java files and build from cli instead of build.rs

* few cli cleanups

* wip java_plugin macro

* cleanup cleanup cleanup

* Update java_plugin macro to use full relative file paths

The java_plugin macro now accepts full relative paths for Java files instead of assuming a fixed directory structure. Documentation and usage examples have been updated to reflect this change, improving flexibility and clarity for specifying file locations.

* Improve iOS geolocation permission and retrieval logic

Added checks for authorization status before requesting location permissions and retrieving location data. Now only requests permission if not determined, and attempts to retrieve cached location before starting location updates, improving efficiency and user experience.

* Add ios_plugin macro for iOS framework metadata

Introduces the ios_plugin! macro in mobile-core-macro for declarative iOS framework metadata embedding. Updates mobile-core to re-export the macro and refactors mobile-geolocation to use ios_plugin! instead of manual linker attributes.

* not needed

* simplify the plugin

* rename mobile-core to platform-bridge and mobile-core-macro to platform-bridge-macro

* Refactor metadata serialization buffer initialization

Replaces inline creation of ConstVec with a named EMPTY constant for buffer initialization in metadata serialization. This improves code clarity and maintainability.

* mobile-geolocation out of dioxus

* take the example also out of dioxus

* remove example from cargo toml

* remove demo css

* revert hello-world example

* revert mac.plist

* revert gitignore

* Remove basic usage example for permissions crate

Deleted the basic_usage.rs example file from the permissions crate. This change may be part of a cleanup or refactoring to remove outdated or unnecessary example code.

* Delete plan.md

* remove dups

* cleanup java build

* cleanup permission symbol extraction

* update todo

* small slop of tested permissions in enums for minimum diff

* Refactor symbol extraction into linker_symbols module

Moved generic symbol extraction logic from android_java.rs and permissions.rs into a new linker_symbols.rs module. Updated both files to use the shared utilities for finding symbol offsets, reducing code duplication and improving maintainability.

* wip consolidate ios and macos as darwin

* Add docs and re-export for MainThreadCell and MainThreadMarker

Expanded documentation for MainThreadCell, clarifying usage and safety. Changed visibility of MainThreadCell to public, improved method docs, and re-exported MainThreadMarker in darwin/mod.rs for easier access.

* Update Darwin framework symbol and section names

Changed the framework symbol prefix from '__DARWIN_FRAMEWORK__' to '__DARWIN_FW__' and updated the Mach-O section name from '__darwin_framework' to '__darwin_fw' for compliance with section name length limits and consistency.

* Fix Info.plist path handling for iOS and macOS

Adds comments and corrects the path to Info.plist for iOS and macOS app bundles. For iOS, Info.plist is at the root of the .app bundle, while for macOS it is inside Contents/. Also improves warning message to include the missing path.

* Handle missing permission symbols and web bundle case

Improves permission extraction by returning an empty manifest when no permission symbols are found or when building a web bundle, as permissions are runtime-only in that case. Adds debug logging for missing symbols and ensures robust handling of these scenarios.

* objc2 over framework

* windows, linux, web all permissions are runtime so cleanup

* only include permissions for build time

* cleanup unnecessary test

* more cleanup

* extract shared into dx-macro-helpers

* rc.3 to 0.7.0 for const-serialize

* Add CustomPermissionBuilder and enhance PermissionBuilder for better permission management

- Introduced `CustomPermissionBuilder` for creating custom permissions with platform-specific identifiers.
- Enhanced `PermissionBuilder` to support custom permissions and location permissions with a clear builder pattern.
- Updated documentation and examples in the macro README to reflect new usage patterns for custom and location permissions.
- Improved parsing logic in the macro to handle both builder and direct construction patterns for permissions.

* clippy fixes

* implement const cbor encoding + decoding for basic types

* Add #[used] attribute to linker sections for optimization protection

- Added #[used] attribute to linker sections in both permission and Android plugin macros to prevent the linker from optimizing away symbols, ensuring they are preserved even if unused.
- Created static references to linker sections to enhance defense-in-depth against optimization issues.

* implement map encoding and decoding

* fix map test

* wip unified linker symbol

* cleanup permissions

* pipe java sources with manganis as well

* all tests passing

* dynamically sized arrays

* smaller str encoding

* swap array and list and add more documentation

* move serialization and deserialization for each struct together

* support both legacy and new assets

* bump const serialize version

* fix asset conversion code

* pull out manganis symbol struct from tuble

* test reading old asset versions

* more list/array cleanup and bump rust version

* fix clippy

* Revert "pipe java sources with manganis as well"

This reverts commit ddea9a459d9ac26f3f037a74d9ae0de3edfced67.

* Revert "cleanup permissions"

This reverts commit 7b18500155237403e179f0783cb8903a4f158b80.

* Revert "wip unified linker symbol"

This reverts commit 813e33feddb30d26868f6ac6e6da318402ac138f.

* Revert "Add #[used] attribute to linker sections for optimization protection"

This reverts commit a94a7a3a5e8eee80060de9d66a53aabfcf554f96.

* update cargo lock

* wip permission through assets

* abit cleanup

* fix reexports

* fix assets

* Fix the permissions piping

* some small cleanups

* checkout const-serialize as per #4932 and fix build

* invoking swift as a package

* geolocation package & example dumping for easy testing

* keep geolocation simple for testing

* cleanups

* pipe everything through __ASSETS__ and cleanup

* remove linker_symbols helper file

* example

* re example

* copy build methos into mobile-plugin-build crate

* Add conditional import for dioxus_07 feature

Introduces a conditional import to use the dioxus_07 crate when the corresponding feature is enabled, improving compatibility with different dioxus versions.

* Improve error handling and JNI reference usage

Adds a null check for Objective-C instance initialization in the iOS geolocation plugin to prevent errors on failed allocation. Refactors Android callback to use local JNI references instead of creating unnecessary global references, reducing potential memory leaks.

* revert changes on playwright

* Update dioxus dependency to 0.7.2 in Cargo.lock

Removed dioxus 0.7.1 from dependencies, retaining only dioxus 0.7.2 for the dioxus-cli-optimization-test package.

* small cleanup

* Add legacy const-serialize 0.7 support to dx-macro-helpers

Introduces support for the const-serialize 0.7 format in dx-macro-helpers by adding a new dependency and export, as well as a helper function for serialization with layout padding. Updates manganis to use the new helper for serializing assets in the legacy format, simplifying code and improving maintainability.

* Add helper for legacy and current linker sections

Introduces generate_link_sections_with_legacy in dx-macro-helpers to emit both legacy and current linker sections for compatibility. Refactors manganis-macro to use this new helper, reducing duplication and improving maintainability.

* Fix lints

* Fix clippy and doctest

* Refactor asset bundling and allow large enum variant

Refactored Asset::bundled to use Vec<u8> instead of ConstVec for reading volatile slices, simplifying the code and improving compatibility. Added #[allow(clippy::large_enum_variant)] to SymbolData enum to suppress Clippy warnings about large enum variants.

* Update license URLs in README to use angle brackets

Changed license URLs in the README from parentheses to angle brackets for improved formatting and clarity.

* Fix asset deserialization with correct buffer type

Replaces the use of `bytes.read()` with `ConstReadBuffer::new(bytes.as_ref())` for asset deserialization, ensuring compatibility with the expected deserialization API.

* Refactor geolocation and asset handling for clarity

Removed unnecessary clones in geolocation example event handlers to simplify state management. Updated SymbolDataOrAsset to store SymbolData in a Box for improved memory handling and adjusted related logic. Fixed documentation example in geolocation lib to include PermissionState in the import.

* Update geolocation example and permissions usage docs

Removes unused variables in the geolocation example's App function. Updates the permissions crate documentation to reflect new API usage with PermissionBuilder and LocationPrecision.

* Update permissions-macro to use permissions crate

Switched imports and dev-dependency from permissions_core to permissions in permissions-macro. Updated documentation and examples to reflect the new crate usage.

* rollback const-serialize changes

* move items around (doesn't compile)

* convert gelocation plugin to example

* remove dx-macro-helpers

* we're just gonna full-send linker everything

* rollback desktop changes

* remove gradle

* fix most compile?

* fix dioxus vers

* small cleanups

* add claude design docs

* add ffi macro

* remove

* loads but not linking yet

* fix output leak

* a little bit closer but not quite

* improve android code gen

* sheeeeee works!!!

* small tweaks to demo

* replace ios/android plugin with ffi macro entirely

* simplify ffi module

* simplify again

* condense / rip out some stuff from manganis

* remove types form public api

* wip, permissions

* oops I broke it

* I unbroke it

* move arch doc

* remove extra manifest types

* drop bindgen file

* remove permission

* small cleanup

* remove unsafecall mtm thing

* yoink perms

* wire up manifest

* Add deeplinks

* add schema generation

* update schema

* wip....

* fix recurse limit schema generation

* wip, muck around with schema

* add inline dioxus.toml support

* merge bundle config schemas

* claude my man

* Remove cdecl, annotate ffi

* rename design docs

* wip: live activities

* wip: apple plugin bundling

* wip

* get live activities linking properly

* small cleanups

* remove sidecars

* remove widget

* yoink legacy

* cleanup

* more fixups

* wire up more things to dioxus.toml

* fix workspace join

* final cleanups

* fmt

* final bit of clippy

* ci tweak

* update schema

* fmt, doc

* update schema docs

---------

Co-authored-by: Evan Almloff <evanalmloff@gmail.com>
Co-authored-by: Jonathan Kelley <jkelleyrtp@gmail.com>
2026-02-13 15:39:38 -08:00

410 lines
14 KiB
Rust

#![doc = include_str!("../README.md")]
#![deny(missing_docs)]
use std::path::PathBuf;
use proc_macro::TokenStream;
use proc_macro2::Span;
use quote::{quote, ToTokens};
use syn::{
parse::{Parse, ParseStream},
parse_macro_input, ItemStruct,
};
pub(crate) mod asset;
pub(crate) mod css_module;
pub(crate) mod ffi;
pub(crate) mod linker;
use crate::css_module::{expand_css_module_struct, CssModuleAttribute};
/// The asset macro collects assets that will be included in the final binary
///
/// # Files
///
/// The file builder collects an arbitrary file. Relative paths are resolved relative to the package root
/// ```rust
/// # use manganis::{asset, Asset};
/// const _: Asset = asset!("/assets/asset.txt");
/// ```
/// Macros like `concat!` and `env!` are supported in the asset path.
/// ```rust
/// # use manganis::{asset, Asset};
/// const _: Asset = asset!(concat!("/assets/", env!("CARGO_CRATE_NAME"), ".dat"));
/// ```
///
/// # Images
///
/// You can collect images which will be automatically optimized with the image builder:
/// ```rust
/// # use manganis::{asset, Asset};
/// const _: Asset = asset!("/assets/image.png");
/// ```
/// Resize the image at compile time to make the assets file size smaller:
/// ```rust
/// # use manganis::{asset, Asset, AssetOptions, ImageSize};
/// const _: Asset = asset!("/assets/image.png", AssetOptions::image().with_size(ImageSize::Manual { width: 52, height: 52 }));
/// ```
/// Or convert the image at compile time to a web friendly format:
/// ```rust
/// # use manganis::{asset, Asset, AssetOptions, ImageSize, ImageFormat};
/// const _: Asset = asset!("/assets/image.png", AssetOptions::image().with_format(ImageFormat::Avif));
/// ```
/// You can mark images as preloaded to make them load faster in your app
/// ```rust
/// # use manganis::{asset, Asset, AssetOptions};
/// const _: Asset = asset!("/assets/image.png", AssetOptions::image().with_preload(true));
/// ```
#[proc_macro]
pub fn asset(input: TokenStream) -> TokenStream {
let asset = parse_macro_input!(input as asset::AssetParser);
quote! { #asset }.into_token_stream().into()
}
/// Resolve an asset at compile time, returning `None` if the asset does not exist.
///
/// This behaves like the `asset!` macro when the asset can be resolved, but mirrors
/// [`option_env!`](core::option_env) by returning an `Option` instead of emitting a compile error
/// when the asset is missing.
///
/// ```rust
/// # use manganis::{asset, option_asset, Asset};
/// const REQUIRED: Asset = asset!("/assets/style.css");
/// const OPTIONAL: Option<Asset> = option_asset!("/assets/maybe.css");
/// ```
#[proc_macro]
pub fn option_asset(input: TokenStream) -> TokenStream {
let asset = parse_macro_input!(input as asset::AssetParser);
asset.expand_option_tokens().into()
}
/// Generate type-safe styles with scoped CSS class names.
///
/// The `css_module` attribute macro creates scoped CSS modules that prevent class name collisions
/// by making each class globally unique. It expands the annotated struct to provide type-safe
/// identifiers for your CSS classes, allowing you to reference styles in your Rust code with
/// compile-time guarantees.
///
/// # Syntax
///
/// The `css_module` attribute takes:
/// - The asset string path - the absolute path (from the crate root) to your CSS file.
/// - Optional `AssetOptions` to configure the processing of your CSS module.
///
/// It must be applied to a unit struct:
/// ```rust, ignore
/// #[css_module("/assets/my-styles.css")]
/// struct Styles;
///
/// #[css_module("/assets/my-styles.css", AssetOptions::css_module().with_minify(true))]
/// struct Styles;
/// ```
///
/// # Generation
///
/// The `css_module` attribute macro does two things:
/// - It generates an asset and automatically inserts it as a stylesheet link in the document.
/// - It expands the annotated struct with snake-case associated constants for your CSS class names.
///
/// ```rust, ignore
/// // This macro usage:
/// #[css_module("/assets/mycss.css")]
/// struct Styles;
///
/// // Will expand the struct to (simplified):
/// struct Styles {}
///
/// impl Styles {
/// // Snake-cased class names can be accessed like this:
/// pub const your_class: &str = "your_class-a1b2c3";
/// }
/// ```
///
/// # CSS Class Name Scoping
///
/// **The macro only processes CSS class selectors (`.class-name`).** Other selectors like IDs (`#id`),
/// element selectors (`div`, `p`), attribute selectors, etc. are left unchanged and not exposed as
/// Rust constants.
///
/// The macro collects all class selectors in your CSS file and transforms them to be globally unique
/// by appending a hash. For example, `.myClass` becomes `.myClass-a1b2c3` where `a1b2c3` is a hash
/// of the file path.
///
/// Class names are converted to snake_case for the Rust constants. For example:
/// - `.fooBar` becomes `Styles::foo_bar`
/// - `.my-class` becomes `Styles::my_class`
///
/// To prevent a class from being scoped, wrap it in `:global()`:
/// ```css
/// /* This class will be scoped */
/// .my-class { color: blue; }
///
/// /* This class will NOT be scoped (no hash added) */
/// :global(.global-class) { color: red; }
///
/// /* Element selectors and other CSS remain unchanged */
/// div { margin: 0; }
/// #my-id { padding: 10px; }
/// ```
///
/// # Using Multiple CSS Modules
///
/// Multiple `css_module` attributes can be used in the same scope by applying them to different structs:
/// ```rust, ignore
/// // First CSS module
/// #[css_module("/assets/styles1.css")]
/// struct Styles;
///
/// // Second CSS module with a different struct name
/// #[css_module("/assets/styles2.css")]
/// struct OtherStyles;
///
/// // Access classes from both:
/// rsx! {
/// div { class: Styles::container }
/// div { class: OtherStyles::button }
/// }
/// ```
///
/// # Asset Options
///
/// Similar to the `asset!()` macro, you can pass optional `AssetOptions` to configure processing:
/// ```rust, ignore
/// #[css_module(
/// "/assets/mycss.css",
/// AssetOptions::css_module()
/// .with_minify(true)
/// .with_preload(false)
/// )]
/// struct Styles;
/// ```
///
/// # Example
///
/// First create a CSS file:
/// ```css
/// /* assets/styles.css */
///
/// .container {
/// padding: 20px;
/// }
///
/// .button {
/// background-color: #373737;
/// }
///
/// :global(.global-text) {
/// font-weight: bold;
/// }
/// ```
///
/// Then use the `css_module` attribute:
/// ```rust, ignore
/// use dioxus::prelude::*;
///
/// fn app() -> Element {
/// #[css_module("/assets/styles.css")]
/// struct Styles;
///
/// rsx! {
/// div { class: Styles::container,
/// button { class: Styles::button, "Click me" }
/// span { class: Styles::global_text, "This uses global class" }
/// }
/// }
/// }
/// ```
#[proc_macro_attribute]
pub fn css_module(input: TokenStream, item: TokenStream) -> TokenStream {
let attribute = parse_macro_input!(input as CssModuleAttribute);
let item_struct = parse_macro_input!(item as ItemStruct);
let mut tokens = proc_macro2::TokenStream::new();
expand_css_module_struct(&mut tokens, &attribute, &item_struct);
tokens.into()
}
/// Generate FFI bindings between Rust and native platforms (Swift/Kotlin)
///
/// This attribute macro parses an `extern "Swift"` or `extern "Kotlin"` block and generates:
/// 1. Opaque type wrappers for foreign types
/// 2. Function implementations with direct JNI/ObjC bindings
/// 3. Linker metadata for the CLI to compile the native source
///
/// # Syntax
///
/// ```rust,ignore
/// #[manganis::ffi("/src/ios")]
/// extern "Swift" {
/// pub type GeolocationPlugin;
/// pub fn get_position(this: &GeolocationPlugin, high_accuracy: bool) -> Option<String>;
/// }
///
/// #[manganis::ffi("/src/android")]
/// extern "Kotlin" {
/// pub type GeolocationPlugin;
/// pub fn get_position(this: &GeolocationPlugin, high_accuracy: bool) -> Option<String>;
/// }
/// ```
///
/// # Path Parameter
///
/// The path in the attribute specifies the native source folder relative to `CARGO_MANIFEST_DIR`:
/// - For Swift: A SwiftPM package folder containing `Package.swift`
/// - For Kotlin: A Gradle project folder containing `build.gradle.kts`
///
/// # Type Declarations
///
/// Use `type Name;` to declare opaque foreign types. These become Rust structs wrapping
/// the native object handle (GlobalRef for JNI, raw pointer for ObjC).
///
/// # Function Declarations
///
/// Functions can be:
/// - **Instance methods**: First argument is `this: &TypeName`
/// - **Static methods**: No `this` argument
///
/// # Supported Types
///
/// - Primitives: `bool`, `i8`-`i64`, `u8`-`u64`, `f32`, `f64`
/// - Strings: `String`, `&str`
/// - Options: `Option<T>` where T is supported
/// - Opaque refs: `&TypeName` for foreign type references
#[proc_macro_attribute]
pub fn ffi(attr: TokenStream, item: TokenStream) -> TokenStream {
use ffi::{FfiAttribute, FfiBridgeParser};
let attr = parse_macro_input!(attr as FfiAttribute);
let item = parse_macro_input!(item as syn::ItemForeignMod);
match FfiBridgeParser::parse_with_attr(attr, item) {
Ok(parser) => parser.generate().into(),
Err(err) => err.to_compile_error().into(),
}
}
fn resolve_path(raw: &str, span: Span) -> Result<PathBuf, AssetParseError> {
// Get the location of the root of the crate which is where all assets are relative to
//
// IE
// /users/dioxus/dev/app/
// is the root of
// /users/dioxus/dev/app/assets/blah.css
let manifest_dir = dunce::canonicalize(
std::env::var("CARGO_MANIFEST_DIR")
.map(PathBuf::from)
.unwrap(),
)
.unwrap();
// 1. the input file should be a pathbuf
let input = PathBuf::from(raw);
let path = if raw.starts_with('.') {
if let Some(local_folder) = span.local_file().as_ref().and_then(|f| f.parent()) {
local_folder.join(raw)
} else {
// If we are running in rust analyzer, just assume the path is valid and return an error when
// we compile if it doesn't exist
if looks_like_rust_analyzer(&span) {
return Ok(
"The asset macro was expanded under Rust Analyzer which doesn't support paths or local assets yet"
.into(),
);
}
// Otherwise, return an error about the version of rust required for relative assets
return Err(AssetParseError::RelativeAssetPath);
}
} else {
manifest_dir.join(raw.trim_start_matches('/'))
};
// 2. absolute path to the asset
let Ok(path) = std::path::absolute(path) else {
return Err(AssetParseError::InvalidPath {
path: input.clone(),
});
};
// 3. Ensure the path exists
let Ok(path) = dunce::canonicalize(path) else {
return Err(AssetParseError::AssetDoesntExist {
path: input.clone(),
});
};
// 4. Ensure the path doesn't escape the crate dir
//
// - Note: since we called canonicalize on both paths, we can safely compare the parent dirs.
// On windows, we can only compare the prefix if both paths are canonicalized (not just absolute)
// https://github.com/rust-lang/rust/issues/42869
if path == manifest_dir || !path.starts_with(manifest_dir) {
return Err(AssetParseError::InvalidPath { path });
}
Ok(path)
}
/// Parse `T`, while also collecting the tokens it was parsed from.
fn parse_with_tokens<T: Parse>(input: ParseStream) -> syn::Result<(T, proc_macro2::TokenStream)> {
let begin = input.cursor();
let t: T = input.parse()?;
let end = input.cursor();
let mut cursor = begin;
let mut tokens = proc_macro2::TokenStream::new();
while cursor != end {
let (tt, next) = cursor.token_tree().unwrap();
tokens.extend(std::iter::once(tt));
cursor = next;
}
Ok((t, tokens))
}
#[derive(Debug)]
enum AssetParseError {
AssetDoesntExist { path: PathBuf },
InvalidPath { path: PathBuf },
RelativeAssetPath,
}
impl std::fmt::Display for AssetParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AssetParseError::AssetDoesntExist { path } => {
write!(f, "Asset at {} doesn't exist", path.display())
}
AssetParseError::InvalidPath { path } => {
write!(
f,
"Asset path {} is invalid. Make sure the asset exists within this crate.",
path.display()
)
}
AssetParseError::RelativeAssetPath => write!(f, "Failed to resolve relative asset path. Relative assets are only supported in rust 1.88+."),
}
}
}
/// Rust analyzer doesn't provide a stable way to detect if macros are running under it.
/// This function uses heuristics to determine if we are running under rust analyzer for better error
/// messages.
fn looks_like_rust_analyzer(span: &Span) -> bool {
// Rust analyzer spans have a struct debug impl compared to rustcs custom debug impl
// RA Example: SpanData { range: 45..58, anchor: SpanAnchor(EditionedFileId(0, Edition2024), ErasedFileAstId { kind: Fn, index: 0, hash: 9CD8 }), ctx: SyntaxContext(4294967036) }
// Rustc Example: #0 bytes(70..83)
let looks_like_rust_analyzer_span = format!("{:?}", span).contains("ctx:");
// The rust analyzer macro expander runs under RUST_ANALYZER_INTERNALS_DO_NOT_USE
let looks_like_rust_analyzer_env = std::env::var("RUST_ANALYZER_INTERNALS_DO_NOT_USE").is_ok();
// The rust analyzer executable is named rust-analyzer-proc-macro-srv
let looks_like_rust_analyzer_exe = std::env::current_exe().ok().is_some_and(|p| {
p.file_stem()
.and_then(|s| s.to_str())
.is_some_and(|s| s.contains("rust-analyzer"))
});
looks_like_rust_analyzer_span || looks_like_rust_analyzer_env || looks_like_rust_analyzer_exe
}