Permission, Building Blocks for Native APIs
This PR introduces a comprehensive linker-based permission management for automatic update of manifest files and android native plugin building foundation for Dioxus. The implementation is inspired by Manganis's linker-based asset collection approach.
Build on top of #4932 so diff is huge for now
Two main motivation for this PR
- To automatically handle the manifest file and info.plist file instead of invoking, ejecting or providing custom file.
- Able to pass in the java file path through linker, copy and bundle them (Opening doors for android native apis development)
Currently packages/geolocation and geolocation example resides within this PR, its just for the ease of testing, and will not be upstreamed
Here is a breakdown of how things are working for now.
Android (Kotlin / Gradle)
Source layout
- Kotlin sources + Gradle wrapper live under
packages/geolocation/android/ - The module builds a release
.aar:android/build/outputs/aar/geolocation-plugin-release.aar
build.rs responsibilities
packages/geolocation/build.rs builds the Gradle module whenever the target triple contains
android:
- Respects the toolchain env exported by the CLI (
DX_ANDROID_JAVA_HOME,DX_ANDROID_SDK_ROOT,DX_ANDROID_NDK_HOME,DX_ANDROID_ARTIFACT_DIR). If those are absent (non-dxbuilds) it falls back toANDROID_*orJAVA_HOME. - Runs
./gradlew assembleReleaseinsidepackages/geolocation/android. - Copies the resulting
.aarinto the CLI-provided artifact staging dir ($DX_ANDROID_ARTIFACT_DIR/geolocation-plugin-release.aaror$OUT_DIR/android-artifacts/...as a fallback). - Emits
cargo:rustc-env=DIOXUS_ANDROID_ARTIFACT=<absolute path>so the Rust macro can reference the built artifact.
Metadata embedded in the Rust binary
In packages/geolocation/src/lib.rs we declare the Android artifact like this:
#[cfg(all(feature = "metadata", target_os = "android"))]
dioxus_platform_bridge::android_plugin!(
plugin = "geolocation",
aar = { env = "DIOXUS_ANDROID_ARTIFACT" },
deps = ["implementation(\"com.google.android.gms:play-services-location:21.3.0\")"]
);
The aar block only needs a stable way to refer to the file that build.rs produced. Emitting a
cargo:rustc-env=... from the build script is the idiomatic Rust approach for passing data from
build.rs to the crate being compiled, which is why each plugin publishes its own environment
variable. Nothing prevents you from using unique names—DIOXUS_GEO_ANDROID_ARTIFACT,
DIOXUS_CAMERA_ANDROID_ARTIFACT, etc.—as long as your macro invocation references the same key that
your build script sets. Automatically inferring the path from the plugin name is brittle because the
artifact location lives in target/ (or another $OUT_DIR) that only build.rs knows about after
running Gradle, so the build script remains the single source of truth.
The macro serializes:
pluginname- Absolute
aarpath (resolved via the env var emitted bybuild.rs) - Any Gradle dependency lines listed in
deps
The metadata is wrapped in SymbolData::AndroidArtifact and stored under the shared
__ASSETS__* symbol prefix (the same one used for assets and permissions). Nothing runs at runtime—
the symbols are just scanned later by the CLI.
What the CLI does (dx serve --android)
- After building the Rust binary,
dxwalks every__ASSETS__*symbol once. When it encounters aSymbolData::AndroidArtifact, it adds it to the manifest alongside assets and permissions. install_android_artifacts()(inpackages/cli/src/build/request.rs) copies each.aarintotarget/dx/.../app/libs/in the generated Gradle project.- For every artifact it ensures the Gradle dependency file contains both:
implementation(files("libs/<artifact>.aar"))- Any additional strings from
deps(e.g. the Play Services location runtime)
- When Gradle runs for the host app, those libraries are already on disk and referenced in
app/build.gradle.kts, so they get packaged automatically.
Note: because the
.aaris built inside the Rust crate, plugin authors do not touch the app’s Gradle project. Everything happens by copying artifacts + editingapp/build.gradle.ktsinside the generated dx workspace.
iOS / macOS (Swift Package)
Source layout
- Swift sources +
Package.swiftlive underpackages/geolocation/ios/ - The Swift Package exports a single product:
GeolocationPlugin
build.rs responsibilities
For Apple targets, build.rs:
- Detects the desired triple (
aarch64-apple-ios,aarch64-apple-ios-sim,x86_64-apple-ios) and finds the matching SDK viaxcrun --show-sdk-path. - Runs
xcrun swift build --package-path ios --configuration <debug|release> --triple <...> --sdk <...> --product GeolocationPlugin. - Locates the resulting static library (
libGeolocationPlugin.a) and emits:cargo:rustc-link-searchfor both the Swift build output and the Xcode toolchain Swift libscargo:rustc-link-lib=static=GeolocationPlugincargo:rustc-link-lib=swiftCompatibility*+-force_loadflags so the ObjC runtime sees the plugin classcargo:rustc-link-lib=framework=CoreLocation/Foundation
At this point the Swift code is already linked into the Mach-O produced by Rust; no additional build steps are required later.
Metadata embedded in the Rust binary
In packages/geolocation/src/lib.rs we declare the Swift package via:
#[cfg(all(feature = "metadata", any(target_os = "ios", target_os = "macos")))]
dioxus_platform_bridge::ios_plugin!(
plugin = "geolocation",
spm = { path = "ios", product = "GeolocationPlugin" }
);
This writes an entry into the shared __ASSETS__* stream as SymbolData::SwiftPackage. Again,
nothing is executed at runtime; it is just discoverable metadata alongside assets and permissions.
What the CLI does (dx serve --ios / --macos)
- The CLI sees the
SymbolData::SwiftPackageentries while it’s already processing the asset stream. - When at least one Swift package is present,
embed_swift_stdlibs()runs (seepackages/cli/src/build/request.rs). This invokesxcrun swift-stdlib-tool --scan-executableon the final app binary and copies the required Swift runtime libraries into the bundle’sFrameworks/folder (iOS) orContents/Frameworks/(macOS).
Because the Swift package was linked during the Rust build, the CLI’s only job is to make sure the Swift runtime ships alongside the executable.
Summary table
| Platform | Native sources live | Compiled by | Metadata stored | CLI responsibilities |
|---|---|---|---|---|
| Android | packages/geolocation/android (Gradle lib) |
build.rs running gradlew assembleRelease |
SymbolData::AndroidArtifact (plugin, path, deps) |
Copy .aar into app/libs, append Gradle dependencies |
| iOS/macOS | packages/geolocation/ios (SwiftPM) |
build.rs running xcrun swift build |
SymbolData::SwiftPackage (plugin, package path, product) |
Embed Swift stdlibs when Swift metadata is present |
This setup keeps each plugin self-contained:
- All native code + build tooling stays inside the crate.
build.rsproduces platform artifacts and emits env vars for linker metadata.- The Dioxus CLI only needs the linker symbols to know what to copy/embed when bundling user apps.
New crates: (Please suggest the names as i am bad at naming stuff)
- permissions: Public API that re-exports permissions-core and permissions-macro for easy integration.
- permissions-core: Types for permission declaration and platform identifiers (Android, IOS, MacOS) (We can extend the identifiers as needed or requested)
- permissions-macro: Procedural Macros (static_permission!) for declaring permissions via linker symbol (So cli can extract the symbols and update the manifest files automatically)
- dx-macro-helpers: Common helpers for linker based stuff, shared by manganis and permission
- platform-bridge: Bareminimal Cross-platform FFI utilities and plugin metadata for Android (JNI) and Darwin (objc2)
- platform-bridge-macro: android_plugin! macro for embedding Java source file paths in binaries via linker symbols
Yey, Android Permission Dialog is working.
Web:
MacOs:
@ealmloff Thank you for the feedback. Other than these nits, What do you think about the overall process? Do you think this is something we should proceed with to upstream it? Or maybe try and figure some other build processes, toolings? I want to discuss some top level ideas on the whole process and how should we proceed.
@ealmloff Thank you for the feedback. Other than these nits, What do you think about the overall process? Do you think this is something we should proceed with to upstream it? Or maybe try and figure some other build processes, toolings? I want to discuss some top level ideas on the whole process and how should we proceed.
@jkelleyrtp may have more thoughts here as he set up most of the android/ios build support in the CLI
From my perspective, I think this is close to the right approach, but I'm not sure this is the right interface yet. The two approaches for collecting bindings seem to be either build scripts (like robius and tauri) or linker-based collection like this and wasm-bindgen. Between the two linker-based collection is a lot easier to consume which will be important as the number of native bindings grows.
Both tauri or robius require you to manually register permissions which adds extra setup for any system libraries. That approach makes permissions more visible, but automatically registering permissions is much easier to use. We may want to think about adding a prompt to accept or reject the new permissions collected by the linker, but overall I think the linker permissions approach is much nicer.
One concern with the linker approach for bindings is symbols that are optimized away. Looking at the example linked in your description, it looks like you/AI had some issues with symbols being dropped that required adding __ensure_permissions_linked. We will need to either make sure the symbol isn't dropped within the macro by adding #[used] or tie the symbol to the usage of the script that is linked so it is only dropped if the bindings are not used
@ealmloff Thank you for the feedback. Other than these nits, What do you think about the overall process? Do you think this is something we should proceed with to upstream it? Or maybe try and figure some other build processes, toolings? I want to discuss some top level ideas on the whole process and how should we proceed.
@jkelleyrtp may have more thoughts here as he set up most of the android/ios build support in the CLI
From my perspective, I think this is close to the right approach, but I'm not sure this is the right interface yet. The two approaches for collecting bindings seem to be either build scripts (like robius and tauri) or linker-based collection like this and wasm-bindgen. Between the two linker-based collection is a lot easier to consume which will be important as the number of native bindings grows.
Both tauri or robius require you to manually register permissions which adds extra setup for any system libraries. That approach makes permissions more visible, but automatically registering permissions is much easier to use. We may want to think about adding a prompt to accept or reject the new permissions collected by the linker, but overall I think the linker permissions approach is much nicer.
One concern with the linker approach for bindings is symbols that are optimized away. Looking at the example linked in your description, it looks like you/AI had some issues with symbols being dropped that required adding
__ensure_permissions_linked. We will need to either make sure the symbol isn't dropped within the macro by adding#[used]or tie the symbol to the usage of the script that is linked so it is only dropped if the bindings are not used
Yes, __ensure_permissions_linked does need some nits, i agree on that part. I started with build script like how robius was working but its a hastle to build dex files copy it and alot of issues when the native plugins will grow. Supporting tauri native plugins would be awesome but i tried and failed as it needs some extra setups that we need to deal with to provide seamless experience. With the growing number of contributors and community i think we can definately provide better abstraction and maybe just sharing android/kotlin files with those plugins should be good. Also, another thing is instead of .java file only, i want to support ,kt file for registering kotlin files and placing it on copying it on correct path for kotlin so we dont have to use java only. (Some plugins are freely available for kotlin and java side of things). Another discussion is on the IOS side of things, building and supporting each native swift package is tricky and challenging like how we are providing seamless for android but i am also not sure if we can do almost everything with objc crates. But the way its growing i think we should be able to do almost majority of things with it so not handling each ios plugin as a native swift package + swift files seems more appealing to me atm.
Going through the linker seems to be the right approach for permissions. Both build scripts and linker will require an end tool to collect the permissions, and I much prefer infrastructure-as-code approaches than the implicit build script. It's nice that not using a permission means not inserting it the Info.plist. Instead of #[used] attributes I would prefer that we make the permission a required argument to the function that requests the permission, and then we perform a volatile read, forcing the symbol to remain. This is similar to how Asset remains because its Display impl forces a volatile read.
We might need to think about Koitlin/Swift source files a bit more since cargo's cache busting system will be more reliable than the linker-based system, at least early on.
However, I'd rather not create a new symbol type for collecting permissions and instead try to reuse the one we use for assets. Though, the asset symbol format is not very flexible (see https://github.com/DioxusLabs/dioxus/issues/4863), so it might require some work on our end to allow different variants as long as they're the same size. Adding a new symbol seems to require duplicating logic in the CLI which we can avoid if we stick with one symbol with different variants.
Also, FWIW, this would be at the top of the list for 0.8 features. If we could get it in 0.7, then it would be at the top of priority behind fixing bugs, that way we can ship native apis while still on 0.7
@jkelleyrtp Thank you for the inputs. All the recommendation and inputs seems super valid and reasonable. After looking into #4863 I do think if we can somehow provide a appropriate way of creating different variants, and we dont need to create multiple symbols, alot of code on the CLI for extracting symbols can be alot more refined and cleaned. For kotlin/swift sources files, I also dont have super huge usecase to get into alot more depth to learn and define the fundamentals so i would definately leave these big chunks to dioxus core members. For the time being i will try to update my codes to more like an enum of LinkerSymbol which can hold either asset or permission and try to cleanup the CLI code abit so we dont go with multiple symbol type and can slip the permissions under MANGANIS symbol. And when we have a concrete plan for #4863 I can update my code for it. Also maybe we can try to slip in the permissions related stuff if possible into the 0.7 that way in with permissions, followed by build system for kotlin/swift and native apis can be added and shipped while still on 0.7.
I think i am able to package swift files as swift package and package it and run. (Like how tauri does for swift files)
I am also able to build each android plugin as a gradle submodule and can be tested like this inside each plugin
./gradlew assembleRelease --no-daemon --warning-mode all
-Oops i broke it lol- my bad it worked
So the current plugin structure is something like this.
Running gradle assembly takes time now, And i expect with the number of apis you add, it will grow exponentially
Here is a breakdown of how things are working for now.
Android (Kotlin / Gradle)
Source layout
- Kotlin sources + Gradle wrapper live under
packages/geolocation/android/ - The module builds a release
.aar:android/build/outputs/aar/geolocation-plugin-release.aar
build.rs responsibilities
packages/geolocation/build.rs builds the Gradle module whenever the target triple contains
android:
- Respects the toolchain env exported by the CLI (
DX_ANDROID_JAVA_HOME,DX_ANDROID_SDK_ROOT,DX_ANDROID_NDK_HOME,DX_ANDROID_ARTIFACT_DIR). If those are absent (non-dxbuilds) it falls back toANDROID_*orJAVA_HOME. - Runs
./gradlew assembleReleaseinsidepackages/geolocation/android. - Copies the resulting
.aarinto the CLI-provided artifact staging dir ($DX_ANDROID_ARTIFACT_DIR/geolocation-plugin-release.aaror$OUT_DIR/android-artifacts/...as a fallback). - Emits
cargo:rustc-env=DIOXUS_ANDROID_ARTIFACT=<absolute path>so the Rust macro can reference the built artifact.
Metadata embedded in the Rust binary
In packages/geolocation/src/lib.rs we declare the Android artifact like this:
#[cfg(all(feature = "metadata", target_os = "android"))]
dioxus_platform_bridge::android_plugin!(
plugin = "geolocation",
aar = { env = "DIOXUS_ANDROID_ARTIFACT" },
deps = ["implementation(\"com.google.android.gms:play-services-location:21.3.0\")"]
);
The aar block only needs a stable way to refer to the file that build.rs produced. Emitting a
cargo:rustc-env=... from the build script is the idiomatic Rust approach for passing data from
build.rs to the crate being compiled, which is why each plugin publishes its own environment
variable. Nothing prevents you from using unique names—DIOXUS_GEO_ANDROID_ARTIFACT,
DIOXUS_CAMERA_ANDROID_ARTIFACT, etc.—as long as your macro invocation references the same key that
your build script sets. Automatically inferring the path from the plugin name is brittle because the
artifact location lives in target/ (or another $OUT_DIR) that only build.rs knows about after
running Gradle, so the build script remains the single source of truth.
The macro serializes:
pluginname- Absolute
aarpath (resolved via the env var emitted bybuild.rs) - Any Gradle dependency lines listed in
deps
The metadata is wrapped in SymbolData::AndroidArtifact and stored under the shared
__ASSETS__* symbol prefix (the same one used for assets and permissions). Nothing runs at runtime—
the symbols are just scanned later by the CLI.
What the CLI does (dx serve --android)
- After building the Rust binary,
dxwalks every__ASSETS__*symbol once. When it encounters aSymbolData::AndroidArtifact, it adds it to the manifest alongside assets and permissions. install_android_artifacts()(inpackages/cli/src/build/request.rs) copies each.aarintotarget/dx/.../app/libs/in the generated Gradle project.- For every artifact it ensures the Gradle dependency file contains both:
implementation(files("libs/<artifact>.aar"))- Any additional strings from
deps(e.g. the Play Services location runtime)
- When Gradle runs for the host app, those libraries are already on disk and referenced in
app/build.gradle.kts, so they get packaged automatically.
Note: because the
.aaris built inside the Rust crate, plugin authors do not touch the app’s Gradle project. Everything happens by copying artifacts + editingapp/build.gradle.ktsinside the generated dx workspace.
iOS / macOS (Swift Package)
Source layout
- Swift sources +
Package.swiftlive underpackages/geolocation/ios/ - The Swift Package exports a single product:
GeolocationPlugin
build.rs responsibilities
For Apple targets, build.rs:
- Detects the desired triple (
aarch64-apple-ios,aarch64-apple-ios-sim,x86_64-apple-ios) and finds the matching SDK viaxcrun --show-sdk-path. - Runs
xcrun swift build --package-path ios --configuration <debug|release> --triple <...> --sdk <...> --product GeolocationPlugin. - Locates the resulting static library (
libGeolocationPlugin.a) and emits:cargo:rustc-link-searchfor both the Swift build output and the Xcode toolchain Swift libscargo:rustc-link-lib=static=GeolocationPlugincargo:rustc-link-lib=swiftCompatibility*+-force_loadflags so the ObjC runtime sees the plugin classcargo:rustc-link-lib=framework=CoreLocation/Foundation
At this point the Swift code is already linked into the Mach-O produced by Rust; no additional build steps are required later.
Metadata embedded in the Rust binary
In packages/geolocation/src/lib.rs we declare the Swift package via:
#[cfg(all(feature = "metadata", any(target_os = "ios", target_os = "macos")))]
dioxus_platform_bridge::ios_plugin!(
plugin = "geolocation",
spm = { path = "ios", product = "GeolocationPlugin" }
);
This writes an entry into the shared __ASSETS__* stream as SymbolData::SwiftPackage. Again,
nothing is executed at runtime; it is just discoverable metadata alongside assets and permissions.
What the CLI does (dx serve --ios / --macos)
- The CLI sees the
SymbolData::SwiftPackageentries while it’s already processing the asset stream. - When at least one Swift package is present,
embed_swift_stdlibs()runs (seepackages/cli/src/build/request.rs). This invokesxcrun swift-stdlib-tool --scan-executableon the final app binary and copies the required Swift runtime libraries into the bundle’sFrameworks/folder (iOS) orContents/Frameworks/(macOS).
Because the Swift package was linked during the Rust build, the CLI’s only job is to make sure the Swift runtime ships alongside the executable.
Summary table
| Platform | Native sources live | Compiled by | Metadata stored | CLI responsibilities |
|---|---|---|---|---|
| Android | packages/geolocation/android (Gradle lib) |
build.rs running gradlew assembleRelease |
SymbolData::AndroidArtifact (plugin, path, deps) |
Copy .aar into app/libs, append Gradle dependencies |
| iOS/macOS | packages/geolocation/ios (SwiftPM) |
build.rs running xcrun swift build |
SymbolData::SwiftPackage (plugin, package path, product) |
Embed Swift stdlibs when Swift metadata is present |
This setup keeps each plugin self-contained:
- All native code + build tooling stays inside the crate.
build.rsproduces platform artifacts and emits env vars for linker metadata.- The Dioxus CLI only needs the linker symbols to know what to copy/embed when bundling user apps.
I have added the geolocation package + example for geolocation in for easy testing so the diff is huge as of now. I need to delete them before we upstream it.
@jkelleyrtp I am sorry for a large diff atm, it has changes from #4932 as well as package/geolocation and example inside. So its easier for you to check. I am going out for few weeks after 2-3 days so i wanted to make sure this PR is in good shape before that. A small review on the build process would be really appreciated. I think the way it turned out is really interesting + approach is something that can be shared across other rust UI projects (I wonder someone can fork tauri plugins and make it compatiable with dioxus as this opens the door for that as well)
@jkelleyrtp I am sorry for a large diff atm, it has changes from #4932 as well as package/geolocation and example inside. So its easier for you to check. I am going out for few weeks after 2-3 days so i wanted to make sure this PR is in good shape before that. A small review on the build process would be really appreciated. I think the way it turned out is really interesting + approach is something that can be shared across other rust UI projects (I wonder someone can fork tauri plugins and make it compatiable with dioxus as this opens the door for that as well)
Awesome! Will try to get it in next week and then hopefully in a patch release soon after that
as someone who will need native permissions for usbc and bluetooth access on android and iOs i love seeing this being built. thank you team and everyone involved