codify "platform" into resolution logic, server-fn/fullstack overhaul (#4596)

overhaul dioxus fullstack using a new implementation of server_fn

This PR is a complete rewrite of dioxus fullstack designed to improve compatibility with axum and to fix a number of compilation errors based on feature flag unification plumbing through macros.
This commit is contained in:
Jonathan Kelley authored and GitHub committed 2025-10-03 18:26:19 -07:00
1 parent 8421c3aef0
commit f2b1c6d97f
424 files changed
+17284 -6624

No files matched your search

+4 -1
View File
@@ -320,7 +320,10 @@ pub(crate) async fn extract_assets_from_file(path: impl AsRef<Path>) -> Result<A
if let Some((_, bundled_asset)) = const_serialize::deserialize_const!(BundledAsset, buffer)
{
tracing::debug!("Found asset at offset {offset}: {:?}", bundled_asset);
tracing::debug!(
"Found asset at offset {offset}: {:?}",
bundled_asset.absolute_source_path()
);
assets.push(bundled_asset);
} 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.");
+3 -2
View File
@@ -715,7 +715,7 @@ impl AppBuilder {
let changed_file = changed_files.first().unwrap();
tracing::info!(
"Hot-patching: {NOTE_STYLE}{}{NOTE_STYLE:#} took {GLOW_STYLE}{:?}ms{GLOW_STYLE:#}",
"Hot-patching: {NOTE_STYLE}{}{NOTE_STYLE:#} took {GLOW_STYLE}{:?}ms{GLOW_STYLE:#} ({})",
changed_file
.display()
.to_string()
@@ -723,7 +723,8 @@ impl AppBuilder {
SystemTime::now()
.duration_since(res.time_start)
.unwrap()
.as_millis()
.as_millis(),
triple
);
self.patches.push(jump_table.clone());
+10 -6
View File
@@ -1191,6 +1191,8 @@ pub fn prepare_wasm_base_module(bytes: &[u8]) -> Result<Vec<u8>> {
})
.collect::<HashMap<_, _>>();
let mut exported = HashSet::new();
// Wasm-bindgen will synthesize imports to satisfy its external calls. This facilitates things
// like inline-js, snippets, and literally the `#[wasm_bindgen]` macro. All calls to JS are
// just `extern "wbg"` blocks!
@@ -1227,9 +1229,10 @@ pub fn prepare_wasm_base_module(bytes: &[u8]) -> Result<Vec<u8>> {
let new_func_id = module.funcs.add_local(builder.local_func(locals));
module
.exports
.add(&format!("__saved_wbg_{}", import.name), new_func_id);
let saved_name = format!("__saved_wbg_{}", import.name);
if exported.insert(saved_name.clone()) {
module.exports.add(&saved_name, new_func_id);
}
make_indirect.push(new_func_id);
}
@@ -1254,9 +1257,10 @@ pub fn prepare_wasm_base_module(bytes: &[u8]) -> Result<Vec<u8>> {
//
// https://github.com/rustwasm/wasm-bindgen/blob/c35cc9369d5e0dc418986f7811a0dd702fb33ef9/crates/cli-support/src/wit/mod.rs#L1505
if name.starts_with("__wbindgen") {
module
.exports
.add(&format!("__saved_wbg_{name}"), func.id());
let saved_name = format!("__saved_wbg_{}", name);
if exported.insert(saved_name.clone()) {
module.exports.add(&saved_name, func.id());
}
}
// This is basically `--export-all` but designed to work around wasm-bindgen not properly gc-ing
+297 -107
View File
@@ -321,7 +321,7 @@
use crate::{
AndroidTools, BuildContext, BundleFormat, DioxusConfig, Error, LinkAction, LinkerFlavor,
Renderer, Result, RustcArgs, TargetAlias, TargetArgs, TraceSrc, WasmBindgen, WasmOptConfig,
Platform, Renderer, Result, RustcArgs, TargetArgs, TraceSrc, WasmBindgen, WasmOptConfig,
Workspace, DX_RUSTC_WRAPPER_ENV_VAR,
};
use anyhow::{bail, Context};
@@ -348,7 +348,7 @@ use std::{
},
time::{SystemTime, UNIX_EPOCH},
};
use target_lexicon::{Architecture, Environment, OperatingSystem, Triple};
use target_lexicon::{Architecture, OperatingSystem, Triple};
use tempfile::TempDir;
use tokio::{io::AsyncBufReadExt, process::Command};
use uuid::Uuid;
@@ -549,105 +549,257 @@ impl BuildRequest {
})?
.clone();
// The crate might enable multiple platforms or no platforms at
// We collect all the platforms it enables first and then select based on the --platform arg
let enabled_renderers =
Self::enabled_cargo_toml_renderers(main_package, args.no_default_features);
let using_dioxus_explicitly = main_package
.dependencies
.iter()
.any(|dep| dep.name == "dioxus");
let mut features = args.features.clone();
let mut no_default_features = args.no_default_features;
// Apply the platform alias if present
let mut target_alias = args.target_alias;
let mut renderer = args.renderer;
let mut bundle = args.bundle;
if let Some(platform) = args.platform {
let (default_target, default_renderer, default_bundle) = platform.into_triple();
target_alias = target_alias.or(default_target);
renderer.renderer = Some(renderer.renderer.unwrap_or(default_renderer));
bundle = Some(bundle.unwrap_or(default_bundle));
}
let mut renderer: Option<Renderer> = match renderer.into() {
Some(renderer) => match enabled_renderers.len() {
0 => Some(renderer),
// The user passed --platform XYZ but already has `default = ["ABC"]` in their Cargo.toml or dioxus = { features = ["abc"] }
// We want to strip out the default platform and use the one they passed, setting no-default-features
_ => {
features.extend(Self::rendererless_features(main_package));
no_default_features = true;
Some(renderer)
}
},
None if !using_dioxus_explicitly => None,
None => match enabled_renderers.as_slice() {
[] => None, // Wait until we resolve everything else first then we will resolve it from the triple
[(renderer, feature)] => {
let targeting_mobile = match (&args.target, target_alias) {
(_, TargetAlias::Android | TargetAlias::Ios) => true,
(Some(target), _) => {
matches!(
(target.environment, target.operating_system),
(Environment::Android, OperatingSystem::IOS(_))
)
}
_ => false,
};
// The renderer usually maps directly to a feature flag, but the mobile crate is an exception.
// If we see the `desktop` renderer in the default features, but the user is targeting mobile,
// we should remove the default `desktop` feature, but still target webview. The feature selection
// logic below will handle adding back in the `mobile` feature flag
if targeting_mobile && feature == "desktop" {
features.extend(Self::rendererless_features(main_package));
no_default_features = true;
}
Some(*renderer)
}
_ => {
bail!(
"Multiple platforms enabled in Cargo.toml. Please specify a platform with `--web`, `--webview`, or `--native` or set a default platform in Cargo.toml"
);
}
},
};
// We usually use the simulator unless --device is passed *or* a device is detected by probing.
// For now, though, since we don't have probing, it just defaults to false
// Tools like xcrun/adb can detect devices
let device = args.device.clone();
// Resolve the target alias and renderer into a concrete target alias.
// If the user didn't pass a platform, but we have a renderer, get the default platform for that renderer
if let (TargetAlias::Unknown, Some(renderer)) = (target_alias, renderer) {
target_alias = renderer.default_platform();
let using_dioxus_explicitly = main_package
.dependencies
.iter()
.any(|dep| dep.name == "dioxus");
/*
Determine which features, triple, profile, etc to pass to the build.
Most of the time, users should use `dx serve --<platform>` where the platform name directly
corresponds to the feature in their cargo.toml. So,
- `dx serve --web` will enable the `web` feature
- `dx serve --mobile` will enable the `mobile` feature
- `dx serve --desktop` will enable the `desktop` feature
In this case, we set default-features to false and then add back the default features that
aren't renderers, and then add the feature for the given renderer (ie web/desktop/mobile).
We call this "no-default-features-stripped."
There are a few cases where the user doesn't need to pass a platform.
- they selected one via `dioxus = { features = ["web"] }`
- they have a single platform in their default features `default = ["web"]`
- there is only a single non-server renderer as a feature `web = ["dioxus/web"], server = ["dioxus/server"]`
- they compose the super triple via triple + bundleformat + features
Note that we only use the names of the features to correspond with the platform.
Platforms are "super triples", meaning they contain information about
- bundle format
- target triple
- how to serve
- enabled features
By default, the --platform presets correspond to:
- web: bundle(web), triple(wasm32), serve(http-serve), features("web")
- desktop: alias to mac/win/linux
- mac: bundle(mac), triple(host), serve(appbundle-open), features("desktop")
- windows: bundle(exefolder), triple(host), serve(run-exe), features("desktop")
- linux: bundle(appimage), triple(host), serve(run-exe), features("desktop")
- ios: bundle(ios), triple(arm64-apple-ios), serve(ios-simulator/xcrun), features("mobile")
- android: bundle(android), triple(arm64-apple-ios), serve(android-emulator/adb), features("mobile")
- server: bundle(server), triple(host), serve(run-exe), features("server") (and disables the client)
- liveview: bundle(liveview), triple(host), serve(run-exe), features("liveview")
- unknown: <auto or default to desktop>
Fullstack usage is inferred from the presence of the fullstack feature or --fullstack.
*/
let mut features = args.features.clone();
let mut no_default_features = args.no_default_features;
let mut triple = args.target.clone();
let mut renderer = args.renderer;
let mut bundle_format = args.bundle;
let mut platform = args.platform;
// the crate might be selecting renderers but the user also passes a renderer. this is weird
// ie dioxus = { features = ["web"] } but also --platform desktop
// anyways, we collect it here in the event we need it if platform is not specified.
let dioxus_direct_renderer = Self::renderer_enabled_by_dioxus_dependency(main_package);
let known_features_as_renderers = Self::features_that_enable_renderers(main_package);
// The crate might enable multiple platforms or no platforms at
// We collect all the platforms it enables first and then select based on the --platform arg
let enabled_renderers = if no_default_features {
vec![]
} else {
Self::enabled_cargo_toml_default_features_renderers(main_package)
};
// We want a real triple to build with, so we'll autodetect it if it's not provided
// The triple ends up being a source of truth for us later hence all this work to figure it out
let triple = match (args.target.clone(), target_alias) {
// If there is an explicit target, use it
(Some(target), _) => target,
// If there is a platform, use it to determine the target triple
(None, platform) => platform.into_target(device.is_some(), &workspace).await?,
};
// Try the easy autodetects.
// - if the user has `dioxus = { features = ["web"] }`
// - if the `default =["web"]` or `default = ["dioxus/web"]`
// - if there's only one non-server platform ie `web = ["dioxus/web"], server = ["dioxus/server"]`
// Only do this if we're explicitly using dioxus
if matches!(platform, Platform::Unknown) && using_dioxus_explicitly {
let auto = dioxus_direct_renderer
.or_else(|| {
if enabled_renderers.len() == 1 {
Some(enabled_renderers[0].clone())
} else {
None
}
})
.or_else(|| {
let non_server_features = known_features_as_renderers
.iter()
.filter(|f| f.1.as_str() != "server")
.collect::<Vec<_>>();
if non_server_features.len() == 1 {
Some(non_server_features[0].clone())
} else {
None
}
});
// If the user didn't pass a renderer, and we didn't find a good default renderer, but they are using dioxus explicitly,
// then we can try to guess the renderer based on the target triple.
if renderer.is_none() && using_dioxus_explicitly {
renderer = Some(Renderer::from_target(&triple));
if let Some((direct, feature)) = auto {
match direct {
_ if feature == "mobile" || feature == "dioxus/mobile" => {
bail!(
"Could not autodetect mobile platform. Use --ios or --android instead."
);
}
Renderer::Webview | Renderer::Native => {
if cfg!(target_os = "macos") {
platform = Platform::MacOS;
} else if cfg!(target_os = "linux") {
platform = Platform::Linux;
} else if cfg!(target_os = "windows") {
platform = Platform::Windows;
}
}
Renderer::Server => platform = Platform::Server,
Renderer::Liveview => platform = Platform::Liveview,
Renderer::Web => platform = Platform::Web,
}
renderer = renderer.or(Some(direct));
}
}
// Resolve the bundle format based on the combination of the target triple and renderer
let bundle = match bundle {
// If there is an explicit bundle format, use it
Some(bundle) => bundle,
// Otherwise guess a bundle format based on the target triple and renderer
None => BundleFormat::from_target(&triple, renderer)?,
// Set the super triple from the platform if it's provided.
// Otherwise, we attempt to guess it from the rest of their inputs.
match platform {
Platform::Unknown => {}
Platform::Web => {
if main_package.features.contains_key("web") && renderer.is_none() {
features.push("web".into());
}
renderer = renderer.or(Some(Renderer::Web));
bundle_format = bundle_format.or(Some(BundleFormat::Web));
triple = triple.or(Some("wasm32-unknown-unknown".parse()?));
no_default_features = true;
}
Platform::MacOS => {
if main_package.features.contains_key("desktop") && renderer.is_none() {
features.push("desktop".into());
}
renderer = renderer.or(Some(Renderer::Webview));
bundle_format = bundle_format.or(Some(BundleFormat::MacOS));
triple = triple.or(Some(Triple::host()));
no_default_features = true;
}
Platform::Windows => {
if main_package.features.contains_key("desktop") && renderer.is_none() {
features.push("desktop".into());
}
renderer = renderer.or(Some(Renderer::Webview));
bundle_format = bundle_format.or(Some(BundleFormat::Windows));
triple = triple.or(Some(Triple::host()));
no_default_features = true;
}
Platform::Linux => {
if main_package.features.contains_key("desktop") && renderer.is_none() {
features.push("desktop".into());
}
renderer = renderer.or(Some(Renderer::Webview));
bundle_format = bundle_format.or(Some(BundleFormat::Linux));
triple = triple.or(Some(Triple::host()));
no_default_features = true;
}
Platform::Ios => {
if main_package.features.contains_key("mobile") && renderer.is_none() {
features.push("mobile".into());
}
renderer = renderer.or(Some(Renderer::Webview));
bundle_format = bundle_format.or(Some(BundleFormat::Ios));
no_default_features = true;
match device.is_some() {
// If targeting device, we want to build for the device which is always aarch64
true => triple = triple.or(Some("aarch64-apple-ios".parse()?)),
// If the host is aarch64, we assume the user wants to build for iOS simulator
false if matches!(Triple::host().architecture, Architecture::Aarch64(_)) => {
triple = triple.or(Some("aarch64-apple-ios-sim".parse()?))
}
// Otherwise, it's the x86_64 simulator, which is just x86_64-apple-ios
_ => triple = triple.or(Some("x86_64-apple-ios".parse()?)),
}
}
Platform::Android => {
if main_package.features.contains_key("mobile") && renderer.is_none() {
features.push("mobile".into());
}
renderer = renderer.or(Some(Renderer::Webview));
bundle_format = bundle_format.or(Some(BundleFormat::Android));
no_default_features = true;
// maybe probe adb?
if let Some(_device_name) = device.as_ref() {
if triple.is_none() {
triple = Some(
crate::get_android_tools()
.context("Failed to get android tools")?
.autodetect_android_device_triple()
.await,
);
}
} else {
triple = triple.or(Some({
match Triple::host().architecture {
Architecture::X86_32(_) => "i686-linux-android".parse()?,
Architecture::X86_64 => "x86_64-linux-android".parse()?,
Architecture::Aarch64(_) => "aarch64-linux-android".parse()?,
_ => "aarch64-linux-android".parse()?,
}
}));
}
}
Platform::Server => {
if main_package.features.contains_key("server") && renderer.is_none() {
features.push("server".into());
}
renderer = renderer.or(Some(Renderer::Server));
bundle_format = bundle_format.or(Some(BundleFormat::Server));
triple = triple.or(Some(Triple::host()));
no_default_features = true;
}
Platform::Liveview => {
if main_package.features.contains_key("liveview") && renderer.is_none() {
features.push("liveview".into());
}
renderer = renderer.or(Some(Renderer::Liveview));
bundle_format = bundle_format.or(Some(BundleFormat::Server));
triple = triple.or(Some(Triple::host()));
no_default_features = true;
}
}
// If no default features are enabled, we need to add the rendererless features
if no_default_features {
features.extend(Self::rendererless_features(main_package));
features.dedup();
features.sort();
}
// The triple will be the triple passed or the host if using dioxus.
let triple = if using_dioxus_explicitly {
triple.context("Could not automatically detect target triple")?
} else {
triple.unwrap_or(Triple::host())
};
// The bundle format will be the bundle format passed or the host.
let bundle = if using_dioxus_explicitly {
bundle_format.context("Could not automatically detect bundle format")?
} else {
bundle_format.unwrap_or(BundleFormat::host())
};
// Add any features required to turn on the client
@@ -657,6 +809,7 @@ impl BuildRequest {
&triple,
renderer,
));
features.dedup();
}
// Set the profile of the build if it's not already set
@@ -714,6 +867,14 @@ impl BuildRequest {
]);
}
// automatically set the getrandom backend for web builds if the user requested it
if matches!(bundle, BundleFormat::Web) && args.wasm_js_cfg {
rustflags.flags.extend(
cargo_config2::Flags::from_space_separated(r#"--cfg getrandom_backend="wasm_js""#)
.flags,
);
}
// Make sure to take into account the RUSTFLAGS env var and the CARGO_TARGET_<triple>_RUSTFLAGS
for env in [
"RUSTFLAGS".to_string(),
@@ -1032,7 +1193,11 @@ impl BuildRequest {
// since that is a really bad user experience.
Message::BuildFinished(finished) => {
if !finished.success {
bail!("cargo build finished with errors.")
bail!(
"cargo build finished with errors for target: {} [{}]",
self.main_target,
self.triple
);
}
}
_ => {}
@@ -1190,7 +1355,6 @@ impl BuildRequest {
async fn write_frameworks(&self, _ctx: &BuildContext, direct_rustc: &RustcArgs) -> Result<()> {
let framework_dir = self.frameworks_folder();
_ = std::fs::create_dir_all(&framework_dir);
// We have some prebuilt stuff that needs to be copied into the framework dir
let openssl_dir = AndroidTools::openssl_lib_dir(&self.triple);
@@ -1208,6 +1372,8 @@ impl BuildRequest {
tracing::debug!("Copying framework from {from:?} to {to:?}");
_ = std::fs::create_dir_all(&framework_dir);
// in dev and on normal oses, we want to symlink the file
// otherwise, just copy it (since in release you want to distribute the framework)
if cfg!(any(windows, unix)) && !self.release {
@@ -1225,6 +1391,11 @@ impl BuildRequest {
}
}
// Always create the framework dir for android
if self.bundle == BundleFormat::Android {
_ = std::fs::create_dir_all(&framework_dir);
}
// On android, the c++_shared flag means we need to copy the libc++_shared.so precompiled
// library to the jniLibs folder
if self.bundle == BundleFormat::Android && arg.contains("-lc++_shared") {
@@ -3352,13 +3523,9 @@ impl BuildRequest {
}
}
/// Return the platforms that are enabled for the package
///
/// Ideally only one platform is enabled but we need to be able to
pub(crate) fn enabled_cargo_toml_renderers(
pub(crate) fn renderer_enabled_by_dioxus_dependency(
package: &krates::cm::Package,
no_default_features: bool,
) -> Vec<(Renderer, String)> {
) -> Option<(Renderer, String)> {
let mut renderers = vec![];
// Attempt to discover the platform directly from the dioxus dependency
@@ -3367,13 +3534,40 @@ impl BuildRequest {
// dioxus = { features = ["web"] }
//
if let Some(dxs) = package.dependencies.iter().find(|dep| dep.name == "dioxus") {
for f in dxs.features.iter() {
if let Some(renderer) = Renderer::autodetect_from_cargo_feature(f) {
renderers.push((renderer, f.clone()));
for feature in dxs.features.iter() {
if let Some(renderer) = Renderer::autodetect_from_cargo_feature(feature) {
renderers.push((renderer, format!("dioxus/{}", feature)));
}
}
}
if renderers.len() != 1 {
return None;
}
Some(renderers[0].clone())
}
pub(crate) fn features_that_enable_renderers(
package: &krates::cm::Package,
) -> Vec<(Renderer, String)> {
package
.features
.keys()
.filter_map(|key| {
Renderer::autodetect_from_cargo_feature(key).map(|v| (v, key.to_string()))
})
.collect()
}
/// Return the platforms that are enabled for the package only from the default features
///
/// Ideally only one platform is enabled but we need to be able to
pub(crate) fn enabled_cargo_toml_default_features_renderers(
package: &krates::cm::Package,
) -> Vec<(Renderer, String)> {
let mut renderers = vec![];
// Start searching through the default features
//
// [features]
@@ -3384,10 +3578,6 @@ impl BuildRequest {
// [features]
// default = ["web"]
// web = ["dioxus/web"]
if no_default_features {
return renderers;
}
let Some(default) = package.features.get("default") else {
return renderers;
};
@@ -4396,7 +4586,7 @@ __wbg_init({{module_or_path: "/{}/{wasm_path}"}}).then((wasm) => {{
///
/// This might stop working if/when cargo stabilizes contents-based fingerprinting.
fn bust_fingerprint(&self, ctx: &BuildContext) -> Result<()> {
if matches!(ctx.mode, BuildMode::Fat | BuildMode::Base { run: true }) {
if matches!(ctx.mode, BuildMode::Fat) {
// `dx` compiles everything with `--target` which ends up with a structure like:
// target/<triple>/<profile>/.fingerprint/<package_name>-<hash>
//
+1 -5
View File
@@ -3,8 +3,7 @@ use anyhow::Context;
use itertools::Itertools;
use std::{path::PathBuf, sync::Arc};
use target_lexicon::{
Aarch64Architecture, Architecture, ArmArchitecture, Environment, OperatingSystem, Triple,
X86_32Architecture,
Aarch64Architecture, Architecture, ArmArchitecture, Triple, X86_32Architecture,
};
use tokio::process::Command;
@@ -236,9 +235,6 @@ impl AndroidTools {
// - We try to match the architecture unless otherwise specified. This is because
// emulators that match the host arch are usually faster.
let mut triple = "aarch64-linux-android".parse::<Triple>().unwrap();
triple.operating_system = OperatingSystem::Linux;
triple.environment = Environment::Android;
triple.architecture = target_lexicon::HOST.architecture;
// TODO: Wire this up with --device flag. (add `-s serial`` flag before `shell` arg)
let output = Command::new(&self.adb)