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>
This commit is contained in:
authored and GitHub committed 2026-02-13 15:39:38 -08:00
1 parent 64cc42e16e
commit eeb60f7173
73 files changed
+13767 -336

No files matched your search

+1
View File
@@ -109,6 +109,7 @@ tracing-subscriber = { version = "0.3.19", features = [
"fmt",
] }
console-subscriber = { version = "0.4.1", optional = true }
schemars = "0.8"
tracing = { workspace = true }
ansi-to-tui = { workspace = true }
ansi-to-html = { workspace = true }
@@ -1,8 +1,5 @@
package dev.dioxus.main;
// need to re-export buildconfig down from the parent
import {{application_id}}.BuildConfig;
typealias BuildConfig = BuildConfig;
package dev.dioxus.main
typealias BuildConfig = {{application_id}}.BuildConfig
class MainActivity : WryActivity()
@@ -1,17 +1,20 @@
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
{{#each gradle_plugins}}
id("{{ this }}")
{{/each}}
}
android {
namespace="{{ application_id }}"
compileSdk = 33
compileSdk = {{ compile_sdk }}
defaultConfig {
applicationId = "{{ application_id }}"
minSdk = 24
targetSdk = 33
minSdk = {{ min_sdk }}
targetSdk = {{ target_sdk }}
versionCode = 1
versionName = "1.0"
versionName = "{{ version }}"
}
{{#if android_bundle}}
signingConfigs {
@@ -48,15 +51,27 @@ android {
}
}
kotlinOptions {
jvmTarget = "1.8"
jvmTarget = "17"
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
buildFeatures {
buildConfig = true
}
sourceSets {
getByName("main") {
java.srcDirs("src/main/kotlin", "src/main/java")
}
}
}
dependencies {
implementation("androidx.webkit:webkit:1.6.1")
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("com.google.android.material:material:1.8.0")
{{#each gradle_dependencies}}
implementation("{{ this }}")
{{/each}}
}
@@ -1,11 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Default permissions -->
<uses-permission android:name="android.permission.INTERNET" />
<application android:hasCode="true" android:supportsRtl="true" android:icon="@mipmap/ic_launcher"
<!-- Permissions from Dioxus.toml [permissions] section -->
{{#each permissions}}
<uses-permission android:name="{{ this }}" />
{{/each}}
<!-- Hardware features from Dioxus.toml [android] section -->
{{#each features}}
<uses-feature android:name="{{ this }}" android:required="true" />
{{/each}}
<!-- Raw manifest XML from Dioxus.toml [android.raw.manifest] -->
{{{ raw_manifest }}}
<application android:hasCode="true" android:icon="@mipmap/ic_launcher"
android:extractNativeLibs="true"
android:allowNativeHeapPointerTagging="false"
android:label="@string/app_name"
android:theme="@style/AppTheme"
android:supportsRtl="{{#if supports_rtl}}{{ supports_rtl }}{{else}}true{{/if}}"
android:theme="{{#if app_theme}}{{ app_theme }}{{else}}@style/AppTheme{{/if}}"
{{#if uses_cleartext_traffic}}android:usesCleartextTraffic="{{ uses_cleartext_traffic }}"{{/if}}
{{#if large_heap}}android:largeHeap="{{ large_heap }}"{{/if}}
android:networkSecurityConfig="@xml/network_security_config">
<activity android:configChanges="orientation|screenLayout|screenSize|keyboardHidden" android:exported="true"
android:label="@string/app_name" android:name="dev.dioxus.main.MainActivity">
@@ -15,6 +33,33 @@
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
{{#each url_schemes}}
<!-- URL scheme deep link: {{ this }}:// -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="{{ this }}" />
</intent-filter>
{{/each}}
{{#each app_link_hosts}}
<!-- App link: https://{{ this }} -->
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" android:host="{{ this }}" />
</intent-filter>
{{/each}}
</activity>
{{#if foreground_service_type}}
<service
android:name=".DioxusForegroundService"
android:foregroundServiceType="{{ foreground_service_type }}"
android:exported="false" />
{{/if}}
</application>
</manifest>
+39 -5
View File
@@ -1,4 +1,3 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
@@ -16,9 +15,9 @@
<string>{{ bundle_name }}</string>
<key>CFBundleVersion</key>
<string>0.1.0</string>
<string>{{ version }}</string>
<key>CFBundleShortVersionString</key>
<string>0.1.0</string>
<string>{{ version }}</string>
<key>CFBundleDevelopmentRegion</key>
<string>en_US</string>
<key>UILaunchStoryboardName</key>
@@ -42,8 +41,6 @@
<string>iPhoneOS</string>
<string>iPadOS</string>
</array>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
@@ -57,5 +54,42 @@
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<!-- Permission usage descriptions from Dioxus.toml [permissions] section -->
{{#each permissions}}
<key>{{ this.key }}</key>
<string>{{ this.description }}</string>
{{/each}}
{{#if url_schemes}}
<!-- URL schemes from Dioxus.toml [deep_links] and [ios] sections -->
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
{{#each url_schemes}}
<string>{{ this }}</string>
{{/each}}
</array>
</dict>
</array>
{{/if}}
{{#if background_modes}}
<!-- Background modes from Dioxus.toml [background] and [ios] sections -->
<key>UIBackgroundModes</key>
<array>
{{#each background_modes}}
<string>{{ this }}</string>
{{/each}}
</array>
{{/if}}
<!-- Additional plist entries from Dioxus.toml [ios.plist] section -->
{{{ plist_entries }}}
<!-- Raw plist XML from Dioxus.toml [ios.raw.info_plist] -->
{{{ raw_plist }}}
</dict>
</plist>
+30 -6
View File
@@ -24,15 +24,39 @@
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>7.4.0</string>
<string>{{ version }}</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSApplicationCategoryType</key>
<string>public.app-category.social-networking</string>
<string>{{ version }}</string>
<key>LSMinimumSystemVersion</key>
<string>10.15</string>
<string>{{ minimum_system_version }}</string>
<!-- Permission usage descriptions from Dioxus.toml [permissions] section -->
{{#each permissions}}
<key>{{ this.key }}</key>
<string>{{ this.description }}</string>
{{/each}}
{{#if url_schemes}}
<!-- URL schemes from Dioxus.toml [deep_links] and [macos] sections -->
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
{{#each url_schemes}}
<string>{{ this }}</string>
{{/each}}
</array>
</dict>
</array>
{{/if}}
<!-- Additional plist entries from Dioxus.toml [macos.plist] section -->
{{{ plist_entries }}}
<!-- Raw plist XML from Dioxus.toml [macos.raw.info_plist] -->
{{{ raw_plist }}}
</dict>
</plist>
File diff suppressed because it is too large. Load diff
+298 -32
View File
@@ -35,9 +35,10 @@ use std::{
use crate::Result;
use anyhow::{bail, Context};
use const_serialize::{serialize_const, ConstVec, SerializeConst};
use const_serialize::{deserialize_const, serialize_const, ConstVec};
use dioxus_cli_opt::AssetManifest;
use manganis::{AssetOptions, AssetVariant, BundledAsset, ImageFormat, ImageSize};
use manganis_core::{AndroidArtifactMetadata, SwiftPackageMetadata, SymbolData};
use object::{File, Object, ObjectSection, ObjectSymbol, ReadCache, ReadRef, Section, Symbol};
use pdb::FallibleIterator;
use rayon::iter::{IntoParallelRefMutIterator, ParallelIterator};
@@ -60,6 +61,7 @@ enum ManganisVersion {
/// The legacy version of the manganis format published with 0.7.0 and 0.7.1
Legacy,
/// The new version of the manganis format 0.7.2 onward
/// This now includes both assets (old BundledAsset format) and permissions (SymbolData format)
New,
}
@@ -69,11 +71,18 @@ impl ManganisVersion {
ManganisVersion::Legacy => {
<manganis_core_07::BundledAsset as const_serialize_07::SerializeConst>::MEMORY_LAYOUT.size()
}
ManganisVersion::New => BundledAsset::MEMORY_LAYOUT.size(),
// For new format, we use a larger buffer size to accommodate variable-length CBOR
// The actual size will be determined by CBOR deserialization
ManganisVersion::New => 4096,
}
}
fn deserialize(&self, data: &[u8]) -> Option<BundledAsset> {
/// Deserialize data, trying multiple formats for backward compatibility
///
/// Tries in order:
/// 1. SymbolData (new unified format) - can contain Asset or Permission
/// 2. BundledAsset (old asset format) - for backward compatibility
fn deserialize(&self, data: &[u8]) -> Option<SymbolDataOrAsset> {
match self {
ManganisVersion::Legacy => {
let buffer = const_serialize_07::ConstReadBuffer::new(data);
@@ -81,18 +90,75 @@ impl ManganisVersion {
let (_, legacy_asset) =
const_serialize_07::deserialize_const!(manganis_core_07::BundledAsset, buffer)?;
Some(legacy_asset_to_modern_asset(&legacy_asset))
Some(SymbolDataOrAsset::Asset(legacy_asset_to_modern_asset(
&legacy_asset,
)))
}
ManganisVersion::New => {
let (_, asset) =
const_serialize::deserialize_const!(manganis_core::BundledAsset, data)?;
// First try SymbolData (new format with enum variant)
// const-serialize deserialization returns (remaining_bytes, value)
// We accept if remaining is empty or contains only padding (zeros)
if let Some((remaining, symbol_data)) = deserialize_const!(SymbolData, data) {
// Check if remaining bytes are all zeros (padding) or empty
// This handles the case where the linker section is larger than the actual data
// Be very lenient with padding - as long as we successfully deserialized, accept it
// The padding is just zeros added to fill the buffer size
let is_valid = remaining.is_empty()
|| remaining.iter().all(|&b| b == 0)
|| remaining.len() <= data.len(); // Allow any amount of padding as long as it's not larger than data
Some(asset)
if is_valid {
return Some(SymbolDataOrAsset::SymbolData(Box::new(symbol_data)));
} else {
tracing::debug!(
"SymbolData deserialized but invalid padding: {} remaining bytes out of {} total (first few bytes: {:?})",
remaining.len(),
data.len(),
&data[..data.len().min(32)]
);
}
} else {
tracing::debug!(
"Failed to deserialize as SymbolData. Data length: {}, first few bytes: {:?}",
data.len(),
&data[..data.len().min(32)]
);
}
// Fallback: try BundledAsset (direct format - assets are now serialized this way)
// This handles assets that were serialized directly as BundledAsset (not wrapped in SymbolData)
if let Some((remaining, asset)) = deserialize_const!(BundledAsset, data) {
// Check if remaining bytes are all zeros (padding) or empty
// Accept any amount of padding as long as it's all zeros (which is what we pad with)
let is_valid = remaining.is_empty() || remaining.iter().all(|&b| b == 0);
if is_valid {
tracing::debug!(
"Successfully deserialized BundledAsset, remaining padding: {} bytes",
remaining.len()
);
return Some(SymbolDataOrAsset::Asset(asset));
} else {
tracing::warn!(
"BundledAsset deserialized but remaining bytes are not all zeros: {} remaining bytes, first few: {:?}",
remaining.len(),
&remaining[..remaining.len().min(16)]
);
}
} else {
tracing::warn!(
"Failed to deserialize as BundledAsset. Data length: {}, first 32 bytes: {:?}",
data.len(),
&data[..data.len().min(32)]
);
}
None
}
}
}
fn serialize(&self, asset: &BundledAsset) -> Vec<u8> {
fn serialize_asset(&self, asset: &BundledAsset) -> Vec<u8> {
match self {
ManganisVersion::Legacy => {
let legacy_asset = modern_asset_to_legacy_asset(asset);
@@ -103,11 +169,71 @@ impl ManganisVersion {
buffer.as_ref().to_vec()
}
ManganisVersion::New => {
// New format: serialize as BundledAsset directly (backward compatible)
// Pad to 4096 bytes to match the linker output size
let buffer = serialize_const(asset, ConstVec::new());
buffer.as_ref().to_vec()
let mut data = buffer.as_ref().to_vec();
if data.len() < 4096 {
data.resize(4096, 0);
}
data
}
}
}
fn serialize_symbol_data(&self, data: &SymbolData) -> Option<Vec<u8>> {
match self {
ManganisVersion::Legacy => None,
ManganisVersion::New => {
let buffer = serialize_const(data, ConstVec::new());
let mut bytes = buffer.as_ref().to_vec();
if bytes.len() < 4096 {
bytes.resize(4096, 0);
}
Some(bytes)
}
}
}
}
/// Result of deserializing a symbol - can be either SymbolData or legacy Asset
#[derive(Debug, Clone)]
#[allow(clippy::large_enum_variant)]
enum SymbolDataOrAsset {
/// New unified format (can contain Asset or Permission)
SymbolData(Box<SymbolData>),
/// Old asset format (backward compatibility)
Asset(BundledAsset),
}
#[derive(Clone, Copy)]
struct AssetWriteEntry {
symbol: ManganisSymbolOffset,
asset_index: usize,
representation: AssetRepresentation,
}
impl AssetWriteEntry {
fn new(
symbol: ManganisSymbolOffset,
asset_index: usize,
representation: AssetRepresentation,
) -> Self {
Self {
symbol,
asset_index,
representation,
}
}
}
#[derive(Clone, Copy)]
enum AssetRepresentation {
/// Serialized as a raw BundledAsset (legacy or new format)
RawBundled,
/// Serialized as SymbolData::Asset (new CBOR format)
SymbolData,
}
fn legacy_asset_to_modern_asset(
@@ -570,9 +696,24 @@ fn find_wasm_symbol_offsets<'a, R: ReadRef<'a>>(
Ok(offsets)
}
/// Result of extracting symbols from a binary file
#[derive(Debug, Clone)]
pub(crate) struct SymbolExtractionResult {
/// Assets found in the binary
pub assets: Vec<BundledAsset>,
/// Android plugin artifacts discovered in the binary
pub android_artifacts: Vec<AndroidArtifactMetadata>,
/// Swift packages discovered in the binary
pub swift_packages: Vec<SwiftPackageMetadata>,
}
/// Find all assets in the given file, hash them, and write them back to the file.
/// Then return an `AssetManifest` containing all the assets found in the file.
pub(crate) async fn extract_assets_from_file(path: impl AsRef<Path>) -> Result<AssetManifest> {
/// Also extracts Android/Swift plugin metadata for FFI bindings.
pub(crate) async fn extract_symbols_from_file(
path: impl AsRef<Path>,
) -> Result<SymbolExtractionResult> {
let path = path.as_ref();
let mut file = open_file_for_writing_with_timeout(
path,
@@ -588,23 +729,82 @@ pub(crate) async fn extract_assets_from_file(path: impl AsRef<Path>) -> Result<A
let offsets = find_symbol_offsets(path, &file_contents, &object_file)?;
let mut assets = Vec::new();
let mut android_artifacts = Vec::new();
let mut swift_packages = Vec::new();
let mut write_entries = Vec::new();
// Read each asset from the data section using the offsets
// Read each symbol from the data section using the offsets
for symbol in offsets.iter().copied() {
let version = symbol.version;
let offset = symbol.offset;
file.seek(std::io::SeekFrom::Start(offset))?;
let mut data_in_range = vec![0; version.size()];
file.read_exact(&mut data_in_range)?;
if let Some(bundled_asset) = version.deserialize(&data_in_range) {
tracing::debug!(
"Found asset at offset {offset}: {:?}",
bundled_asset.absolute_source_path()
);
assets.push(bundled_asset);
// Read data from file_contents (already loaded into memory)
// Use a large buffer for variable length data, but don't exceed file size
let buffer_size = version
.size()
.min(file_contents.len().saturating_sub(offset as usize));
if buffer_size == 0 {
tracing::warn!("Symbol at offset {offset} is beyond file size");
continue;
}
let data_in_range = if (offset as usize) + buffer_size <= file_contents.len() {
&file_contents[offset as usize..(offset as usize) + buffer_size]
} else {
tracing::warn!("Found an asset at offset {offset} that could not be deserialized. This may be caused by a mismatch between your dioxus and dioxus-cli versions.");
&file_contents[offset as usize..]
};
// Try to deserialize - const-serialize will handle variable-length data correctly
// The deserialization should work even with padding (zeros) at the end
if let Some(result) = version.deserialize(data_in_range) {
match result {
SymbolDataOrAsset::SymbolData(symbol_data) => match *symbol_data {
SymbolData::Asset(asset) => {
tracing::debug!(
"Found asset (via SymbolData) at offset {offset}: {:?}",
asset.absolute_source_path()
);
let asset_index = assets.len();
assets.push(asset);
write_entries.push(AssetWriteEntry::new(
symbol,
asset_index,
AssetRepresentation::SymbolData,
));
}
SymbolData::AndroidArtifact(meta) => {
tracing::debug!(
"Found Android artifact declaration for plugin {}",
meta.plugin_name.as_str()
);
android_artifacts.push(meta);
}
SymbolData::SwiftPackage(meta) => {
tracing::debug!(
"Found Swift package declaration for plugin {}",
meta.plugin_name.as_str()
);
swift_packages.push(meta);
}
_ => {}
},
SymbolDataOrAsset::Asset(asset) => {
tracing::debug!(
"Found asset (old format) at offset {offset}: {:?}",
asset.absolute_source_path()
);
let asset_index = assets.len();
assets.push(asset);
write_entries.push(AssetWriteEntry::new(
symbol,
asset_index,
AssetRepresentation::RawBundled,
));
}
}
} else {
tracing::warn!("Found a symbol at offset {offset} that could not be deserialized. This may be caused by a mismatch between your dioxus and dioxus-cli versions, or the symbol may be in an unsupported format.");
}
}
@@ -613,14 +813,47 @@ pub(crate) async fn extract_assets_from_file(path: impl AsRef<Path>) -> Result<A
.par_iter_mut()
.for_each(dioxus_cli_opt::add_hash_to_asset);
// Write back the assets to the binary file
for (symbol, asset) in offsets.into_iter().zip(&assets) {
let version = symbol.version;
let offset = symbol.offset;
let new_data = version.serialize(asset);
// Write back only assets to the binary file (permissions are not modified)
for entry in write_entries {
let version = entry.symbol.version;
let offset = entry.symbol.offset;
let asset = assets
.get(entry.asset_index)
.copied()
.expect("asset index collected from symbol scan");
file.seek(std::io::SeekFrom::Start(offset))?;
file.write_all(new_data.as_ref())?;
match entry.representation {
AssetRepresentation::RawBundled => {
tracing::debug!("Writing asset to offset {offset}: {:?}", asset);
let new_data = version.serialize_asset(&asset);
if new_data.len() > version.size() {
tracing::warn!(
"Asset at offset {offset} serialized to {} bytes, but buffer is only {} bytes. Truncating output.",
new_data.len(),
version.size()
);
}
write_serialized_bytes(&mut file, offset, &new_data, version.size())?;
}
AssetRepresentation::SymbolData => {
tracing::debug!("Writing asset (SymbolData) to offset {offset}: {:?}", asset);
let Some(new_data) = version.serialize_symbol_data(&SymbolData::Asset(asset))
else {
tracing::warn!(
"Symbol at offset {offset} was stored as SymbolData but the binary format only supports raw assets"
);
continue;
};
if new_data.len() > version.size() {
tracing::warn!(
"SymbolData asset at offset {offset} serialized to {} bytes, but buffer is only {} bytes. Truncating output.",
new_data.len(),
version.size()
);
}
write_serialized_bytes(&mut file, offset, &new_data, version.size())?;
}
}
}
// Ensure the file is flushed to disk
@@ -645,12 +878,23 @@ pub(crate) async fn extract_assets_from_file(path: impl AsRef<Path>) -> Result<A
}
}
// Finally, create the asset manifest
Ok(SymbolExtractionResult {
assets,
android_artifacts,
swift_packages,
})
}
/// Find all assets in the given file, hash them, and write them back to the file.
/// Then return an `AssetManifest` containing all the assets found in the file.
///
/// This is a convenience function that extracts symbols and returns only assets.
pub(crate) async fn extract_assets_from_file(path: impl AsRef<Path>) -> Result<AssetManifest> {
let result = extract_symbols_from_file(path).await?;
let mut manifest = AssetManifest::default();
for asset in assets {
for asset in result.assets {
manifest.insert_asset(asset);
}
Ok(manifest)
}
@@ -680,3 +924,25 @@ async fn open_file_for_writing_with_timeout(
}
}
}
fn write_serialized_bytes(
file: &mut std::fs::File,
offset: u64,
data: &[u8],
buffer_size: usize,
) -> Result<()> {
use std::io::SeekFrom;
file.seek(SeekFrom::Start(offset))?;
if data.len() <= buffer_size {
file.write_all(data)?;
if data.len() < buffer_size {
let padding = vec![0; buffer_size - data.len()];
file.write_all(&padding)?;
}
} else {
file.write_all(&data[..buffer_size])?;
}
Ok(())
}
+8
View File
@@ -109,6 +109,14 @@ impl BuildContext {
})
}
pub(crate) fn status_compiling_native_plugins(&self, detail: impl Into<String>) {
_ = self.tx.unbounded_send(BuilderUpdate::Progress {
stage: BuildStage::CompilingNativePlugins {
detail: detail.into(),
},
});
}
pub(crate) fn status_codesigning(&self) {
_ = self.tx.unbounded_send(BuilderUpdate::Progress {
stage: BuildStage::CodeSigning,
+915
View File
@@ -0,0 +1,915 @@
//! iOS/macOS Swift package manifest helpers and compilation.
use crate::Result;
use anyhow::Context;
use manganis_core::SwiftPackageMetadata;
use std::path::{Path, PathBuf};
use target_lexicon::{OperatingSystem, Triple};
use tokio::process::Command;
/// Create a proper framework bundle from a dylib for iOS/macOS.
///
/// iOS uses a flat structure while macOS uses a versioned structure.
/// Both require an Info.plist for proper App Store submission.
pub async fn create_framework_bundle(
dylib_path: &Path,
framework_name: &str,
output_dir: &Path,
target_triple: &Triple,
bundle_identifier: &str,
) -> Result<PathBuf> {
let is_ios = matches!(target_triple.operating_system, OperatingSystem::IOS(_));
let min_os_version = if is_ios { "13.0" } else { "11.0" };
let framework_dir = output_dir.join(format!("{}.framework", framework_name));
// Remove existing framework if present
if framework_dir.exists() {
std::fs::remove_dir_all(&framework_dir)?;
}
if is_ios {
// iOS uses flat structure: Framework.framework/FrameworkName + Info.plist
std::fs::create_dir_all(&framework_dir)?;
// Copy dylib as the framework executable (no extension)
let exec_path = framework_dir.join(framework_name);
std::fs::copy(dylib_path, &exec_path)?;
// Set the install name using install_name_tool
let output = Command::new("xcrun")
.arg("install_name_tool")
.arg("-id")
.arg(format!(
"@rpath/{}.framework/{}",
framework_name, framework_name
))
.arg(&exec_path)
.output()
.await?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
anyhow::bail!("install_name_tool failed: {}", stderr);
}
// Create Info.plist
let info_plist = format!(
r#"<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>{framework_name}</string>
<key>CFBundleIdentifier</key>
<string>{bundle_identifier}</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>{framework_name}</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>MinimumOSVersion</key>
<string>{min_os_version}</string>
<key>CFBundleSupportedPlatforms</key>
<array>
<string>iPhoneOS</string>
</array>
</dict>
</plist>"#
);
std::fs::write(framework_dir.join("Info.plist"), info_plist)?;
} else {
// macOS uses versioned structure with symlinks
let versions_a = framework_dir.join("Versions").join("A");
let resources_dir = versions_a.join("Resources");
std::fs::create_dir_all(&resources_dir)?;
// Copy dylib as the framework executable
let exec_path = versions_a.join(framework_name);
std::fs::copy(dylib_path, &exec_path)?;
// Set install name
let output = Command::new("xcrun")
.arg("install_name_tool")
.arg("-id")
.arg(format!(
"@rpath/{}.framework/Versions/A/{}",
framework_name, framework_name
))
.arg(&exec_path)
.output()
.await?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
anyhow::bail!("install_name_tool failed: {}", stderr);
}
// Create Info.plist in Resources
let info_plist = format!(
r#"<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>{framework_name}</string>
<key>CFBundleIdentifier</key>
<string>{bundle_identifier}</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>{framework_name}</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSMinimumSystemVersion</key>
<string>{min_os_version}</string>
</dict>
</plist>"#
);
std::fs::write(resources_dir.join("Info.plist"), info_plist)?;
// Create symbolic links (required for macOS framework structure)
let versions_dir = framework_dir.join("Versions");
#[cfg(unix)]
{
std::os::unix::fs::symlink("A", versions_dir.join("Current"))?;
std::os::unix::fs::symlink(
format!("Versions/Current/{}", framework_name),
framework_dir.join(framework_name),
)?;
std::os::unix::fs::symlink(
"Versions/Current/Resources",
framework_dir.join("Resources"),
)?;
}
}
tracing::debug!(
"Created {} framework bundle: {}",
if is_ios { "iOS" } else { "macOS" },
framework_dir.display()
);
Ok(framework_dir)
}
/// Compile Swift sources and return the path to the dynamic framework bundle.
///
/// This function:
/// 1. Generates an umbrella Package.swift that includes all Swift plugins
/// 2. Runs `swift build` to compile into a dynamic library
/// 3. Wraps the dylib in a proper .framework bundle for iOS/macOS
/// 4. Returns the path to the resulting `.framework` bundle
pub async fn compile_swift_sources(
swift_sources: &[SwiftPackageMetadata],
target_triple: &Triple,
build_dir: &Path,
release: bool,
) -> Result<Option<PathBuf>> {
if swift_sources.is_empty() {
return Ok(None);
}
tracing::debug!(
"Compiling {} Swift plugin(s) for {}",
swift_sources.len(),
target_triple
);
// Create the plugins build directory
let plugins_dir = build_dir.join("swift-plugins");
std::fs::create_dir_all(&plugins_dir)?;
// Copy and prepare all Swift source packages
let mut plugin_paths = Vec::new();
for source in swift_sources {
let source_path = PathBuf::from(source.package_path.as_str());
let plugin_name = source.plugin_name.as_str();
let product_name = source.product.as_str();
if !source_path.exists() {
tracing::warn!(
"Swift package path does not exist: {} (for plugin {})",
source_path.display(),
plugin_name
);
continue;
}
let dest_path = plugins_dir.join(plugin_name);
if dest_path.exists() {
std::fs::remove_dir_all(&dest_path)?;
}
copy_dir_recursive(&source_path, &dest_path)?;
// Modify the Package.swift to produce a dynamic library
if let Err(e) = modify_package_for_dynamic_library(&dest_path, product_name) {
tracing::warn!("Failed to modify Package.swift for dynamic library: {}", e);
}
plugin_paths.push((plugin_name.to_string(), product_name.to_string(), dest_path));
tracing::debug!(
"Copied Swift plugin '{}' from {} to {}",
plugin_name,
source_path.display(),
plugins_dir.join(plugin_name).display()
);
}
if plugin_paths.is_empty() {
tracing::warn!("No valid Swift packages found to compile");
return Ok(None);
}
// Determine Swift target triple and SDK
let (swift_triple, sdk_name) = swift_target_and_sdk(target_triple)?;
let sdk_path = lookup_sdk_path(&sdk_name).await?;
// Build configuration
let configuration = if release { "release" } else { "debug" };
// Build each plugin package individually
for (plugin_name, product_name, package_path) in &plugin_paths {
tracing::debug!(
"Building Swift plugin '{}' (product: {})",
plugin_name,
product_name
);
let build_path = package_path.join(".build");
let mut cmd = Command::new("xcrun");
cmd.args(["swift", "build"])
.arg("--package-path")
.arg(package_path)
.arg("--configuration")
.arg(configuration)
.arg("--triple")
.arg(&swift_triple)
.arg("--sdk")
.arg(&sdk_path)
.arg("--product")
.arg(product_name)
.arg("--build-path")
.arg(&build_path);
tracing::debug!("Running: xcrun swift build for {}", product_name);
let output = cmd.output().await?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
anyhow::bail!(
"Swift build failed for plugin '{}':\n{}\n{}",
plugin_name,
stdout,
stderr
);
}
if !output.stderr.is_empty() {
let stderr = String::from_utf8_lossy(&output.stderr);
tracing::debug!("Swift build warnings for {}:\n{}", plugin_name, stderr);
}
}
// Find the output dynamic library for each plugin
// Swift puts the output in .build/<triple>/<configuration>/lib<ProductName>.dylib
// or .build/<configuration>/lib<ProductName>.dylib depending on the version
let mut all_dylibs = Vec::new();
for (_, product_name, package_path) in &plugin_paths {
let build_path = package_path.join(".build");
let lib_name = format!("lib{}.dylib", product_name);
let lib_search_paths = [
build_path.join(&swift_triple).join(configuration),
build_path.join(configuration),
build_path.clone(),
];
let mut found = false;
for search_path in &lib_search_paths {
let lib_path = search_path.join(&lib_name);
if lib_path.exists() {
tracing::debug!("Found Swift dynamic library: {}", lib_path.display());
all_dylibs.push((product_name.clone(), lib_path));
found = true;
break;
}
}
if !found {
tracing::warn!(
"Could not find compiled Swift dynamic library for product '{}' (expected {})",
product_name,
lib_name
);
}
}
if all_dylibs.is_empty() {
tracing::warn!("No Swift dynamic libraries were compiled successfully");
return Ok(None);
}
// For dynamic libraries, we need to wrap each in a framework bundle
// If there's only one library, create a single framework
// If there are multiple, we'll create frameworks for each (they're independent)
// The first one is the "primary" framework that gets returned
let (_primary_name, primary_dylib) = all_dylibs.remove(0);
// Create the framework bundle from the dylib
// Use "DioxusSwiftPlugins" as the umbrella framework name
let framework_name = "DioxusSwiftPlugins";
let bundle_identifier = "com.dioxus.swift.plugins";
let framework_path = create_framework_bundle(
&primary_dylib,
framework_name,
build_dir,
target_triple,
bundle_identifier,
)
.await?;
// If there are additional dylibs, create separate framework bundles for them
for (name, dylib_path) in all_dylibs {
let extra_framework = create_framework_bundle(
&dylib_path,
&name,
build_dir,
target_triple,
&format!("com.dioxus.swift.{}", name.to_lowercase()),
)
.await?;
tracing::debug!(
"Created additional framework: {}",
extra_framework.display()
);
}
Ok(Some(framework_path))
}
/// Modify a Package.swift to produce a dynamic library instead of static.
/// This is needed for runtime class lookup via NSClassFromString.
fn modify_package_for_dynamic_library(package_path: &Path, product_name: &str) -> Result<()> {
let package_swift_path = package_path.join("Package.swift");
if !package_swift_path.exists() {
anyhow::bail!(
"Package.swift not found at {}",
package_swift_path.display()
);
}
let content = std::fs::read_to_string(&package_swift_path)?;
// Replace .static with .dynamic for the library type
let modified = content
.replace("type: .static", "type: .dynamic")
.replace("type:.static", "type: .dynamic");
// If no library type was specified, we need to add it
// Look for .library(name: "ProductName", targets: [...]) and change to
// .library(name: "ProductName", type: .dynamic, targets: [...])
let pattern = format!(
r#".library\s*\(\s*name\s*:\s*"{}"\s*,\s*targets"#,
regex::escape(product_name)
);
let replacement = format!(
r#".library(name: "{}", type: .dynamic, targets"#,
product_name
);
let modified = if let Ok(re) = regex::Regex::new(&pattern) {
re.replace_all(&modified, replacement.as_str()).to_string()
} else {
modified
};
std::fs::write(&package_swift_path, modified)?;
Ok(())
}
/// Convert a Rust target triple to Swift target triple and SDK name
fn swift_target_and_sdk(triple: &Triple) -> Result<(String, String)> {
use target_lexicon::{Architecture, Environment, OperatingSystem};
// Check if this is a simulator target using the environment field
let is_simulator = triple.environment == Environment::Sim;
let swift_triple = match (&triple.architecture, &triple.operating_system) {
(Architecture::Aarch64(_), OperatingSystem::IOS(_)) => {
if is_simulator {
"arm64-apple-ios-simulator"
} else {
"arm64-apple-ios"
}
}
(Architecture::Aarch64(_), OperatingSystem::MacOSX { .. } | OperatingSystem::Darwin(_)) => {
"arm64-apple-macosx"
}
(Architecture::X86_64, OperatingSystem::IOS(_)) => "x86_64-apple-ios-simulator",
(Architecture::X86_64, OperatingSystem::MacOSX { .. } | OperatingSystem::Darwin(_)) => {
"x86_64-apple-macosx"
}
_ => anyhow::bail!("Unsupported target for Swift compilation: {}", triple),
};
let sdk_name = match &triple.operating_system {
OperatingSystem::IOS(_) => {
// Check if this is a simulator target using the environment field
if is_simulator {
"iphonesimulator"
} else {
"iphoneos"
}
}
OperatingSystem::MacOSX { .. } | OperatingSystem::Darwin(_) => "macosx",
_ => anyhow::bail!(
"Unsupported operating system for Swift compilation: {:?}",
triple.operating_system
),
};
Ok((swift_triple.to_string(), sdk_name.to_string()))
}
/// Look up the SDK path using xcrun
async fn lookup_sdk_path(sdk_name: &str) -> Result<String> {
let output = Command::new("xcrun")
.args(["--sdk", sdk_name, "--show-sdk-path"])
.output()
.await
.context("Failed to run xcrun to find SDK path")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
anyhow::bail!("Failed to find SDK '{}': {}", sdk_name, stderr);
}
let sdk_path = String::from_utf8(output.stdout)
.context("Invalid UTF-8 in SDK path")?
.trim()
.to_string();
if sdk_path.is_empty() {
anyhow::bail!("SDK path for '{}' is empty", sdk_name);
}
Ok(sdk_path)
}
/// Recursively copy a directory
fn copy_dir_recursive(src: &Path, dst: &Path) -> Result<()> {
std::fs::create_dir_all(dst)?;
for entry in std::fs::read_dir(src)? {
let entry = entry?;
let ty = entry.file_type()?;
let src_path = entry.path();
let dst_path = dst.join(entry.file_name());
if ty.is_dir() {
// Skip .build directories
if entry.file_name() == ".build" {
continue;
}
copy_dir_recursive(&src_path, &dst_path)?;
} else {
std::fs::copy(&src_path, &dst_path)?;
}
}
Ok(())
}
/// Extract Swift metadata from object files in link arguments
pub fn extract_swift_metadata_from_link_args(
link_args: &[String],
workspace_dir: &Path,
) -> Vec<SwiftPackageMetadata> {
let mut swift_packages = Vec::new();
// Look through rlibs and object files for Swift metadata
for arg in link_args {
let path = PathBuf::from(arg);
// Only process files in our workspace
if !path.starts_with(workspace_dir) {
continue;
}
// Check for .rlib files
if arg.ends_with(".rlib") {
if let Ok(swift_meta) = extract_swift_from_rlib(&path) {
swift_packages.extend(swift_meta);
}
}
// Check for .o files
else if arg.ends_with(".o") || arg.ends_with(".obj") {
if let Ok(swift_meta) = extract_swift_from_object(&path) {
swift_packages.extend(swift_meta);
}
}
}
// Deduplicate by plugin name
swift_packages.sort_by(|a, b| a.plugin_name.as_str().cmp(b.plugin_name.as_str()));
swift_packages.dedup_by(|a, b| a.plugin_name.as_str() == b.plugin_name.as_str());
swift_packages
}
/// Extract Swift metadata from an rlib file
fn extract_swift_from_rlib(rlib_path: &Path) -> Result<Vec<SwiftPackageMetadata>> {
let mut results = Vec::new();
let rlib_contents = std::fs::read(rlib_path)?;
let mut reader = ar::Archive::new(std::io::Cursor::new(rlib_contents));
while let Some(Ok(entry)) = reader.next_entry() {
let name = std::str::from_utf8(entry.header().identifier()).unwrap_or_default();
// Only process .o files
if !name.ends_with(".rcgu.o") && !name.ends_with(".obj") {
continue;
}
// Read the object file contents
let mut obj_contents = Vec::new();
std::io::Read::read_to_end(&mut std::io::BufReader::new(entry), &mut obj_contents)?;
if let Ok(swift_meta) = extract_swift_from_bytes(&obj_contents) {
results.extend(swift_meta);
}
}
Ok(results)
}
/// Extract Swift metadata from an object file
fn extract_swift_from_object(obj_path: &Path) -> Result<Vec<SwiftPackageMetadata>> {
let obj_contents = std::fs::read(obj_path)?;
extract_swift_from_bytes(&obj_contents)
}
/// Extract Swift metadata from raw object file bytes
fn extract_swift_from_bytes(bytes: &[u8]) -> Result<Vec<SwiftPackageMetadata>> {
use manganis_core::SymbolData;
use object::{Object, ObjectSection, ObjectSymbol};
let mut results = Vec::new();
let file = match object::File::parse(bytes) {
Ok(f) => f,
Err(_) => return Ok(results),
};
// Look for __ASSETS__ symbols
for symbol in file.symbols() {
let name = match symbol.name() {
Ok(n) => n,
Err(_) => continue,
};
if !name.starts_with("__ASSETS__") {
continue;
}
// Try to get the symbol's data
if let Some(section_idx) = symbol.section().index() {
if let Ok(section) = file.section_by_index(section_idx) {
if let Ok(data) = section.data() {
// Try to find the symbol data in the section
let addr = symbol.address();
let section_addr = section.address();
let offset = (addr - section_addr) as usize;
if offset < data.len() {
let symbol_data = &data[offset..];
// Try to deserialize as SymbolData
if let Some((_, SymbolData::SwiftPackage(meta))) =
const_serialize::deserialize_const!(SymbolData, symbol_data)
{
results.push(meta);
}
}
}
}
}
}
Ok(results)
}
/// Recursively collect all Swift source files from a directory
fn collect_swift_files(dir: &Path) -> Result<Vec<PathBuf>> {
let mut swift_files = Vec::new();
if !dir.exists() {
return Ok(swift_files);
}
for entry in std::fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
// Recursively collect from subdirectories
swift_files.extend(collect_swift_files(&path)?);
} else if path.extension().is_some_and(|ext| ext == "swift") {
swift_files.push(path);
}
}
Ok(swift_files)
}
/// Information about an Apple Widget Extension to compile
pub struct AppleWidgetSource {
/// Path to the Swift package source directory
pub source_path: PathBuf,
/// Display name for the widget (shown in system UI)
pub display_name: String,
/// Bundle ID suffix (appended to app bundle ID)
pub bundle_id_suffix: String,
/// Minimum deployment target (e.g., "16.0")
pub deployment_target: String,
/// Swift module name for the widget.
/// This MUST match the module name used by the main app's Swift plugin
/// for ActivityKit type matching to work (e.g., both must define
/// `ModuleName.LocationPermissionAttributes` as the same type).
pub module_name: String,
}
/// Compile an Apple Widget Extension from a Swift package source.
///
/// Widget Extensions are compiled as executables (not libraries) and bundled
/// as .appex bundles which are installed in the app's PlugIns folder.
///
/// **Important**: Widget extensions are XPC services that require special initialization.
/// We use `-e _NSExtensionMain` as the entry point instead of the default `_main` that
/// Swift generates with `@main`. The `_NSExtensionMain` entry point (provided by Foundation):
/// 1. Sets up the XPC listener
/// 2. Initializes ExtensionFoundation's `_EXRunningExtension` singleton
/// 3. Registers with PlugInKit
/// 4. Then calls your Widget code
///
/// # Arguments
/// * `widget` - Widget extension source configuration
/// * `target_triple` - The target platform (e.g., aarch64-apple-ios)
/// * `build_dir` - Directory for intermediate build files
/// * `app_bundle_id` - The main app's bundle identifier (widget ID is derived from this)
/// * `release` - Whether to build in release mode
///
/// # Returns
/// Path to the compiled .appex bundle, ready to be installed to PlugIns/
pub async fn compile_apple_widget(
widget: &AppleWidgetSource,
target_triple: &Triple,
build_dir: &Path,
app_bundle_id: &str,
release: bool,
) -> Result<PathBuf> {
use target_lexicon::OperatingSystem;
// Validate we're on an Apple platform
let is_ios = matches!(target_triple.operating_system, OperatingSystem::IOS(_));
let is_macos = matches!(
target_triple.operating_system,
OperatingSystem::MacOSX { .. } | OperatingSystem::Darwin(_)
);
if !is_ios && !is_macos {
anyhow::bail!(
"Apple Widget Extensions are only supported on iOS and macOS, not {:?}",
target_triple.operating_system
);
}
// Validate source path exists
if !widget.source_path.exists() {
anyhow::bail!(
"Widget Extension source path does not exist: {}",
widget.source_path.display()
);
}
tracing::debug!(
"Compiling Apple Widget Extension '{}' for {}",
widget.display_name,
target_triple
);
// Create the widget build directory
let widget_build_dir = build_dir.join("widget-extensions");
std::fs::create_dir_all(&widget_build_dir)?;
// Copy the Swift package to build directory
// Use the bundle_id_suffix as a unique name since the folder name might just be "widget"
let widget_name = widget.bundle_id_suffix.replace("-", "_");
let source_dir = widget_build_dir.join(format!("{}_src", widget_name));
if source_dir.exists() {
std::fs::remove_dir_all(&source_dir)?;
}
copy_dir_recursive(&widget.source_path, &source_dir)?;
// Get Swift target triple and SDK
let (swift_triple, sdk_name) = swift_target_and_sdk(target_triple)?;
// Collect all Swift source files from the Sources directory
let swift_sources_dir = source_dir.join("Sources");
let swift_files = collect_swift_files(&swift_sources_dir)?;
if swift_files.is_empty() {
anyhow::bail!(
"No Swift source files found in widget extension Sources directory: {}",
swift_sources_dir.display()
);
}
tracing::debug!(
"Found {} Swift files for widget: {:?}",
swift_files.len(),
swift_files
);
// Build output path
let exec_path = widget_build_dir.join(&widget_name);
// Compile the widget extension using swiftc directly
// Widget extensions are XPC services that require _NSExtensionMain as the entry point
let mut cmd = Command::new("xcrun");
cmd.arg("--sdk").arg(&sdk_name).arg("swiftc");
// Add all Swift source files
for swift_file in &swift_files {
cmd.arg(swift_file);
}
// Output executable
cmd.arg("-o").arg(&exec_path);
// Target triple with proper iOS version
// Format: arm64-apple-ios17.0 or arm64-apple-ios17.0-simulator
let is_simulator = swift_triple.contains("simulator");
let base_triple = swift_triple.replace("-simulator", "");
let swift_target = if is_simulator {
format!("{}{}-simulator", base_triple, widget.deployment_target)
} else {
format!("{}{}", base_triple, widget.deployment_target)
};
cmd.arg("-target").arg(&swift_target);
// Module name - use a consistent name that matches the main app's plugin module
// This is critical for ActivityKit type matching between app and widget
cmd.arg("-module-name").arg(&widget.module_name);
// Optimization flags
if release {
cmd.arg("-O").arg("-whole-module-optimization");
}
// Extension-specific flags
cmd.arg("-application-extension");
// Critical: Use _NSExtensionMain as the entry point for widget extensions
// Without this, the widget crashes because ExtensionFoundation's singleton isn't initialized
cmd.arg("-Xlinker")
.arg("-e")
.arg("-Xlinker")
.arg("_NSExtensionMain");
// Link Objective-C runtime (required for Swift/ObjC interop)
cmd.arg("-lobjc");
// Link required frameworks
cmd.arg("-framework").arg("Foundation");
cmd.arg("-framework").arg("SwiftUI");
cmd.arg("-framework").arg("WidgetKit");
cmd.arg("-framework").arg("ActivityKit");
tracing::debug!("Running swiftc for widget: {:?}", cmd);
let output = cmd.output().await?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
anyhow::bail!(
"Swift compilation failed for widget extension '{}':\n{}\n{}",
widget_name,
stdout,
stderr
);
}
tracing::debug!("Compiled widget executable: {}", exec_path.display());
// Create the .appex bundle
let appex_name = format!("{}.appex", widget_name);
let appex_dir = widget_build_dir.join(&appex_name);
// Remove existing appex if present
if appex_dir.exists() {
std::fs::remove_dir_all(&appex_dir)?;
}
std::fs::create_dir_all(&appex_dir)?;
// Copy the executable into the appex bundle
let bundle_exec = appex_dir.join(&widget_name);
std::fs::copy(&exec_path, &bundle_exec)?;
// Create Info.plist for the widget extension
let widget_bundle_id = format!("{}.{}", app_bundle_id, widget.bundle_id_suffix);
let min_os_version = &widget.deployment_target;
let platform_info = if is_ios {
format!(
r#" <key>MinimumOSVersion</key>
<string>{min_os_version}</string>
<key>CFBundleSupportedPlatforms</key>
<array>
<string>iPhoneOS</string>
</array>
<key>UIDeviceFamily</key>
<array>
<integer>1</integer>
<integer>2</integer>
</array>"#
)
} else {
format!(
r#" <key>LSMinimumSystemVersion</key>
<string>{min_os_version}</string>
<key>CFBundleSupportedPlatforms</key>
<array>
<string>MacOSX</string>
</array>"#
)
};
let info_plist = format!(
r#"<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleDisplayName</key>
<string>{display_name}</string>
<key>CFBundleExecutable</key>
<string>{widget_name}</string>
<key>CFBundleIdentifier</key>
<string>{widget_bundle_id}</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>{widget_name}</string>
<key>CFBundlePackageType</key>
<string>XPC!</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
{platform_info}
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.widgetkit-extension</string>
</dict>
<key>NSSupportsLiveActivities</key>
<true/>
</dict>
</plist>"#,
display_name = widget.display_name,
widget_name = widget_name,
widget_bundle_id = widget_bundle_id,
platform_info = platform_info,
);
std::fs::write(appex_dir.join("Info.plist"), info_plist)?;
tracing::debug!("Created Widget Extension bundle: {}", appex_dir.display());
Ok(appex_dir)
}
+717
View File
@@ -0,0 +1,717 @@
//! Maps unified Dioxus.toml config to platform-specific manifest data.
//!
//! This module converts cross-platform declarations (permissions, deep links,
//! background modes) into platform-specific identifiers:
//! - Android: `<uses-permission>` entries, intent filters, foreground service types
//! - iOS/macOS: Info.plist keys, URL schemes, UIBackgroundModes
use crate::config::{
AndroidConfig, BackgroundConfig, DeepLinkConfig, IosConfig, LocationPrecision, MacosConfig,
PermissionsConfig, StorageAccess,
};
/// Android permission entry for AndroidManifest.xml
#[derive(Debug, Clone)]
pub struct AndroidPermissionEntry {
/// Full Android permission string (e.g., "android.permission.CAMERA")
pub permission: String,
/// User-facing description (used for documentation)
pub description: String,
}
/// iOS/macOS plist entry for Info.plist
#[derive(Debug, Clone)]
pub struct PlistEntry {
/// Plist key (e.g., "NSCameraUsageDescription")
pub key: String,
/// User-facing description shown in permission dialogs
pub value: String,
}
/// Maps unified permissions, deep links, and background modes to platform-specific identifiers
#[derive(Debug, Default)]
pub struct ManifestMapper {
pub android_permissions: Vec<AndroidPermissionEntry>,
pub android_features: Vec<String>,
pub ios_plist_entries: Vec<PlistEntry>,
pub macos_plist_entries: Vec<PlistEntry>,
/// URL schemes for iOS CFBundleURLTypes (merged from deep_links.schemes + ios.url_schemes)
pub ios_url_schemes: Vec<String>,
/// URL schemes for macOS CFBundleURLTypes (merged from deep_links.schemes + macos.url_schemes)
pub macos_url_schemes: Vec<String>,
/// URL schemes for Android intent-filter (merged from deep_links.schemes + android.url_schemes)
pub android_url_schemes: Vec<String>,
/// Associated domains for iOS (from deep_links.hosts → "applinks:host")
pub ios_associated_domains: Vec<String>,
/// Android intent filters from config (android.intent_filters)
pub android_intent_filters: Vec<crate::config::AndroidIntentFilter>,
/// App link hosts for Android auto-verify (from deep_links.hosts)
pub android_app_link_hosts: Vec<String>,
/// iOS UIBackgroundModes (merged from BackgroundConfig + ios.background_modes)
pub ios_background_modes: Vec<String>,
/// Android foreground service types (from BackgroundConfig + android.foreground_service_types)
pub android_foreground_service_types: Vec<String>,
}
impl ManifestMapper {
/// Create a new permission mapper from the unified config
pub fn from_config(
permissions: &PermissionsConfig,
deep_links: &DeepLinkConfig,
background: &BackgroundConfig,
android: &AndroidConfig,
ios: &IosConfig,
macos: &MacosConfig,
) -> Self {
let mut mapper = Self::default();
// Map unified permissions
mapper.map_location(permissions);
mapper.map_camera(permissions);
mapper.map_microphone(permissions);
mapper.map_notifications(permissions);
mapper.map_photos(permissions);
mapper.map_bluetooth(permissions);
mapper.map_background_location(permissions);
mapper.map_contacts(permissions);
mapper.map_calendar(permissions);
mapper.map_biometrics(permissions);
mapper.map_nfc(permissions);
mapper.map_motion(permissions);
mapper.map_health(permissions);
mapper.map_speech(permissions);
mapper.map_media_library(permissions);
mapper.map_siri(permissions);
mapper.map_homekit(permissions);
mapper.map_local_network(permissions);
mapper.map_nearby_wifi(permissions);
// Add raw Android permissions
for (perm, config) in &android.permissions {
mapper.android_permissions.push(AndroidPermissionEntry {
permission: perm.clone(),
description: config.description.clone(),
});
}
// Add Android features
mapper.android_features.extend(android.features.clone());
// Map deep links
mapper.map_deep_links(deep_links, android, ios, macos);
// Map background modes
mapper.map_background_modes(background, android, ios);
// Log mapped permissions for debugging
for perm in &mapper.android_permissions {
tracing::debug!(
"Android permission: {} - {}",
perm.permission,
perm.description
);
}
for entry in &mapper.ios_plist_entries {
tracing::debug!("iOS plist: {} = {}", entry.key, entry.value);
}
mapper
}
fn map_location(&mut self, permissions: &PermissionsConfig) {
if let Some(loc) = &permissions.location {
let android_perm = match loc.precision {
LocationPrecision::Fine => "android.permission.ACCESS_FINE_LOCATION",
LocationPrecision::Coarse => "android.permission.ACCESS_COARSE_LOCATION",
};
self.android_permissions.push(AndroidPermissionEntry {
permission: android_perm.to_string(),
description: loc.description.clone(),
});
// For fine location, also add coarse as it's often needed
if loc.precision == LocationPrecision::Fine {
self.android_permissions.push(AndroidPermissionEntry {
permission: "android.permission.ACCESS_COARSE_LOCATION".to_string(),
description: loc.description.clone(),
});
}
self.ios_plist_entries.push(PlistEntry {
key: "NSLocationWhenInUseUsageDescription".to_string(),
value: loc.description.clone(),
});
self.macos_plist_entries.push(PlistEntry {
key: "NSLocationUsageDescription".to_string(),
value: loc.description.clone(),
});
}
}
fn map_camera(&mut self, permissions: &PermissionsConfig) {
if let Some(cam) = &permissions.camera {
self.android_permissions.push(AndroidPermissionEntry {
permission: "android.permission.CAMERA".to_string(),
description: cam.description.clone(),
});
self.ios_plist_entries.push(PlistEntry {
key: "NSCameraUsageDescription".to_string(),
value: cam.description.clone(),
});
self.macos_plist_entries.push(PlistEntry {
key: "NSCameraUsageDescription".to_string(),
value: cam.description.clone(),
});
}
}
fn map_microphone(&mut self, permissions: &PermissionsConfig) {
if let Some(mic) = &permissions.microphone {
self.android_permissions.push(AndroidPermissionEntry {
permission: "android.permission.RECORD_AUDIO".to_string(),
description: mic.description.clone(),
});
self.ios_plist_entries.push(PlistEntry {
key: "NSMicrophoneUsageDescription".to_string(),
value: mic.description.clone(),
});
self.macos_plist_entries.push(PlistEntry {
key: "NSMicrophoneUsageDescription".to_string(),
value: mic.description.clone(),
});
}
}
fn map_notifications(&mut self, permissions: &PermissionsConfig) {
if let Some(notif) = &permissions.notifications {
self.android_permissions.push(AndroidPermissionEntry {
permission: "android.permission.POST_NOTIFICATIONS".to_string(),
description: notif.description.clone(),
});
// iOS notifications are handled at runtime, no plist entry needed
}
}
fn map_photos(&mut self, permissions: &PermissionsConfig) {
if let Some(photos) = &permissions.photos {
match photos.access {
StorageAccess::Read => {
self.android_permissions.push(AndroidPermissionEntry {
permission: "android.permission.READ_MEDIA_IMAGES".to_string(),
description: photos.description.clone(),
});
self.ios_plist_entries.push(PlistEntry {
key: "NSPhotoLibraryUsageDescription".to_string(),
value: photos.description.clone(),
});
}
StorageAccess::Write => {
self.android_permissions.push(AndroidPermissionEntry {
permission: "android.permission.WRITE_EXTERNAL_STORAGE".to_string(),
description: photos.description.clone(),
});
self.ios_plist_entries.push(PlistEntry {
key: "NSPhotoLibraryAddUsageDescription".to_string(),
value: photos.description.clone(),
});
}
StorageAccess::ReadWrite => {
self.android_permissions.push(AndroidPermissionEntry {
permission: "android.permission.READ_MEDIA_IMAGES".to_string(),
description: photos.description.clone(),
});
self.android_permissions.push(AndroidPermissionEntry {
permission: "android.permission.WRITE_EXTERNAL_STORAGE".to_string(),
description: photos.description.clone(),
});
self.ios_plist_entries.push(PlistEntry {
key: "NSPhotoLibraryUsageDescription".to_string(),
value: photos.description.clone(),
});
self.ios_plist_entries.push(PlistEntry {
key: "NSPhotoLibraryAddUsageDescription".to_string(),
value: photos.description.clone(),
});
}
}
self.macos_plist_entries.push(PlistEntry {
key: "NSPhotoLibraryUsageDescription".to_string(),
value: photos.description.clone(),
});
}
}
fn map_bluetooth(&mut self, permissions: &PermissionsConfig) {
if let Some(bt) = &permissions.bluetooth {
self.android_permissions.push(AndroidPermissionEntry {
permission: "android.permission.BLUETOOTH_CONNECT".to_string(),
description: bt.description.clone(),
});
self.android_permissions.push(AndroidPermissionEntry {
permission: "android.permission.BLUETOOTH_SCAN".to_string(),
description: bt.description.clone(),
});
self.ios_plist_entries.push(PlistEntry {
key: "NSBluetoothAlwaysUsageDescription".to_string(),
value: bt.description.clone(),
});
self.macos_plist_entries.push(PlistEntry {
key: "NSBluetoothAlwaysUsageDescription".to_string(),
value: bt.description.clone(),
});
}
}
fn map_background_location(&mut self, permissions: &PermissionsConfig) {
if let Some(bg_loc) = &permissions.background_location {
self.android_permissions.push(AndroidPermissionEntry {
permission: "android.permission.ACCESS_BACKGROUND_LOCATION".to_string(),
description: bg_loc.description.clone(),
});
self.ios_plist_entries.push(PlistEntry {
key: "NSLocationAlwaysAndWhenInUseUsageDescription".to_string(),
value: bg_loc.description.clone(),
});
}
}
fn map_contacts(&mut self, permissions: &PermissionsConfig) {
if let Some(contacts) = &permissions.contacts {
match contacts.access {
StorageAccess::Read => {
self.android_permissions.push(AndroidPermissionEntry {
permission: "android.permission.READ_CONTACTS".to_string(),
description: contacts.description.clone(),
});
}
StorageAccess::Write => {
self.android_permissions.push(AndroidPermissionEntry {
permission: "android.permission.WRITE_CONTACTS".to_string(),
description: contacts.description.clone(),
});
}
StorageAccess::ReadWrite => {
self.android_permissions.push(AndroidPermissionEntry {
permission: "android.permission.READ_CONTACTS".to_string(),
description: contacts.description.clone(),
});
self.android_permissions.push(AndroidPermissionEntry {
permission: "android.permission.WRITE_CONTACTS".to_string(),
description: contacts.description.clone(),
});
}
}
self.ios_plist_entries.push(PlistEntry {
key: "NSContactsUsageDescription".to_string(),
value: contacts.description.clone(),
});
self.macos_plist_entries.push(PlistEntry {
key: "NSContactsUsageDescription".to_string(),
value: contacts.description.clone(),
});
}
}
fn map_calendar(&mut self, permissions: &PermissionsConfig) {
if let Some(cal) = &permissions.calendar {
match cal.access {
StorageAccess::Read => {
self.android_permissions.push(AndroidPermissionEntry {
permission: "android.permission.READ_CALENDAR".to_string(),
description: cal.description.clone(),
});
}
StorageAccess::Write => {
self.android_permissions.push(AndroidPermissionEntry {
permission: "android.permission.WRITE_CALENDAR".to_string(),
description: cal.description.clone(),
});
}
StorageAccess::ReadWrite => {
self.android_permissions.push(AndroidPermissionEntry {
permission: "android.permission.READ_CALENDAR".to_string(),
description: cal.description.clone(),
});
self.android_permissions.push(AndroidPermissionEntry {
permission: "android.permission.WRITE_CALENDAR".to_string(),
description: cal.description.clone(),
});
}
}
self.ios_plist_entries.push(PlistEntry {
key: "NSCalendarsUsageDescription".to_string(),
value: cal.description.clone(),
});
self.macos_plist_entries.push(PlistEntry {
key: "NSCalendarsUsageDescription".to_string(),
value: cal.description.clone(),
});
}
}
fn map_biometrics(&mut self, permissions: &PermissionsConfig) {
if let Some(bio) = &permissions.biometrics {
self.android_permissions.push(AndroidPermissionEntry {
permission: "android.permission.USE_BIOMETRIC".to_string(),
description: bio.description.clone(),
});
self.ios_plist_entries.push(PlistEntry {
key: "NSFaceIDUsageDescription".to_string(),
value: bio.description.clone(),
});
}
}
fn map_nfc(&mut self, permissions: &PermissionsConfig) {
if let Some(nfc) = &permissions.nfc {
self.android_permissions.push(AndroidPermissionEntry {
permission: "android.permission.NFC".to_string(),
description: nfc.description.clone(),
});
self.ios_plist_entries.push(PlistEntry {
key: "NFCReaderUsageDescription".to_string(),
value: nfc.description.clone(),
});
}
}
fn map_motion(&mut self, permissions: &PermissionsConfig) {
if let Some(motion) = &permissions.motion {
self.android_permissions.push(AndroidPermissionEntry {
permission: "android.permission.ACTIVITY_RECOGNITION".to_string(),
description: motion.description.clone(),
});
self.ios_plist_entries.push(PlistEntry {
key: "NSMotionUsageDescription".to_string(),
value: motion.description.clone(),
});
}
}
fn map_health(&mut self, permissions: &PermissionsConfig) {
if let Some(health) = &permissions.health {
self.android_permissions.push(AndroidPermissionEntry {
permission: "android.permission.BODY_SENSORS".to_string(),
description: health.description.clone(),
});
match health.access {
StorageAccess::Read => {
self.ios_plist_entries.push(PlistEntry {
key: "NSHealthShareUsageDescription".to_string(),
value: health.description.clone(),
});
}
StorageAccess::Write => {
self.ios_plist_entries.push(PlistEntry {
key: "NSHealthUpdateUsageDescription".to_string(),
value: health.description.clone(),
});
}
StorageAccess::ReadWrite => {
self.ios_plist_entries.push(PlistEntry {
key: "NSHealthShareUsageDescription".to_string(),
value: health.description.clone(),
});
self.ios_plist_entries.push(PlistEntry {
key: "NSHealthUpdateUsageDescription".to_string(),
value: health.description.clone(),
});
}
}
}
}
fn map_speech(&mut self, permissions: &PermissionsConfig) {
if let Some(speech) = &permissions.speech {
// Speech recognition uses microphone on Android
self.android_permissions.push(AndroidPermissionEntry {
permission: "android.permission.RECORD_AUDIO".to_string(),
description: speech.description.clone(),
});
self.ios_plist_entries.push(PlistEntry {
key: "NSSpeechRecognitionUsageDescription".to_string(),
value: speech.description.clone(),
});
}
}
fn map_media_library(&mut self, permissions: &PermissionsConfig) {
if let Some(media) = &permissions.media_library {
self.android_permissions.push(AndroidPermissionEntry {
permission: "android.permission.READ_MEDIA_AUDIO".to_string(),
description: media.description.clone(),
});
self.ios_plist_entries.push(PlistEntry {
key: "NSAppleMusicUsageDescription".to_string(),
value: media.description.clone(),
});
}
}
fn map_siri(&mut self, permissions: &PermissionsConfig) {
if let Some(siri) = &permissions.siri {
// Siri is iOS only
self.ios_plist_entries.push(PlistEntry {
key: "NSSiriUsageDescription".to_string(),
value: siri.description.clone(),
});
}
}
fn map_homekit(&mut self, permissions: &PermissionsConfig) {
if let Some(homekit) = &permissions.homekit {
// HomeKit is iOS only
self.ios_plist_entries.push(PlistEntry {
key: "NSHomeKitUsageDescription".to_string(),
value: homekit.description.clone(),
});
}
}
fn map_local_network(&mut self, permissions: &PermissionsConfig) {
if let Some(network) = &permissions.local_network {
// Local network is iOS only
self.ios_plist_entries.push(PlistEntry {
key: "NSLocalNetworkUsageDescription".to_string(),
value: network.description.clone(),
});
}
}
fn map_nearby_wifi(&mut self, permissions: &PermissionsConfig) {
if let Some(wifi) = &permissions.nearby_wifi {
// Nearby WiFi is Android only
self.android_permissions.push(AndroidPermissionEntry {
permission: "android.permission.NEARBY_WIFI_DEVICES".to_string(),
description: wifi.description.clone(),
});
}
}
/// Map deep link config to platform-specific URL schemes, associated domains, and intent filters
fn map_deep_links(
&mut self,
deep_links: &DeepLinkConfig,
android: &AndroidConfig,
ios: &IosConfig,
macos: &MacosConfig,
) {
// Merge unified schemes with platform-specific overrides
let mut ios_schemes: Vec<String> = deep_links.schemes.clone();
ios_schemes.extend(ios.url_schemes.clone());
ios_schemes.dedup();
self.ios_url_schemes = ios_schemes;
let mut macos_schemes: Vec<String> = deep_links.schemes.clone();
macos_schemes.extend(macos.url_schemes.clone());
macos_schemes.dedup();
self.macos_url_schemes = macos_schemes;
let mut android_schemes: Vec<String> = deep_links.schemes.clone();
android_schemes.extend(android.url_schemes.clone());
android_schemes.dedup();
self.android_url_schemes = android_schemes;
// Map universal link hosts to iOS associated domains
for host in &deep_links.hosts {
self.ios_associated_domains.push(format!("applinks:{host}"));
}
// Store app link hosts for Android auto-verify intent filters
self.android_app_link_hosts = deep_links.hosts.clone();
// Add explicit Android intent filters from config
self.android_intent_filters = android.intent_filters.clone();
}
/// Map background mode config to platform-specific background capabilities
fn map_background_modes(
&mut self,
background: &BackgroundConfig,
android: &AndroidConfig,
ios: &IosConfig,
) {
// Build iOS UIBackgroundModes from unified config
let mut ios_modes: Vec<String> = Vec::new();
if background.location {
ios_modes.push("location".to_string());
}
if background.audio {
ios_modes.push("audio".to_string());
}
if background.fetch {
ios_modes.push("fetch".to_string());
}
if background.remote_notifications {
ios_modes.push("remote-notification".to_string());
}
if background.voip {
ios_modes.push("voip".to_string());
}
if background.bluetooth {
ios_modes.push("bluetooth-central".to_string());
ios_modes.push("bluetooth-peripheral".to_string());
}
if background.external_accessory {
ios_modes.push("external-accessory".to_string());
}
if background.processing {
ios_modes.push("processing".to_string());
}
// Merge platform-specific overrides
for mode in &ios.background_modes {
if !ios_modes.contains(mode) {
ios_modes.push(mode.clone());
}
}
self.ios_background_modes = ios_modes;
// Build Android foreground service types and permissions
let mut android_types: Vec<String> = Vec::new();
if background.location {
self.android_permissions.push(AndroidPermissionEntry {
permission: "android.permission.ACCESS_BACKGROUND_LOCATION".to_string(),
description: "Background location updates".to_string(),
});
}
if background.audio {
android_types.push("mediaPlayback".to_string());
}
if background.voip {
android_types.push("phoneCall".to_string());
}
if background.bluetooth {
android_types.push("connectedDevice".to_string());
}
// Merge platform-specific overrides
for stype in &android.foreground_service_types {
if !android_types.contains(stype) {
android_types.push(stype.clone());
}
}
// If we have any foreground service types, add the FOREGROUND_SERVICE permission
if !android_types.is_empty() {
self.android_permissions.push(AndroidPermissionEntry {
permission: "android.permission.FOREGROUND_SERVICE".to_string(),
description: "Run foreground services".to_string(),
});
}
self.android_foreground_service_types = android_types;
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::{LocationPermission, SimplePermission};
#[test]
fn test_location_permission_mapping() {
let permissions = PermissionsConfig {
location: Some(LocationPermission {
precision: LocationPrecision::Fine,
description: "Track your runs".to_string(),
}),
..Default::default()
};
let mapper = ManifestMapper::from_config(
&permissions,
&DeepLinkConfig::default(),
&BackgroundConfig::default(),
&AndroidConfig::default(),
&IosConfig::default(),
&MacosConfig::default(),
);
// Should have both fine and coarse for Android
assert!(mapper
.android_permissions
.iter()
.any(|p| p.permission == "android.permission.ACCESS_FINE_LOCATION"));
assert!(mapper
.android_permissions
.iter()
.any(|p| p.permission == "android.permission.ACCESS_COARSE_LOCATION"));
// Should have iOS location plist entry
assert!(mapper
.ios_plist_entries
.iter()
.any(|e| e.key == "NSLocationWhenInUseUsageDescription"));
}
#[test]
fn test_camera_permission_mapping() {
let permissions = PermissionsConfig {
camera: Some(SimplePermission {
description: "Take photos".to_string(),
}),
..Default::default()
};
let mapper = ManifestMapper::from_config(
&permissions,
&DeepLinkConfig::default(),
&BackgroundConfig::default(),
&AndroidConfig::default(),
&IosConfig::default(),
&MacosConfig::default(),
);
assert!(mapper
.android_permissions
.iter()
.any(|p| p.permission == "android.permission.CAMERA"));
assert!(mapper
.ios_plist_entries
.iter()
.any(|e| e.key == "NSCameraUsageDescription"));
}
#[test]
fn test_android_camera_permission_data() {
let permissions = PermissionsConfig {
camera: Some(SimplePermission {
description: "Take photos".to_string(),
}),
..Default::default()
};
let mapper = ManifestMapper::from_config(
&permissions,
&DeepLinkConfig::default(),
&BackgroundConfig::default(),
&AndroidConfig::default(),
&IosConfig::default(),
&MacosConfig::default(),
);
assert!(mapper
.android_permissions
.iter()
.any(|p| p.permission == "android.permission.CAMERA"));
}
}
+2
View File
@@ -12,7 +12,9 @@ mod assets;
mod builder;
mod cache;
mod context;
mod ios_swift;
mod manifest;
mod manifest_mapper;
mod patch;
mod pre_render;
mod request;
File diff suppressed because it is too large. Load diff
+4 -3
View File
@@ -11,6 +11,7 @@ use dioxus_component_manifest::{
component_manifest_schema, CargoDependency, Component, ComponentDependency,
};
use git2::Repository;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use tokio::{process::Command, task::JoinSet};
use tracing::debug;
@@ -292,7 +293,7 @@ impl ComponentCommand {
let crate_package = workspace.find_main_package(None)?;
Ok(workspace
.load_dioxus_config(crate_package)?
.load_dioxus_config(crate_package, None)?
.unwrap_or_default())
}
@@ -331,7 +332,7 @@ impl ComponentCommand {
/// Arguments for the default or custom remote registry
/// If both values are None, the default registry will be used
#[derive(Clone, Debug, Parser, Default, Serialize, Deserialize)]
#[derive(Clone, Debug, Parser, Default, Serialize, Deserialize, JsonSchema)]
pub struct RemoteComponentRegistry {
/// The url of the component registry
#[arg(long)]
@@ -449,7 +450,7 @@ impl RemoteComponentRegistry {
/// Arguments for a component registry
/// Either a path to a local directory or a remote git repo (with optional rev)
#[derive(Clone, Debug, Parser, Default, Serialize, Deserialize)]
#[derive(Clone, Debug, Parser, Default, Serialize, Deserialize, JsonSchema)]
pub struct ComponentRegistry {
/// The remote repo args
#[clap(flatten)]
+19
View File
@@ -24,6 +24,14 @@ pub(crate) enum Config {
/// Set CLI settings.
#[command(subcommand)]
Set(Setting),
/// Generate JSON schema for Dioxus.toml configuration.
/// Useful for IDE autocomplete and validation.
Schema {
/// Output file path. If not provided, prints to stdout.
#[clap(long, short)]
out: Option<PathBuf>,
},
}
#[derive(Debug, Clone, Copy, Deserialize, Subcommand)]
@@ -117,6 +125,17 @@ impl Config {
})?;
tracing::info!(dx_src = ?TraceSrc::Dev, "🚩 CLI setting `{setting}` has been set.");
}
Config::Schema { out } => {
let schema = crate::config::generate_manifest_schema();
let json = serde_json::to_string_pretty(&schema)?;
match out {
Some(path) => {
std::fs::write(&path, format!("{json}\n"))?;
tracing::info!(dx_src = ?TraceSrc::Dev, "Schema written to {}", path.display());
}
None => println!("{json}"),
}
}
}
Ok(StructuredOutput::Success)
+2 -1
View File
@@ -1,7 +1,8 @@
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub(crate) struct ApplicationConfig {
/// The path where global assets will be added when components are added with `dx components add`
#[serde(default)]
+12 -10
View File
@@ -1,8 +1,9 @@
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use std::{collections::HashMap, str::FromStr};
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
pub(crate) struct BundleConfig {
#[serde(default)]
pub(crate) identifier: Option<String>,
@@ -32,7 +33,7 @@ pub(crate) struct BundleConfig {
pub(crate) android: Option<AndroidSettings>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
pub(crate) struct DebianSettings {
// OS-specific settings:
/// the list of debian dependencies.
@@ -88,7 +89,7 @@ pub(crate) struct DebianSettings {
pub post_remove_script: Option<PathBuf>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
pub(crate) struct WixSettings {
#[serde(default)]
pub(crate) language: Vec<(String, Option<PathBuf>)>,
@@ -137,10 +138,11 @@ pub(crate) struct WixSettings {
/// It is recommended that you set this value in your tauri config file to avoid accidental changes in your upgrade code
/// whenever you want to change your product name.
#[serde(default)]
#[schemars(with = "Option<String>")]
pub upgrade_code: Option<uuid::Uuid>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
pub(crate) struct MacOsSettings {
#[serde(default)]
pub(crate) bundle_version: Option<String>,
@@ -177,7 +179,7 @@ fn default_hardened_runtime() -> bool {
true
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
pub(crate) struct WindowsSettings {
#[serde(default)]
pub(crate) digest_algorithm: Option<String>,
@@ -215,7 +217,7 @@ pub(crate) struct WindowsSettings {
pub sign_command: Option<CustomSignCommandSettings>,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub(crate) struct NsisSettings {
#[serde(default)]
pub(crate) template: Option<PathBuf>,
@@ -246,7 +248,7 @@ pub(crate) struct NsisSettings {
pub minimum_webview2_version: Option<String>,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub(crate) enum NSISInstallerMode {
#[default]
CurrentUser,
@@ -254,7 +256,7 @@ pub(crate) enum NSISInstallerMode {
Both,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub(crate) enum WebviewInstallMode {
Skip,
DownloadBootstrapper { silent: bool },
@@ -270,7 +272,7 @@ impl Default for WebviewInstallMode {
}
// Because all four fields must appear at the same time, there is no need for an Option
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub(crate) struct AndroidSettings {
pub(crate) jks_file: PathBuf,
pub(crate) jks_password: String,
@@ -278,7 +280,7 @@ pub(crate) struct AndroidSettings {
pub(crate) key_password: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct CustomSignCommandSettings {
/// The command to run to sign the binary.
pub cmd: String,
+2 -1
View File
@@ -1,9 +1,10 @@
use crate::component::ComponentRegistry;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
/// Configuration for the `dioxus component` commands
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
pub(crate) struct ComponentConfig {
/// The component registry to default to when adding components
#[serde(default)]
+90 -1
View File
@@ -1,10 +1,12 @@
use crate::config::component::ComponentConfig;
use super::*;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub(crate) struct DioxusConfig {
#[serde(default)]
pub(crate) application: ApplicationConfig,
#[serde(default)]
@@ -15,6 +17,85 @@ pub(crate) struct DioxusConfig {
#[serde(default)]
pub(crate) components: ComponentConfig,
/// Unified permissions configuration.
/// Permissions declared here are automatically mapped to platform-specific
/// identifiers (AndroidManifest.xml, Info.plist, etc.)
#[serde(default)]
pub(crate) permissions: PermissionsConfig,
/// Unified deep linking configuration.
/// URL schemes and universal links declared here are mapped to platform-specific
/// configurations. Use `[ios]`, `[android]`, `[macos]` sections for overrides.
#[serde(default)]
pub(crate) deep_links: DeepLinkConfig,
/// Unified background mode configuration.
/// Background capabilities declared here are mapped to platform-specific
/// configurations. Use `[ios]`, `[android]` sections for overrides.
#[serde(default)]
pub(crate) background: BackgroundConfig,
/// iOS-specific configuration.
#[serde(default)]
pub(crate) ios: IosConfig,
/// Android-specific configuration.
#[serde(default)]
pub(crate) android: AndroidConfig,
/// macOS-specific configuration.
#[serde(default)]
pub(crate) macos: MacosConfig,
/// Windows-specific configuration.
#[serde(default)]
pub(crate) windows: WindowsConfig,
/// Linux-specific configuration.
#[serde(default)]
pub(crate) linux: LinuxConfig,
}
/// Platform identifier for bundle resolution.
/// This is separate from the CLI's Platform enum which includes Server and Unknown variants.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BundlePlatform {
Ios,
Android,
MacOS,
Windows,
Linux,
Web,
}
impl From<crate::BundleFormat> for BundlePlatform {
fn from(format: crate::BundleFormat) -> Self {
match format {
crate::BundleFormat::Ios => BundlePlatform::Ios,
crate::BundleFormat::Android => BundlePlatform::Android,
crate::BundleFormat::MacOS => BundlePlatform::MacOS,
crate::BundleFormat::Windows => BundlePlatform::Windows,
crate::BundleFormat::Linux => BundlePlatform::Linux,
crate::BundleFormat::Web | crate::BundleFormat::Server => BundlePlatform::Web,
}
}
}
impl DioxusConfig {
/// Get the resolved bundle identifier for a specific platform.
/// Platform-specific identifiers override the base bundle identifier.
pub fn resolved_identifier(&self, platform: BundlePlatform) -> Option<&str> {
let platform_override = match platform {
BundlePlatform::Ios => self.ios.identifier.as_deref(),
BundlePlatform::Android => self.android.identifier.as_deref(),
BundlePlatform::MacOS => self.macos.identifier.as_deref(),
BundlePlatform::Windows => self.windows.identifier.as_deref(),
BundlePlatform::Linux => self.linux.identifier.as_deref(),
BundlePlatform::Web => None,
};
platform_override.or(self.bundle.identifier.as_deref())
}
}
impl Default for DioxusConfig {
@@ -60,6 +141,14 @@ impl Default for DioxusConfig {
},
bundle: BundleConfig::default(),
components: ComponentConfig::default(),
permissions: PermissionsConfig::default(),
deep_links: DeepLinkConfig::default(),
background: BackgroundConfig::default(),
ios: IosConfig::default(),
android: AndroidConfig::default(),
macos: MacosConfig::default(),
windows: WindowsConfig::default(),
linux: LinuxConfig::default(),
}
}
}
+425
View File
@@ -0,0 +1,425 @@
//! Extract and merge inline Dioxus.toml configuration from Rust source files.
//!
//! This module allows examples and tests to embed configuration in their doc comments:
//!
//! ```rust
//! #![allow(unused)]
//! //! This example demonstrates iOS geolocation
//! //!
//! //! ```dioxus.toml
//! //! [bundle]
//! //! identifier = "com.example.geolocation"
//! //!
//! //! [permissions]
//! //! location = { precision = "fine", description = "Track location" }
//! //! ```
//! ```
//!
//! The embedded config is deep-merged with the workspace/app Dioxus.toml,
//! with inline values taking precedence.
use std::path::Path;
/// Extract embedded Dioxus.toml configuration from a Rust source file.
///
/// Parses the file's module-level doc comments (`//!`) looking for a fenced
/// code block marked as `dioxus.toml`. Returns the parsed TOML as a generic
/// Value for merging.
///
/// Handles inner attributes (`#![...]`) that may appear before doc comments.
///
/// # Example
///
/// ```rust,ignore
/// let source = r#"
/// #![allow(unused)]
/// //! Example showing permissions
/// //!
/// //! ```dioxus.toml
/// //! [permissions]
/// //! camera = { description = "Take photos" }
/// //! ```
///
/// fn main() {}
/// "#;
///
/// let config = extract_inline_config(source).unwrap();
/// ```
pub fn extract_inline_config(source: &str) -> Option<toml::Value> {
let doc_content = extract_module_doc_comments(source)?;
let toml_content = extract_toml_block(&doc_content)?;
toml::from_str(&toml_content).ok()
}
/// Extract inline config from a file path.
pub fn extract_inline_config_from_file(path: &Path) -> Option<toml::Value> {
let source = std::fs::read_to_string(path).ok()?;
extract_inline_config(&source)
}
/// Extract module-level doc comments (`//!`) from the beginning of a source file.
///
/// Handles:
/// - Inner attributes (`#![...]`) that may precede doc comments
/// - Empty lines between attributes and doc comments
/// - Multi-line inner attributes
fn extract_module_doc_comments(source: &str) -> Option<String> {
let mut doc_lines = Vec::new();
let mut in_doc_section = false;
let mut in_multiline_attr = false;
for line in source.lines() {
let trimmed = line.trim();
// Handle multi-line inner attributes (tracking bracket depth would be
// more robust, but this handles the common case)
if in_multiline_attr {
if trimmed.ends_with(']') || trimmed.contains(")]") {
in_multiline_attr = false;
}
continue;
}
// Skip empty lines before we've started collecting doc comments
if !in_doc_section && trimmed.is_empty() {
continue;
}
// Skip inner attributes (#![...])
if trimmed.starts_with("#![") {
// Check if it's a multi-line attribute (doesn't close on same line)
if !trimmed.ends_with(']') {
in_multiline_attr = true;
}
continue;
}
// Also handle inner attributes with the #! on one line and [ on another (rare but possible)
// and block-style attributes like #![cfg(...)]
if trimmed.starts_with("#!") {
continue;
}
// Check for module doc comment
if let Some(content) = trimmed.strip_prefix("//!") {
in_doc_section = true;
// Remove the leading space if present
let content = content.strip_prefix(' ').unwrap_or(content);
doc_lines.push(content.to_string());
} else if in_doc_section {
// We've hit a non-doc-comment line after starting, stop collecting
break;
} else if !trimmed.is_empty() {
// Hit actual code before any doc comments
break;
}
}
if doc_lines.is_empty() {
None
} else {
Some(doc_lines.join("\n"))
}
}
/// Extract the content of a ```dioxus.toml fenced code block.
fn extract_toml_block(doc_content: &str) -> Option<String> {
let mut in_toml_block = false;
let mut toml_lines = Vec::new();
for line in doc_content.lines() {
if !in_toml_block {
// Look for opening fence with dioxus.toml marker
let trimmed = line.trim();
if trimmed == "```dioxus.toml" || trimmed == "```toml dioxus.toml" {
in_toml_block = true;
continue;
}
} else {
// Check for closing fence
if line.trim() == "```" {
break;
}
toml_lines.push(line);
}
}
if toml_lines.is_empty() {
None
} else {
Some(toml_lines.join("\n"))
}
}
/// Deep merge two TOML values, with `override_val` taking precedence.
///
/// - For tables: recursively merge, with override values winning
/// - For arrays: override replaces base entirely
/// - For other values: override replaces base
pub fn merge_toml_values(base: toml::Value, override_val: toml::Value) -> toml::Value {
match (base, override_val) {
// Both are tables - deep merge
(toml::Value::Table(mut base_table), toml::Value::Table(override_table)) => {
for (key, override_value) in override_table {
match base_table.remove(&key) {
Some(base_value) => {
// Recursively merge
base_table.insert(key, merge_toml_values(base_value, override_value));
}
None => {
// Key only in override, just insert
base_table.insert(key, override_value);
}
}
}
toml::Value::Table(base_table)
}
// Override is not a table, or base is not a table - override wins
(_, override_val) => override_val,
}
}
/// Merge an inline config with a base DioxusConfig.
///
/// The inline config (from doc comments) takes precedence over the base config.
pub fn merge_with_inline_config(
base: &super::DioxusConfig,
inline: toml::Value,
) -> Result<super::DioxusConfig, toml::de::Error> {
// Convert base to toml::Value
let base_str = toml::to_string(base).expect("DioxusConfig should serialize");
let base_value: toml::Value = toml::from_str(&base_str).expect("Should parse back");
// Merge
let merged = merge_toml_values(base_value, inline);
// Convert back to DioxusConfig
merged.try_into()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_module_doc_comments() {
let source = r#"//! This is an example
//! with multiple lines
//!
//! And a blank line
fn main() {}
"#;
let docs = extract_module_doc_comments(source).unwrap();
assert!(docs.contains("This is an example"));
assert!(docs.contains("with multiple lines"));
assert!(docs.contains("And a blank line"));
}
#[test]
fn test_extract_doc_comments_with_inner_attrs() {
let source = r#"#![allow(unused)]
#![cfg(feature = "something")]
//! This is an example
//! with inner attributes before it
fn main() {}
"#;
let docs = extract_module_doc_comments(source).unwrap();
assert!(docs.contains("This is an example"));
assert!(docs.contains("with inner attributes before it"));
assert!(!docs.contains("allow"));
assert!(!docs.contains("cfg"));
}
#[test]
fn test_extract_doc_comments_with_multiline_attr() {
let source = r#"#![cfg(
all(
feature = "something",
target_os = "ios"
)
)]
//! This is an example
fn main() {}
"#;
let docs = extract_module_doc_comments(source).unwrap();
assert!(docs.contains("This is an example"));
assert!(!docs.contains("cfg"));
assert!(!docs.contains("feature"));
}
#[test]
fn test_extract_toml_block() {
let doc = r#"This is an example
```dioxus.toml
[bundle]
identifier = "com.test"
```
More text here"#;
let toml = extract_toml_block(doc).unwrap();
assert!(toml.contains("[bundle]"));
assert!(toml.contains("identifier"));
}
#[test]
fn test_extract_inline_config() {
let source = r#"//! Example app
//!
//! ```dioxus.toml
//! [bundle]
//! identifier = "com.example.test"
//!
//! [permissions]
//! camera = { description = "Take photos" }
//! ```
fn main() {}
"#;
let config = extract_inline_config(source).unwrap();
let table = config.as_table().unwrap();
assert!(table.contains_key("bundle"));
assert!(table.contains_key("permissions"));
let bundle = table["bundle"].as_table().unwrap();
assert_eq!(bundle["identifier"].as_str().unwrap(), "com.example.test");
}
#[test]
fn test_extract_inline_config_with_attrs() {
let source = r#"#![allow(unused)]
#![cfg(target_os = "ios")]
//! iOS Example
//!
//! ```dioxus.toml
//! [bundle]
//! identifier = "com.example.ios"
//! ```
fn main() {}
"#;
let config = extract_inline_config(source).unwrap();
let table = config.as_table().unwrap();
let bundle = table["bundle"].as_table().unwrap();
assert_eq!(bundle["identifier"].as_str().unwrap(), "com.example.ios");
}
#[test]
fn test_no_inline_config() {
let source = r#"//! Just a regular example
//! No config here
fn main() {}
"#;
assert!(extract_inline_config(source).is_none());
}
#[test]
fn test_merge_toml_values_simple() {
let base: toml::Value = toml::from_str(
r#"
[bundle]
identifier = "com.base"
publisher = "Base Publisher"
"#,
)
.unwrap();
let override_val: toml::Value = toml::from_str(
r#"
[bundle]
identifier = "com.override"
"#,
)
.unwrap();
let merged = merge_toml_values(base, override_val);
let table = merged.as_table().unwrap();
let bundle = table["bundle"].as_table().unwrap();
// Override wins for identifier
assert_eq!(bundle["identifier"].as_str().unwrap(), "com.override");
// Base value preserved for publisher
assert_eq!(bundle["publisher"].as_str().unwrap(), "Base Publisher");
}
#[test]
fn test_merge_toml_values_deep() {
let base: toml::Value = toml::from_str(
r#"
[permissions]
camera = { description = "Base camera" }
location = { precision = "coarse", description = "Base location" }
"#,
)
.unwrap();
let override_val: toml::Value = toml::from_str(
r#"
[permissions]
location = { precision = "fine" }
microphone = { description = "Record audio" }
"#,
)
.unwrap();
let merged = merge_toml_values(base, override_val);
let perms = merged["permissions"].as_table().unwrap();
// camera untouched
assert_eq!(
perms["camera"]["description"].as_str().unwrap(),
"Base camera"
);
// location: precision overridden, description preserved
assert_eq!(perms["location"]["precision"].as_str().unwrap(), "fine");
assert_eq!(
perms["location"]["description"].as_str().unwrap(),
"Base location"
);
// microphone added
assert_eq!(
perms["microphone"]["description"].as_str().unwrap(),
"Record audio"
);
}
#[test]
fn test_merge_adds_new_sections() {
let base: toml::Value = toml::from_str(
r#"
[bundle]
identifier = "com.base"
"#,
)
.unwrap();
let override_val: toml::Value = toml::from_str(
r#"
[permissions]
camera = { description = "Photos" }
"#,
)
.unwrap();
let merged = merge_toml_values(base, override_val);
let table = merged.as_table().unwrap();
// Both sections present
assert!(table.contains_key("bundle"));
assert!(table.contains_key("permissions"));
}
}
File diff suppressed because it is too large. Load diff
+4
View File
@@ -2,11 +2,15 @@ mod app;
mod bundle;
mod component;
mod dioxus_config;
mod inline_config;
mod manifest;
mod serve;
mod web;
pub(crate) use app::*;
pub(crate) use bundle::*;
pub(crate) use dioxus_config::*;
pub(crate) use inline_config::*;
pub(crate) use manifest::*;
pub(crate) use serve::*;
pub(crate) use web::*;
+10 -9
View File
@@ -1,7 +1,8 @@
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub(crate) struct WebConfig {
#[serde(default)]
pub(crate) app: WebAppConfig,
@@ -42,7 +43,7 @@ impl Default for WebConfig {
}
/// The wasm-opt configuration
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
pub(crate) struct WasmOptConfig {
/// The wasm-opt level to use for release builds [default: s]
/// Options:
@@ -83,7 +84,7 @@ pub(crate) struct WasmOptConfig {
}
/// The wasm-opt level to use for release web builds [default: Z]
#[derive(Default, Debug, Copy, Clone, Serialize, Deserialize)]
#[derive(Default, Debug, Copy, Clone, Serialize, Deserialize, JsonSchema)]
pub(crate) enum WasmOptLevel {
/// Optimize aggressively for size
#[serde(rename = "z")]
@@ -109,7 +110,7 @@ pub(crate) enum WasmOptLevel {
Four,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub(crate) struct WebAppConfig {
#[serde(default = "default_title")]
pub(crate) title: String,
@@ -125,12 +126,12 @@ impl Default for WebAppConfig {
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub(crate) struct WebProxyConfig {
pub(crate) backend: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub(crate) struct WebWatcherConfig {
#[serde(default = "watch_path_default")]
pub(crate) watch_path: Vec<PathBuf>,
@@ -156,14 +157,14 @@ fn watch_path_default() -> Vec<PathBuf> {
vec![PathBuf::from("src"), PathBuf::from("examples")]
}
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub(crate) struct WebResourceConfig {
pub(crate) dev: WebDevResourceConfig,
pub(crate) style: Option<Vec<PathBuf>>,
pub(crate) script: Option<Vec<PathBuf>>,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub(crate) struct WebDevResourceConfig {
#[serde(default)]
pub(crate) style: Vec<PathBuf>,
@@ -171,7 +172,7 @@ pub(crate) struct WebDevResourceConfig {
pub(crate) script: Vec<PathBuf>,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema)]
pub(crate) struct WebHttpsConfig {
pub(crate) enabled: Option<bool>,
pub(crate) mkcert: Option<bool>,
+1
View File
@@ -878,6 +878,7 @@ impl TraceController {
),
Config::FormatPrint {} => ("config format-print".to_string(), json!({})),
Config::CustomHtml {} => ("config custom-html".to_string(), json!({})),
Config::Schema { out } => ("config schema".to_string(), json!({ "out": out })),
Config::Set(setting) => (
format!("config set {}", setting),
match setting {
+9
View File
@@ -582,6 +582,15 @@ impl Output {
BuildStage::CompressingAssets => lines.push("Compressing assets".yellow()),
BuildStage::RunningBindgen => lines.push("Running wasm-bindgen".yellow()),
BuildStage::RunningGradle => lines.push("Running gradle assemble".yellow()),
BuildStage::CompilingNativePlugins { detail } => {
// detail is "Swift build: name" — split into label (yellow) and name (white)
if let Some((label, name)) = detail.split_once(": ") {
lines.push(format!("{label}: ").yellow());
lines.push(name.white());
} else {
lines.push(detail.clone().yellow());
}
}
BuildStage::CodeSigning => lines.push("Code signing app".yellow()),
BuildStage::Bundling => lines.push("Bundling app".yellow()),
BuildStage::CopyingAssets {
+44 -8
View File
@@ -390,7 +390,23 @@ impl Workspace {
Ok(package)
}
pub fn load_dioxus_config(&self, package: NodeId) -> Result<Option<DioxusConfig>> {
/// Load the Dioxus.toml configuration for a package.
///
/// Optionally accepts a source file path to extract inline configuration from doc comments.
/// Inline config is merged with the base Dioxus.toml, with inline values taking precedence.
///
/// This allows examples and binaries to embed config in their doc comments:
/// ```rust,ignore
/// //! ```dioxus.toml
/// //! [bundle]
/// //! identifier = "com.example.app"
/// //! ```
/// ```
pub fn load_dioxus_config(
&self,
package: NodeId,
source_file: Option<&Path>,
) -> Result<Option<DioxusConfig>> {
// Walk up from the cargo.toml to the root of the workspace looking for Dioxus.toml
let mut current_dir = self.krates[package]
.manifest_path
@@ -426,15 +442,35 @@ impl Workspace {
.to_path_buf();
}
let Some(dioxus_conf_file) = dioxus_conf_file else {
return Ok(None);
// Load base config from Dioxus.toml (if it exists)
let base_config: Option<DioxusConfig> = match &dioxus_conf_file {
Some(path) => {
let content = std::fs::read_to_string(path)?;
Some(toml::from_str(&content).map_err(|err| {
anyhow::anyhow!("Failed to parse Dioxus.toml at {path:?}: {err}")
})?)
}
None => None,
};
toml::from_str::<DioxusConfig>(&std::fs::read_to_string(&dioxus_conf_file)?)
.map_err(|err| {
anyhow::anyhow!("Failed to parse Dioxus.toml at {dioxus_conf_file:?}: {err}")
})
.map(Some)
// Extract inline config from source file (if provided)
let inline_config = source_file.and_then(crate::config::extract_inline_config_from_file);
// Merge configs: inline overrides base
match (base_config, inline_config) {
(Some(base), Some(inline)) => crate::config::merge_with_inline_config(&base, inline)
.map(Some)
.map_err(|err| anyhow::anyhow!("Failed to merge inline config: {err}")),
(Some(base), None) => Ok(Some(base)),
(None, Some(inline)) => {
// No Dioxus.toml, but we have inline config - use defaults + inline
let base = DioxusConfig::default();
crate::config::merge_with_inline_config(&base, inline)
.map(Some)
.map_err(|err| anyhow::anyhow!("Failed to merge inline config: {err}"))
}
(None, None) => Ok(None),
}
}
/// Create a new gitignore map for this target crate
+3
View File
@@ -108,6 +108,9 @@ pub enum BuildStage {
},
Bundling,
RunningGradle,
CompilingNativePlugins {
detail: String,
},
CodeSigning,
Success,
Failed,
+7 -5
View File
@@ -22,8 +22,10 @@ use std::{fmt::Debug, hash::Hash, path::PathBuf};
pub struct BundledAsset {
/// The absolute path of the asset
absolute_source_path: ConstStr,
/// The bundled path of the asset
bundled_path: ConstStr,
/// The options for the asset
options: AssetOptions,
}
@@ -147,32 +149,32 @@ impl Asset {
pub fn bundled(&self) -> BundledAsset {
// Read the slice using volatile reads to prevent the compiler from optimizing
// away the read at compile time
fn read_slice_volatile(bundled: &'static [u8]) -> const_serialize_07::ConstVec<u8> {
fn read_slice_volatile(bundled: &'static [u8]) -> Vec<u8> {
let ptr = bundled as *const [u8] as *const u8;
let len = bundled.len();
if ptr.is_null() {
panic!("Tried to use an asset that was not bundled. Make sure you are compiling dx as the linker");
}
let mut bytes = const_serialize_07::ConstVec::new();
let mut bytes = Vec::with_capacity(len);
for byte in 0..len {
// SAFETY: We checked that the pointer was not null above. The pointer is valid for reads and
// since we are reading a u8 there are no alignment requirements
let byte = unsafe { std::ptr::read_volatile(ptr.add(byte)) };
bytes = bytes.push(byte);
bytes.push(byte);
}
bytes
}
let bundled = (self.bundled)();
let bytes = read_slice_volatile(bundled);
let read = bytes.as_ref();
let read = bytes.as_slice();
let asset = deserialize_const!(BundledAsset, read).expect("Failed to deserialize asset. Make sure you built with the matching version of the Dioxus CLI").1;
// If the asset wasn't bundled with the newer format, try the legacy format
if asset.bundled_path() == BundledAsset::PLACEHOLDER_HASH {
let bundled = (self.legacy)();
let bytes = read_slice_volatile(bundled);
let read = bytes.read();
let read = const_serialize_07::ConstReadBuffer::new(bytes.as_ref());
let asset = const_serialize_07::deserialize_const!(BundledAsset, read).expect("Failed to deserialize asset. Make sure you built with the matching version of the Dioxus CLI").1;
asset
} else {
+123
View File
@@ -0,0 +1,123 @@
use crate::BundledAsset;
use const_serialize::{ConstStr, SerializeConst};
use const_serialize_08 as const_serialize;
use std::hash::Hash;
/// Unified symbol data that can represent both assets and permissions
///
/// This enum is used to serialize different types of metadata into the binary
/// using the same `__ASSETS__` symbol prefix. The CBOR format allows for
/// self-describing data, making it easy to add new variants in the future.
///
/// Variant order does NOT matter for CBOR enum serialization - variants are
/// matched by name (string), not by position or tag value.
#[derive(Debug, Clone, Copy, PartialEq, Eq, SerializeConst)]
#[repr(C, u8)]
#[allow(clippy::large_enum_variant)]
#[non_exhaustive]
pub enum SymbolData {
/// An asset that should be bundled with the application
Asset(BundledAsset),
/// Android plugin metadata (prebuilt artifacts + Gradle deps)
AndroidArtifact(AndroidArtifactMetadata),
/// Swift package metadata (SPM location + product)
SwiftPackage(SwiftPackageMetadata),
}
/// Platform categories for permission mapping
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, SerializeConst)]
pub enum Platform {
Android,
Ios,
Macos,
}
/// Bit flags for supported platforms
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, SerializeConst)]
pub struct PlatformFlags(u8);
impl PlatformFlags {
pub const fn new() -> Self {
Self(0)
}
}
impl Default for PlatformFlags {
fn default() -> Self {
Self::new()
}
}
impl PlatformFlags {
pub const fn with_platform(mut self, platform: Platform) -> Self {
self.0 |= 1 << platform as u8;
self
}
pub const fn supports(&self, platform: Platform) -> bool {
(self.0 & (1 << platform as u8)) != 0
}
pub const fn all() -> Self {
Self(0b000111) // Android + iOS + macOS
}
pub const fn mobile() -> Self {
Self(0b000011) // Android + iOS
}
}
/// Platform-specific permission identifiers
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct PlatformIdentifiers {
pub android: Option<ConstStr>,
pub ios: Option<ConstStr>,
pub macos: Option<ConstStr>,
}
/// Metadata describing an Android plugin artifact (.aar) that must be copied into the host Gradle project.
#[derive(Debug, Clone, Copy, PartialEq, Eq, SerializeConst)]
pub struct AndroidArtifactMetadata {
pub plugin_name: ConstStr,
pub artifact_path: ConstStr,
pub gradle_dependencies: ConstStr,
}
impl AndroidArtifactMetadata {
pub const fn new(
plugin_name: &'static str,
artifact_path: &'static str,
gradle_dependencies: &'static str,
) -> Self {
Self {
plugin_name: ConstStr::new(plugin_name),
artifact_path: ConstStr::new(artifact_path),
gradle_dependencies: ConstStr::new(gradle_dependencies),
}
}
}
/// Metadata for a Swift package that needs to be linked into the app (iOS/macOS).
#[derive(Debug, Clone, Copy, PartialEq, Eq, SerializeConst)]
pub struct SwiftPackageMetadata {
pub plugin_name: ConstStr,
pub package_path: ConstStr,
pub product: ConstStr,
}
impl SwiftPackageMetadata {
pub const fn new(
plugin_name: &'static str,
package_path: &'static str,
product: &'static str,
) -> Self {
Self {
plugin_name: ConstStr::new(plugin_name),
package_path: ConstStr::new(package_path),
product: ConstStr::new(product),
}
}
}
@@ -21,3 +21,6 @@ pub use css_module::*;
mod css_module_parser;
pub use css_module_parser::*;
mod ffi;
pub use ffi::*;
@@ -1,4 +1,4 @@
use crate::{resolve_path, AssetParseError};
use crate::{linker::generate_link_section, resolve_path, AssetParseError};
use macro_string::MacroString;
use proc_macro2::TokenStream as TokenStream2;
use quote::{quote, ToTokens};
@@ -88,7 +88,7 @@ impl AssetParser {
// Generate the link section for the asset. The link section includes the source path and the
// output path of the asset. We force the asset to be included in the binary even if it is unused
// if the asset is unhashed
let link_section = crate::generate_link_section(quote!(__ASSET), &asset_hash);
let link_section = generate_link_section(quote!(__ASSET), &asset_hash);
// generate the asset::new method to deprecate the `./assets/blah.css` syntax
let constructor = if asset.is_relative() {
File diff suppressed because it is too large. Load diff
+61 -3
View File
@@ -13,10 +13,9 @@ use syn::{
pub(crate) mod asset;
pub(crate) mod css_module;
pub(crate) mod ffi;
pub(crate) mod linker;
use linker::generate_link_section;
use crate::css_module::{expand_css_module_struct, CssModuleAttribute};
/// The asset macro collects assets that will be included in the final binary
@@ -208,7 +207,7 @@ pub fn option_asset(input: TokenStream) -> TokenStream {
/// fn app() -> Element {
/// #[css_module("/assets/styles.css")]
/// struct Styles;
///
///
/// rsx! {
/// div { class: Styles::container,
/// button { class: Styles::button, "Click me" }
@@ -226,6 +225,65 @@ pub fn css_module(input: TokenStream, item: TokenStream) -> TokenStream {
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
//
+70 -30
View File
@@ -1,43 +1,83 @@
use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use quote::ToTokens;
/// We store description of the assets an application uses in the executable.
/// We use the `link_section` attribute embed an extra section in the executable.
/// We force rust to store a serialized representation of the asset description
/// inside a particular region of the binary, with the label "manganis".
/// After linking, the "manganis" sections of the different object files will be merged.
pub fn generate_link_section(asset: impl ToTokens, asset_hash: &str) -> TokenStream2 {
/// Generate a linker section for embedding arbitrary data in the binary
///
/// This is a generic version that allows customizing the serialization function,
/// buffer type, and copy bytes function. Used by both asset and FFI metadata embedding.
pub fn generate_link_section_inner(
item: TokenStream2,
hash: &str,
prefix: &str,
serialize_fn: TokenStream2,
copy_bytes_fn: TokenStream2,
buffer_type: TokenStream2,
) -> TokenStream2 {
let position = proc_macro2::Span::call_site();
let export_name = syn::LitStr::new(&format!("__ASSETS__{}", asset_hash), position);
let legacy_export_name = syn::LitStr::new(&format!("__MANGANIS__{}", asset_hash), position);
quote::quote! {
// We bundle both the legacy and new link sections for compatibility with older CLIs
static __LEGACY_LINK_SECTION: &'static [u8] = {
// First serialize the asset into a constant sized buffer
const __BUFFER: manganis::macro_helpers::const_serialize_07::ConstVec<u8> = manganis::macro_helpers::serialize_asset_07(&#asset);
// Then pull out the byte slice
const __BYTES: &[u8] = __BUFFER.as_ref();
// And the length of the byte slice
const __LEN: usize = __BYTES.len();
// Now that we have the size of the asset, copy the bytes into a static array
#[unsafe(export_name = #legacy_export_name)]
static __LINK_SECTION: [u8; __LEN] = manganis::macro_helpers::copy_bytes(__BYTES);
&__LINK_SECTION
};
let export_name = syn::LitStr::new(&format!("{}{}", prefix, hash), position);
quote! {
#[used]
static __LINK_SECTION: &'static [u8] = {
// First serialize the asset into a constant sized buffer
const __BUFFER: manganis::macro_helpers::const_serialize::ConstVec<u8> = manganis::macro_helpers::serialize_asset(&#asset);
// Then pull out the byte slice
const __BUFFER: #buffer_type = #serialize_fn(&#item);
const __BYTES: &[u8] = __BUFFER.as_ref();
// And the length of the byte slice
const __LEN: usize = __BYTES.len();
// Now that we have the size of the asset, copy the bytes into a static array
#[unsafe(export_name = #export_name)]
static __LINK_SECTION: [u8; __LEN] = manganis::macro_helpers::copy_bytes(__BYTES);
#[used]
static __LINK_SECTION: [u8; __LEN] = #copy_bytes_fn(__BYTES);
&__LINK_SECTION
};
}
}
/// Generate a linker section for embedding asset data in the binary
///
/// This function creates a static array containing the serialized asset data
/// and exports it with the __ASSETS__ prefix for unified symbol collection.
/// Uses the generic linker helper from dx-macro-helpers for consistency.
pub fn generate_link_section(asset: impl ToTokens, asset_hash: &str) -> TokenStream2 {
let item = asset;
let hash: &str = asset_hash;
let prefix_current: &str = "__ASSETS__";
let prefix_legacy: &str = "__MANGANIS__";
let serialize_fn_current = quote! { manganis::macro_helpers::serialize_asset };
let serialize_fn_legacy = quote! { manganis::macro_helpers::serialize_asset_07 };
let copy_bytes_fn = quote! { manganis::macro_helpers::copy_bytes };
let buffer_type_current = quote! { manganis::macro_helpers::ConstVec<u8, 4096> };
let buffer_type_legacy = quote! { manganis::macro_helpers::const_serialize_07::ConstVec<u8> };
let position = proc_macro2::Span::call_site();
let export_name = syn::LitStr::new(&format!("{}{}", prefix_current, hash), position);
let legacy_export_name = syn::LitStr::new(&format!("{}{}", prefix_legacy, hash), position);
let used_attr = if false {
quote! { #[used] }
} else {
quote! {}
};
quote! {
// We bundle both the legacy and new link sections for compatibility.
static __LEGACY_LINK_SECTION: &'static [u8] = {
const __BUFFER: #buffer_type_legacy = #serialize_fn_legacy(&#item);
const __BYTES: &[u8] = __BUFFER.as_ref();
const __LEN: usize = __BYTES.len();
#used_attr
#[unsafe(export_name = #legacy_export_name)]
static __LINK_SECTION: [u8; __LEN] = #copy_bytes_fn(__BYTES);
&__LINK_SECTION
};
static __LINK_SECTION: &'static [u8] = {
const __BUFFER: #buffer_type_current = #serialize_fn_current(&#item);
const __BYTES: &[u8] = __BUFFER.as_ref();
const __LEN: usize = __BYTES.len();
#used_attr
#[unsafe(export_name = #export_name)]
static __LINK_SECTION: [u8; __LEN] = #copy_bytes_fn(__BYTES);
&__LINK_SECTION
};
}
+11
View File
@@ -16,8 +16,19 @@ keywords = ["assets"]
const-serialize = { workspace = true }
manganis-core = { workspace = true }
manganis-macro = { workspace = true }
thiserror = { workspace = true }
const-serialize-07 = { package = "const-serialize", version = "=0.7.2" }
[target.'cfg(target_os = "android")'.dependencies]
jni = "0.21"
ndk-context = "0.1.1"
[target.'cfg(target_os = "ios")'.dependencies]
objc2 = "0.6.3"
[target.'cfg(target_os = "macos")'.dependencies]
objc2 = "0.6.3"
[features]
default = []
dioxus = ["manganis-core/dioxus"]
+18 -14
View File
@@ -1,23 +1,17 @@
# Manganis
The Manganis allows you to submit assets to any build tool that supports collecting assets. It makes it easy to self-host assets that are distributed throughout your libraries. Manganis also handles optimizing, converting, and fetching assets.
Manganis is a tool for submitting assets and native source files to the program linker. It makes it easy to self-host assets and native plugins that are distributed throughout your libraries.
If you defined this in a component library:
## Assets
Including assets is as simple as using the `asset!()` macro:
```rust
use manganis::{Asset, asset};
const AVIF_ASSET: Asset = manganis::asset!("/assets/image.png");
const STYLE: Asset = asset!("/assets/style.css");
```
AVIF_ASSET will be set to a new file name that will be served by some CLI. That file can be collected by any package that depends on the component library.
If you have assets that may not always be bundled, you can fall back gracefully with `option_asset!`:
```rust
use manganis::{Asset, asset, option_asset};
const REQUIRED: Asset = asset!("/assets/style.css");
const OPTIONAL: Option<Asset> = option_asset!("/assets/missing.css");
```
After cargo builds your app, the asset path is embedded directly in the data section. A tool like the Dioxus CLI can extract this metadata and post-process these assets.
```rust
use manganis::{ImageFormat, Asset, asset, ImageSize, AssetOptions};
@@ -34,6 +28,16 @@ pub const RESIZED_PNG_ASSET: Asset =
pub const AVIF_ASSET: Asset = asset!("/assets/image.png", AssetOptions::image().with_format(ImageFormat::Avif));
```
## Adding Support to Your CLI
## option_asset
To add support for your CLI, you need to integrate with the [manganis_cli_support](https://github.com/DioxusLabs/manganis/tree/main/cli-support) crate. This crate provides utilities to collect assets that integrate with the Manganis macro. It makes it easy to integrate an asset collection and optimization system into a build tool.
If you have assets that may not always be bundled, you can fall back gracefully with `option_asset!`:
```rust
use manganis::{Asset, asset, option_asset};
const REQUIRED: Asset = asset!("/assets/style.css");
const OPTIONAL: Option<Asset> = option_asset!("/assets/missing.css");
```
## Native FFI Bindings
Manganis provides the `#[ffi]` attribute macro for generating direct FFI bindings between Rust and native platforms (Swift/Kotlin). See the `geolocation-native-plugin` example for usage.
@@ -0,0 +1,63 @@
use jni::{objects::JObject, JNIEnv, JavaVM};
use std::sync::OnceLock;
/// Cached reference to the Android activity.
static ACTIVITY: OnceLock<jni::objects::GlobalRef> = OnceLock::new();
static JAVA_VM: OnceLock<JavaVM> = OnceLock::new();
/// Execute a JNI operation with a cached activity reference.
///
/// This function handles the boilerplate of getting the JavaVM and Activity
/// references, caching them for subsequent calls. It's the foundation for
/// most Android mobile API operations.
///
/// # Arguments
///
/// * `f` - A closure that receives a mutable JNIEnv and the Activity JObject
///
/// # Returns
///
/// Returns `Some(R)` if the operation succeeds, `None` if there's an error
/// getting the VM or Activity references.
///
/// # Example
///
/// ```rust,no_run
/// use dioxus_platform_bridge::android::with_activity;
///
/// let result = with_activity(|env, activity| {
/// // Your JNI operations here
/// Some(42)
/// });
/// ```
pub fn with_activity<F, R>(f: F) -> Option<R>
where
F: FnOnce(&mut JNIEnv<'_>, &JObject<'_>) -> Option<R>,
{
let ctx = ndk_context::android_context();
let vm = if let Some(vm) = JAVA_VM.get() {
vm
} else {
let raw_vm = unsafe { JavaVM::from_raw(ctx.vm().cast()) }.ok()?;
let _ = JAVA_VM.set(raw_vm);
JAVA_VM.get()?
};
let mut env = vm.attach_current_thread().ok()?;
let activity = if let Some(activity) = ACTIVITY.get() {
activity
} else {
let raw_activity = unsafe { JObject::from_raw(ctx.context() as jni::sys::jobject) };
let global = env.new_global_ref(&raw_activity).ok()?;
match ACTIVITY.set(global) {
Ok(()) => ACTIVITY.get().unwrap(),
Err(global) => {
drop(global);
ACTIVITY.get()?
}
}
};
let activity_obj = activity.as_obj();
f(&mut env, &activity_obj)
}
@@ -0,0 +1,132 @@
use jni::{
objects::{GlobalRef, JClass, JObject, JValue},
sys::jlong,
JNIEnv, NativeMethod,
};
use crate::android::java::Result;
/// Generic callback system for loading DEX classes and registering native methods
pub struct CallbackSystem {
bytecode: &'static [u8],
class_name: &'static str,
callback_name: &'static str,
callback_signature: &'static str,
}
impl CallbackSystem {
/// Create a new callback system
///
/// # Arguments
///
/// * `bytecode` - The compiled DEX bytecode
/// * `class_name` - The fully qualified Java class name
/// * `callback_name` - The name of the native callback method
/// * `callback_signature` - The JNI signature of the callback method
pub fn new(
bytecode: &'static [u8],
class_name: &'static str,
callback_name: &'static str,
callback_signature: &'static str,
) -> Self {
Self {
bytecode,
class_name,
callback_name,
callback_signature,
}
}
/// Load the DEX class and register the native callback method
///
/// This function handles the boilerplate of:
/// 1. Creating an InMemoryDexClassLoader
/// 2. Loading the specified class
/// 3. Registering the native callback method
///
/// # Returns
///
/// Returns a `GlobalRef` to the loaded class, or an error if loading fails
pub fn load_and_register(&self, env: &mut JNIEnv<'_>) -> Result<GlobalRef> {
let callback_class = self.load_dex_class(env)?;
self.register_native_callback(env, &callback_class)?;
let global = env.new_global_ref(callback_class)?;
Ok(global)
}
/// Load the DEX class from bytecode
fn load_dex_class<'a>(&self, env: &mut JNIEnv<'a>) -> Result<JClass<'a>> {
const IN_MEMORY_LOADER: &str = "dalvik/system/InMemoryDexClassLoader";
// Create a ByteBuffer from our DEX bytecode
let byte_buffer = unsafe {
env.new_direct_byte_buffer(self.bytecode.as_ptr() as *mut u8, self.bytecode.len())
}?;
// Create an InMemoryDexClassLoader with our DEX bytecode
let dex_class_loader = env.new_object(
IN_MEMORY_LOADER,
"(Ljava/nio/ByteBuffer;Ljava/lang/ClassLoader;)V",
&[
JValue::Object(&byte_buffer),
JValue::Object(&JObject::null()),
],
)?;
// Load our class
let class_name = env.new_string(self.class_name)?;
let class = env
.call_method(
&dex_class_loader,
"loadClass",
"(Ljava/lang/String;)Ljava/lang/Class;",
&[JValue::Object(&class_name)],
)?
.l()?;
Ok(class.into())
}
/// Register the native callback method with the Java class
fn register_native_callback<'a>(
&self,
env: &mut JNIEnv<'a>,
callback_class: &JClass<'a>,
) -> Result<()> {
env.register_native_methods(
callback_class,
&[NativeMethod {
name: self.callback_name.into(),
sig: self.callback_signature.into(),
fn_ptr: rust_callback as *mut _,
}],
)?;
Ok(())
}
}
/// Generic native callback function called from Java
///
/// SAFETY: This function is called from Java and must maintain proper memory safety.
/// The handler pointer is valid as long as the Manager exists (see Drop implementation).
#[no_mangle]
unsafe extern "C" fn rust_callback<'a>(
mut env: JNIEnv<'a>,
_class: JObject<'a>,
handler_ptr_high: jlong,
handler_ptr_low: jlong,
location: JObject<'a>,
) {
// Reconstruct the pointer from two i64 values (for 64-bit pointers)
#[cfg(not(target_pointer_width = "64"))]
compile_error!("Only 64-bit Android targets are supported");
let handler_ptr_raw: usize =
((handler_ptr_high as u64) << 32 | handler_ptr_low as u64) as usize;
// Convert to our callback function pointer
let callback: fn(&mut JNIEnv, JObject) = unsafe { std::mem::transmute(handler_ptr_raw) };
// Use the local reference for this JNI frame; avoid leaking a global ref.
callback(&mut env, location);
}
@@ -0,0 +1,127 @@
use jni::{
objects::{JClass, JObject, JObjectArray, JString, JValue, JValueOwned},
JNIEnv,
};
/// Result type for JNI operations
pub type Result<T> = std::result::Result<T, jni::errors::Error>;
/// Helper functions for common JNI operations
/// Create a new Java string tied to the current JNI frame
pub fn new_string<'env>(env: &mut JNIEnv<'env>, s: &str) -> Result<JString<'env>> {
env.new_string(s)
}
/// Create a new object array
pub fn new_object_array<'env>(
env: &mut JNIEnv<'env>,
len: i32,
element_class: &str,
) -> Result<JObjectArray<'env>> {
env.new_object_array(len, element_class, &JObject::null())
}
/// Set an element in an object array
pub fn set_object_array_element<'env>(
env: &mut JNIEnv<'env>,
array: &JObjectArray<'env>,
index: i32,
element: JString<'env>,
) -> Result<()> {
env.set_object_array_element(array, index, element)
}
/// Call a static method on a class
pub fn call_static_method<'env, 'obj>(
env: &mut JNIEnv<'env>,
class: &JClass<'env>,
method_name: &str,
signature: &str,
args: &[JValue<'env, 'obj>],
) -> Result<JValueOwned<'env>> {
env.call_static_method(class, method_name, signature, args)
}
/// Call an instance method on an object
pub fn call_method<'env, 'obj>(
env: &mut JNIEnv<'env>,
obj: &JObject<'env>,
method_name: &str,
signature: &str,
args: &[JValue<'env, 'obj>],
) -> Result<JValueOwned<'env>> {
env.call_method(obj, method_name, signature, args)
}
/// Find a Java class by name
pub fn find_class<'env>(env: &mut JNIEnv<'env>, class_name: &str) -> Result<JClass<'env>> {
env.find_class(class_name)
}
/// Create a new object instance
pub fn new_object<'env, 'obj>(
env: &mut JNIEnv<'env>,
class_name: &str,
signature: &str,
args: &[JValue<'env, 'obj>],
) -> Result<JObject<'env>> {
env.new_object(class_name, signature, args)
}
/// Check if a permission is granted (Activity.checkSelfPermission)
pub fn check_self_permission(
env: &mut JNIEnv,
activity: &JObject,
permission: &str,
) -> Result<bool> {
let permission_string = new_string(env, permission)?;
let status = env.call_method(
activity,
"checkSelfPermission",
"(Ljava/lang/String;)I",
&[JValue::Object(&permission_string)],
)?;
const PERMISSION_GRANTED: i32 = 0;
Ok(status.i()? == PERMISSION_GRANTED)
}
/// Request permissions via a helper class's static method
///
/// This uses PermissionsHelper.requestPermissionsOnUiThread(pattern)
/// to request permissions on the UI thread.
pub fn request_permissions_via_helper(
env: &mut JNIEnv,
helper_class: &JClass,
activity: &JObject,
permissions: JObjectArray,
request_code: i32,
) -> Result<()> {
env.call_static_method(
helper_class,
"requestPermissionsOnUiThread",
"(Landroid/app/Activity;[Ljava/lang/String;I)V",
&[
JValue::Object(activity),
JValue::Object(&permissions.into()),
JValue::Int(request_code),
],
)?;
Ok(())
}
/// Load a Java class from the APK's classloader
pub fn load_class_from_classloader<'env>(
env: &mut JNIEnv<'env>,
class_name: &str,
) -> Result<JClass<'env>> {
let class_name_jstring = new_string(env, class_name)?;
let class = env.call_static_method(
"java/lang/Class",
"forName",
"(Ljava/lang/String;)Ljava/lang/Class;",
&[JValue::Object(&class_name_jstring)],
)?;
Ok(class.l()?.into())
}
@@ -0,0 +1,9 @@
//! Android metadata wrappers for linker-based collection.
pub use manganis_core::AndroidArtifactMetadata;
pub type AndroidMetadataBuffer = crate::macro_helpers::ConstVec<u8, 4096>;
pub const fn serialize_android_metadata(meta: &AndroidArtifactMetadata) -> AndroidMetadataBuffer {
crate::macro_helpers::serialize_android_artifact(meta)
}
@@ -0,0 +1,39 @@
//! Android-specific utilities for mobile APIs
#[cfg(target_os = "android")]
pub mod activity;
#[cfg(target_os = "android")]
pub mod callback;
#[cfg(target_os = "android")]
pub mod java;
#[cfg(target_os = "android")]
pub mod metadata;
#[doc(hidden)]
pub mod macro_helpers {
//! Helper functions for macro expansion
//!
//! These functions are used internally by the `android_plugin!()` macro
//! and should not be used directly.
/// Copy a slice into a constant sized buffer at compile time
pub const fn copy_bytes<const N: usize>(bytes: &[u8]) -> [u8; N] {
let mut out = [0; N];
let mut i = 0;
while i < N {
out[i] = bytes[i];
i += 1;
}
out
}
}
#[cfg(target_os = "android")]
pub use activity::*;
#[cfg(target_os = "android")]
pub use callback::*;
#[cfg(target_os = "android")]
pub use java::*;
#[cfg(target_os = "android")]
pub use metadata::*;
@@ -0,0 +1,29 @@
//! Darwin (iOS/macOS) shared utilities for objc2-based APIs
//!
//! This module provides shared utilities for both iOS and macOS platforms
//! since they share the same Objective-C runtime and threading requirements
//! through objc2.
/// Re-export MainThreadMarker for convenience
pub use objc2::MainThreadMarker;
/// Macro helpers for FFI code generation
pub mod macro_helpers {
pub use crate::macro_helpers::copy_bytes;
}
pub use manganis_core::SwiftPackageMetadata;
pub mod metadata {
use manganis_core::SwiftPackageMetadata;
/// Buffer type for serialized Swift metadata
#[doc(hidden)]
pub type SwiftMetadataBuffer = crate::macro_helpers::ConstVec<u8, 4096>;
/// Serialize Swift package metadata for linker embedding
#[doc(hidden)]
pub const fn serialize_swift_metadata(meta: &SwiftPackageMetadata) -> SwiftMetadataBuffer {
crate::macro_helpers::serialize_swift_package(meta)
}
}
+52 -3
View File
@@ -1,5 +1,4 @@
#![doc = include_str!("../README.md")]
#![deny(missing_docs)]
#[doc(hidden)]
pub mod macro_helpers;
@@ -8,6 +7,56 @@ pub use manganis_macro::css_module;
pub use manganis_macro::option_asset;
pub use manganis_core::{
Asset, AssetOptions, AssetVariant, BundledAsset, CssAssetOptions, CssModuleAssetOptions,
FolderAssetOptions, ImageAssetOptions, ImageFormat, ImageSize, JsAssetOptions,
// Core asset types
Asset,
AssetOptions,
AssetVariant,
BundledAsset,
// Standard asset options
CssAssetOptions,
CssModuleAssetOptions,
FolderAssetOptions,
ImageAssetOptions,
ImageFormat,
ImageSize,
JsAssetOptions,
};
// Re-export metadata types for FFI and sidecar macros
pub use manganis_core::SwiftPackageMetadata;
pub use manganis_core::SymbolData;
// FFI utilities and plugin metadata for Dioxus mobile platform APIs
//
// This crate provides common patterns and utilities for implementing
// mobile platform APIs in Dioxus applications. It handles the
// boilerplate for JNI (Android) and objc2 (iOS/macOS) bindings, build scripts,
// and platform-specific resource management.
/// Android platform utilities
#[doc(hidden)]
pub mod android;
/// Darwin (iOS/macOS) platform utilities
#[doc(hidden)]
#[cfg(any(target_os = "ios", target_os = "macos"))]
pub mod darwin;
#[cfg(target_os = "android")]
pub use android::*;
// Export darwin module for iOS, macOS, and when metadata feature is enabled (for FFI macro)
#[cfg(any(target_os = "ios", target_os = "macos"))]
pub use darwin::*;
/// Re-export commonly used types for convenience
#[cfg(target_os = "android")]
pub use jni;
// Re-export objc2 for FFI macro generated code
#[cfg(any(target_os = "ios", target_os = "macos"))]
pub use objc2;
/// Re-export the ffi attribute macro for native FFI bindings
/// This macro generates direct FFI bindings between Rust and native platforms (Swift/Kotlin)
pub use manganis_macro::ffi;
+120 -28
View File
@@ -1,11 +1,44 @@
// Re-export const_serialize types for generated code.
pub use const_serialize;
use const_serialize::{serialize_const, ConstVec, SerializeConst};
pub use const_serialize::{ConstStr, ConstVec, SerializeConst};
pub use const_serialize_07;
use const_serialize_07::{
serialize_const as serialize_const_07, ConstVec as ConstVec07,
SerializeConst as SerializeConst07,
use const_serialize_07::ConstVec as ConstVec07;
use manganis_core::{
AndroidArtifactMetadata, AssetOptions, BundledAsset, SwiftPackageMetadata, SymbolData,
};
use manganis_core::{AssetOptions, BundledAsset};
/// Copy a slice into a constant sized buffer at compile time
///
/// This is a generic utility that works with any byte slice and can be used
/// in const contexts to create fixed-size arrays from dynamic slices.
pub const fn copy_bytes<const N: usize>(bytes: &[u8]) -> [u8; N] {
let mut out = [0; N];
let mut i = 0;
while i < N {
out[i] = bytes[i];
i += 1;
}
out
}
/// Serialize a SymbolData value into a const buffer
///
/// This is used by the widget!() macro and other symbol-based macros to embed
/// metadata into the binary using the 4096-byte buffer format.
pub const fn serialize_symbol_data(symbol_data: &SymbolData) -> ConstVec<u8, 4096> {
dx_macro_helpers::serialize_to_const_with_max_padded::<4096>(symbol_data)
}
/// Serialize Android artifact metadata (wrapped in `SymbolData::AndroidArtifact`).
pub const fn serialize_android_artifact(meta: &AndroidArtifactMetadata) -> ConstVec<u8, 4096> {
serialize_symbol_data(&SymbolData::AndroidArtifact(*meta))
}
/// Serialize Swift package metadata (wrapped in `SymbolData::SwiftPackage`).
pub const fn serialize_swift_package(meta: &SwiftPackageMetadata) -> ConstVec<u8, 4096> {
serialize_symbol_data(&SymbolData::SwiftPackage(*meta))
}
/// Create a bundled asset from the input path, the content hash, and the asset options
pub const fn create_bundled_asset(input_path: &str, asset_config: AssetOptions) -> BundledAsset {
@@ -26,25 +59,16 @@ pub const fn create_bundled_asset_relative(
}
/// Serialize an asset to a const buffer
pub const fn serialize_asset(asset: &BundledAsset) -> ConstVec<u8> {
let data = ConstVec::new();
let mut data = serialize_const(asset, data);
// Reserve the maximum size of the asset
while data.len() < <BundledAsset as SerializeConst>::MEMORY_LAYOUT.size() {
data = data.push(0);
}
data
///
/// Serializes the asset directly (not wrapped in SymbolData) for simplicity.
/// Uses a 4096-byte buffer and pads to the full size to match linker section size.
pub const fn serialize_asset(asset: &BundledAsset) -> ConstVec<u8, 4096> {
dx_macro_helpers::serialize_to_const_with_max_padded::<4096>(asset)
}
/// Serialize an asset to a const buffer in the legacy 0.7 format
pub const fn serialize_asset_07(asset: &BundledAsset) -> ConstVec07<u8> {
let data = ConstVec07::new();
let mut data = serialize_const_07(asset, data);
// Reserve the maximum size of the asset
while data.len() < <BundledAsset as SerializeConst07>::MEMORY_LAYOUT.size() {
data = data.push(0);
}
data
dx_macro_helpers::serialize_to_const_with_layout_padded_07(asset)
}
/// Deserialize a const buffer into a BundledAsset
@@ -55,13 +79,81 @@ pub const fn deserialize_asset(bytes: &[u8]) -> BundledAsset {
}
}
/// Copy a slice into a constant sized buffer at compile time
pub const fn copy_bytes<const N: usize>(bytes: &[u8]) -> [u8; N] {
let mut out = [0; N];
let mut i = 0;
while i < N {
out[i] = bytes[i];
i += 1;
pub mod dx_macro_helpers {
use const_serialize::{ConstVec, SerializeConst};
/// Serialize a value to a const buffer, padding to the specified size
///
/// This is a generic helper that works with any type implementing `SerializeConst`.
/// It serializes the value and then pads the buffer to the specified memory layout size.
pub const fn serialize_to_const<T: SerializeConst>(
value: &T,
memory_layout_size: usize,
) -> ConstVec<u8> {
let data = ConstVec::new();
let mut data = const_serialize::serialize_const(value, data);
// Reserve the maximum size of the type
while data.len() < memory_layout_size {
data = data.push(0);
}
data
}
/// Serialize a value to a const buffer with a fixed maximum size, padding to the specified size
///
/// This variant uses a `ConstVec` with a fixed maximum size (e.g., `ConstVec<u8, 4096>`)
/// and then pads to the specified memory layout size.
///
/// This function serializes directly into the larger buffer to avoid overflow issues
/// when the serialized data exceeds the default 1024-byte buffer size.
pub const fn serialize_to_const_with_max<const MAX_SIZE: usize>(
value: &impl SerializeConst,
memory_layout_size: usize,
) -> ConstVec<u8, MAX_SIZE> {
// Serialize using the default buffer, then copy into the larger buffer
let serialized = const_serialize::serialize_const(value, ConstVec::new());
let mut data: ConstVec<u8, MAX_SIZE> = ConstVec::new_with_max_size();
data = data.extend(serialized.as_ref());
// Reserve the maximum size of the type (pad to MEMORY_LAYOUT size)
while data.len() < memory_layout_size {
data = data.push(0);
}
data
}
/// Serialize a value to a const buffer and pad to the full buffer size
///
/// This is useful for linker section generation that expects a fixed-size buffer.
pub const fn serialize_to_const_with_max_padded<const MAX_SIZE: usize>(
value: &impl SerializeConst,
) -> ConstVec<u8, MAX_SIZE> {
let serialized = const_serialize::serialize_const(value, ConstVec::new());
let mut data: ConstVec<u8, MAX_SIZE> = ConstVec::new_with_max_size();
data = data.extend(serialized.as_ref());
while data.len() < MAX_SIZE {
data = data.push(0);
}
data
}
/// Serialize a value using the legacy 0.7 const-serialize format and pad to layout size
///
/// Note: The legacy ConstVec has a 1024-byte limit. If MEMORY_LAYOUT.size() exceeds this,
/// we pad only up to the buffer limit to avoid overflow.
pub const fn serialize_to_const_with_layout_padded_07<T: const_serialize_07::SerializeConst>(
value: &T,
) -> const_serialize_07::ConstVec<u8> {
let data = const_serialize_07::ConstVec::new();
let mut data = const_serialize_07::serialize_const(value, data);
// Pad to MEMORY_LAYOUT size, but cap at 1024 bytes (the legacy buffer limit)
let target_size = if T::MEMORY_LAYOUT.size() > 1024 {
1024
} else {
T::MEMORY_LAYOUT.size()
};
while data.len() < target_size {
data = data.push(0);
}
data
}
out
}